diff --git a/app/src/main/java/dev/yokai/core/di/DomainModule.kt b/app/src/main/java/dev/yokai/core/di/DomainModule.kt index 1e33b14d91..0ae891ce9f 100644 --- a/app/src/main/java/dev/yokai/core/di/DomainModule.kt +++ b/app/src/main/java/dev/yokai/core/di/DomainModule.kt @@ -1,9 +1,13 @@ package dev.yokai.core.di +import dev.yokai.data.chapter.ChapterRepositoryImpl import dev.yokai.domain.extension.repo.ExtensionRepoRepository import dev.yokai.data.extension.repo.ExtensionRepoRepositoryImpl import dev.yokai.data.library.custom.CustomMangaRepositoryImpl import dev.yokai.data.manga.MangaRepositoryImpl +import dev.yokai.domain.chapter.ChapterRepository +import dev.yokai.domain.chapter.interactor.GetAvailableScanlators +import dev.yokai.domain.chapter.interactor.GetChapters import dev.yokai.domain.extension.interactor.TrustExtension import dev.yokai.domain.extension.repo.interactor.CreateExtensionRepo import dev.yokai.domain.extension.repo.interactor.DeleteExtensionRepo @@ -44,5 +48,9 @@ class DomainModule : InjektModule { addSingletonFactory { MangaRepositoryImpl(get()) } addFactory { GetLibraryManga(get()) } + + addSingletonFactory { ChapterRepositoryImpl(get()) } + addFactory { GetAvailableScanlators(get()) } + addFactory { GetChapters(get()) } } } diff --git a/app/src/main/java/dev/yokai/data/chapter/ChapterRepositoryImpl.kt b/app/src/main/java/dev/yokai/data/chapter/ChapterRepositoryImpl.kt new file mode 100644 index 0000000000..95b2375261 --- /dev/null +++ b/app/src/main/java/dev/yokai/data/chapter/ChapterRepositoryImpl.kt @@ -0,0 +1,21 @@ +package dev.yokai.data.chapter + +import dev.yokai.data.DatabaseHandler +import dev.yokai.domain.chapter.ChapterRepository +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.util.system.toInt +import kotlinx.coroutines.flow.Flow + +class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepository { + override suspend fun getChapters(mangaId: Long, filterScanlators: Boolean): List = + handler.awaitList { chaptersQueries.getChaptersByMangaId(mangaId, filterScanlators.toInt().toLong(), Chapter::mapper) } + + override fun getChaptersAsFlow(mangaId: Long, filterScanlators: Boolean): Flow> = + handler.subscribeToList { chaptersQueries.getChaptersByMangaId(mangaId, filterScanlators.toInt().toLong(), Chapter::mapper) } + + override suspend fun getScanlatorsByChapter(mangaId: Long): List = + handler.awaitList { chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() } } + + override fun getScanlatorsByChapterAsFlow(mangaId: Long): Flow> = + handler.subscribeToList { chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() } } +} diff --git a/app/src/main/java/dev/yokai/domain/chapter/ChapterRepository.kt b/app/src/main/java/dev/yokai/domain/chapter/ChapterRepository.kt new file mode 100644 index 0000000000..34c0d23e75 --- /dev/null +++ b/app/src/main/java/dev/yokai/domain/chapter/ChapterRepository.kt @@ -0,0 +1,12 @@ +package dev.yokai.domain.chapter + +import eu.kanade.tachiyomi.data.database.models.Chapter +import kotlinx.coroutines.flow.Flow + +interface ChapterRepository { + suspend fun getChapters(mangaId: Long, filterScanlators: Boolean): List + fun getChaptersAsFlow(mangaId: Long, filterScanlators: Boolean): Flow> + + suspend fun getScanlatorsByChapter(mangaId: Long): List + fun getScanlatorsByChapterAsFlow(mangaId: Long): Flow> +} diff --git a/app/src/main/java/dev/yokai/domain/chapter/interactor/GetAvailableScanlators.kt b/app/src/main/java/dev/yokai/domain/chapter/interactor/GetAvailableScanlators.kt new file mode 100644 index 0000000000..e5dfceb7d5 --- /dev/null +++ b/app/src/main/java/dev/yokai/domain/chapter/interactor/GetAvailableScanlators.kt @@ -0,0 +1,10 @@ +package dev.yokai.domain.chapter.interactor + +import dev.yokai.domain.chapter.ChapterRepository + +class GetAvailableScanlators( + private val chapterRepository: ChapterRepository, +) { + suspend fun await(mangaId: Long) = chapterRepository.getScanlatorsByChapter(mangaId) + fun subscribe(mangaId: Long) = chapterRepository.getScanlatorsByChapterAsFlow(mangaId) +} diff --git a/app/src/main/java/dev/yokai/domain/chapter/interactor/GetChapters.kt b/app/src/main/java/dev/yokai/domain/chapter/interactor/GetChapters.kt new file mode 100644 index 0000000000..bc7e130830 --- /dev/null +++ b/app/src/main/java/dev/yokai/domain/chapter/interactor/GetChapters.kt @@ -0,0 +1,11 @@ +package dev.yokai.domain.chapter.interactor + +import dev.yokai.domain.chapter.ChapterRepository + +class GetChapters( + private val chapterRepository: ChapterRepository, +) { + suspend fun await(mangaId: Long, filterScanlators: Boolean) = chapterRepository.getChapters(mangaId, filterScanlators) + fun subscribe(mangaId: Long, filterScanlators: Boolean) = chapterRepository.getChaptersAsFlow(mangaId, filterScanlators) + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt index bf11df0a28..450bc71203 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt @@ -37,6 +37,36 @@ interface Chapter : SChapter, Serializable { } } } + + fun mapper( + id: Long, + mangaId: Long, + url: String, + name: String, + scanlator: String?, + read: Boolean, + bookmark: Boolean, + lastPageRead: Long, + pagesLeft: Long, + chapterNumber: Double, + sourceOrder: Long, + dateFetch: Long, + dateUpload: Long, + ): Chapter = create().apply { + this.id = id + this.manga_id = mangaId + this.url = url + this.name = name + this.scanlator = scanlator + this.read = read + this.bookmark = bookmark + this.last_page_read = lastPageRead.toInt() + this.pages_left = pagesLeft.toInt() + this.chapter_number = chapterNumber.toFloat() + this.source_order = sourceOrder.toInt() + this.date_fetch = dateFetch + this.date_upload = dateUpload + } } fun copyFrom(other: Chapter) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt index 342c710b44..998baf9082 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/LibraryManga.kt @@ -13,8 +13,7 @@ class LibraryManga : MangaImpl() { var bookmarkCount: Int = 0 - val totalChapters - get() = read + unread + var totalChapters: Int = 0 val hasRead get() = read > 0 @@ -79,6 +78,7 @@ class LibraryManga : MangaImpl() { this.update_strategy = updateStrategy.toInt().let(updateStrategyAdapter::decode) this.read = readCount.roundToInt() this.unread = maxOf((total - readCount).roundToInt(), 0) + this.totalChapters = readCount.roundToInt() this.bookmarkCount = bookmarkCount.roundToInt() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 520b47cf1f..99dbe833ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.library +import dev.yokai.domain.chapter.interactor.GetChapters import dev.yokai.domain.manga.interactor.GetLibraryManga import dev.yokai.util.isLewd import eu.kanade.tachiyomi.R @@ -75,6 +76,8 @@ class LibraryPresenter( ) : BaseCoroutinePresenter(), DownloadQueue.DownloadListener { private val getLibraryManga: GetLibraryManga by injectLazy() + private val getChapters: GetChapters by injectLazy() + private val context = preferences.context private val viewContext get() = view?.view?.context @@ -1295,7 +1298,7 @@ class LibraryPresenter( presenterScope.launch { withContext(Dispatchers.IO) { mangaList.forEach { list -> - val chapters = db.getChapters(list).executeAsBlocking().filter { !it.read } + val chapters = runBlocking { getChapters.await(list.id!!, true) }.filter { !it.read } downloadManager.downloadChapters(list, chapters) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index ec79622821..ea753d787e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -1364,7 +1364,7 @@ class MangaDetailsController : rangeMode = RangeMode.Download return } - R.id.download_unread -> presenter.allChapters.filter { !it.read } + R.id.download_unread -> presenter.chapters.filter { !it.read } R.id.download_all -> presenter.allChapters else -> emptyList() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index f02aeec829..e5eec22bc9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -10,6 +10,8 @@ import coil3.request.CachePolicy import coil3.request.ImageRequest import coil3.request.SuccessResult import com.hippo.unifile.UniFile +import dev.yokai.domain.chapter.interactor.GetAvailableScanlators +import dev.yokai.domain.chapter.interactor.GetChapters import dev.yokai.domain.library.custom.model.CustomMangaInfo import dev.yokai.domain.storage.StorageManager import eu.kanade.tachiyomi.BuildConfig @@ -88,6 +90,8 @@ class MangaDetailsPresenter( chapterFilter: ChapterFilter = Injekt.get(), internal val storageManager: StorageManager = Injekt.get(), ) : BaseCoroutinePresenter(), DownloadQueue.DownloadListener { + private val getAvailableScanlators: GetAvailableScanlators by injectLazy() + private val getChapters: GetChapters by injectLazy() private val customMangaManager: CustomMangaManager by injectLazy() private val mangaShortcutManager: MangaShortcutManager by injectLazy() @@ -118,6 +122,7 @@ class MangaDetailsPresenter( val headerItem by lazy { MangaHeaderItem(manga, view?.fromCatalogue == true) } var tabletChapterHeaderItem: MangaHeaderItem? = null var allChapterScanlators: Set = emptySet() + fun onFirstLoad() { val controller = view ?: return headerItem.isTablet = controller.isTablet @@ -170,13 +175,18 @@ class MangaDetailsPresenter( } private suspend fun getChapters() { - val chapters = db.getChapters(manga).executeOnIO().map { it.toModel() } + val chapters = getChapters.await(manga.id!!, isScanlatorFiltered()).map { it.toModel() } // Find downloaded chapters setDownloadedChapters(chapters) - allChapterScanlators = chapters.flatMap { ChapterUtil.getScanlators(it.chapter.scanlator) }.toSet() + allChapterScanlators = + if (!isScanlatorFiltered()) { + chapters.flatMap { ChapterUtil.getScanlators(it.chapter.scanlator) } + } else { + getAvailableScanlators.await(manga.id!!) + }.toSet() // Store the last emission - allChapters = chapters + allChapters = if (!isScanlatorFiltered()) chapters else getChapters.await(manga.id!!, false).map { it.toModel() } this.chapters = applyChapterFilters(chapters) } @@ -283,7 +293,7 @@ class MangaDetailsPresenter( fun hasDownloads(): Boolean = allChapters.any { it.isDownloaded } fun getUnreadChaptersSorted() = - allChapters.filter { !it.read && it.status == Download.State.NOT_DOWNLOADED }.distinctBy { it.name } + chapters.filter { !it.read && it.status == Download.State.NOT_DOWNLOADED }.distinctBy { it.name } .sortedWith(chapterSort.sortComparator(true)) fun startDownloadingNow(chapter: Chapter) { @@ -664,6 +674,8 @@ class MangaDetailsPresenter( } } + private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true + fun currentFilters(): String { val filtersId = mutableListOf() filtersId.add(if (manga.readFilter(preferences) == Manga.CHAPTER_SHOW_READ) R.string.read else null) @@ -672,7 +684,7 @@ class MangaDetailsPresenter( filtersId.add(if (manga.downloadedFilter(preferences) == Manga.CHAPTER_SHOW_NOT_DOWNLOADED) R.string.not_downloaded else null) filtersId.add(if (manga.bookmarkedFilter(preferences) == Manga.CHAPTER_SHOW_BOOKMARKED) R.string.bookmarked else null) filtersId.add(if (manga.bookmarkedFilter(preferences) == Manga.CHAPTER_SHOW_NOT_BOOKMARKED) R.string.not_bookmarked else null) - filtersId.add(if (manga.filtered_scanlators?.isNotEmpty() == true) R.string.scanlators else null) + filtersId.add(if (isScanlatorFiltered()) R.string.scanlators else null) return filtersId.filterNotNull() .joinToString(", ") { view?.view?.context?.getString(it) ?: "" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/BooleanExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/BooleanExtensions.kt index aae8f3caab..ae01fa9685 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/BooleanExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/BooleanExtensions.kt @@ -1,3 +1,4 @@ package eu.kanade.tachiyomi.util.system fun Boolean.toInt() = if (this) 1 else 0 +fun Int.toBoolean() = this == 1 diff --git a/app/src/main/sqldelight/tachiyomi/data/chapters.sq b/app/src/main/sqldelight/tachiyomi/data/chapters.sq index 9664191dd9..9f003957ab 100644 --- a/app/src/main/sqldelight/tachiyomi/data/chapters.sq +++ b/app/src/main/sqldelight/tachiyomi/data/chapters.sq @@ -1,6 +1,4 @@ import kotlin.Boolean; -import kotlin.Float; -import kotlin.Long; CREATE TABLE chapters( _id INTEGER NOT NULL PRIMARY KEY, @@ -12,13 +10,29 @@ CREATE TABLE chapters( bookmark INTEGER AS Boolean NOT NULL, last_page_read INTEGER NOT NULL, pages_left INTEGER NOT NULL, - chapter_number REAL AS Float NOT NULL, + chapter_number REAL NOT NULL, source_order INTEGER NOT NULL, - date_fetch INTEGER AS Long NOT NULL, - date_upload INTEGER AS Long NOT NULL, + date_fetch INTEGER NOT NULL, + date_upload INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES mangas (_id) ON DELETE CASCADE ); CREATE INDEX chapters_manga_id_index ON chapters(manga_id); CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0; + +getChaptersByMangaId: +SELECT C.* +FROM chapters AS C +LEFT JOIN scanlators_view AS S +ON C.manga_id = S.manga_id +AND ifnull(C.scanlator, 'N/A') = ifnull(S.name, '//') -- I assume if it's N/A it shouldn't be filtered +WHERE C.manga_id = :manga_id +AND ( + :apply_filter = 0 OR S.name IS NULL +); + +getScanlatorsByMangaId: +SELECT scanlator +FROM chapters +WHERE manga_id = :mangaId; diff --git a/app/src/main/sqldelight/tachiyomi/migrations/22.sqm b/app/src/main/sqldelight/tachiyomi/migrations/22.sqm new file mode 100644 index 0000000000..17374e2122 --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/migrations/22.sqm @@ -0,0 +1,41 @@ +CREATE VIEW scanlators_view AS +SELECT S.* FROM ( + WITH RECURSIVE split(seq, _id, name, str) AS ( -- Probably should migrate this to its own table someday + SELECT 0, mangas._id, NULL, replace(ifnull(mangas.filtered_scanlators, ''), ' & ', '[.]')||'[.]' FROM mangas WHERE mangas._id + UNION ALL SELECT + seq+1, + _id, + substr(str, 0, instr(str, '[.]')), + substr(str, instr(str, '[.]')+3) + FROM split WHERE str != '' + ) + SELECT _id AS manga_id, name FROM split WHERE split.seq != 0 ORDER BY split.seq ASC +) AS S; + +DROP VIEW IF EXISTS library_view; +CREATE VIEW library_view AS +SELECT + M.*, + coalesce(C.total, 0) AS total, + coalesce(C.read_count, 0) AS has_read, + coalesce(C.bookmark_count, 0) AS bookmark_count, + coalesce(MC.category_id, 0) AS category +FROM mangas AS M +LEFT JOIN ( + SELECT + chapters.manga_id, + count(*) AS total, + sum(read) AS read_count, + sum(bookmark) AS bookmark_count + FROM chapters + LEFT JOIN scanlators_view AS filtered_scanlators + ON chapters.manga_id = filtered_scanlators.manga_id + AND ifnull(chapters.scanlator, 'N/A') = ifnull(filtered_scanlators.name, '//') + WHERE filtered_scanlators.name IS NULL + GROUP BY chapters.manga_id +) AS C +ON M._id = C.manga_id +LEFT JOIN (SELECT * FROM mangas_categories) AS MC +ON MC.manga_id = M._id +WHERE M.favorite = 1 +ORDER BY M.title; diff --git a/app/src/main/sqldelight/tachiyomi/view/library_view.sq b/app/src/main/sqldelight/tachiyomi/view/library_view.sq index 13e96d369a..21ae52a66b 100644 --- a/app/src/main/sqldelight/tachiyomi/view/library_view.sq +++ b/app/src/main/sqldelight/tachiyomi/view/library_view.sq @@ -13,18 +13,8 @@ LEFT JOIN ( sum(read) AS read_count, sum(bookmark) AS bookmark_count FROM chapters - LEFT JOIN ( - WITH RECURSIVE split(seq, _id, name, str) AS ( -- Probably should migrate this to its own table someday - SELECT 0, mangas._id, NULL, replace(ifnull(mangas.filtered_scanlators, ''), ' & ', '[.]')||'[.]' FROM mangas - UNION ALL SELECT - seq+1, - _id, - substr(str, 0, instr(str, '[.]')), - substr(str, instr(str, '[.]')+3) - FROM split WHERE str != '' - ) SELECT _id, name FROM split WHERE split.seq != 0 ORDER BY split.seq ASC - ) AS filtered_scanlators - ON chapters.manga_id = filtered_scanlators._id + LEFT JOIN scanlators_view AS filtered_scanlators + ON chapters.manga_id = filtered_scanlators.manga_id AND ifnull(chapters.scanlator, 'N/A') = ifnull(filtered_scanlators.name, '//') -- I assume if it's N/A it shouldn't be filtered WHERE filtered_scanlators.name IS NULL GROUP BY chapters.manga_id diff --git a/app/src/main/sqldelight/tachiyomi/view/scanlators_view.sq b/app/src/main/sqldelight/tachiyomi/view/scanlators_view.sq new file mode 100644 index 0000000000..bc165d824a --- /dev/null +++ b/app/src/main/sqldelight/tachiyomi/view/scanlators_view.sq @@ -0,0 +1,13 @@ +CREATE VIEW scanlators_view AS +SELECT S.* FROM ( + WITH RECURSIVE split(seq, _id, name, str) AS ( -- Probably should migrate this to its own table someday + SELECT 0, mangas._id, NULL, replace(ifnull(mangas.filtered_scanlators, ''), ' & ', '[.]')||'[.]' FROM mangas WHERE mangas._id + UNION ALL SELECT + seq+1, + _id, + substr(str, 0, instr(str, '[.]')), + substr(str, instr(str, '[.]')+3) + FROM split WHERE str != '' + ) + SELECT _id AS manga_id, name FROM split WHERE split.seq != 0 ORDER BY split.seq ASC +) AS S;