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 { ) = buildList {
if (!BuildConfig.DEBUG) add(CrashlyticsLogWriter()) 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" 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, @ProtoNumber(805) var customGenre: List<String>? = null,
) { ) {
fun getMangaImpl(): MangaImpl { fun getMangaImpl(): MangaImpl {
return MangaImpl().apply { return MangaImpl(
url = this@BackupManga.url source = this.source,
url = this.url,
).apply {
title = this@BackupManga.title title = this@BackupManga.title
artist = this@BackupManga.artist artist = this@BackupManga.artist
author = this@BackupManga.author author = this@BackupManga.author
@ -67,7 +69,6 @@ data class BackupManga(
status = this@BackupManga.status status = this@BackupManga.status
thumbnail_url = this@BackupManga.thumbnailUrl thumbnail_url = this@BackupManga.thumbnailUrl
favorite = this@BackupManga.favorite favorite = this@BackupManga.favorite
source = this@BackupManga.source
date_added = this@BackupManga.dateAdded date_added = this@BackupManga.dateAdded
viewer_flags = ( viewer_flags = (
this@BackupManga.viewer_flags this@BackupManga.viewer_flags

View file

@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.data.database.models
import android.content.Context import android.content.Context
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.ui.library.LibrarySort import eu.kanade.tachiyomi.ui.library.LibrarySort
import java.io.Serializable
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import java.io.Serializable
interface Category : Serializable { interface Category : Serializable {
@ -56,7 +56,21 @@ interface Category : Serializable {
fun mangaOrderToString(): String = fun mangaOrderToString(): String =
if (mangaSort != null) mangaSort.toString() else mangaOrder.joinToString("/") 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 { companion object {
const val sourceSplitter = "◘•◘"
const val langSplitter = "⨼⨦⨠"
var lastCategoriesAddedTo = emptySet<Int>() var lastCategoriesAddedTo = emptySet<Int>()
fun create(name: String): Category = CategoryImpl().apply { fun create(name: String): Category = CategoryImpl().apply {

View file

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

View file

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.data.database.models 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 kotlin.math.roundToInt
import yokai.data.updateStrategyAdapter
data class LibraryManga( data class LibraryManga(
val manga: Manga,
var unread: Int = 0, var unread: Int = 0,
var read: Int = 0, var read: Int = 0,
var category: Int = 0, var category: Int = 0,
@ -13,41 +13,11 @@ data class LibraryManga(
var latestUpdate: Long = 0, var latestUpdate: Long = 0,
var lastRead: Long = 0, var lastRead: Long = 0,
var lastFetch: 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 val hasRead
get() = read > 0 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 { 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( fun mapper(
// manga // manga
id: Long, id: Long,
@ -78,34 +48,37 @@ data class LibraryManga(
latestUpdate: Long, latestUpdate: Long,
lastRead: Long, lastRead: Long,
lastFetch: Long, lastFetch: Long,
): LibraryManga = createBlank(categoryId.toInt()).apply { ): LibraryManga = LibraryManga(
this.id = id manga = Manga.mapper(
this.source = source id = id,
this.url = url source = source,
this.artist = artist url = url,
this.author = author artist = artist,
this.description = description author = author,
this.genre = genre description = description,
this.title = title genre = genre,
this.status = status.toInt() title = title,
this.thumbnail_url = thumbnailUrl status = status,
this.favorite = favorite thumbnailUrl = thumbnailUrl,
this.last_update = lastUpdate ?: 0L favorite = favorite,
this.initialized = initialized lastUpdate = lastUpdate,
this.viewer_flags = viewerFlags.toInt() initialized = initialized,
this.hide_title = hideTitle viewerFlags = viewerFlags,
this.chapter_flags = chapterFlags.toInt() hideTitle = hideTitle,
this.date_added = dateAdded ?: 0L chapterFlags = chapterFlags,
this.filtered_scanlators = filteredScanlators dateAdded = dateAdded,
this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode) filteredScanlators = filteredScanlators,
this.cover_last_modified = coverLastModified updateStrategy = updateStrategy,
this.read = readCount.roundToInt() coverLastModified = coverLastModified,
this.unread = maxOf((total - readCount).roundToInt(), 0) ),
this.totalChapters = total.toInt() read = readCount.roundToInt(),
this.bookmarkCount = bookmarkCount.roundToInt() unread = maxOf((total - readCount).roundToInt(), 0),
this.latestUpdate = latestUpdate totalChapters = total.toInt(),
this.lastRead = lastRead bookmarkCount = bookmarkCount.roundToInt(),
this.lastFetch = lastFetch 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) } id?.let { MangaCoverMetadata.setVibrantColor(it, value) }
} }
fun Manga.Companion.create(source: Long) = MangaImpl().apply { fun Manga.Companion.create(url: String, title: String, source: Long = 0) =
this.source = source MangaImpl(
} source = source,
url = url,
fun Manga.Companion.create(pathUrl: String, title: String, source: Long = 0) = MangaImpl().apply { ).apply {
url = pathUrl this.title = title
this.title = title }
this.source = source
}
fun Manga.Companion.mapper( fun Manga.Companion.mapper(
id: Long, id: Long,
@ -213,14 +211,12 @@ fun Manga.Companion.mapper(
filteredScanlators: String?, filteredScanlators: String?,
updateStrategy: Long, updateStrategy: Long,
coverLastModified: Long, coverLastModified: Long,
) = create(source).apply { ) = create(url, title, source).apply {
this.id = id this.id = id
this.url = url
this.artist = artist this.artist = artist
this.author = author this.author = author
this.description = description this.description = description
this.genre = genre this.genre = genre
this.title = title
this.status = status.toInt() this.status = status.toInt()
this.thumbnail_url = thumbnailUrl this.thumbnail_url = thumbnailUrl
this.favorite = favorite 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()) { data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List<ChapterHistory> = emptyList()) {
companion object { companion object {
fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl()) fun createBlank() = MangaChapterHistory(
MangaImpl(null, -1, ""),
ChapterImpl(),
HistoryImpl(),
)
fun mapper( fun mapper(
// manga // manga

View file

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

View file

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

View file

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

View file

@ -185,14 +185,14 @@ class LibraryUpdateNotifier(private val context: Context) {
val manga = it.key val manga = it.key
val chapters = it.value val chapters = it.value
val chapterNames = chapters.map { chapter -> val chapterNames = chapters.map { chapter ->
chapter.preferredChapterName(context, manga, preferences) chapter.preferredChapterName(context, manga.manga, preferences)
} }
notifications.add( notifications.add(
Pair( Pair(
context.notification(Notifications.CHANNEL_NEW_CHAPTERS) { context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_yokai) setSmallIcon(R.drawable.ic_yokai)
try { try {
val request = ImageRequest.Builder(context).data(manga.cover()) val request = ImageRequest.Builder(context).data(manga.manga.cover())
.networkCachePolicy(CachePolicy.DISABLED) .networkCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.ENABLED) .diskCachePolicy(CachePolicy.ENABLED)
.transformations(CircleCropTransformation()) .transformations(CircleCropTransformation())
@ -205,7 +205,7 @@ class LibraryUpdateNotifier(private val context: Context) {
} catch (_: Exception) { } catch (_: Exception) {
} }
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
setContentTitle(manga.title) setContentTitle(manga.manga.title)
color = ContextCompat.getColor(context, R.color.secondaryTachiyomi) color = ContextCompat.getColor(context, R.color.secondaryTachiyomi)
val chaptersNames = if (chapterNames.size > MAX_CHAPTERS) { val chaptersNames = if (chapterNames.size > MAX_CHAPTERS) {
"${chapterNames.take(MAX_CHAPTERS - 1).joinToString(", ")}, " + "${chapterNames.take(MAX_CHAPTERS - 1).joinToString(", ")}, " +
@ -224,7 +224,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent( setContentIntent(
NotificationReceiver.openChapterPendingActivity( NotificationReceiver.openChapterPendingActivity(
context, context,
manga, manga.manga,
chapters.first(), chapters.first(),
), ),
) )
@ -233,7 +233,7 @@ class LibraryUpdateNotifier(private val context: Context) {
context.getString(MR.strings.mark_as_read), context.getString(MR.strings.mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast( NotificationReceiver.markAsReadPendingBroadcast(
context, context,
manga, manga.manga,
chapters, chapters,
Notifications.ID_NEW_CHAPTERS, Notifications.ID_NEW_CHAPTERS,
), ),
@ -243,13 +243,13 @@ class LibraryUpdateNotifier(private val context: Context) {
context.getString(MR.strings.view_chapters), context.getString(MR.strings.view_chapters),
NotificationReceiver.openChapterPendingActivity( NotificationReceiver.openChapterPendingActivity(
context, context,
manga, manga.manga,
Notifications.ID_NEW_CHAPTERS, Notifications.ID_NEW_CHAPTERS,
), ),
) )
setAutoCancel(true) setAutoCancel(true)
}, },
manga.id.hashCode(), manga.manga.id.hashCode(),
), ),
) )
} }
@ -281,13 +281,13 @@ class LibraryUpdateNotifier(private val context: Context) {
NotificationCompat.BigTextStyle() NotificationCompat.BigTextStyle()
.bigText( .bigText(
updates.keys.joinToString("\n") { updates.keys.joinToString("\n") {
it.title.chop(45) it.manga.title.chop(45)
}, },
), ),
) )
} }
} else if (!preferences.hideNotificationContent().get()) { } else if (!preferences.hideNotificationContent().get()) {
setContentText(updates.keys.first().title.chop(45)) setContentText(updates.keys.first().manga.title.chop(45))
} }
priority = NotificationCompat.PRIORITY_HIGH priority = NotificationCompat.PRIORITY_HIGH
setGroup(Notifications.GROUP_NEW_CHAPTERS) 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.isLTR
import eu.kanade.tachiyomi.util.system.timeSpanFromNow import eu.kanade.tachiyomi.util.system.timeSpanFromNow
import eu.kanade.tachiyomi.util.system.withDefContext import eu.kanade.tachiyomi.util.system.withDefContext
import java.util.*
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.injectLazy 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.category.interactor.GetCategories
import yokai.domain.chapter.interactor.GetChapter import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.history.interactor.GetHistory 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. * Adapter storing a list of manga in a certain category.
@ -117,8 +117,8 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
*/ */
fun indexOf(manga: Manga): Int { fun indexOf(manga: Manga): Int {
return currentItems.indexOfFirst { return currentItems.indexOfFirst {
if (it is LibraryItem) { if (it is LibraryMangaItem) {
it.manga.id == manga.id it.manga.manga.id == manga.id
} else { } else {
false false
} }
@ -142,7 +142,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
*/ */
fun allIndexOf(manga: Manga): List<Int> { fun allIndexOf(manga: Manga): List<Int> {
return currentItems.mapIndexedNotNull { index, it -> 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 index
} else { } else {
null null
@ -164,7 +164,7 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
} else { } else {
val filteredManga = withDefContext { mangas.filter { it.filter(s) } } val filteredManga = withDefContext { mangas.filter { it.filter(s) } }
if (filteredManga.isEmpty() && controller?.presenter?.showAllCategories == false) { 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) } val blankItem = catId?.let { controller.presenter.blankItem(it) }
updateDataSet(blankItem ?: emptyList()) updateDataSet(blankItem ?: emptyList())
} else { } else {
@ -202,18 +202,19 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
vibrateOnCategoryChange(item.category.name) vibrateOnCategoryChange(item.category.name)
item.category.name item.category.name
} }
is LibraryItem -> { is LibraryPlaceholderItem -> {
val text = if (item.manga.isBlank()) { item.header?.category?.name.orEmpty()
return item.header?.category?.name.orEmpty() }
} else { is LibraryMangaItem -> {
val text =
when (getSort(position)) { when (getSort(position)) {
LibrarySort.DragAndDrop -> { 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 // 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) category ?: context.getString(MR.strings.default_value)
} else { } else {
val title = item.manga.title val title = item.manga.manga.title
if (preferences.removeArticles().get()) { if (preferences.removeArticles().get()) {
title.removeArticles().chop(15) title.removeArticles().chop(15)
} else { } else {
@ -222,14 +223,14 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
} }
} }
LibrarySort.DateFetched -> { LibrarySort.DateFetched -> {
val id = item.manga.id ?: return "" val id = item.manga.manga.id ?: return ""
// FIXME: Don't do blocking // FIXME: Don't do blocking
val history = runBlocking { getChapter.awaitAll(id, false) } val history = runBlocking { getChapter.awaitAll(id, false) }
val last = history.maxOfOrNull { it.date_fetch } val last = history.maxOfOrNull { it.date_fetch }
context.timeSpanFromNow(MR.strings.fetched_, last ?: 0) context.timeSpanFromNow(MR.strings.fetched_, last ?: 0)
} }
LibrarySort.LastRead -> { LibrarySort.LastRead -> {
val id = item.manga.id ?: return "" val id = item.manga.manga.id ?: return ""
// FIXME: Don't do blocking // FIXME: Don't do blocking
val history = runBlocking { getHistory.awaitAllByMangaId(id) } val history = runBlocking { getHistory.awaitAllByMangaId(id) }
val last = history.maxOfOrNull { it.last_read } val last = history.maxOfOrNull { it.last_read }
@ -256,21 +257,20 @@ class LibraryCategoryAdapter(val controller: LibraryController?) :
} }
} }
LibrarySort.LatestChapter -> { LibrarySort.LatestChapter -> {
context.timeSpanFromNow(MR.strings.updated_, item.manga.last_update) context.timeSpanFromNow(MR.strings.updated_, item.manga.manga.last_update)
} }
LibrarySort.DateAdded -> { LibrarySort.DateAdded -> {
context.timeSpanFromNow(MR.strings.added_, item.manga.date_added) context.timeSpanFromNow(MR.strings.added_, item.manga.manga.date_added)
} }
LibrarySort.Title -> { LibrarySort.Title -> {
val title = if (preferences.removeArticles().get()) { val title = if (preferences.removeArticles().get()) {
item.manga.title.removeArticles() item.manga.manga.title.removeArticles()
} else { } else {
item.manga.title item.manga.manga.title
} }
getFirstLetter(title) getFirstLetter(title)
} }
} }
}
if (!isSingleCategory) { if (!isSingleCategory) {
vibrateOnCategoryChange(item.header?.category?.name.orEmpty()) vibrateOnCategoryChange(item.header?.category?.name.orEmpty())
} }

View file

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

View file

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

View file

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

View file

@ -1,205 +1,48 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.content.Context import android.content.Context
import android.view.View import androidx.annotation.CallSuper
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.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFilterable import eu.davidea.flexibleadapter.items.IFilterable
import eu.davidea.flexibleadapter.items.IFlexible 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.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.source.SourceManager 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 uy.kohesive.injekt.injectLazy
import yokai.domain.ui.UiPreferences import yokai.domain.ui.UiPreferences
class LibraryItem( abstract class LibraryItem(
val manga: LibraryManga,
header: LibraryHeaderItem, header: LibraryHeaderItem,
private val context: Context?, internal val context: Context?,
) : AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> { ) : AbstractSectionableItem<LibraryHolder, LibraryHeaderItem>(header), IFilterable<String> {
var downloadCount = -1
var unreadType = 2
var sourceLanguage: String? = null
var filter = "" var filter = ""
private val sourceManager: SourceManager by injectLazy() internal val sourceManager: SourceManager by injectLazy()
private val uiPreferences: UiPreferences by injectLazy() private val uiPreferences: UiPreferences by injectLazy()
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val uniformSize: Boolean internal val uniformSize: Boolean
get() = uiPreferences.uniformGrid().get() get() = uiPreferences.uniformGrid().get()
private val libraryLayout: Int internal val libraryLayout: Int
get() = preferences.libraryLayout().get() get() = preferences.libraryLayout().get()
val hideReadingButton: Boolean val hideReadingButton: Boolean
get() = preferences.hideStartReadingButton().get() get() = preferences.hideStartReadingButton().get()
override fun getLayoutRes(): Int { @CallSuper
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)
}
}
override fun bindViewHolder( override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: LibraryHolder, holder: LibraryHolder,
position: Int, position: Int,
payloads: MutableList<Any?>?, payloads: MutableList<Any?>?,
) { ) {
if (holder is LibraryGridHolder && !holder.fixedSize) {
holder.setFreeformCoverRatio(manga, adapter.recyclerView as? AutofitRecyclerView)
}
holder.onSetValues(this) holder.onSetValues(this)
(holder as? LibraryGridHolder)?.setSelected(adapter.isSelected(position)) (holder as? LibraryGridHolder)?.setSelected(adapter.isSelected(position))
val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams (holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams)?.isFullSpan = this is LibraryPlaceholderItem
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()
} }
companion object { companion object {

View file

@ -39,22 +39,25 @@ class LibraryListHolder(
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root) setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
binding.title.isVisible = true binding.title.isVisible = true
binding.constraintLayout.minHeight = 56.dpToPx binding.constraintLayout.minHeight = 56.dpToPx
if (item.manga.isBlank()) { if (item is LibraryPlaceholderItem) {
binding.constraintLayout.minHeight = 0 binding.constraintLayout.minHeight = 0
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height = ViewGroup.MarginLayoutParams.WRAP_CONTENT height = ViewGroup.MarginLayoutParams.WRAP_CONTENT
} }
if (item.manga.status == -1) { when (item.type) {
binding.title.text = null is LibraryPlaceholderItem.Type.Blank -> {
binding.title.isVisible = false binding.title.text = itemView.context.getString(
} else { if (adapter.hasActiveFilters && item.type.mangaCount >= 1) {
binding.title.text = itemView.context.getString( MR.strings.no_matches_for_filters_short
if (adapter.hasActiveFilters && item.manga.realMangaCount >= 1) { } else {
MR.strings.no_matches_for_filters_short MR.strings.category_is_empty
} 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.title.textAlignment = View.TEXT_ALIGNMENT_CENTER
binding.card.isVisible = false binding.card.isVisible = false
@ -63,6 +66,9 @@ class LibraryListHolder(
binding.subtitle.isVisible = false binding.subtitle.isVisible = false
return return
} }
if (item !is LibraryMangaItem) error("${item::class.qualifiedName} is not a valid item")
binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.constraintLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height = 52.dpToPx height = 52.dpToPx
} }
@ -71,16 +77,16 @@ class LibraryListHolder(
binding.title.textAlignment = View.TEXT_ALIGNMENT_TEXT_START binding.title.textAlignment = View.TEXT_ALIGNMENT_TEXT_START
// Update the binding.title of the manga. // 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) setUnreadBadge(binding.unreadDownloadBadge.badgeView, item)
val authorArtist = val authorArtist =
if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) { if (item.manga.manga.author == item.manga.manga.artist || item.manga.manga.artist.isNullOrBlank()) {
item.manga.author?.trim() ?: "" item.manga.manga.author?.trim() ?: ""
} else { } else {
listOfNotNull( listOfNotNull(
item.manga.author?.trim()?.takeIf { it.isNotBlank() }, item.manga.manga.author?.trim()?.takeIf { it.isNotBlank() },
item.manga.artist?.trim()?.takeIf { it.isNotBlank() }, item.manga.manga.artist?.trim()?.takeIf { it.isNotBlank() },
).joinToString(", ") ).joinToString(", ")
} }
@ -95,7 +101,7 @@ class LibraryListHolder(
// Update the cover. // Update the cover.
binding.coverThumbnail.dispose() binding.coverThumbnail.dispose()
binding.coverThumbnail.loadManga(item.manga) binding.coverThumbnail.loadManga(item.manga.manga)
} }
override fun onActionStateChanged(position: Int, actionState: Int) { 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.source.SourceManager
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.library.LibraryGroup 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.ui.main.MainActivity
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
@ -368,11 +369,12 @@ class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: Attri
suspend fun checkForManhwa(sourceManager: SourceManager) { suspend fun checkForManhwa(sourceManager: SourceManager) {
if (checked) return if (checked) return
withIOContext { withIOContext {
val libraryManga = controller?.presenter?.allLibraryItems ?: return@withIOContext val libraryManga = controller?.presenter?.currentLibraryItems ?: return@withIOContext
checked = true checked = true
var types = mutableSetOf<StringResource>() var types = mutableSetOf<StringResource>()
libraryManga.forEach { 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_MANHWA, Manga.TYPE_WEBTOON -> types.add(MR.strings.manhwa)
Manga.TYPE_MANHUA -> types.add(MR.strings.manhua) Manga.TYPE_MANHUA -> types.add(MR.strings.manhua)
Manga.TYPE_COMIC -> types.add(MR.strings.comic) 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.compatToolTipText
import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.util.view.withFadeTransaction
import kotlin.math.roundToInt
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import kotlin.math.roundToInt
import android.R as AR import android.R as AR
class StatsController : BaseLegacyController<StatsControllerBinding>() { class StatsController : BaseLegacyController<StatsControllerBinding>() {
@ -61,7 +61,7 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
} }
private fun handleGeneralStats() { 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) scoresList = getScoresList(mangaTracks)
with(binding) { with(binding) {
viewDetailLayout.isVisible = mangaDistinct.isNotEmpty() viewDetailLayout.isVisible = mangaDistinct.isNotEmpty()
@ -76,8 +76,8 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
} }
statsTrackedMangaText.text = mangaTracks.count { it.second.isNotEmpty() }.toString() statsTrackedMangaText.text = mangaTracks.count { it.second.isNotEmpty() }.toString()
statsChaptersDownloadedText.text = mangaDistinct.sumOf { presenter.getDownloadCount(it) }.toString() statsChaptersDownloadedText.text = mangaDistinct.sumOf { presenter.getDownloadCount(it) }.toString()
statsTotalTagsText.text = mangaDistinct.flatMap { it.getTags() }.distinct().count().toString() statsTotalTagsText.text = mangaDistinct.flatMap { it.manga.getTags() }.distinct().count().toString()
statsMangaLocalText.text = mangaDistinct.count { it.isLocal() }.toString() statsMangaLocalText.text = mangaDistinct.count { it.manga.isLocal() }.toString()
statsGlobalUpdateMangaText.text = presenter.getGlobalUpdateManga().count().toString() statsGlobalUpdateMangaText.text = presenter.getGlobalUpdateManga().count().toString()
statsSourcesText.text = presenter.getSources().count().toString() statsSourcesText.text = presenter.getSources().count().toString()
statsTrackersText.text = presenter.getLoggedTrackers().count().toString() statsTrackersText.text = presenter.getLoggedTrackers().count().toString()
@ -105,7 +105,7 @@ class StatsController : BaseLegacyController<StatsControllerBinding>() {
val pieEntries = ArrayList<PieEntry>() val pieEntries = ArrayList<PieEntry>()
val mangaStatusDistributionList = statusMap.mapNotNull { (status, color) -> 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 if (status == SManga.UNKNOWN && libraryCount == 0) return@mapNotNull null
pieEntries.add(PieEntry(libraryCount.toFloat(), activity!!.mapStatus(status))) pieEntries.add(PieEntry(libraryCount.toFloat(), activity!!.mapStatus(status)))
StatusDistributionItem(activity!!.mapStatus(status), libraryCount, color) StatusDistributionItem(activity!!.mapStatus(status), libraryCount, color)

View file

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

View file

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

View file

@ -12,8 +12,7 @@ data class CustomMangaInfo(
val genre: String? = null, val genre: String? = null,
val status: Int? = null, val status: Int? = null,
) { ) {
fun toManga() = MangaImpl().apply { fun toManga() = MangaImpl(id = this.mangaId).apply {
id = this@CustomMangaInfo.mangaId
title = this@CustomMangaInfo.title ?: "" title = this@CustomMangaInfo.title ?: ""
author = this@CustomMangaInfo.author author = this@CustomMangaInfo.author
artist = this@CustomMangaInfo.artist 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) { fun setChapterOrder(sorting: Int, order: Int) {
setChapterFlags(sorting, CHAPTER_SORTING_MASK) setChapterFlags(sorting, CHAPTER_SORTING_MASK)
setChapterFlags(order, CHAPTER_SORT_MASK) setChapterFlags(order, CHAPTER_SORT_MASK)