refactor(library): Store sectioned library instead of flatten version of it (#336)

* refactor(library): Store sectioned first before flattening it out

* fix: Fix build, also rename some variables and move stuff around

* chore: Replace findCurrentCategory with currentCategory getter

* fix: Empty category can't be collapsed

* chore: Disable file log for debug build

* fix: Entry always displayed on default category

* refactor: Specify id, source, and url directly from MangaImpl constructor

* refactor: Make LibraryManga not extend MangaImpl

* refactor: Separate placeholder from LibraryManga

* fix: Default category should always be at the very beginning

* fix: Accidentally made the entries invisible

* fix: Default category disappear everytime a new category is added
This commit is contained in:
Ahmad Ansori Palembani 2025-01-05 18:15:34 +07:00 committed by GitHub
parent e415fd4ef2
commit cae0332ef9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 899 additions and 801 deletions

View file

@ -300,7 +300,7 @@ fun buildLogWritersToAdd(
) = buildList {
if (!BuildConfig.DEBUG) add(CrashlyticsLogWriter())
if (logPath != null) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose))
if (logPath != null && !BuildConfig.DEBUG) add(RollingUniFileLogWriter(logPath = logPath, isVerbose = isVerbose))
}
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"

View file

@ -57,8 +57,10 @@ data class BackupManga(
@ProtoNumber(805) var customGenre: List<String>? = null,
) {
fun getMangaImpl(): MangaImpl {
return MangaImpl().apply {
url = this@BackupManga.url
return MangaImpl(
source = this.source,
url = this.url,
).apply {
title = this@BackupManga.title
artist = this@BackupManga.artist
author = this@BackupManga.author
@ -67,7 +69,6 @@ data class BackupManga(
status = this@BackupManga.status
thumbnail_url = this@BackupManga.thumbnailUrl
favorite = this@BackupManga.favorite
source = this@BackupManga.source
date_added = this@BackupManga.dateAdded
viewer_flags = (
this@BackupManga.viewer_flags

View file

@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.data.database.models
import android.content.Context
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.ui.library.LibrarySort
import java.io.Serializable
import yokai.i18n.MR
import yokai.util.lang.getString
import java.io.Serializable
interface Category : Serializable {
@ -56,7 +56,21 @@ interface Category : Serializable {
fun mangaOrderToString(): String =
if (mangaSort != null) mangaSort.toString() else mangaOrder.joinToString("/")
// For dynamic categories
fun dynamicHeaderKey(): String {
if (!isDynamic) throw IllegalStateException("This category is not a dynamic category")
return when {
sourceId != null -> "${name}$sourceSplitter${sourceId}"
langId != null -> "${langId}$langSplitter${name}"
else -> name
}
}
companion object {
const val sourceSplitter = "◘•◘"
const val langSplitter = "⨼⨦⨠"
var lastCategoriesAddedTo = emptySet<Int>()
fun create(name: String): Category = CategoryImpl().apply {

View file

@ -32,10 +32,14 @@ class CategoryImpl : Category {
val category = other as Category
if (isDynamic && category.isDynamic) return dynamicHeaderKey() == category.dynamicHeaderKey()
return name == category.name
}
override fun hashCode(): Int {
if (isDynamic) return dynamicHeaderKey().hashCode()
return name.hashCode()
}
}

View file

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.ui.library.LibraryItem
import eu.kanade.tachiyomi.domain.manga.models.Manga
import kotlin.math.roundToInt
import yokai.data.updateStrategyAdapter
data class LibraryManga(
val manga: Manga,
var unread: Int = 0,
var read: Int = 0,
var category: Int = 0,
@ -13,41 +13,11 @@ data class LibraryManga(
var latestUpdate: Long = 0,
var lastRead: Long = 0,
var lastFetch: Long = 0,
) : MangaImpl() {
var realMangaCount = 0
get() = if (isBlank()) field else throw IllegalStateException("realMangaCount is only accessible by placeholders")
set(value) {
if (!isBlank()) throw IllegalStateException("realMangaCount can only be set by placeholders")
field = value
}
) {
val hasRead
get() = read > 0
@Transient
var items: List<LibraryItem>? = null
get() = if (isHidden()) field else throw IllegalStateException("items only accessible by placeholders")
set(value) {
if (!isHidden()) throw IllegalStateException("items can only be set by placeholders")
field = value
}
companion object {
fun createBlank(categoryId: Int): LibraryManga = LibraryManga().apply {
title = ""
id = Long.MIN_VALUE
category = categoryId
}
fun createHide(categoryId: Int, title: String, hiddenItems: List<LibraryItem>): LibraryManga =
createBlank(categoryId).apply {
this.title = title
this.status = -1
this.read = hiddenItems.size
this.items = hiddenItems
}
fun mapper(
// manga
id: Long,
@ -78,34 +48,37 @@ data class LibraryManga(
latestUpdate: Long,
lastRead: Long,
lastFetch: Long,
): LibraryManga = createBlank(categoryId.toInt()).apply {
this.id = id
this.source = source
this.url = url
this.artist = artist
this.author = author
this.description = description
this.genre = genre
this.title = title
this.status = status.toInt()
this.thumbnail_url = thumbnailUrl
this.favorite = favorite
this.last_update = lastUpdate ?: 0L
this.initialized = initialized
this.viewer_flags = viewerFlags.toInt()
this.hide_title = hideTitle
this.chapter_flags = chapterFlags.toInt()
this.date_added = dateAdded ?: 0L
this.filtered_scanlators = filteredScanlators
this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode)
this.cover_last_modified = coverLastModified
this.read = readCount.roundToInt()
this.unread = maxOf((total - readCount).roundToInt(), 0)
this.totalChapters = total.toInt()
this.bookmarkCount = bookmarkCount.roundToInt()
this.latestUpdate = latestUpdate
this.lastRead = lastRead
this.lastFetch = lastFetch
}
): LibraryManga = LibraryManga(
manga = Manga.mapper(
id = id,
source = source,
url = url,
artist = artist,
author = author,
description = description,
genre = genre,
title = title,
status = status,
thumbnailUrl = thumbnailUrl,
favorite = favorite,
lastUpdate = lastUpdate,
initialized = initialized,
viewerFlags = viewerFlags,
hideTitle = hideTitle,
chapterFlags = chapterFlags,
dateAdded = dateAdded,
filteredScanlators = filteredScanlators,
updateStrategy = updateStrategy,
coverLastModified = coverLastModified,
),
read = readCount.roundToInt(),
unread = maxOf((total - readCount).roundToInt(), 0),
totalChapters = total.toInt(),
bookmarkCount = bookmarkCount.roundToInt(),
category = categoryId.toInt(),
latestUpdate = latestUpdate,
lastRead = lastRead,
lastFetch = lastFetch,
)
}
}

View file

@ -182,15 +182,13 @@ var Manga.vibrantCoverColor: Int?
id?.let { MangaCoverMetadata.setVibrantColor(it, value) }
}
fun Manga.Companion.create(source: Long) = MangaImpl().apply {
this.source = source
}
fun Manga.Companion.create(pathUrl: String, title: String, source: Long = 0) = MangaImpl().apply {
url = pathUrl
this.title = title
this.source = source
}
fun Manga.Companion.create(url: String, title: String, source: Long = 0) =
MangaImpl(
source = source,
url = url,
).apply {
this.title = title
}
fun Manga.Companion.mapper(
id: Long,
@ -213,14 +211,12 @@ fun Manga.Companion.mapper(
filteredScanlators: String?,
updateStrategy: Long,
coverLastModified: Long,
) = create(source).apply {
) = create(url, title, source).apply {
this.id = id
this.url = url
this.artist = artist
this.author = author
this.description = description
this.genre = genre
this.title = title
this.status = status.toInt()
this.thumbnail_url = thumbnailUrl
this.favorite = favorite

View file

@ -12,7 +12,11 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List<ChapterHistory> = emptyList()) {
companion object {
fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl())
fun createBlank() = MangaChapterHistory(
MangaImpl(null, -1, ""),
ChapterImpl(),
HistoryImpl(),
)
fun mapper(
// manga

View file

@ -8,13 +8,11 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import uy.kohesive.injekt.injectLazy
open class MangaImpl : Manga {
override var id: Long? = null
override var source: Long = -1
override lateinit var url: String
open class MangaImpl(
override var id: Long? = null,
override var source: Long = -1,
override var url: String = "",
) : Manga {
private val customMangaManager: CustomMangaManager by injectLazy()
@ -107,7 +105,7 @@ open class MangaImpl : Manga {
}
override fun hashCode(): Int {
return if (::url.isInitialized) {
return if (url.isNotBlank()) {
url.hashCode()
} else {
(id ?: 0L).hashCode()

View file

@ -4,6 +4,7 @@ import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.domain.manga.models.Manga
import java.nio.charset.StandardCharsets
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
@ -26,7 +27,6 @@ import yokai.domain.library.custom.interactor.GetCustomManga
import yokai.domain.library.custom.interactor.RelinkCustomManga
import yokai.domain.library.custom.model.CustomMangaInfo
import yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo
import java.nio.charset.StandardCharsets
class CustomMangaManager(val context: Context) {
private val scope = CoroutineScope(Dispatchers.IO)
@ -176,8 +176,7 @@ class CustomMangaManager(val context: Context) {
val status: Int? = null,
) {
fun toManga() = MangaImpl().apply {
id = this@MangaJson.id
fun toManga() = MangaImpl(id = this.id).apply {
title = this@MangaJson.title ?: ""
author = this@MangaJson.author
artist = this@MangaJson.artist
@ -272,9 +271,6 @@ class CustomMangaManager(val context: Context) {
}
}
private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) = MangaImpl().apply {
this.id = id
this.copyFromComicInfo(comicInfo)
this.title = comicInfo.series?.value ?: ""
}
private fun mangaFromComicInfoObject(id: Long, comicInfo: ComicInfo) =
MangaImpl(id = id).apply { this.copyFromComicInfo(comicInfo) }
}

View file

@ -173,16 +173,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val mangaList = (
if (savedMangasList != null) {
val mangas = getLibraryManga.await().filter {
it.id in savedMangasList
}.distinctBy { it.id }
val mangas =
getLibraryManga.await()
.filter { it.manga.id in savedMangasList }
.distinctBy { it.manga.id }
val categoryId = inputData.getInt(KEY_CATEGORY, -1)
if (categoryId > -1) categoryIds.add(categoryId)
mangas
} else {
getMangaToUpdate()
}
).sortedBy { it.title }
).sortedBy { it.manga.title }
return withIOContext {
try {
@ -227,7 +228,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateChaptersJob(mangaToAdd: List<LibraryManga>) {
// Initialize the variables holding the progress of the updates.
mangaToUpdate.addAll(mangaToAdd)
mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.source })
mangaToUpdateMap.putAll(mangaToAdd.groupBy { it.manga.source })
checkIfMassiveUpdate()
coroutineScope {
val list = mangaToUpdateMap.keys.map { source ->
@ -257,42 +258,42 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateDetails(mangaToUpdate: List<LibraryManga>) = coroutineScope {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
val asyncList = mangaToUpdate.groupBy { it.source }.values.map { list ->
val asyncList = mangaToUpdate.groupBy { it.manga.source }.values.map { list ->
async {
requestSemaphore.withPermit {
list.forEach { manga ->
ensureActive()
val source = sourceManager.get(manga.source) as? HttpSource ?: return@async
val source = sourceManager.get(manga.manga.source) as? HttpSource ?: return@async
notifier.showProgressNotification(
manga,
manga.manga,
count.andIncrement,
mangaToUpdate.size,
)
ensureActive()
val networkManga = try {
source.getMangaDetails(manga.copy())
source.getMangaDetails(manga.manga.copy())
} catch (e: java.lang.Exception) {
Logger.e(e)
null
}
if (networkManga != null) {
manga.prepareCoverUpdate(coverCache, networkManga, false)
val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga)
manga.initialized = true
manga.manga.prepareCoverUpdate(coverCache, networkManga, false)
val thumbnailUrl = manga.manga.thumbnail_url
manga.manga.copyFrom(networkManga)
manga.manga.initialized = true
val request: ImageRequest =
if (thumbnailUrl != manga.thumbnail_url) {
if (thumbnailUrl != manga.manga.thumbnail_url) {
// load new covers in background
ImageRequest.Builder(context).data(manga.cover())
ImageRequest.Builder(context).data(manga.manga.cover())
.memoryCachePolicy(CachePolicy.DISABLED).build()
} else {
ImageRequest.Builder(context).data(manga.cover())
ImageRequest.Builder(context).data(manga.manga.cover())
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.WRITE_ONLY)
.build()
}
context.imageLoader.execute(request)
updateManga.await(manga.toMangaUpdate())
updateManga.await(manga.manga.toMangaUpdate())
}
}
}
@ -313,9 +314,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val loggedServices = trackManager.services.filter { it.isLogged }
mangaToUpdate.forEach { manga ->
notifier.showProgressNotification(manga, count++, mangaToUpdate.size)
notifier.showProgressNotification(manga.manga, count++, mangaToUpdate.size)
val tracks = getTrack.awaitAllByMangaId(manga.id!!)
val tracks = getTrack.awaitAllByMangaId(manga.manga.id!!)
tracks.forEach { track ->
val service = trackManager.getService(track.sync_id)
@ -324,7 +325,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val newTrack = service.refresh(track)
insertTrack.await(newTrack)
syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.id!!, false), track, service)
syncChaptersWithTrackServiceTwoWay(getChapter.awaitAll(manga.manga.id!!, false), track, service)
} catch (e: Exception) {
Logger.e(e)
}
@ -376,7 +377,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private fun checkIfMassiveUpdate() {
val largestSourceSize = mangaToUpdate
.groupBy { it.source }
.groupBy { it.manga.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (largestSourceSize > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
@ -391,7 +392,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val httpSource = sourceManager.get(source) as? HttpSource ?: return false
while (count < mangaToUpdateMap[source]!!.size) {
val manga = mangaToUpdateMap[source]!![count]
val shouldDownload = manga.shouldDownloadNewChapters(preferences)
val shouldDownload = manga.manga.shouldDownloadNewChapters(preferences)
if (updateMangaChapters(manga, this.count.andIncrement, httpSource, shouldDownload)) {
hasDownloads = true
}
@ -410,15 +411,15 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
try {
var hasDownloads = false
ensureActive()
notifier.showProgressNotification(manga, progress, mangaToUpdate.size)
val fetchedChapters = source.getChapterList(manga.copy())
notifier.showProgressNotification(manga.manga, progress, mangaToUpdate.size)
val fetchedChapters = source.getChapterList(manga.manga.copy())
if (fetchedChapters.isNotEmpty()) {
val newChapters = syncChaptersWithSource(fetchedChapters, manga, source)
val newChapters = syncChaptersWithSource(fetchedChapters, manga.manga, source)
if (newChapters.first.isNotEmpty()) {
if (shouldDownload) {
downloadChapters(
manga,
manga.manga,
newChapters.first.sortedBy { it.chapter_number },
)
hasDownloads = true
@ -428,24 +429,24 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
if (deleteRemoved && newChapters.second.isNotEmpty()) {
val removedChapters = newChapters.second.filter {
downloadManager.isChapterDownloaded(it, manga) &&
downloadManager.isChapterDownloaded(it, manga.manga) &&
newChapters.first.none { newChapter ->
newChapter.chapter_number == it.chapter_number && it.scanlator.isNullOrBlank()
}
}
if (removedChapters.isNotEmpty()) {
downloadManager.deleteChapters(removedChapters, manga, source)
downloadManager.deleteChapters(removedChapters, manga.manga, source)
}
}
if (newChapters.first.size + newChapters.second.size > 0) {
sendUpdate(manga.id)
sendUpdate(manga.manga.id)
}
}
return@coroutineScope hasDownloads
} catch (e: Exception) {
if (e !is CancellationException) {
failedUpdates[manga] = e.message
Logger.e { "Failed updating: ${manga.title}: $e" }
failedUpdates[manga.manga] = e.message
Logger.e { "Failed updating: ${manga.manga.title}: $e" }
}
return@coroutineScope false
}
@ -461,17 +462,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val restrictions = preferences.libraryUpdateMangaRestriction().get()
return mangaToAdd.filter { manga ->
when {
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_completed)
MANGA_NON_COMPLETED in restrictions && manga.manga.status == SManga.COMPLETED -> {
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_completed)
}
MANGA_HAS_UNREAD in restrictions && manga.unread != 0 -> {
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_caught_up)
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_caught_up)
}
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead -> {
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_started)
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_started)
}
manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> {
skippedUpdates[manga] = context.getString(MR.strings.skipped_reason_not_always_update)
manga.manga.update_strategy != UpdateStrategy.ALWAYS_UPDATE -> {
skippedUpdates[manga.manga] = context.getString(MR.strings.skipped_reason_not_always_update)
}
else -> {
return@filter true
@ -503,10 +504,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) {
categoryIds.addAll(categoriesToUpdate)
libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.id }
libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.manga.id }
} else {
categoryIds.addAll(getCategories.await().mapNotNull { it.id } + 0)
libraryManga.distinctBy { it.id }
libraryManga.distinctBy { it.manga.id }
}
}
@ -564,13 +565,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
private fun addMangaToQueue(categoryId: Int, manga: List<LibraryManga>) {
val mangas = filterMangaToUpdate(manga).sortedBy { it.title }
val mangas = filterMangaToUpdate(manga).sortedBy { it.manga.title }
categoryIds.add(categoryId)
addManga(mangas)
}
private fun addCategory(categoryId: Int) {
val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.title }
val mangas = filterMangaToUpdate(runBlocking { getMangaToUpdate(categoryId) }).sortedBy { it.manga.title }
categoryIds.add(categoryId)
addManga(mangas)
}
@ -579,7 +580,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val distinctManga = mangaToAdd.filter { it !in mangaToUpdate }
mangaToUpdate.addAll(distinctManga)
checkIfMassiveUpdate()
distinctManga.groupBy { it.source }.forEach {
distinctManga.groupBy { it.manga.source }.forEach {
// if added queue items is a new source not in the async list or an async list has
// finished running
if (mangaToUpdateMap[it.key].isNullOrEmpty()) {
@ -727,9 +728,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (mangaToUse != null) {
builder.putLongArray(
KEY_MANGAS,
mangaToUse.firstOrNull()?.id?.let { longArrayOf(it) } ?: longArrayOf(),
mangaToUse.firstOrNull()?.manga?.id?.let { longArrayOf(it) } ?: longArrayOf(),
)
extraManga = mangaToUse.subList(1, mangaToUse.size).mapNotNull { it.id }
extraManga = mangaToUse.subList(1, mangaToUse.size).mapNotNull { it.manga.id }
}
}
val inputData = builder.build()

View file

@ -185,14 +185,14 @@ class LibraryUpdateNotifier(private val context: Context) {
val manga = it.key
val chapters = it.value
val chapterNames = chapters.map { chapter ->
chapter.preferredChapterName(context, manga, preferences)
chapter.preferredChapterName(context, manga.manga, preferences)
}
notifications.add(
Pair(
context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_yokai)
try {
val request = ImageRequest.Builder(context).data(manga.cover())
val request = ImageRequest.Builder(context).data(manga.manga.cover())
.networkCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.transformations(CircleCropTransformation())
@ -205,7 +205,7 @@ class LibraryUpdateNotifier(private val context: Context) {
} catch (_: Exception) {
}
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
setContentTitle(manga.title)
setContentTitle(manga.manga.title)
color = ContextCompat.getColor(context, R.color.secondaryTachiyomi)
val chaptersNames = if (chapterNames.size > MAX_CHAPTERS) {
"${chapterNames.take(MAX_CHAPTERS - 1).joinToString(", ")}, " +
@ -224,7 +224,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(
NotificationReceiver.openChapterPendingActivity(
context,
manga,
manga.manga,
chapters.first(),
),
)
@ -233,7 +233,7 @@ class LibraryUpdateNotifier(private val context: Context) {
context.getString(MR.strings.mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(
context,
manga,
manga.manga,
chapters,
Notifications.ID_NEW_CHAPTERS,
),
@ -243,13 +243,13 @@ class LibraryUpdateNotifier(private val context: Context) {
context.getString(MR.strings.view_chapters),
NotificationReceiver.openChapterPendingActivity(
context,
manga,
manga.manga,
Notifications.ID_NEW_CHAPTERS,
),
)
setAutoCancel(true)
},
manga.id.hashCode(),
manga.manga.id.hashCode(),
),
)
}
@ -281,13 +281,13 @@ class LibraryUpdateNotifier(private val context: Context) {
NotificationCompat.BigTextStyle()
.bigText(
updates.keys.joinToString("\n") {
it.title.chop(45)
it.manga.title.chop(45)
},
),
)
}
} else if (!preferences.hideNotificationContent().get()) {
setContentText(updates.keys.first().title.chop(45))
setContentText(updates.keys.first().manga.title.chop(45))
}
priority = NotificationCompat.PRIORITY_HIGH
setGroup(Notifications.GROUP_NEW_CHAPTERS)

View file

@ -13,15 +13,15 @@ import eu.kanade.tachiyomi.util.lang.removeArticles
import eu.kanade.tachiyomi.util.system.isLTR
import eu.kanade.tachiyomi.util.system.timeSpanFromNow
import eu.kanade.tachiyomi.util.system.withDefContext
import java.util.*
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.injectLazy
import yokai.domain.ui.UiPreferences
import yokai.i18n.MR
import yokai.util.lang.getString
import java.util.*
import yokai.domain.category.interactor.GetCategories
import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.history.interactor.GetHistory
import yokai.domain.ui.UiPreferences
import yokai.i18n.MR
import yokai.util.lang.getString
/**
* Adapter storing a list of manga in a certain category.
@ -117,8 +117,8 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
*/
fun indexOf(manga: Manga): Int {
return currentItems.indexOfFirst {
if (it is LibraryItem) {
it.manga.id == manga.id
if (it is LibraryMangaItem) {
it.manga.manga.id == manga.id
} else {
false
}
@ -142,7 +142,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
*/
fun allIndexOf(manga: Manga): List<Int> {
return currentItems.mapIndexedNotNull { index, it ->
if (it is LibraryItem && it.manga.id == manga.id) {
if (it is LibraryMangaItem && it.manga.manga.id == manga.id) {
index
} else {
null
@ -164,7 +164,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
} else {
val filteredManga = withDefContext { mangas.filter { it.filter(s) } }
if (filteredManga.isEmpty() && controller?.presenter?.showAllCategories == false) {
val catId = mangas.firstOrNull()?.let { it.header?.catId ?: it.manga.category }
val catId = (mangas.firstOrNull() as? LibraryMangaItem)?.let { it.header?.catId ?: it.manga.category }
val blankItem = catId?.let { controller.presenter.blankItem(it) }
updateDataSet(blankItem ?: emptyList())
} else {
@ -202,18 +202,19 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
vibrateOnCategoryChange(item.category.name)
item.category.name
}
is LibraryItem -> {
val text = if (item.manga.isBlank()) {
return item.header?.category?.name.orEmpty()
} else {
is LibraryPlaceholderItem -> {
item.header?.category?.name.orEmpty()
}
is LibraryMangaItem -> {
val text =
when (getSort(position)) {
LibrarySort.DragAndDrop -> {
if (item.header.category.isDynamic && item.manga.id != null) {
if (item.header.category.isDynamic && item.manga.manga.id != null) {
// FIXME: Don't do blocking
val category = runBlocking { getCategories.awaitByMangaId(item.manga.id!!) }.firstOrNull()?.name
val category = runBlocking { getCategories.awaitByMangaId(item.manga.manga.id!!) }.firstOrNull()?.name
category ?: context.getString(MR.strings.default_value)
} else {
val title = item.manga.title
val title = item.manga.manga.title
if (preferences.removeArticles().get()) {
title.removeArticles().chop(15)
} else {
@ -222,14 +223,14 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
}
}
LibrarySort.DateFetched -> {
val id = item.manga.id ?: return ""
val id = item.manga.manga.id ?: return ""
// FIXME: Don't do blocking
val history = runBlocking { getChapter.awaitAll(id, false) }
val last = history.maxOfOrNull { it.date_fetch }
context.timeSpanFromNow(MR.strings.fetched_, last ?: 0)
}
LibrarySort.LastRead -> {
val id = item.manga.id ?: return ""
val id = item.manga.manga.id ?: return ""
// FIXME: Don't do blocking
val history = runBlocking { getHistory.awaitAllByMangaId(id) }
val last = history.maxOfOrNull { it.last_read }
@ -256,21 +257,20 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
}
}
LibrarySort.LatestChapter -> {
context.timeSpanFromNow(MR.strings.updated_, item.manga.last_update)
context.timeSpanFromNow(MR.strings.updated_, item.manga.manga.last_update)
}
LibrarySort.DateAdded -> {
context.timeSpanFromNow(MR.strings.added_, item.manga.date_added)
context.timeSpanFromNow(MR.strings.added_, item.manga.manga.date_added)
}
LibrarySort.Title -> {
val title = if (preferences.removeArticles().get()) {
item.manga.title.removeArticles()
item.manga.manga.title.removeArticles()
} else {
item.manga.title
item.manga.manga.title
}
getFirstLetter(title)
}
}
}
if (!isSingleCategory) {
vibrateOnCategoryChange(item.header?.category?.name.orEmpty())
}

View file

@ -451,7 +451,7 @@ open class LibraryController(
private fun setActiveCategory() {
val currentCategory = presenter.categories.indexOfFirst {
if (presenter.showAllCategories) it.order == activeCategory else presenter.currentCategory == it.id
if (presenter.showAllCategories) it.order == activeCategory else presenter.currentCategoryId == it.id
}
if (currentCategory > -1) {
binding.categoryRecycler.setCategories(currentCategory)
@ -521,14 +521,13 @@ open class LibraryController(
}
private fun openRandomManga(global: Boolean) {
val items = if (global) {
presenter.allLibraryItems
} else {
adapter.currentItems
}.filter { (it is LibraryItem && !it.manga.isBlank() && !it.manga.isHidden() && (!it.manga.initialized || it.manga.unread > 0)) }
val items =
if (global) { presenter.currentLibraryItems } else { adapter.currentItems }
.filterIsInstance<LibraryMangaItem>()
.filter { !it.manga.manga.initialized || it.manga.unread > 0 }
if (items.isNotEmpty()) {
val item = items.random() as LibraryItem
openManga(item.manga)
val item = items.random() as LibraryMangaItem
openManga(item.manga.manga)
}
}
@ -662,7 +661,7 @@ open class LibraryController(
createActionModeIfNeeded()
}
if (presenter.libraryItems.isNotEmpty() && !isSubClass) {
if (presenter.libraryItemsToDisplay.isNotEmpty() && !isSubClass) {
presenter.restoreLibrary()
if (justStarted) {
val activityBinding = activityBinding ?: return
@ -706,7 +705,7 @@ open class LibraryController(
if (!LibraryUpdateJob.isRunning(context)) {
when {
!presenter.showAllCategories && presenter.groupType == BY_DEFAULT -> {
presenter.findCurrentCategory()?.let {
presenter.currentCategory?.let {
updateLibrary(it)
}
}
@ -904,7 +903,7 @@ open class LibraryController(
}
} else {
val newOffset =
presenter.categories.indexOfFirst { presenter.currentCategory == it.id } +
presenter.categories.indexOfFirst { presenter.currentCategoryId == it.id } +
(if (next) 1 else -1)
if (if (!next) {
newOffset > -1
@ -1013,7 +1012,7 @@ open class LibraryController(
override fun getSpanSize(position: Int): Int {
if (libraryLayout == LibraryItem.LAYOUT_LIST) return managerSpanCount
val item = this@LibraryController.mAdapter?.getItem(position)
return if (item is LibraryHeaderItem || item is SearchGlobalItem || (item is LibraryItem && item.manga.isBlank())) {
return if (item is LibraryHeaderItem || item is SearchGlobalItem || item is LibraryPlaceholderItem) {
managerSpanCount
} else {
1
@ -1459,7 +1458,7 @@ open class LibraryController(
adapter.removeAllScrollableHeaders()
}
adapter.setFilter(query)
if (presenter.allLibraryItems.isEmpty()) return true
if (presenter.currentLibraryItems.isEmpty()) return true
viewScope.launchUI {
adapter.performFilterAsync()
}
@ -1478,7 +1477,6 @@ open class LibraryController(
}
private fun setSelection(manga: Manga, selected: Boolean) {
if (manga.isBlank()) return
val currentMode = adapter.mode
if (selected) {
if (selectedMangas.add(manga)) {
@ -1528,7 +1526,7 @@ open class LibraryController(
toggleSelection(position)
return
}
val manga = (adapter.getItem(position) as? LibraryItem)?.manga ?: return
val manga = (adapter.getItem(position) as? LibraryMangaItem)?.manga?.manga ?: return
val activity = activity ?: return
val chapter = presenter.getFirstUnread(manga) ?: return
activity.apply {
@ -1544,9 +1542,8 @@ open class LibraryController(
}
private fun toggleSelection(position: Int) {
val item = adapter.getItem(position) as? LibraryItem ?: return
if (item.manga.isBlank()) return
setSelection(item.manga, !adapter.isSelected(position))
val item = adapter.getItem(position) as? LibraryMangaItem ?: return
setSelection(item.manga.manga, !adapter.isSelected(position))
invalidateActionMode()
}
@ -1562,14 +1559,14 @@ open class LibraryController(
* @return true if the item should be selected, false otherwise.
*/
override fun onItemClick(view: View?, position: Int): Boolean {
val item = adapter.getItem(position) as? LibraryItem ?: return false
val item = adapter.getItem(position) as? LibraryMangaItem ?: return false
return if (adapter.mode == SelectableAdapter.Mode.MULTI) {
snack?.dismiss()
lastClickPosition = position
toggleSelection(position)
false
} else {
openManga(item.manga)
openManga(item.manga.manga)
false
}
}
@ -1591,10 +1588,10 @@ open class LibraryController(
*/
override fun onItemLongClick(position: Int) {
val item = adapter.getItem(position)
if (item !is LibraryItem) return
if (item !is LibraryMangaItem) return
snack?.dismiss()
if (libraryLayout == LibraryItem.LAYOUT_COVER_ONLY_GRID && actionMode == null) {
snack = view?.snack(item.manga.title) {
snack = view?.snack(item.manga.manga.title) {
anchorView = activityBinding?.bottomNav
view.elevation = 15f.dpToPx
}
@ -1648,9 +1645,9 @@ open class LibraryController(
}
private fun setSelection(position: Int, selected: Boolean = true) {
val item = adapter.getItem(position) as? LibraryItem ?: return
val item = adapter.getItem(position) as? LibraryMangaItem ?: return
setSelection(item.manga, selected)
setSelection(item.manga.manga, selected)
invalidateActionMode()
}
@ -1674,7 +1671,7 @@ open class LibraryController(
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition)
val item = adapter.getItem(fromPosition) as? LibraryItem ?: return false
val item = adapter.getItem(fromPosition) as? LibraryMangaItem ?: return false
val newHeader = adapter.getSectionHeader(toPosition) as? LibraryHeaderItem
if (toPosition < 1) return false
return (adapter.getItem(toPosition) !is LibraryHeaderItem) && (
@ -1696,11 +1693,11 @@ open class LibraryController(
destroyActionModeIfNeeded()
// if nothing moved
if (lastItemPosition == null) return
val item = adapter.getItem(position) as? LibraryItem ?: return
val item = adapter.getItem(position) as? LibraryMangaItem ?: return
val newHeader = adapter.getSectionHeader(position) as? LibraryHeaderItem
val libraryItems = getSectionItems(adapter.getSectionHeader(position), item)
.filterIsInstance<LibraryItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id }
.filterIsInstance<LibraryMangaItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id }
if (newHeader?.category?.id == item.manga.category) {
presenter.rearrangeCategory(item.manga.category, mangaIds)
} else {
@ -1832,8 +1829,8 @@ open class LibraryController(
if (category?.isDynamic == false && sortBy == LibrarySort.DragAndDrop.categoryValue) {
val item = adapter.findCategoryHeader(catId) ?: return
val libraryItems = adapter.getSectionItems(item)
.filterIsInstance<LibraryItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryItem)?.manga?.id }
.filterIsInstance<LibraryMangaItem>()
val mangaIds = libraryItems.mapNotNull { (it as? LibraryMangaItem)?.manga?.manga?.id }
presenter.rearrangeCategory(catId, mangaIds)
} else {
presenter.sortCategory(catId, sortBy)

View file

@ -64,23 +64,24 @@ class LibraryGridHolder(
* @param item the manga item to bind.
*/
override fun onSetValues(item: LibraryItem) {
if (item !is LibraryMangaItem) throw IllegalStateException("Only LibraryMangaItem can use grid holder")
// Update the title and subtitle of the manga.
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
binding.playButton.transitionName = "library chapter $bindingAdapterPosition transition"
binding.constraintLayout.isVisible = !item.manga.isBlank()
binding.title.text = item.manga.title.highlightText(item.filter, color)
binding.behindTitle.text = item.manga.title
val mangaColor = item.manga.dominantCoverColors
binding.constraintLayout.isVisible = item.manga.manga.id != Long.MIN_VALUE
binding.title.text = item.manga.manga.title.highlightText(item.filter, color)
binding.behindTitle.text = item.manga.manga.title
val mangaColor = item.manga.manga.dominantCoverColors
binding.coverConstraint.backgroundColor = mangaColor?.first ?: itemView.context.getResourceColor(R.attr.background)
binding.behindTitle.setTextColor(
mangaColor?.second ?: itemView.context.getResourceColor(R.attr.colorOnBackground),
)
val authorArtist = if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
item.manga.author?.trim() ?: ""
val authorArtist = if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) {
item.manga.manga.author?.trim() ?: ""
} else {
listOfNotNull(
item.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.artist?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() },
).joinToString(", ")
}
binding.subtitle.text = authorArtist.highlightText(item.filter, color)
@ -101,7 +102,7 @@ class LibraryGridHolder(
// Update the cover.
binding.coverThumbnail.dispose()
setCover(item.manga)
setCover(item.manga.manga)
}
override fun toggleActivation() {

View file

@ -5,9 +5,6 @@ import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import com.google.android.material.card.MaterialCardView
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.system.getResourceColor
@ -17,9 +14,7 @@ import eu.kanade.tachiyomi.util.view.setCards
* Generic class used to hold the displayed data of a manga in the library.
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @param listener a listener to react to the single tap and long tap events.
*/
abstract class LibraryHolder(
view: View,
val adapter: LibraryCategoryAdapter,
@ -43,7 +38,7 @@ abstract class LibraryHolder(
*/
abstract fun onSetValues(item: LibraryItem)
fun setUnreadBadge(badge: LibraryBadge, item: LibraryItem) {
fun setUnreadBadge(badge: LibraryBadge, item: LibraryMangaItem) {
val showTotal = item.header.category.sortingMode() == LibrarySort.TotalChapters
badge.setUnreadDownload(
when {
@ -54,7 +49,7 @@ abstract class LibraryHolder(
},
when {
item.downloadCount == -1 -> -1
item.manga.isLocal() -> -2
item.manga.manga.isLocal() -> -2
else -> item.downloadCount
},
showTotal,
@ -63,7 +58,7 @@ abstract class LibraryHolder(
)
}
fun setReadingButton(item: LibraryItem) {
fun setReadingButton(item: LibraryMangaItem) {
itemView.findViewById<View>(R.id.play_layout)?.isVisible =
item.manga.unread > 0 && !item.hideReadingButton
}
@ -80,8 +75,8 @@ abstract class LibraryHolder(
override fun onLongClick(view: View?): Boolean {
return if (adapter.isLongPressDragEnabled) {
val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryItem)?.manga
if (manga != null && !isDraggable && !manga.isBlank() && !manga.isHidden()) {
val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryMangaItem)?.manga
if (manga != null && !isDraggable) {
adapter.mItemLongClickListener.onItemLongClick(flexibleAdapterPosition)
toggleActivation()
true

View file

@ -1,205 +1,48 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.annotation.CallSuper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFilterable
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.seriesType
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import uy.kohesive.injekt.injectLazy
import yokai.domain.ui.UiPreferences
class LibraryItem(
val manga: LibraryManga,
abstract class LibraryItem(
header: LibraryHeaderItem,
private val context: Context?,
internal val context: Context?,
) : AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> {
var downloadCount = -1
var unreadType = 2
var sourceLanguage: String? = null
var filter = ""
private val sourceManager: SourceManager by injectLazy()
internal val sourceManager: SourceManager by injectLazy()
private val uiPreferences: UiPreferences by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
private val uniformSize: Boolean
internal val uniformSize: Boolean
get() = uiPreferences.uniformGrid().get()
private val libraryLayout: Int
internal val libraryLayout: Int
get() = preferences.libraryLayout().get()
val hideReadingButton: Boolean
get() = preferences.hideStartReadingButton().get()
override fun getLayoutRes(): Int {
return if (libraryLayout == LAYOUT_LIST || manga.isBlank()) {
R.layout.manga_list_item
} else {
R.layout.manga_grid_item
}
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
val parent = adapter.recyclerView
return if (parent is AutofitRecyclerView) {
val libraryLayout = libraryLayout
val isFixedSize = uniformSize
if (libraryLayout == LAYOUT_LIST || manga.isBlank()) {
LibraryListHolder(view, adapter as LibraryCategoryAdapter)
} else {
view.apply {
val isStaggered = parent.layoutManager is StaggeredGridLayoutManager
val binding = MangaGridItemBinding.bind(this)
binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID
if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) {
binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID
binding.card.setCardForegroundColor(
ContextCompat.getColorStateList(
context,
R.color.library_comfortable_grid_foreground,
),
)
}
if (isFixedSize) {
binding.constraintLayout.layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
binding.coverThumbnail.maxHeight = Int.MAX_VALUE
binding.coverThumbnail.minimumHeight = 0
binding.constraintLayout.minHeight = 0
binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
binding.coverThumbnail.adjustViewBounds = false
binding.coverThumbnail.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
dimensionRatio = "2:3"
}
}
if (libraryLayout != LAYOUT_COMFORTABLE_GRID) {
binding.card.updateLayoutParams<ConstraintLayout.LayoutParams> {
bottomMargin = (if (isStaggered) 2 else 6).dpToPx
}
}
binding.setBGAndFG(libraryLayout)
}
val gridHolder = LibraryGridHolder(
view,
adapter as LibraryCategoryAdapter,
libraryLayout == LAYOUT_COMPACT_GRID,
isFixedSize,
)
if (!isFixedSize) {
gridHolder.setFreeformCoverRatio(manga, parent)
}
gridHolder
}
} else {
LibraryListHolder(view, adapter as LibraryCategoryAdapter)
}
}
@CallSuper
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LibraryHolder,
position: Int,
payloads: MutableList<Any?>?,
) {
if (holder is LibraryGridHolder && !holder.fixedSize) {
holder.setFreeformCoverRatio(manga, adapter.recyclerView as? AutofitRecyclerView)
}
holder.onSetValues(this)
(holder as? LibraryGridHolder)?.setSelected(adapter.isSelected(position))
val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams
layoutParams?.isFullSpan = manga.isBlank()
if (libraryLayout == LAYOUT_COVER_ONLY_GRID) {
holder.itemView.compatToolTipText = manga.title
}
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return !manga.isBlank() && header.category.isDragAndDrop
}
override fun isEnabled(): Boolean {
return !manga.isBlank()
}
override fun isSelectable(): Boolean {
return !manga.isBlank()
}
/**
* Filters a manga depending on a query.
*
* @param constraint the query to apply.
* @return true if the manga should be included, false otherwise.
*/
override fun filter(constraint: String): Boolean {
filter = constraint
if (manga.isBlank() && manga.title.isBlank()) {
return constraint.isEmpty()
}
val sourceName by lazy { sourceManager.getOrStub(manga.source).name }
return manga.title.contains(constraint, true) ||
(manga.author?.contains(constraint, true) ?: false) ||
(manga.artist?.contains(constraint, true) ?: false) ||
sourceName.contains(constraint, true) ||
if (constraint.contains(",")) {
val genres = manga.genre?.split(", ")
constraint.split(",").all { containsGenre(it.trim(), genres) }
} else {
containsGenre(constraint, manga.genre?.split(", "))
}
}
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
if (tag.trim().isEmpty()) return true
context ?: return false
val seriesType by lazy { manga.seriesType(context, sourceManager) }
return if (tag.startsWith("-")) {
val realTag = tag.substringAfter("-")
genres?.find {
it.trim().equals(realTag, ignoreCase = true) || seriesType.equals(realTag, true)
} == null
} else {
genres?.find {
it.trim().equals(tag, ignoreCase = true) || seriesType.equals(tag, true)
} != null
}
}
override fun equals(other: Any?): Boolean {
if (other is LibraryItem) {
return manga.id == other.manga.id && manga.category == other.manga.category
}
return false
}
override fun hashCode(): Int {
return 31 * manga.id!!.hashCode() + header!!.hashCode()
(holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams)?.isFullSpan = this is LibraryPlaceholderItem
}
companion object {

View file

@ -39,22 +39,25 @@ class LibraryListHolder(
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
binding.title.isVisible = true
binding.constraintLayout.minHeight = 56.dpToPx
if (item.manga.isBlank()) {
if (item is LibraryPlaceholderItem) {
binding.constraintLayout.minHeight = 0
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height = ViewGroup.MarginLayoutParams.WRAP_CONTENT
}
if (item.manga.status == -1) {
binding.title.text = null
binding.title.isVisible = false
} else {
binding.title.text = itemView.context.getString(
if (adapter.hasActiveFilters && item.manga.realMangaCount >= 1) {
MR.strings.no_matches_for_filters_short
} else {
MR.strings.category_is_empty
},
)
when (item.type) {
is LibraryPlaceholderItem.Type.Blank -> {
binding.title.text = itemView.context.getString(
if (adapter.hasActiveFilters && item.type.mangaCount >= 1) {
MR.strings.no_matches_for_filters_short
} else {
MR.strings.category_is_empty
},
)
}
is LibraryPlaceholderItem.Type.Hidden -> {
binding.title.text = null
binding.title.isVisible = false
}
}
binding.title.textAlignment = View.TEXT_ALIGNMENT_CENTER
binding.card.isVisible = false
@ -63,6 +66,9 @@ class LibraryListHolder(
binding.subtitle.isVisible = false
return
}
if (item !is LibraryMangaItem) error("${item::class.qualifiedName} is not a valid item")
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height = 52.dpToPx
}
@ -71,16 +77,16 @@ class LibraryListHolder(
binding.title.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
// Update the binding.title of the manga.
binding.title.text = item.manga.title.highlightText(item.filter, color)
binding.title.text = item.manga.manga.title.highlightText(item.filter, color)
setUnreadBadge(binding.unreadDownloadBadge.badgeView, item)
val authorArtist =
if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
item.manga.author?.trim() ?: ""
if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) {
item.manga.manga.author?.trim() ?: ""
} else {
listOfNotNull(
item.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.artist?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() },
).joinToString(", ")
}
@ -95,7 +101,7 @@ class LibraryListHolder(
// Update the cover.
binding.coverThumbnail.dispose()
binding.coverThumbnail.loadManga(item.manga)
binding.coverThumbnail.loadManga(item.manga.manga)
}
override fun onActionStateChanged(position: Int, actionState: Int) {

View file

@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.seriesType
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
class LibraryMangaItem(
val manga: LibraryManga,
header: LibraryHeaderItem,
context: Context?,
) : LibraryItem(header, context) {
var downloadCount = -1
var unreadType = 2
var sourceLanguage: String? = null
override fun getLayoutRes(): Int {
return if (libraryLayout == LAYOUT_LIST) {
R.layout.manga_list_item
} else {
R.layout.manga_grid_item
}
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
val listHolder by lazy { LibraryListHolder(view, adapter as LibraryCategoryAdapter) }
val parent = adapter.recyclerView
if (parent !is AutofitRecyclerView) return listHolder
val libraryLayout = libraryLayout
val isFixedSize = uniformSize
if (libraryLayout == LAYOUT_LIST) { return listHolder }
view.apply {
val isStaggered = parent.layoutManager is StaggeredGridLayoutManager
val binding = MangaGridItemBinding.bind(this)
binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID
if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) {
binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID
binding.card.setCardForegroundColor(
ContextCompat.getColorStateList(
context,
R.color.library_comfortable_grid_foreground,
),
)
}
if (isFixedSize) {
binding.constraintLayout.layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
binding.coverThumbnail.maxHeight = Int.MAX_VALUE
binding.coverThumbnail.minimumHeight = 0
binding.constraintLayout.minHeight = 0
binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
binding.coverThumbnail.adjustViewBounds = false
binding.coverThumbnail.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
dimensionRatio = "2:3"
}
}
if (libraryLayout != LAYOUT_COMFORTABLE_GRID) {
binding.card.updateLayoutParams<ConstraintLayout.LayoutParams> {
bottomMargin = (if (isStaggered) 2 else 6).dpToPx
}
}
binding.setBGAndFG(libraryLayout)
}
val gridHolder = LibraryGridHolder(
view,
adapter as LibraryCategoryAdapter,
libraryLayout == LAYOUT_COMPACT_GRID,
isFixedSize,
)
if (!isFixedSize) {
gridHolder.setFreeformCoverRatio(manga.manga, parent)
}
return gridHolder
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LibraryHolder,
position: Int,
payloads: MutableList<Any?>?,
) {
if (holder is LibraryGridHolder && !holder.fixedSize) {
holder.setFreeformCoverRatio(manga.manga, adapter.recyclerView as? AutofitRecyclerView)
}
super.bindViewHolder(adapter, holder, position, payloads)
if (libraryLayout == LAYOUT_COVER_ONLY_GRID) {
holder.itemView.compatToolTipText = manga.manga.title
}
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return header.category.isDragAndDrop
}
override fun isEnabled(): Boolean {
return true
}
override fun isSelectable(): Boolean {
return true
}
/**
* Filters a manga depending on a query.
*
* @param constraint the query to apply.
* @return true if the manga should be included, false otherwise.
*/
override fun filter(constraint: String): Boolean {
filter = constraint
if (manga.manga.title.isBlank()) {
return constraint.isEmpty()
}
val sourceName by lazy { sourceManager.getOrStub(manga.manga.source).name }
return manga.manga.title.contains(constraint, true) ||
(manga.manga.author?.contains(constraint, true) ?: false) ||
(manga.manga.artist?.contains(constraint, true) ?: false) ||
sourceName.contains(constraint, true) ||
if (constraint.contains(",")) {
val genres = manga.manga.genre?.split(", ")
constraint.split(",").all { containsGenre(it.trim(), genres) }
} else {
containsGenre(constraint, manga.manga.genre?.split(", "))
}
}
private fun containsGenre(tag: String, genres: List<String>?): Boolean {
if (tag.trim().isEmpty()) return true
context ?: return false
val seriesType by lazy { manga.manga.seriesType(context, sourceManager) }
return if (tag.startsWith("-")) {
val realTag = tag.substringAfter("-")
genres?.find {
it.trim().equals(realTag, ignoreCase = true) || seriesType.equals(realTag, true)
} == null
} else {
genres?.find {
it.trim().equals(tag, ignoreCase = true) || seriesType.equals(tag, true)
} != null
}
}
override fun equals(other: Any?): Boolean {
if (other is LibraryMangaItem) {
return manga.manga.id == other.manga.manga.id && manga.category == other.manga.category
}
return false
}
override fun hashCode(): Int {
return 31 * manga.manga.id.hashCode() + header!!.hashCode()
}
}

View file

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.ui.library
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
/**
* Placeholder item to indicate if the category is hidden or empty/filtered out.
*/
class LibraryPlaceholderItem (
val category: Int,
val type: Type,
header: LibraryHeaderItem,
context: Context?,
) : LibraryItem(header, context) {
override fun getLayoutRes(): Int = R.layout.manga_list_item
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): LibraryHolder {
return LibraryListHolder(view, adapter as LibraryCategoryAdapter)
}
override fun filter(constraint: String): Boolean {
filter = constraint
if (type !is Type.Hidden || type.title.isBlank()) return constraint.isEmpty()
return type.title.contains(constraint, true)
}
override fun equals(other: Any?): Boolean {
if (other is LibraryPlaceholderItem) {
return category == other.category
}
return false
}
override fun hashCode(): Int {
return 31 * Long.MIN_VALUE.hashCode() + header!!.hashCode()
}
sealed class Type {
data class Hidden(val title: String, val hiddenItems: List<LibraryMangaItem>) : Type()
data class Blank(val mangaCount: Int) : Type()
}
companion object {
fun hidden(category: Int, header: LibraryHeaderItem, context: Context?, title: String, hiddenItems: List<LibraryMangaItem>) =
LibraryPlaceholderItem(category, Type.Hidden(title, hiddenItems), header, context)
fun blank(category: Int, header: LibraryHeaderItem, context: Context?, mangaCount: Int = 0) =
LibraryPlaceholderItem(category, Type.Blank(mangaCount), header, context)
}
}

View file

@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.library.LibraryGroup
import eu.kanade.tachiyomi.ui.library.LibraryMangaItem
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.launchUI
@ -368,11 +369,12 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri
suspend fun checkForManhwa(sourceManager: SourceManager) {
if (checked) return
withIOContext {
val libraryManga = controller?.presenter?.allLibraryItems ?: return@withIOContext
val libraryManga = controller?.presenter?.currentLibraryItems ?: return@withIOContext
checked = true
var types = mutableSetOf<StringResource>()
libraryManga.forEach {
when (it.manga.seriesType(sourceManager = sourceManager)) {
if (it !is LibraryMangaItem) return@forEach
when (it.manga.manga.seriesType(sourceManager = sourceManager)) {
Manga.TYPE_MANHWA, Manga.TYPE_WEBTOON -> types.add(MR.strings.manhwa)
Manga.TYPE_MANHUA -> types.add(MR.strings.manhua)
Manga.TYPE_COMIC -> types.add(MR.strings.comic)

View file

@ -28,9 +28,9 @@ import eu.kanade.tachiyomi.util.system.roundToTwoDecimal
import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import kotlin.math.roundToInt
import yokai.i18n.MR
import yokai.util.lang.getString
import kotlin.math.roundToInt
import android.R as AR
class StatsController : BaseLegacyController<StatsControllerBinding>() {
@ -61,7 +61,7 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
}
private fun handleGeneralStats() {
val mangaTracks = mangaDistinct.map { it to presenter.getTracks(it) }
val mangaTracks = mangaDistinct.map { it to presenter.getTracks(it.manga) }
scoresList = getScoresList(mangaTracks)
with(binding) {
viewDetailLayout.isVisible = mangaDistinct.isNotEmpty()
@ -76,8 +76,8 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
}
statsTrackedMangaText.text = mangaTracks.count { it.second.isNotEmpty() }.toString()
statsChaptersDownloadedText.text = mangaDistinct.sumOf { presenter.getDownloadCount(it) }.toString()
statsTotalTagsText.text = mangaDistinct.flatMap { it.getTags() }.distinct().count().toString()
statsMangaLocalText.text = mangaDistinct.count { it.isLocal() }.toString()
statsTotalTagsText.text = mangaDistinct.flatMap { it.manga.getTags() }.distinct().count().toString()
statsMangaLocalText.text = mangaDistinct.count { it.manga.isLocal() }.toString()
statsGlobalUpdateMangaText.text = presenter.getGlobalUpdateManga().count().toString()
statsSourcesText.text = presenter.getSources().count().toString()
statsTrackersText.text = presenter.getLoggedTrackers().count().toString()
@ -105,7 +105,7 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
val pieEntries = ArrayList<PieEntry>()
val mangaStatusDistributionList = statusMap.mapNotNull { (status, color) ->
val libraryCount = mangaDistinct.count { it.status == status }
val libraryCount = mangaDistinct.count { it.manga.status == status }
if (status == SManga.UNKNOWN && libraryCount == 0) return@mapNotNull null
pieEntries.add(PieEntry(libraryCount.toFloat(), activity!!.mapStatus(status)))
StatusDistributionItem(activity!!.mapStatus(status), libraryCount, color)

View file

@ -65,19 +65,19 @@ class StatsPresenter(
val includedCategories = prefs.libraryUpdateCategories().get().map(String::toInt)
val excludedCategories = prefs.libraryUpdateCategoriesExclude().get().map(String::toInt)
val restrictions = prefs.libraryUpdateMangaRestriction().get()
return libraryMangas.groupBy { it.id }
return libraryMangas.groupBy { it.manga.id }
.filterNot { it.value.any { manga -> manga.category in excludedCategories } }
.filter { includedCategories.isEmpty() || it.value.any { manga -> manga.category in includedCategories } }
.filterNot {
val manga = it.value.first()
(MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED) ||
(MANGA_NON_COMPLETED in restrictions && manga.manga.status == SManga.COMPLETED) ||
(MANGA_HAS_UNREAD in restrictions && manga.unread != 0) ||
(MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasRead)
}
}
fun getDownloadCount(manga: LibraryManga): Int {
return downloadManager.getDownloadCount(manga)
return downloadManager.getDownloadCount(manga.manga)
}
fun get10PointScore(track: Track): Float? {

View file

@ -153,7 +153,7 @@ class StatsDetailsPresenter(
private suspend fun setupSeriesType() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.seriesType() }
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.seriesType() }
libraryFormat.forEach { (seriesType, mangaList) ->
currentStats?.add(
@ -173,7 +173,7 @@ class StatsDetailsPresenter(
private suspend fun setupStatus() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.status }
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.status }
libraryFormat.forEach { (status, mangaList) ->
currentStats?.add(
@ -263,7 +263,7 @@ class StatsDetailsPresenter(
private suspend fun setupTrackers() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip()
.map { it to getTracks(it).ifEmpty { listOf(null) } }
.map { it to getTracks(it.manga).ifEmpty { listOf(null) } }
.flatMap { it.second.map { track -> it.first to track } }
val loggedServices = trackManager.services.filter { it.isLogged }
@ -292,7 +292,7 @@ class StatsDetailsPresenter(
private suspend fun setupSources() {
currentStats = ArrayList()
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.source }
val libraryFormat = mangasDistinct.filterByChip().groupBy { it.manga.source }
libraryFormat.forEach { (sourceId, mangaList) ->
val source = sourceManager.getOrStub(sourceId)
@ -339,10 +339,10 @@ class StatsDetailsPresenter(
private suspend fun setupTags() {
currentStats = ArrayList()
val mangaFiltered = mangasDistinct.filterByChip()
val tags = mangaFiltered.flatMap { it.getTags() }.distinctBy { it.uppercase() }
val tags = mangaFiltered.flatMap { it.manga.getTags() }.distinctBy { it.uppercase() }
val libraryFormat = tags.map { tag ->
tag to mangaFiltered.filter {
it.getTags().any { mangaTag -> mangaTag.equals(tag, true) }
it.manga.getTags().any { mangaTag -> mangaTag.equals(tag, true) }
}
}
@ -433,7 +433,7 @@ class StatsDetailsPresenter(
this
} else {
filter { manga ->
context.mapSeriesType(manga.seriesType()) in selectedSeriesType
context.mapSeriesType(manga.manga.seriesType()) in selectedSeriesType
}
}
}
@ -443,7 +443,7 @@ class StatsDetailsPresenter(
this
} else {
filter { manga ->
context.mapStatus(manga.status) in selectedStatus
context.mapStatus(manga.manga.status) in selectedStatus
}
}
}
@ -463,7 +463,7 @@ class StatsDetailsPresenter(
this
} else {
filter { manga ->
manga.source in selectedSource.map { it.id }
manga.manga.source in selectedSource.map { it.id }
}
}
}
@ -504,10 +504,10 @@ class StatsDetailsPresenter(
* Get language name of a manga
*/
private fun LibraryManga.getLanguage(): String {
val code = if (isLocal()) {
LocalSource.getMangaLang(this)
val code = if (manga.isLocal()) {
LocalSource.getMangaLang(this.manga)
} else {
sourceManager.get(source)?.lang
sourceManager.get(manga.source)?.lang
} ?: return context.getString(MR.strings.unknown)
return LocaleHelper.getLocalizedDisplayName(code)
}
@ -516,7 +516,7 @@ class StatsDetailsPresenter(
* Get mean score rounded to two decimal of a list of manga
*/
private suspend fun List<LibraryManga>.getMeanScoreRounded(): Double? {
val mangaTracks = this.map { it to getTracks(it) }
val mangaTracks = this.map { it to getTracks(it.manga) }
val scoresList = mangaTracks.filter { it.second.isNotEmpty() }
.mapNotNull { it.second.getMeanScoreByTracker() }
return if (scoresList.isEmpty()) null else scoresList.average().roundToTwoDecimal()
@ -526,7 +526,7 @@ class StatsDetailsPresenter(
* Get mean score rounded to int of a single manga
*/
private suspend fun LibraryManga.getMeanScoreToInt(): Int? {
val mangaTracks = getTracks(this)
val mangaTracks = getTracks(this.manga)
val scoresList = mangaTracks.filter { it.score > 0 }
.mapNotNull { it.get10PointScore() }
return if (scoresList.isEmpty()) null else scoresList.average().roundToInt().coerceIn(1..10)
@ -550,8 +550,8 @@ class StatsDetailsPresenter(
}
private suspend fun LibraryManga.getStartYear(): Int? {
if (getChapter.awaitAll(id!!, false).any { it.read }) {
val chapters = getHistory.awaitAllByMangaId(id!!).filter { it.last_read > 0 }
if (getChapter.awaitAll(manga.id!!, false).any { it.read }) {
val chapters = getHistory.awaitAllByMangaId(manga.id!!).filter { it.last_read > 0 }
val date = chapters.minOfOrNull { it.last_read } ?: return null
val cal = Calendar.getInstance().apply { timeInMillis = date }
return if (date <= 0L) null else cal.get(Calendar.YEAR)
@ -564,7 +564,7 @@ class StatsDetailsPresenter(
}
private fun getEnabledSources(): List<Source> {
return mangasDistinct.mapNotNull { sourceManager.get(it.source) }
return mangasDistinct.mapNotNull { sourceManager.get(it.manga.source) }
.distinct().sortedBy { it.name }
}
@ -589,7 +589,7 @@ class StatsDetailsPresenter(
}
private suspend fun List<LibraryManga>.getReadDuration(): Long {
return sumOf { manga -> getHistory.awaitAllByMangaId(manga.id!!).sumOf { it.time_read } }
return sumOf { manga -> getHistory.awaitAllByMangaId(manga.manga.id!!).sumOf { it.time_read } }
}
/**

View file

@ -335,7 +335,7 @@ class ReaderViewModel(
val info = delegatedSource.fetchMangaFromChapterUrl(url)
if (info != null) {
val (sChapter, sManga, chapters) = info
val manga = Manga.create(sourceId).apply { copyFrom(sManga) }
val manga = Manga.create(sManga.url, sManga.title, sourceId).apply { copyFrom(sManga) }
val chapter = Chapter.create().apply { copyFrom(sChapter) }
val id = insertManga.await(manga)
manga.id = id ?: manga.id

View file

@ -12,8 +12,7 @@ data class CustomMangaInfo(
val genre: String? = null,
val status: Int? = null,
) {
fun toManga() = MangaImpl().apply {
id = this@CustomMangaInfo.mangaId
fun toManga() = MangaImpl(id = this.mangaId).apply {
title = this@CustomMangaInfo.title ?: ""
author = this@CustomMangaInfo.author
artist = this@CustomMangaInfo.artist

View file

@ -82,9 +82,6 @@ interface Manga : SManga {
}
}
fun isBlank() = id == Long.MIN_VALUE
fun isHidden() = status == -1
fun setChapterOrder(sorting: Int, order: Int) {
setChapterFlags(sorting, CHAPTER_SORTING_MASK)
setChapterFlags(order, CHAPTER_SORT_MASK)