From bd16db8823dd5409dea4a821b661a16dfba2ee04 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 1 Dec 2024 09:58:40 +0700 Subject: [PATCH 001/166] chore: FIXME note [skip ci] --- app/src/main/java/yokai/core/RollingUniFileLogWriter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt b/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt index c2c7dc7898..631e966cfd 100644 --- a/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt +++ b/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive import kotlinx.coroutines.newSingleThreadContext +// FIXME: Only keep 5 logs "globally" /** * Copyright (c) 2024 Touchlab * SPDX-License-Identifier: Apache-2.0 From d7e3a970d84866c7e4459114f069744f955e40e6 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 2 Dec 2024 07:36:56 +0700 Subject: [PATCH 002/166] chore: Bump version to v1.9.0 Preparing for release --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 196599202f..a6326ad233 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ fun runCommand(command: String): String { return String(byteOut.toByteArray()).trim() } -val _versionName = "1.8.5.12" +val _versionName = "1.9.0" val betaCount by lazy { val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") From 29aa80104dec72813497ab4b8206b19f7ecddb89 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 2 Dec 2024 09:05:27 +0700 Subject: [PATCH 003/166] refactor(version): Better version comparator --- .../java/yokai/domain/base/models/Version.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/yokai/domain/base/models/Version.kt b/app/src/main/java/yokai/domain/base/models/Version.kt index 77e5232fe0..f68a40ba35 100644 --- a/app/src/main/java/yokai/domain/base/models/Version.kt +++ b/app/src/main/java/yokai/domain/base/models/Version.kt @@ -21,17 +21,21 @@ data class Version( // On nightly we only care about build number if (type == Type.NIGHTLY) return build.compareTo(other.build) - var rt = (major.compareTo(other.major) + - minor.compareTo(other.minor) + - patch.compareTo(other.patch)).compareTo(0) - // check if it's a hotfix (1.2.3 vs 1.2.3.1) - if (rt == 0) rt = hotfix.compareTo(other.hotfix) - // if semver is equal, check version stage (release (3) > beta (2) > alpha (1)) - if (rt == 0) rt = stage.weight.compareTo(other.stage.weight) - // if everything are equal, we compare build number. This only matters on unstable (beta and nightly) releases - if (rt == 0) rt = build.compareTo(other.build) + val currentVer = listOf(major, minor, patch, hotfix, stage.weight, build) + val otherVer = listOf(other.major, other.minor, other.patch, other.hotfix, other.stage.weight, other.build) - return rt + // In case my brain fried and left out a value + if (currentVer.size != otherVer.size) throw RuntimeException("Version lists' size must be the same") + + for (i in 1..currentVer.size) { + when (currentVer[i - 1].compareTo(otherVer[i - 1])) { + 0 -> if (i == currentVer.size) return 0 else continue + 1 -> return 1 + else -> return -1 + } + } + + return 0 } override fun toString(): String { From 737681173cd43536ff4cb400cc858d594e54988b Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 2 Dec 2024 09:25:01 +0700 Subject: [PATCH 004/166] test: Test if major and minor version bump is checked properly --- .../eu/kanade/tachiyomi/data/updater/AppUpdateCheckerTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/updater/AppUpdateCheckerTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/updater/AppUpdateCheckerTest.kt index 3d0af1c07d..803a8acaf0 100644 --- a/app/src/test/java/eu/kanade/tachiyomi/data/updater/AppUpdateCheckerTest.kt +++ b/app/src/test/java/eu/kanade/tachiyomi/data/updater/AppUpdateCheckerTest.kt @@ -76,6 +76,8 @@ class AppUpdateCheckerTest { @Test fun `Prod should get latest Prod build (Check for Betas)`() { assertTrue(isNewVersion("1.2.4-b1", "1.2.3")) + assertTrue(isNewVersion("1.3.0-b1", "1.2.3")) + assertTrue(isNewVersion("2.0.0-b1", "1.2.3")) } @Test From 2fd6146d32de58a9088b0340dd7763d6472113d3 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 2 Dec 2024 09:25:28 +0700 Subject: [PATCH 005/166] sync: Sync project --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/ISSUE_TEMPLATE/issue_report.yml | 2 +- CHANGELOG.md | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 26b3bf7cf3..646bdb9e4f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -35,7 +35,7 @@ body: required: true - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. required: true - - label: I have updated the app to version **[1.8.5.12](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.8.5.13](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have checked through the app settings for my feature. required: true diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index 52c8844186..212bdee2b7 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -100,7 +100,7 @@ body: required: true - label: I have tried the [troubleshooting guide](https://mihon.app/help/). required: true - - label: I have updated the app to version **[1.8.5.12](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.8.5.13](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have updated all installed extensions. required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca1d717b0..6f4c752d95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - `Changes` - Behaviour/visual changes - `Fixes` - Bugfixes - `Translation` - Translation changes/updates -- `Other` - Technical stuff +- `Other` - Technical changes/updates ## [Unreleased] @@ -79,6 +79,11 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Fixed Keyboard is covering web page inputs - Increased `tryToSetForeground` delay to fix potential crashes +## [1.8.5.13] + +### Fixed +- Fix version checker + ## [1.8.5.12] ### Fixed From 7f05d160395e9bf2b39b302973f9d39915770f67 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 2 Dec 2024 13:13:37 +0700 Subject: [PATCH 006/166] fix(sqldelight): Custom query function Basically the same as "executeAsOneOrNull" but without the result count check --- .../yokai/data/chapter/ChapterRepositoryImpl.kt | 4 ++-- .../java/yokai/data/manga/MangaRepositoryImpl.kt | 4 ++-- .../kotlin/yokai/data/AndroidDatabaseHandler.kt | 8 ++++++++ .../commonMain/kotlin/yokai/data/DatabaseHandler.kt | 5 +++++ .../kotlin/yokai/data/util/SqlDelightUtil.kt | 13 +++++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 data/src/commonMain/kotlin/yokai/data/util/SqlDelightUtil.kt diff --git a/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt b/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt index 6128154908..feabecc904 100644 --- a/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt +++ b/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt @@ -23,7 +23,7 @@ class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepos handler.awaitList { chaptersQueries.getChaptersByUrl(url, filterScanlators.toInt().toLong(), Chapter::mapper) } override suspend fun getChapterByUrl(url: String, filterScanlators: Boolean): Chapter? = - handler.awaitOneOrNull { chaptersQueries.getChaptersByUrl(url, filterScanlators.toInt().toLong(), Chapter::mapper) } + handler.awaitFirstOrNull { chaptersQueries.getChaptersByUrl(url, filterScanlators.toInt().toLong(), Chapter::mapper) } override suspend fun getChaptersByUrlAndMangaId( url: String, @@ -39,7 +39,7 @@ class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepos mangaId: Long, filterScanlators: Boolean ): Chapter? = - handler.awaitOneOrNull { + handler.awaitFirstOrNull { chaptersQueries.getChaptersByUrlAndMangaId(url, mangaId, filterScanlators.toInt().toLong(), Chapter::mapper) } diff --git a/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt b/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt index 41ea95a04a..c67790ff55 100644 --- a/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt +++ b/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt @@ -16,7 +16,7 @@ class MangaRepositoryImpl(private val handler: DatabaseHandler) : MangaRepositor handler.awaitList { mangasQueries.findAll(Manga::mapper) } override suspend fun getMangaByUrlAndSource(url: String, source: Long): Manga? = - handler.awaitOneOrNull { mangasQueries.findByUrlAndSource(url, source, Manga::mapper) } + handler.awaitFirstOrNull { mangasQueries.findByUrlAndSource(url, source, Manga::mapper) } override suspend fun getMangaById(id: Long): Manga? = handler.awaitOneOrNull { mangasQueries.findById(id, Manga::mapper) } @@ -37,7 +37,7 @@ class MangaRepositoryImpl(private val handler: DatabaseHandler) : MangaRepositor handler.subscribeToList { library_viewQueries.findAll(LibraryManga::mapper) } override suspend fun getDuplicateFavorite(title: String, source: Long): Manga? = - handler.awaitOneOrNull { mangasQueries.findDuplicateFavorite(title.lowercase(), source, Manga::mapper) } + handler.awaitFirstOrNull { mangasQueries.findDuplicateFavorite(title.lowercase(), source, Manga::mapper) } override suspend fun update(update: MangaUpdate): Boolean { return try { diff --git a/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt b/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt index 977a065c66..b0d578fa36 100644 --- a/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt +++ b/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext +import yokai.data.util.executeAsFirstOrNull class AndroidDatabaseHandler( val db: Database, @@ -53,6 +54,13 @@ class AndroidDatabaseHandler( return dispatch(inTransaction) { block(db).executeAsOneOrNull() } } + override suspend fun awaitFirstOrNull( + inTransaction: Boolean, + block: suspend Database.() -> Query + ): T? { + return dispatch(inTransaction) { block(db).executeAsFirstOrNull() } + } + override suspend fun awaitOneOrNullExecutable( inTransaction: Boolean, block: suspend Database.() -> ExecutableQuery, diff --git a/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt b/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt index bc7c145d98..a73171dc29 100644 --- a/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt +++ b/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt @@ -27,6 +27,11 @@ interface DatabaseHandler { block: suspend Database.() -> Query ): T? + suspend fun awaitFirstOrNull( + inTransaction: Boolean = false, + block: suspend Database.() -> Query + ): T? + suspend fun awaitOneOrNullExecutable( inTransaction: Boolean = false, block: suspend Database.() -> ExecutableQuery, diff --git a/data/src/commonMain/kotlin/yokai/data/util/SqlDelightUtil.kt b/data/src/commonMain/kotlin/yokai/data/util/SqlDelightUtil.kt new file mode 100644 index 0000000000..d469d7402e --- /dev/null +++ b/data/src/commonMain/kotlin/yokai/data/util/SqlDelightUtil.kt @@ -0,0 +1,13 @@ +package yokai.data.util + +import app.cash.sqldelight.ExecutableQuery +import app.cash.sqldelight.db.QueryResult + +fun ExecutableQuery.executeAsFirst(): T { + return executeAsFirstOrNull() ?: throw NullPointerException("ResultSet returned null for $this") +} + +fun ExecutableQuery.executeAsFirstOrNull(): T? = execute { cursor -> + if (!cursor.next().value) return@execute QueryResult.Value(null) + QueryResult.Value(mapper(cursor)) +}.value From fbb4112eac466f41f9aa8c547645c813dfde186b Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 06:56:00 +0700 Subject: [PATCH 007/166] fix(ExtensionInstallerJob): Don't crash when request list is empty --- .../eu/kanade/tachiyomi/extension/ExtensionInstallerJob.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallerJob.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallerJob.kt index a1476ed537..4e842a5417 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallerJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallerJob.kt @@ -183,6 +183,9 @@ class ExtensionInstallerJob(val context: Context, workerParams: WorkerParameters .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() } + + if (requests.isEmpty()) return + var workContinuation = WorkManager.getInstance(context) .beginUniqueWork(TAG, ExistingWorkPolicy.REPLACE, requests.first()) for (i in 1 until requests.size) { From b9f7d18d3d86924e1424396e364f67c8d5e98313 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 07:01:07 +0700 Subject: [PATCH 008/166] fix(library): NPE --- .../main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt index b9c721637b..73e10f7fd9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt @@ -80,8 +80,8 @@ abstract class LibraryHolder( override fun onLongClick(view: View?): Boolean { return if (adapter.isLongPressDragEnabled) { - val manga = (adapter.getItem(flexibleAdapterPosition) as LibraryItem).manga - if (!isDraggable && !manga.isBlank() && !manga.isHidden()) { + val manga = (adapter.getItem(flexibleAdapterPosition) as? LibraryItem)?.manga + if (manga != null && !isDraggable && !manga.isBlank() && !manga.isHidden()) { adapter.mItemLongClickListener.onItemLongClick(flexibleAdapterPosition) toggleActivation() true From ea9407b49d6f03b17160e17b42924f3956a1a66e Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 07:33:03 +0700 Subject: [PATCH 009/166] fix(DownloadQueue): Fix ConcurrentModificationException --- .../tachiyomi/data/download/model/DownloadQueue.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt index faef31ccbf..285082fa64 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt @@ -43,7 +43,7 @@ class DownloadQueue( if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) { download.status = Download.State.NOT_DOWNLOADED } - downloadListeners.forEach { it.updateDownload(download) } + callListeners(download) if (removed) { updatedRelay.call(Unit) } @@ -73,7 +73,7 @@ class DownloadQueue( if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) { download.status = Download.State.NOT_DOWNLOADED } - downloadListeners.forEach { it.updateDownload(download) } + callListeners(download) } queue.clear() store.clear() @@ -102,7 +102,10 @@ class DownloadQueue( } private fun callListeners(download: Download) { - downloadListeners.forEach { it.updateDownload(download) } + val iterator = downloadListeners.iterator() + while (iterator.hasNext()) { + iterator.next().updateDownload(download) + } } // private fun setPagesSubject(pages: List?, subject: PublishSubject?) { From d2fdbf8717ebca2e4b398576f6387c0308ae01fd Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 08:40:36 +0700 Subject: [PATCH 010/166] fix(DownloadQueue): Use CopyOnWriteArrayList to further prevent ConcurrentModificationException --- .../eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt index 285082fa64..f11d1e7d22 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt @@ -20,7 +20,7 @@ class DownloadQueue( private val updatedRelay = PublishRelay.create() - private val downloadListeners = mutableListOf() + private val downloadListeners: MutableList = CopyOnWriteArrayList() private var scope = MainScope() From 8c96e8d4b6a05be897b6dce72b57ed58aef0dcd8 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 13:46:01 +0700 Subject: [PATCH 011/166] docs: Sync changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4c752d95..8e1e045851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Update dependency com.android.tools:desugar_jdk_libs to v2.1.3 - Update moko to v0.24.2 - Refactor trackers to use DTOs (@MajorTanya) + - Fix AniList `ALSearchItem.status` nullibility (@Secozzi) - Replace Injekt with Koin - Remove unnecessary permission added by Firebase - Remove unnecessary features added by Firebase @@ -77,7 +78,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Update dependency co.touchlab:kermit to v2.0.5 - Replace WebView to use Compose (@arkon) - Fixed Keyboard is covering web page inputs -- Increased `tryToSetForeground` delay to fix potential crashes +- Increased `tryToSetForeground` delay to fix potential crashes (@nonproto) ## [1.8.5.13] From 723abbe520fa7dbd2cfd3b567ec1c469d14d48de Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 15:00:05 +0700 Subject: [PATCH 012/166] fix(CategoryPresenter): Run setCategories on Main thread Fixes GH-273 --- .../java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt index 3ff00eb7ca..362aec1b83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt @@ -100,7 +100,9 @@ class CategoryPresenter( scope.launch { deleteCategories.awaitOne(safeCategory.toLong()) categories.remove(category) - controller.setCategories(categories.map(::CategoryItem)) + withContext(Dispatchers.Main) { + controller.setCategories(categories.map(::CategoryItem)) + } } } From 30d7b389a5f1f3f3567436a5a8970af945245783 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 16:06:56 +0700 Subject: [PATCH 013/166] fix: Fix Komga unread count (again) --- .../sqldelight/tachiyomi/migrations/27.sqm | 50 +++++++++++++++++++ .../sqldelight/tachiyomi/view/library_view.sq | 2 +- .../tachiyomi/view/scanlators_view.sq | 2 +- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 data/src/commonMain/sqldelight/tachiyomi/migrations/27.sqm diff --git a/data/src/commonMain/sqldelight/tachiyomi/migrations/27.sqm b/data/src/commonMain/sqldelight/tachiyomi/migrations/27.sqm new file mode 100644 index 0000000000..41a751d91b --- /dev/null +++ b/data/src/commonMain/sqldelight/tachiyomi/migrations/27.sqm @@ -0,0 +1,50 @@ +DROP VIEW IF EXISTS scanlators_view; +CREATE VIEW scanlators_view AS +SELECT S.* FROM ( + WITH RECURSIVE split(seq, _id, name, str) AS ( + SELECT 0, mangas._id, NULL, mangas.filtered_scanlators||' [.] ' FROM mangas WHERE mangas._id + UNION ALL SELECT + seq+1, + _id, + substr(str, 0, instr(str, ' [.] ')), + substr(str, instr(str, ' [.] ')+5) + FROM split WHERE str != '' + ) + SELECT _id AS manga_id, name FROM split WHERE name != '' 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, + coalesce(C.latestUpload, 0) AS latestUpload, + coalesce(C.lastRead, 0) AS lastRead, + coalesce(C.lastFetch, 0) AS lastFetch +FROM mangas AS M +LEFT JOIN ( + SELECT + chapters.manga_id, + count(*) AS total, + sum(read) AS read_count, + sum(bookmark) AS bookmark_count, + coalesce(max(chapters.date_upload), 0) AS latestUpload, + coalesce(max(history.history_last_read), 0) AS lastRead, + coalesce(max(chapters.date_fetch), 0) AS lastFetch + FROM chapters + LEFT JOIN scanlators_view AS filtered_scanlators + ON chapters.manga_id = filtered_scanlators.manga_id + AND chapters.scanlator = filtered_scanlators.name + LEFT JOIN history + ON chapters._id = history.history_chapter_id + 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/data/src/commonMain/sqldelight/tachiyomi/view/library_view.sq b/data/src/commonMain/sqldelight/tachiyomi/view/library_view.sq index 4a5bcde4b1..60cb170d9f 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/view/library_view.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/view/library_view.sq @@ -21,7 +21,7 @@ LEFT JOIN ( 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, '//') -- I assume if it's N/A it shouldn't be filtered + AND chapters.scanlator = filtered_scanlators.name LEFT JOIN history ON chapters._id = history.history_chapter_id WHERE filtered_scanlators.name IS NULL diff --git a/data/src/commonMain/sqldelight/tachiyomi/view/scanlators_view.sq b/data/src/commonMain/sqldelight/tachiyomi/view/scanlators_view.sq index 744626d74e..d336ab90da 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/view/scanlators_view.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/view/scanlators_view.sq @@ -10,5 +10,5 @@ SELECT S.* FROM ( substr(str, instr(str, ' [.] ')+5) FROM split WHERE str != '' ) - SELECT _id AS manga_id, name FROM split WHERE split.seq != 0 ORDER BY split.seq ASC + SELECT _id AS manga_id, name FROM split WHERE name != '' ORDER BY split.seq ASC ) AS S; From 31773876b40e1631b15d21b4cf36c87c5af34012 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 16:42:38 +0700 Subject: [PATCH 014/166] docs: Sync changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e1e045851..b870b63448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Fix issues with shizuku in a multi-user setup (@Redjard) - Fix some regional/variant languages is not listed in app language option - Fix browser not opening in some cases in Honor devices (@MajorTanya) +- Fix "ConcurrentModificationException" crashes +- Fix Komga unread badge, again ### Other - Simplify network helper code From 50fecd5350c9e4aec04ff76c6849c331ca0d29ed Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 17:00:41 +0700 Subject: [PATCH 015/166] fix(library): Workaround NPE when querying library by retrying once REF: https://github.com/sqldelight/sqldelight/issues/4194#issuecomment-2417466333 --- .../java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 b024ac77ad..eaf1a42a2b 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 @@ -812,7 +812,8 @@ class LibraryPresenter( private fun getLibraryFlow(): Flow { return combine( getCategories.subscribe(), - getLibraryManga.subscribe(), + // FIXME: Remove retry once a real solution is found + getLibraryManga.subscribe().retry(1), getPreferencesFlow(), preferences.removeArticles().changes(), fetchLibrary From ca982d93d129d1943d9729dc6f789f1b1d989e44 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 17:02:22 +0700 Subject: [PATCH 016/166] fix: Missing import --- .../main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt | 1 + 1 file changed, 1 insertion(+) 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 eaf1a42a2b..241dd10df1 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 @@ -60,6 +60,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.retry import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking From 11ef44732159ef1bbc130b29d5f1afe67fb6ad49 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 3 Dec 2024 17:09:47 +0700 Subject: [PATCH 017/166] fix(library): Only retry if it's NPE --- .../java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 241dd10df1..34e43533c1 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 @@ -814,7 +814,7 @@ class LibraryPresenter( return combine( getCategories.subscribe(), // FIXME: Remove retry once a real solution is found - getLibraryManga.subscribe().retry(1), + getLibraryManga.subscribe().retry(1) { e -> e is NullPointerException }, getPreferencesFlow(), preferences.removeArticles().changes(), fetchLibrary From b4c6820ca4d78cfce6b4fe2aabeab7fd8c19a81d Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 4 Dec 2024 05:17:30 +0700 Subject: [PATCH 018/166] chore(deps): Update dependency io.coil-kt.coil3:coil-bom to v3.0.4 --- CHANGELOG.md | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b870b63448..694aa6e3bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,7 +75,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Update kotlin monorepo to v2.0.21 - Update dependency androidx.work:work-runtime-ktx to v2.10.0 - Update dependency androidx.core:core-ktx to v1.15.0 -- Update dependency io.coil-kt.coil3:coil-bom to v3.0.3 +- Update dependency io.coil-kt.coil3:coil-bom to v3.0.4 - Update xml.serialization to v0.90.3 - Update dependency co.touchlab:kermit to v2.0.5 - Replace WebView to use Compose (@arkon) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fa446ba32a..4820889eea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ aboutlibraries = { module = "com.mikepenz:aboutlibraries-compose-m3", version.re chucker-library-no-op = { module = "com.github.ChuckerTeam.Chucker:library-no-op", version.ref = "chucker" } chucker-library = { module = "com.github.ChuckerTeam.Chucker:library", version.ref = "chucker" } -coil3-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.0.3" } +coil3-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.0.4" } coil3 = { module = "io.coil-kt.coil3:coil" } coil3-svg = { module = "io.coil-kt.coil3:coil-svg" } coil3-gif = { module = "io.coil-kt.coil3:coil-gif" } From 6df9e4f7455ec76ae3fc1b079d91b38f531d90c6 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 4 Dec 2024 05:24:45 +0700 Subject: [PATCH 019/166] fix: Set max bitmap size for covers --- .../presentation/core/util/coil/ImageViewExtensions.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/yokai/presentation/core/util/coil/ImageViewExtensions.kt b/app/src/main/java/yokai/presentation/core/util/coil/ImageViewExtensions.kt index f5cad561f5..7f1f864ae5 100644 --- a/app/src/main/java/yokai/presentation/core/util/coil/ImageViewExtensions.kt +++ b/app/src/main/java/yokai/presentation/core/util/coil/ImageViewExtensions.kt @@ -6,7 +6,9 @@ import coil3.ImageLoader import coil3.imageLoader import coil3.request.Disposable import coil3.request.ImageRequest +import coil3.request.maxBitmapSize import coil3.size.Precision +import coil3.size.Size import coil3.size.SizeResolver import coil3.target.ImageViewTarget import eu.kanade.tachiyomi.data.coil.CoverViewTarget @@ -15,6 +17,8 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga import yokai.domain.manga.models.MangaCover import yokai.domain.manga.models.cover +private const val MAX_BITMAP_SIZE = 2048 + fun ImageView.loadManga( manga: Manga, imageLoader: ImageLoader = context.imageLoader, @@ -25,6 +29,7 @@ fun ImageView.loadManga( .target(LibraryMangaImageTarget(this, manga)) .precision(Precision.INEXACT) .size(SizeResolver.ORIGINAL) + .maxBitmapSize(Size(MAX_BITMAP_SIZE, MAX_BITMAP_SIZE)) .apply(builder) .build() return imageLoader.enqueue(request) @@ -44,6 +49,7 @@ fun ImageView.loadManga( .target(target ?: CoverViewTarget(this, progress)) .precision(Precision.INEXACT) .size(SizeResolver.ORIGINAL) + .maxBitmapSize(Size(MAX_BITMAP_SIZE, MAX_BITMAP_SIZE)) .apply(builder) .build() return imageLoader.enqueue(request) From 19ea7cbebdc2c5dbcbe7e4e2c7bdb64f3acc213e Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 4 Dec 2024 06:01:14 +0700 Subject: [PATCH 020/166] fix(reader): Fix potential NPE --- .../main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 8acc8e1b99..da7f99e2b8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -356,8 +356,8 @@ class ReaderActivity : BaseActivity() { if (viewModel.needsInit()) { fromUrl = handleIntentAction(intent) if (!fromUrl) { - val manga = intent.extras!!.getLong("manga", -1) - val chapter = intent.extras!!.getLong("chapter", -1) + val manga = intent.extras?.getLong("manga", -1L) ?: -1L + val chapter = intent.extras?.getLong("chapter", -1L) ?: -1L if (manga == -1L || chapter == -1L) { finish() return From 106737371f6db1c9bfd0c01b04a88d648a93464b Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 4 Dec 2024 12:05:19 +0700 Subject: [PATCH 021/166] chore: Flow version of firstOrNull --- .../yokai/data/manga/MangaRepositoryImpl.kt | 3 ++ .../yokai/domain/manga/MangaRepository.kt | 1 + .../yokai/domain/manga/interactor/GetManga.kt | 1 + .../yokai/data/AndroidDatabaseHandler.kt | 5 +++ .../kotlin/yokai/data/DatabaseHandler.kt | 2 ++ .../kotlin/yokai/data/util/SqlDelightUtil.kt | 35 +++++++++++++++++++ 6 files changed, 47 insertions(+) diff --git a/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt b/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt index c67790ff55..a1efba5a17 100644 --- a/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt +++ b/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt @@ -30,6 +30,9 @@ class MangaRepositoryImpl(private val handler: DatabaseHandler) : MangaRepositor override fun getMangaListAsFlow(): Flow> = handler.subscribeToList { mangasQueries.findAll(Manga::mapper) } + override fun getMangaByUrlAndSourceAsFlow(url: String, source: Long): Flow = + handler.subscribeToFirstOrNull { mangasQueries.findByUrlAndSource(url, source, Manga::mapper) } + override suspend fun getLibraryManga(): List = handler.awaitList { library_viewQueries.findAll(LibraryManga::mapper) } diff --git a/app/src/main/java/yokai/domain/manga/MangaRepository.kt b/app/src/main/java/yokai/domain/manga/MangaRepository.kt index eda4861a13..c81f8ad478 100644 --- a/app/src/main/java/yokai/domain/manga/MangaRepository.kt +++ b/app/src/main/java/yokai/domain/manga/MangaRepository.kt @@ -9,6 +9,7 @@ import yokai.domain.manga.models.MangaUpdate interface MangaRepository { suspend fun getMangaList(): List suspend fun getMangaByUrlAndSource(url: String, source: Long): Manga? + fun getMangaByUrlAndSourceAsFlow(url: String, source: Long): Flow suspend fun getMangaById(id: Long): Manga? suspend fun getFavorites(): List suspend fun getReadNotFavorites(): List diff --git a/app/src/main/java/yokai/domain/manga/interactor/GetManga.kt b/app/src/main/java/yokai/domain/manga/interactor/GetManga.kt index 588044c9ed..9a89581fda 100644 --- a/app/src/main/java/yokai/domain/manga/interactor/GetManga.kt +++ b/app/src/main/java/yokai/domain/manga/interactor/GetManga.kt @@ -7,6 +7,7 @@ class GetManga ( ) { suspend fun awaitAll() = mangaRepository.getMangaList() fun subscribeAll() = mangaRepository.getMangaListAsFlow() + fun subscribeByUrlAndSource(url: String, source: Long) = mangaRepository.getMangaByUrlAndSourceAsFlow(url, source) suspend fun awaitByUrlAndSource(url: String, source: Long) = mangaRepository.getMangaByUrlAndSource(url, source) suspend fun awaitById(id: Long) = mangaRepository.getMangaById(id) diff --git a/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt b/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt index b0d578fa36..ed8cbe3b1f 100644 --- a/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt +++ b/data/src/androidMain/kotlin/yokai/data/AndroidDatabaseHandler.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext import yokai.data.util.executeAsFirstOrNull +import yokai.data.util.mapToFirstOrNull class AndroidDatabaseHandler( val db: Database, @@ -80,6 +81,10 @@ class AndroidDatabaseHandler( return block(db).asFlow().mapToOneOrNull(queryDispatcher) } + override fun subscribeToFirstOrNull(block: Database.() -> Query): Flow { + return block(db).asFlow().mapToFirstOrNull(queryDispatcher) + } + /* override fun subscribeToPagingSource( countQuery: Database.() -> Query, diff --git a/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt b/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt index a73171dc29..59d261ed31 100644 --- a/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt +++ b/data/src/commonMain/kotlin/yokai/data/DatabaseHandler.kt @@ -43,6 +43,8 @@ interface DatabaseHandler { fun subscribeToOneOrNull(block: Database.() -> Query): Flow + fun subscribeToFirstOrNull(block: Database.() -> Query): Flow + /* fun subscribeToPagingSource( countQuery: Database.() -> Query, diff --git a/data/src/commonMain/kotlin/yokai/data/util/SqlDelightUtil.kt b/data/src/commonMain/kotlin/yokai/data/util/SqlDelightUtil.kt index d469d7402e..b4d2369dc3 100644 --- a/data/src/commonMain/kotlin/yokai/data/util/SqlDelightUtil.kt +++ b/data/src/commonMain/kotlin/yokai/data/util/SqlDelightUtil.kt @@ -1,7 +1,12 @@ package yokai.data.util import app.cash.sqldelight.ExecutableQuery +import app.cash.sqldelight.Query import app.cash.sqldelight.db.QueryResult +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext fun ExecutableQuery.executeAsFirst(): T { return executeAsFirstOrNull() ?: throw NullPointerException("ResultSet returned null for $this") @@ -11,3 +16,33 @@ fun ExecutableQuery.executeAsFirstOrNull(): T? = execute { cursor - if (!cursor.next().value) return@execute QueryResult.Value(null) QueryResult.Value(mapper(cursor)) }.value + +suspend fun ExecutableQuery.awaitAsFirst(): T { + return awaitAsFirstOrNull() + ?: throw NullPointerException("ResultSet returned null for $this") +} + +suspend fun ExecutableQuery.awaitAsFirstOrNull(): T? = execute { cursor -> + // If the cursor isn't async, we want to preserve the blocking semantics and execute it synchronously + when (val next = cursor.next()) { + is QueryResult.AsyncValue -> { + QueryResult.AsyncValue { + if (!next.await()) return@AsyncValue null + mapper(cursor) + } + } + + is QueryResult.Value -> { + if (!next.value) return@execute QueryResult.Value(null) + QueryResult.Value(mapper(cursor)) + } + } +}.await() + +fun Flow>.mapToFirstOrNull( + context: CoroutineContext, +): Flow = map { + withContext(context) { + it.awaitAsFirstOrNull() + } +} From 39d891aa88fd8ba322dd1e239cf06645072c55af Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 4 Dec 2024 12:07:16 +0700 Subject: [PATCH 022/166] docs: FIXME note --- .../kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index fd7ac561a9..47f96e7195 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -45,6 +45,7 @@ import yokai.domain.manga.interactor.UpdateManga import yokai.domain.manga.models.MangaUpdate import yokai.domain.ui.UiPreferences +// FIXME: Refactor to use AndroidX Paging /** * Presenter of [BrowseSourceController]. */ From cb265d2225eecaa899efb25c62c94df1c30c36b8 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 4 Dec 2024 16:52:51 +0700 Subject: [PATCH 023/166] fix(browse): Restart pager from controller Hopefully fix desync issue after opening an entry --- .../tachiyomi/ui/source/browse/BrowseSourceController.kt | 9 ++++----- .../tachiyomi/ui/source/browse/BrowseSourcePresenter.kt | 4 ---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index cf52ded8df..5a36e21dde 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -180,11 +180,10 @@ open class BrowseSourceController(bundle: Bundle) : } return } - if (presenter.items.isNotEmpty()) { - onAddPage(1, presenter.items) - } else { - binding.progress.isVisible = true - } + + binding.progress.isVisible = true + + presenter.restartPager() } override fun onDestroyView(view: View) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index 47f96e7195..96ab9fb6f3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -72,7 +72,6 @@ open class BrowseSourcePresenter( var filtersChanged = false - var items = mutableListOf() val page: Int get() = pager.currentPage @@ -129,7 +128,6 @@ open class BrowseSourcePresenter( } } filtersChanged = false - restartPager() } } @@ -172,7 +170,6 @@ open class BrowseSourcePresenter( val browseAsList = preferences.browseAsList() val sourceListType = preferences.libraryLayout() val outlineCovers = uiPreferences.outlineOnCovers() - items.clear() // Prepare the pager. pagerJob?.cancel() @@ -190,7 +187,6 @@ open class BrowseSourcePresenter( val items = mangas.map { BrowseSourceItem(it, browseAsList, sourceListType, outlineCovers) } - this@BrowseSourcePresenter.items.addAll(items) withUIContext { view?.onAddPage(page, items) } } catch (error: Exception) { Logger.e(error) { "Unable to prepare a page" } From 2e128177350f8687de2659a3edd08a2adc5eb7e8 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 4 Dec 2024 17:30:02 +0700 Subject: [PATCH 024/166] refactor(browse): Move stuff around --- .../ui/source/browse/BrowseSourcePresenter.kt | 39 ++++++++++++------- .../tachiyomi/ui/source/browse/Pager.kt | 2 +- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index 96ab9fb6f3..d880757d08 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -161,8 +162,7 @@ open class BrowseSourcePresenter( // Create a new pager. pager = createPager( query, - filters.takeIf { it.isNotEmpty() || query.isBlank() } - ?: source.getFilterList(), + filters.takeIf { it.isNotEmpty() || query.isBlank() } ?: source.getFilterList(), ) val sourceId = source.id @@ -174,24 +174,33 @@ open class BrowseSourcePresenter( // Prepare the pager. pagerJob?.cancel() pagerJob = presenterScope.launchIO { - pager.results().onEach { (page, second) -> - try { - val mangas = second + pager.asFlow() + .map { (first, second) -> + first to second .map { networkToLocalManga(it, sourceId) } .filter { !preferences.hideInLibraryItems().get() || !it.favorite } - if (mangas.isEmpty() && page == 1) { - withUIContext { view?.onAddPageError(NoResultsException()) } - return@onEach + } + .onEach { initializeMangas(it.second) } + .map { (first, second) -> + first to second.map { + BrowseSourceItem( + it, + browseAsList, + sourceListType, + outlineCovers, + ) } - initializeMangas(mangas) - val items = mangas.map { - BrowseSourceItem(it, browseAsList, sourceListType, outlineCovers) - } - withUIContext { view?.onAddPage(page, items) } - } catch (error: Exception) { + } + .catch { error -> Logger.e(error) { "Unable to prepare a page" } } - }.collect() + .collectLatest { (page, mangas) -> + if (mangas.isEmpty() && page == 1) { + withUIContext { view?.onAddPageError(NoResultsException()) } + return@collectLatest + } + withUIContext { view?.onAddPage(page, mangas) } + } } // Request first page. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/Pager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/Pager.kt index 5f8004cc7d..0eb58f818b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/Pager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/Pager.kt @@ -16,7 +16,7 @@ abstract class Pager(var currentPage: Int = 1) { protected val results = MutableSharedFlow>>() - fun results(): SharedFlow>> { + fun asFlow(): SharedFlow>> { return results.asSharedFlow() } From cd978388b2ccd9e2c9d645accb735c732bc21c90 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 4 Dec 2024 18:00:29 +0700 Subject: [PATCH 025/166] chore: Adjust fixme note [skip ci] Now that I think about it, moving to AndroidX paging is useless, we need to move to Compose to fully fix the desync issue --- .../kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index d880757d08..1685a1b883 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -46,7 +46,7 @@ import yokai.domain.manga.interactor.UpdateManga import yokai.domain.manga.models.MangaUpdate import yokai.domain.ui.UiPreferences -// FIXME: Refactor to use AndroidX Paging +// FIXME: Migrate to Compose /** * Presenter of [BrowseSourceController]. */ From 0406160452bc011ee5c17fb4cf29ddbad1d53402 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 5 Dec 2024 12:03:18 +0700 Subject: [PATCH 026/166] ci: Push to mirror repo with github actions --- .github/workflows/mirror.yml | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/mirror.yml diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml new file mode 100644 index 0000000000..a0432e3224 --- /dev/null +++ b/.github/workflows/mirror.yml @@ -0,0 +1,47 @@ +# REF: https://github.com/JamesRobionyRogers/GitHub-to-GitBucket-Action/blob/47a44e9/.github/workflows/push-to-gitbucket.yml +name: Mirror Repository + +on: + workflow_dispatch: + push: + branches: + - master + +jobs: + mirror: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Fetch all branches + run: | + git fetch --all + git fetch --tags + + - name: List branches + run: | + git branch -a + + # Enables tracking of remote branches ensuring all branches are pushed to mirror repo + - name: Track remote branches + run: | + for branch in $(git branch -r | grep -v '\->'); do + local_branch=${branch#origin/} + git branch --track "$local_branch" "$branch" || true + done + + - name: Push to mirror repo (GitLab) + env: + MIRROR_URL: "gitlab.com/null2264/yokai.git" + MIRROR_USERNAME: ${{ secrets.MIRROR_USERNAME }} + MIRROR_AUTH: ${{ secrets.MIRROR_AUTH }} + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + + git remote add mirror https://$MIRROR_USERNAME:$MIRROR_AUTH@$MIRROR_URL + git push --mirror -v mirror From 3b9eb8a30a3179766109830f701df8a47c2006df Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 5 Dec 2024 12:05:25 +0700 Subject: [PATCH 027/166] ci: Always exit 0 --- .github/workflows/mirror.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index a0432e3224..2b7aa27c0b 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -44,4 +44,4 @@ jobs: git config --global user.email "actions@github.com" git remote add mirror https://$MIRROR_USERNAME:$MIRROR_AUTH@$MIRROR_URL - git push --mirror -v mirror + git push --mirror -v mirror || true From 6bb2f5f94e2cb456c6a3edc62983f2433cfa4be9 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 5 Dec 2024 12:06:44 +0700 Subject: [PATCH 028/166] ci: Adjust concurrency --- .github/workflows/mirror.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 2b7aa27c0b..0913db2ae4 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -7,6 +7,10 @@ on: branches: - master +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: mirror: runs-on: ubuntu-latest From ffac6293f0e1ae34875eed7aa82f94d0b7c3cd5c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:11:25 +0700 Subject: [PATCH 029/166] fix(deps): Update dependency org.conscrypt:conscrypt-android to v2.5.3 (#275) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4820889eea..6bd44d2ef4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,7 @@ coil3-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" } compose-theme-adapter3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version = "0.33.2-alpha" } conductor = { module = "com.bluelinelabs:conductor", version = "4.0.0-preview-4" } conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version = "3.0.0" } -conscrypt = { module = "org.conscrypt:conscrypt-android", version = "2.5.2" } +conscrypt = { module = "org.conscrypt:conscrypt-android", version = "2.5.3" } desugar = { module = "com.android.tools:desugar_jdk_libs", version = "2.1.3" } directionalviewpager = { module = "com.github.tachiyomiorg:DirectionalViewPager", version = "1.0.0" } disklrucache = { module = "com.jakewharton:disklrucache", version = "2.0.2" } From 4c74e4e78b0e3d130a6803dbc1be47cdcc3cc825 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:11:45 +0700 Subject: [PATCH 030/166] chore(deps): Update agp to v8.7.3 (#276) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/androidx.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index e612ce08b9..79c411f324 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,6 +1,6 @@ [versions] activity = "1.9.3" -agp = "8.7.2" +agp = "8.7.3" lifecycle = "2.8.7" [libraries] From 23db3244ced19fa214e8e10e52ef5aec288b0fc9 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 5 Dec 2024 14:12:00 +0700 Subject: [PATCH 031/166] docs: Sync changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 694aa6e3bc..4349f5188d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Update activity to v1.9.3 - Update lifecycle to v2.8.7 - Update dependency me.zhanghai.android.libarchive:library to v1.1.4 -- Update agp to v8.7.2 +- Update agp to v8.7.3 - Update junit5 monorepo to v5.11.3 - Update dependency androidx.test.ext:junit to v1.2.1 - Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.3.8 @@ -81,6 +81,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Replace WebView to use Compose (@arkon) - Fixed Keyboard is covering web page inputs - Increased `tryToSetForeground` delay to fix potential crashes (@nonproto) +- Update dependency org.conscrypt:conscrypt-android to v2.5.3 ## [1.8.5.13] From 332f3f7ee6ffec42c6688d47f70afc8e7de2359c Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 5 Dec 2024 14:49:14 +0700 Subject: [PATCH 032/166] refactor: Separate isHardwareThresholdExceeded from isMaxTextureSizeExceeded --- .../data/coil/TachiyomiImageDecoder.kt | 2 +- .../ui/reader/viewer/ReaderPageImageView.kt | 2 +- .../kanade/tachiyomi/util/system/ImageUtil.kt | 28 ++++++++++++++----- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt index bd0fb6619e..acadf2ce5f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt @@ -50,7 +50,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && options.bitmapConfig == Bitmap.Config.HARDWARE && - !ImageUtil.isMaxTextureSizeExceeded(bitmap) + !ImageUtil.isHardwareThresholdExceeded(bitmap) ) { val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false) if (hwBitmap != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index 4589e07a05..9f1aa9cb56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -234,7 +234,7 @@ open class ReaderPageImageView @JvmOverloads constructor( is BufferedSource -> { // SSIV doesn't tile bitmaps, so if the image exceeded max texture size it won't load regardless. if (!isWebtoon || ImageUtil.isMaxTextureSizeExceeded(data)) { - setHardwareConfig(!ImageUtil.isMaxTextureSizeExceeded(data)) + setHardwareConfig(!ImageUtil.isHardwareThresholdExceeded(data)) setImage(ImageSource.inputStream(data.inputStream())) isVisible = true return@apply diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt index 823af81bef..88a9202234 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt @@ -776,20 +776,34 @@ object ImageUtil { return options } - fun isMaxTextureSizeExceeded(source: BufferedSource): Boolean = - extractImageOptions(source).let { opts -> isMaxTextureSizeExceeded(opts.outWidth, opts.outHeight) } + fun isHardwareThresholdExceeded(source: BufferedSource): Boolean = extractImageOptions(source).let { opts -> + isHardwareThresholdExceeded(opts.outWidth, opts.outHeight) + } - fun isMaxTextureSizeExceeded(drawable: BitmapDrawable): Boolean = - isMaxTextureSizeExceeded(drawable.bitmap) + fun isHardwareThresholdExceeded(drawable: BitmapDrawable): Boolean = + isHardwareThresholdExceeded(drawable.bitmap) - fun isMaxTextureSizeExceeded(bitmap: Bitmap): Boolean = - isMaxTextureSizeExceeded(bitmap.width, bitmap.height) + fun isHardwareThresholdExceeded(bitmap: Bitmap): Boolean = + isHardwareThresholdExceeded(bitmap.width, bitmap.height) var hardwareBitmapThreshold: Int = GLUtil.SAFE_TEXTURE_LIMIT - private fun isMaxTextureSizeExceeded(width: Int, height: Int): Boolean { + private fun isHardwareThresholdExceeded(width: Int, height: Int): Boolean { if (minOf(width, height) <= 0) return false return maxOf(width, height) > hardwareBitmapThreshold } + + fun isMaxTextureSizeExceeded(source: BufferedSource): Boolean = extractImageOptions(source).let { opts -> + isMaxTextureSizeExceeded(opts.outWidth, opts.outHeight) + } + + fun isMaxTextureSizeExceeded(bitmap: Bitmap): Boolean = + isMaxTextureSizeExceeded(bitmap.width, bitmap.height) + + private fun isMaxTextureSizeExceeded(width: Int, height: Int): Boolean { + if (minOf(width, height) <= 0) return false + + return maxOf(width, height) > GLUtil.DEVICE_TEXTURE_LIMIT + } } From 5396b0408e58f4d1f5de49c4be79acd6a6f31948 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 6 Dec 2024 07:51:51 +0700 Subject: [PATCH 033/166] chore: Only keep 5 log files and 5 rolled log files --- .../yokai/core/RollingUniFileLogWriter.kt | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt b/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt index 631e966cfd..540ba531cb 100644 --- a/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt +++ b/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt @@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.withIOContext import java.io.IOException +import java.io.OutputStream import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date @@ -27,7 +28,6 @@ import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive import kotlinx.coroutines.newSingleThreadContext -// FIXME: Only keep 5 logs "globally" /** * Copyright (c) 2024 Touchlab * SPDX-License-Identifier: Apache-2.0 @@ -40,6 +40,7 @@ import kotlinx.coroutines.newSingleThreadContext class RollingUniFileLogWriter( private val logPath: UniFile, private val rollOnSize: Long = 10 * 1024 * 1024, // 10MB + private val maxRolledLogFiles: Int = 5, private val maxLogFiles: Int = 5, private val messageStringFormatter: MessageStringFormatter = DefaultFormatter, private val messageDateFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) @@ -96,11 +97,11 @@ class RollingUniFileLogWriter( } private fun rollLogs() { - if (pathForLogIndex(maxLogFiles - 1)?.exists() == true) { - pathForLogIndex(maxLogFiles - 1)?.delete() + if (pathForLogIndex(maxRolledLogFiles - 1)?.exists() == true) { + pathForLogIndex(maxRolledLogFiles - 1)?.delete() } - (0..<(maxLogFiles - 1)).reversed().forEach { + (0..<(maxRolledLogFiles - 1)).reversed().forEach { val sourcePath = pathForLogIndex(it) val targetFileName = fileNameForLogIndex(it + 1) if (sourcePath?.exists() == true) { @@ -116,7 +117,8 @@ class RollingUniFileLogWriter( private fun fileNameForLogIndex(index: Int): String { val date = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) - return if (index == 0) "${date}-${BuildConfig.BUILD_TYPE}.log" else "${date}-${BuildConfig.BUILD_TYPE} (${index}).log" + val name = "${date}-${BuildConfig.BUILD_TYPE}" + return if (index == 0) "${name}.log" else "$name (${index}).log" } private fun pathForLogIndex(index: Int, create: Boolean = false): UniFile? { @@ -130,7 +132,27 @@ class RollingUniFileLogWriter( maybeRollLogs(fileSize(logFilePath)) } - fun openNewOutput() = pathForLogIndex(0, true)?.openOutputStream(true) + fun openNewOutput(): OutputStream? { + val newLog = pathForLogIndex(0, true) + val dupes = mutableMapOf>() + logPath + .listFiles { file, filename -> + val match = LOG_FILE_REGEX.find(filename) + match?.groupValues?.get(1)?.let { key -> + dupes["${key}.log"] = dupes["${key}.log"].orEmpty() + listOf(file) + } + + match == null + } + .orEmpty() + .sortedByDescending { it.name } + .drop(maxLogFiles - 1) + .forEach { + it.delete() + dupes[it.name]?.forEach { f -> f.delete() } + } + return newLog?.openOutputStream(true) + } var currentLogSink = openNewOutput() @@ -158,4 +180,8 @@ class RollingUniFileLogWriter( } private fun fileSize(path: UniFile?) = path?.length() ?: -1L + + companion object { + private val LOG_FILE_REGEX = """(\d+-\d+-\d+-${BuildConfig.BUILD_TYPE}) \(\d+\)\.log""".toRegex() + } } From 2c36be8b8f419ee1a1ae22d07cc435a1669da465 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 6 Dec 2024 18:51:46 +0700 Subject: [PATCH 034/166] fix: Actually keep 5 log files --- app/src/main/java/yokai/core/RollingUniFileLogWriter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt b/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt index 540ba531cb..4760cd5dbd 100644 --- a/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt +++ b/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt @@ -146,7 +146,7 @@ class RollingUniFileLogWriter( } .orEmpty() .sortedByDescending { it.name } - .drop(maxLogFiles - 1) + .drop(maxLogFiles) .forEach { it.delete() dupes[it.name]?.forEach { f -> f.delete() } From c66bf9b2800aa61334a754680d0145677e968129 Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Date: Sat, 7 Dec 2024 07:03:42 +0700 Subject: [PATCH 035/166] fix: Always use software bitmap on certain devices --- CHANGELOG.md | 1 + .../controllers/SettingsAdvancedController.kt | 4 +- .../kanade/tachiyomi/util/system/ImageUtil.kt | 98 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4349f5188d..3db16f13d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Add category hopper long-press action to open random series from **any** category - Add option to enable reader debug mode - Add option to adjust reader's hardware bitmap threshold (@AntsyLich) + - Always use software bitmap on certain devices (@MajorTanya) - Add option to scan local entries from `/storage/(sdcard|emulated/0)/Android/data//files/local` ### Changes diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt index 7a85b80c0c..6f2616dd61 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt @@ -58,6 +58,7 @@ import eu.kanade.tachiyomi.ui.setting.switchPreference import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.lang.addBetaTag import eu.kanade.tachiyomi.util.system.GLUtil +import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.disableItems import eu.kanade.tachiyomi.util.system.isPackageInstalled import eu.kanade.tachiyomi.util.system.launchIO @@ -416,7 +417,8 @@ class SettingsAdvancedController : SettingsLegacyController() { entries = entryMap.values.toList() entryValues = entryMap.keys.toList() - isVisible = GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT + isVisible = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED && + GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT basePreferences.hardwareBitmapThreshold().changesIn(viewScope) { threshold -> summary = context.getString(MR.strings.pref_hardware_bitmap_threshold_summary, entryMap[threshold].orEmpty()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt index 88a9202234..c199e07133 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt @@ -789,11 +789,109 @@ object ImageUtil { var hardwareBitmapThreshold: Int = GLUtil.SAFE_TEXTURE_LIMIT private fun isHardwareThresholdExceeded(width: Int, height: Int): Boolean { + if (HARDWARE_BITMAP_UNSUPPORTED) return true + if (minOf(width, height) <= 0) return false return maxOf(width, height) > hardwareBitmapThreshold } + /** + * Taken from Coil + * (https://github.com/coil-kt/coil/blob/1674d3516f061aeacbe749a435b1924f9648fd41/coil-core/src/androidMain/kotlin/coil3/util/hardwareBitmaps.kt) + * --- + * Maintains a list of devices with broken/incomplete/unstable hardware bitmap implementations. + * + * Model names are retrieved from + * [Google's official device list](https://support.google.com/googleplay/answer/1727131?hl=en). + * + */ + val HARDWARE_BITMAP_UNSUPPORTED = when (Build.VERSION.SDK_INT) { + 26 -> run { + val model = Build.MODEL ?: return@run false + // Samsung Galaxy (ALL) + if (model.removePrefix("SAMSUNG-").startsWith("SM-")) return@run true + val device = Build.DEVICE ?: return@run false + return@run device in arrayOf( + "nora", "nora_8917", "nora_8917_n", // Moto E5 + "james", "rjames_f", "rjames_go", "pettyl", // Moto E5 Play + "hannah", "ahannah", "rhannah", // Moto E5 Plus + "ali", "ali_n", // Moto G6 + "aljeter", "aljeter_n", "jeter", // Moto G6 Play + "evert", "evert_n", "evert_nt", // Moto G6 Plus + "G3112", "G3116", "G3121", "G3123", "G3125", // Xperia XA1 + "G3412", "G3416", "G3421", "G3423", "G3426", // Xperia XA1 Plus + "G3212", "G3221", "G3223", "G3226", // Xperia XA1 Ultra + "BV6800Pro", // BlackView BV6800Pro + "CatS41", // Cat S41 + "Hi9Pro", // CHUWI Hi9 Pro + "manning", // Lenovo K8 Note + "N5702L", // NUU Mobile G3 + ) + } + 27 -> run { + val device = Build.DEVICE ?: return@run false + return@run device in arrayOf( + "mcv1s", // LG Tribute Empire + "mcv3", // LG K11 + "mcv5a", // LG Q7 + "mcv7a", // LG Stylo 4 + "A30ATMO", // T-Mobile REVVL 2 + "A70AXLTMO", // T-Mobile REVVL 2 PLUS + "A3A_8_4G_TMO", // Alcatel 9027W + "Edison_CKT", // Alcatel ONYX + "EDISON_TF", // Alcatel TCL XL2 + "FERMI_TF", // Alcatel A501DL + "U50A_ATT", // Alcatel TETRA + "U50A_PLUS_ATT", // Alcatel 5059R + "U50A_PLUS_TF", // Alcatel TCL LX + "U50APLUSTMO", // Alcatel 5059Z + "U5A_PLUS_4G", // Alcatel 1X + "RCT6513W87DK5e", // RCA Galileo Pro + "RCT6873W42BMF9A", // RCA Voyager + "RCT6A03W13", // RCA 10 Viking + "RCT6B03W12", // RCA Atlas 10 Pro + "RCT6B03W13", // RCA Atlas 10 Pro+ + "RCT6T06E13", // RCA Artemis 10 + "A3_Pro", // Umidigi A3 Pro + "One", // Umidigi One + "One_Max", // Umidigi One Max + "One_Pro", // Umidigi One Pro + "Z2", // Umidigi Z2 + "Z2_PRO", // Umidigi Z2 Pro + "Armor_3", // Ulefone Armor 3 + "Armor_6", // Ulefone Armor 6 + "Blackview", // Blackview BV6000 + "BV9500", // Blackview BV9500 + "BV9500Pro", // Blackview BV9500Pro + "A6L-C", // Nuu A6L-C + "N5002LA", // Nuu A7L + "N5501LA", // Nuu A5L + "Power_2_Pro", // Leagoo Power 2 Pro + "Power_5", // Leagoo Power 5 + "Z9", // Leagoo Z9 + "V0310WW", // Blu VIVO VI+ + "V0330WW", // Blu VIVO XI + "A3", // BenQ A3 + "ASUS_X018_4", // Asus ZenFone Max Plus M1 (ZB570TL) + "C210AE", // Wiko Life + "fireball", // DROID Incredible 4G LTE + "ILA_X1", // iLA X1 + "Infinix-X605_sprout", // Infinix NOTE 5 Stylus + "j7maxlte", // Samsung Galaxy J7 Max + "KING_KONG_3", // Cubot King Kong 3 + "M10500", // Packard Bell M10500 + "S70", // Altice ALTICE S70 + "S80Lite", // Doogee S80Lite + "SGINO6", // SGiNO 6 + "st18c10bnn", // Barnes and Noble BNTV650 + "TECNO-CA8", // Tecno CAMON X Pro, + "SHIFT6m", // SHIFT 6m + ) + } + else -> false + } + fun isMaxTextureSizeExceeded(source: BufferedSource): Boolean = extractImageOptions(source).let { opts -> isMaxTextureSizeExceeded(opts.outWidth, opts.outHeight) } From b3fbc0bf399f74d6cdb54c1254763c3597419372 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 7 Dec 2024 13:13:08 +0700 Subject: [PATCH 036/166] refactor: Port upstream's download cache system --- .../tachiyomi/data/download/DownloadCache.kt | 393 +++++++++++++----- .../data/download/DownloadManager.kt | 34 +- .../tachiyomi/data/download/Downloader.kt | 16 +- 3 files changed, 321 insertions(+), 122 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index d237008307..3ff36e5a1e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -1,23 +1,53 @@ package eu.kanade.tachiyomi.data.download +import android.app.Application import android.content.Context +import android.net.Uri +import co.touchlab.kermit.Logger import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.domain.manga.models.Manga +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.util.storage.DiskUtil +import eu.kanade.tachiyomi.util.system.extension +import eu.kanade.tachiyomi.util.system.launchIO +import eu.kanade.tachiyomi.util.system.launchNonCancellableIO +import eu.kanade.tachiyomi.util.system.nameWithoutExtension +import java.io.File +import java.util.concurrent.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.protobuf.ProtoBuf import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy -import yokai.domain.manga.interactor.GetManga import yokai.domain.storage.StorageManager -import java.util.concurrent.* /** * Cache where we dump the downloads directory from the filesystem. This class is needed because @@ -37,6 +67,13 @@ class DownloadCache( private val storageManager: StorageManager = Injekt.get(), ) { + val scope = CoroutineScope(Dispatchers.IO) + + private val _changes: Channel = Channel(Channel.UNLIMITED) + val changes = _changes.receiveAsFlow() + .onStart { emit(Unit) } + .shareIn(scope, SharingStarted.Lazily, 1) + /** * The interval after which this cache should be invalidated. 1 hour shouldn't cause major * issues, as the cache is only used for UI feedback. @@ -47,12 +84,38 @@ class DownloadCache( * The last time the cache was refreshed. */ private var lastRenew = 0L + private var renewalJob: Job? = null - private var mangaFiles: MutableMap> = mutableMapOf() + private val _isInitializing = MutableStateFlow(false) + val isInitializing = _isInitializing + .debounce(1000L) // Don't notify if it finishes quickly enough + .stateIn(scope, SharingStarted.WhileSubscribed(), false) - val scope = CoroutineScope(Job() + Dispatchers.IO) + private val diskCacheFile: File + get() = File(context.cacheDir, "dl_index_cache_v3") + + private val rootDownloadsDirLock = Mutex() + private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) init { + // Attempt to read cache file + scope.launch { + rootDownloadsDirLock.withLock { + try { + if (diskCacheFile.exists()) { + val diskCache = diskCacheFile.inputStream().use { + ProtoBuf.decodeFromByteArray(it.readBytes()) + } + rootDownloadsDir = diskCache + lastRenew = System.currentTimeMillis() + } + } catch (e: Throwable) { + Logger.e(e) { "Failed to initialize disk cache" } + diskCacheFile.delete() + } + } + } + storageManager.changes .onEach { forceRenewCache() } // invalidate cache .launchIn(scope) @@ -71,12 +134,18 @@ class DownloadCache( return provider.findChapterDir(chapter, manga, source) != null } - checkRenew() + renewCache() - val files = mangaFiles[manga.id]?.toHashSet() ?: return false - return provider.getValidChapterDirNames(chapter).any { chapName -> - files.any { chapName.equals(it, true) || "$chapName.cbz".equals(it, true) } + val sourceDir = rootDownloadsDir.sourceDirs[manga.source] + if (sourceDir != null) { + val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga)] + if (mangaDir != null) { + return provider.getValidChapterDirNames( + chapter, + ).any { it in mangaDir.chapterDirs } + } } + return false } /** @@ -85,84 +154,137 @@ class DownloadCache( * @param manga the manga to check. */ fun getDownloadCount(manga: Manga, forceCheckFolder: Boolean = false): Int { - checkRenew() + renewCache() + val sourceDir = rootDownloadsDir.sourceDirs[manga.source] if (forceCheckFolder) { val source = sourceManager.get(manga.source) ?: return 0 val mangaDir = provider.findMangaDir(manga, source) if (mangaDir != null) { - val listFiles = - mangaDir.listFiles { _, filename -> !filename.endsWith(Downloader.TMP_DIR_SUFFIX) } + val listFiles = mangaDir.listFiles { _, filename -> !filename.endsWith(Downloader.TMP_DIR_SUFFIX) } if (!listFiles.isNullOrEmpty()) { return listFiles.size } } return 0 } else { - val files = mangaFiles[manga.id] ?: return 0 - return files.filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) }.size - } - } - - /** - * Checks if the cache needs a renewal and performs it if needed. - */ - @Synchronized - private fun checkRenew() { - if (lastRenew + renewInterval < System.currentTimeMillis()) { - renew() - lastRenew = System.currentTimeMillis() + if (sourceDir != null) { + val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga)] + if (mangaDir != null) { + return mangaDir.chapterDirs.size + } + } + return 0 } } fun forceRenewCache() { - renew() - lastRenew = System.currentTimeMillis() + lastRenew = 0L + renewalJob?.cancel() + diskCacheFile.delete() + renewCache() } /** * Renews the downloads cache. */ - private fun renew() { - val onlineSources = sourceManager.getOnlineSources() + private fun renewCache() { + if (lastRenew + renewInterval >= System.currentTimeMillis() || renewalJob?.isActive == true) { + return + } - val sourceDirs = storageManager.getDownloadsDirectory()?.listFiles().orEmpty() - .associate { it.name to SourceDirectory(it) }.mapNotNullKeys { entry -> - onlineSources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id + renewalJob = scope.launchIO { + if (lastRenew == 0L) { + _isInitializing.emit(true) } - val getManga: GetManga by injectLazy() - val mangas = runBlocking(Dispatchers.IO) { getManga.awaitAll().groupBy { it.source } } + val sources = getSources() - sourceDirs.forEach { sourceValue -> - val sourceMangaRaw = mangas[sourceValue.key]?.toMutableSet() ?: return@forEach - val sourceMangaPair = sourceMangaRaw.partition { it.favorite } + val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } - val sourceDir = sourceValue.value + rootDownloadsDirLock.withLock { + rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory()) - val mangaDirs = sourceDir.dir.listFiles().orEmpty().mapNotNull { mangaDir -> - val name = mangaDir.name ?: return@mapNotNull null - val chapterDirs = mangaDir.listFiles().orEmpty().mapNotNull { chapterFile -> chapterFile.name?.substringBeforeLast(".cbz") }.toHashSet() - name to MangaDirectory(mangaDir, chapterDirs) - }.toMap() + val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty() + .filter { it.isDirectory && !it.name.isNullOrBlank() } + .mapNotNull { dir -> + val sourceId = sourceMap[dir.name!!.lowercase()] + sourceId?.let { it to SourceDirectory(dir) } + } + .toMap() - val trueMangaDirs = mangaDirs.mapNotNull { mangaDir -> - val manga = findManga(sourceMangaPair.first, mangaDir.key, sourceValue.key) ?: findManga(sourceMangaPair.second, mangaDir.key, sourceValue.key) - val id = manga?.id ?: return@mapNotNull null - id to mangaDir.value.files - }.toMap() + rootDownloadsDir.sourceDirs = sourceDirs - mangaFiles.putAll(trueMangaDirs) + sourceDirs.values + .map { sourceDir -> + async { + sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty() + .filter { it.isDirectory && !it.name.isNullOrBlank() } + .associate { it.name!! to MangaDirectory(it) } + + sourceDir.mangaDirs.values.forEach { mangaDir -> + val chapterDirs = mangaDir.dir?.listFiles().orEmpty() + .mapNotNull { + when { + // Ignore incomplete downloads + it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true -> null + // Folder of images + it.isDirectory -> it.name + // CBZ files + it.isFile && it.extension == "cbz" -> it.nameWithoutExtension + // Anything else is irrelevant + else -> null + } + } + .toMutableSet() + + mangaDir.chapterDirs = chapterDirs + } + } + } + .awaitAll() + + _isInitializing.emit(false) + } + }.also { + it.invokeOnCompletion(onCancelling = true) { exception -> + if (exception != null && exception !is CancellationException) { + Logger.e(exception) { "DownloadCache: failed to create cache" } + } + lastRenew = System.currentTimeMillis() + notifyChanges() + } } + + // Mainly to notify the indexing notifier UI + notifyChanges() } - /** - * Searches a manga list and matches the given mangakey and source key - */ - private fun findManga(mangaList: List, mangaKey: String, sourceKey: Long): Manga? { - return mangaList.find { - DiskUtil.buildValidFilename(it.originalTitle).equals(mangaKey, ignoreCase = true) && it.source == sourceKey + private fun getSources(): List { + return sourceManager.getOnlineSources() + } + + private fun notifyChanges() { + scope.launchNonCancellableIO { + _changes.send(Unit) + } + updateDiskCache() + } + + private var updateDiskCacheJob: Job? = null + private fun updateDiskCache() { + updateDiskCacheJob?.cancel() + updateDiskCacheJob = scope.launchIO { + delay(1000) + ensureActive() + val bytes = ProtoBuf.encodeToByteArray(rootDownloadsDir) + ensureActive() + try { + diskCacheFile.writeBytes(bytes) + } catch (e: Throwable) { + Logger.e(e) { "Failed to write disk cache file" } + } } } @@ -173,15 +295,30 @@ class DownloadCache( * @param mangaUniFile the directory of the manga. * @param manga the manga of the chapter. */ - @Synchronized - fun addChapter(chapterDirName: String, manga: Manga) { - val id = manga.id ?: return - val files = mangaFiles[id] - if (files == null) { - mangaFiles[id] = mutableSetOf(chapterDirName) - } else { - mangaFiles[id]?.add(chapterDirName) + suspend fun addChapter(chapterDirName: String, mangaUniFile: UniFile?, manga: Manga) { + rootDownloadsDirLock.withLock { + // Retrieve the cached source directory or cache a new one + var sourceDir = rootDownloadsDir.sourceDirs[manga.source] + if (sourceDir == null) { + val source = sourceManager.get(manga.source) ?: return + val sourceUniFile = provider.findSourceDir(source) ?: return + sourceDir = SourceDirectory(sourceUniFile) + rootDownloadsDir.sourceDirs += manga.source to sourceDir + } + + // Retrieve the cached manga directory or cache a new one + val mangaDirName = provider.getMangaDirName(manga) + var mangaDir = sourceDir.mangaDirs[mangaDirName] + if (mangaDir == null) { + mangaDir = MangaDirectory(mangaUniFile) + sourceDir.mangaDirs += mangaDirName to mangaDir + } + + // Save the chapter directory + mangaDir.chapterDirs += chapterDirName } + + notifyChanges() } /** @@ -190,26 +327,35 @@ class DownloadCache( * @param chapters the list of chapter to remove. * @param manga the manga of the chapter. */ - @Synchronized - fun removeChapters(chapters: List, manga: Manga) { - val id = manga.id ?: return - for (chapter in chapters) { - val list = provider.getValidChapterDirNames(chapter) - list.forEach { fileName -> - mangaFiles[id]?.firstOrNull { fileName.equals(it, true) }?.let { chapterFile -> - mangaFiles[id]?.remove(chapterFile) + suspend fun removeChapters(chapters: List, manga: Manga) { + rootDownloadsDirLock.withLock { + val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return + val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga)] ?: return + chapters.forEach { chapter -> + provider.getValidChapterDirNames(chapter).forEach { + if (it in mangaDir.chapterDirs) { + mangaDir.chapterDirs -= it + } } } } + + notifyChanges() } - fun removeFolders(folders: List, manga: Manga) { - val id = manga.id ?: return - for (chapter in folders) { - if (mangaFiles[id] != null && chapter in mangaFiles[id]!!) { - mangaFiles[id]?.remove(chapter) + suspend fun removeChapterFolders(folders: List, manga: Manga) { + rootDownloadsDirLock.withLock { + val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return + val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga)] ?: return + + folders.forEach { chapter -> + if (chapter in mangaDir.chapterDirs) { + mangaDir.chapterDirs -= chapter + } } } + + notifyChanges() } /*fun renameFolder(from: String, to: String, source: Long) { @@ -230,34 +376,25 @@ class DownloadCache( * * @param manga the manga to remove. */ - @Synchronized - fun removeManga(manga: Manga) { - mangaFiles.remove(manga.id) + suspend fun removeManga(manga: Manga) { + rootDownloadsDirLock.withLock { + val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return + val mangaDirName = provider.getMangaDirName(manga) + if (sourceDir.mangaDirs.containsKey(mangaDirName)) { + sourceDir.mangaDirs -= mangaDirName + } + } + + notifyChanges() } - /** - * Class to store the files under the root downloads directory. - */ - private class RootDirectory( - val dir: UniFile, - var files: Map = hashMapOf(), - ) + suspend fun removeSource(source: Source) { + rootDownloadsDirLock.withLock { + rootDownloadsDir.sourceDirs -= source.id + } - /** - * Class to store the files under a source directory. - */ - private class SourceDirectory( - val dir: UniFile, - var files: Map> = hashMapOf(), - ) - - /** - * Class to store the files under a manga directory. - */ - private class MangaDirectory( - val dir: UniFile, - var files: MutableSet = hashSetOf(), - ) + notifyChanges() + } /** * Returns a new map containing only the key entries of [transform] that are not null. @@ -282,3 +419,53 @@ class DownloadCache( return destination } } + +/** + * Class to store the files under the root downloads directory. + */ +@Serializable +private class RootDirectory( + @Serializable(with = UniFileAsStringSerializer::class) + val dir: UniFile?, + var sourceDirs: Map = hashMapOf(), +) + +/** + * Class to store the files under a source directory. + */ +@Serializable +private class SourceDirectory( + @Serializable(with = UniFileAsStringSerializer::class) + val dir: UniFile?, + var mangaDirs: Map = hashMapOf(), +) + +/** + * Class to store the files under a manga directory. + */ +@Serializable +private class MangaDirectory( + @Serializable(with = UniFileAsStringSerializer::class) + val dir: UniFile?, + var chapterDirs: MutableSet = hashSetOf(), +) + +private object UniFileAsStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: UniFile?) { + return if (value == null) { + encoder.encodeNull() + } else { + encoder.encodeString(value.uri.toString()) + } + } + + override fun deserialize(decoder: Decoder): UniFile? { + return if (decoder.decodeNotNullMark()) { + UniFile.fromUri(Injekt.get(), Uri.parse(decoder.decodeString())) + } else { + decoder.decodeNull() + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 619854bcde..ac0521a940 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.util.system.ImageUtil +import eu.kanade.tachiyomi.util.system.launchIO import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -100,7 +101,7 @@ class DownloadManager(val context: Context) { */ fun clearQueue(isNotification: Boolean = false) { deletePendingDownloads(*downloader.queue.toTypedArray()) - downloader.clearQueue(isNotification) + downloader.removeFromQueue(isNotification) DownloadJob.callListeners(false, this) } @@ -298,7 +299,7 @@ class DownloadManager(val context: Context) { * @param manga the manga of the chapters. * @param source the source of the chapters. */ - fun cleanupChapters(allChapters: List, manga: Manga, source: Source, removeRead: Boolean, removeNonFavorite: Boolean): Int { + suspend fun cleanupChapters(allChapters: List, manga: Manga, source: Source, removeRead: Boolean, removeNonFavorite: Boolean): Int { var cleaned = 0 if (removeNonFavorite && !manga.favorite) { @@ -311,7 +312,7 @@ class DownloadManager(val context: Context) { val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source) cleaned += filesWithNoChapter.size - cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga) + cache.removeChapterFolders(filesWithNoChapter.mapNotNull { it.name }, manga) filesWithNoChapter.forEach { it.delete() } if (removeRead) { @@ -341,12 +342,23 @@ class DownloadManager(val context: Context) { * @param manga the manga to delete. * @param source the source of the manga. */ - fun deleteManga(manga: Manga, source: Source) { - downloader.clearQueue(manga, true) - queue.remove(manga) - provider.findMangaDir(manga, source)?.delete() - cache.removeManga(manga) - queue.updateListeners() + fun deleteManga(manga: Manga, source: Source, removeQueued: Boolean = true) { + launchIO { + if (removeQueued) { + downloader.removeFromQueue(manga, true) + queue.remove(manga) + queue.updateListeners() + } + provider.findMangaDir(manga, source)?.delete() + cache.removeManga(manga) + + // Delete source directory if empty + val sourceDir = provider.findSourceDir(source) + if (sourceDir?.listFiles()?.isEmpty() == true) { + sourceDir.delete() + cache.removeSource(source) + } + } } /** @@ -377,7 +389,7 @@ class DownloadManager(val context: Context) { * @param oldChapter the existing chapter with the old name. * @param newChapter the target chapter with the new name. */ - fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) { + suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) { val oldNames = provider.getValidChapterDirNames(oldChapter).map { listOf(it, "$it.cbz") }.flatten() var newName = provider.getChapterDirName(newChapter, includeId = downloadPreferences.downloadWithId().get()) val mangaDir = provider.getMangaDir(manga, source) @@ -395,7 +407,7 @@ class DownloadManager(val context: Context) { if (oldDownload.renameTo(newName)) { cache.removeChapters(listOf(oldChapter), manga) - cache.addChapter(newName, manga) + cache.addChapter(newName, mangaDir, manga) } else { Logger.e { "Could not rename downloaded chapter: ${oldNames.joinToString()}" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index be0db1d260..f66d79cfe3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -28,6 +28,9 @@ import eu.kanade.tachiyomi.util.system.launchNow import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.system.writeText +import java.io.File +import java.util.* +import java.util.zip.* import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -50,13 +53,10 @@ import yokai.core.archive.ZipWriter import yokai.core.metadata.COMIC_INFO_FILE import yokai.core.metadata.ComicInfo import yokai.core.metadata.getComicInfo +import yokai.domain.category.interactor.GetCategories import yokai.domain.download.DownloadPreferences import yokai.i18n.MR import yokai.util.lang.getString -import java.io.File -import java.util.* -import java.util.zip.* -import yokai.domain.category.interactor.GetCategories /** * This class is the one in charge of downloading chapters. @@ -192,7 +192,7 @@ class Downloader( * * @param isNotification value that determines if status is set (needed for view updates) */ - fun clearQueue(isNotification: Boolean = false) { + fun removeFromQueue(isNotification: Boolean = false) { destroySubscription() // Needed to update the chapter view @@ -210,7 +210,7 @@ class Downloader( * * @param isNotification value that determines if status is set (needed for view updates) */ - fun clearQueue(manga: Manga, isNotification: Boolean = false) { + fun removeFromQueue(manga: Manga, isNotification: Boolean = false) { // Needed to update the chapter view if (isNotification) { queue.filter { it.status == Download.State.QUEUE && it.manga.id == manga.id } @@ -566,7 +566,7 @@ class Downloader( * @param tmpDir the directory where the download is currently stored. * @param dirname the real (non temporary) directory name of the download. */ - private fun ensureSuccessfulDownload( + private suspend fun ensureSuccessfulDownload( download: Download, mangaDir: UniFile, tmpDir: UniFile, @@ -602,7 +602,7 @@ class Downloader( } else { tmpDir.renameTo(dirname) } - cache.addChapter(dirname, download.manga) + cache.addChapter(dirname, mangaDir, download.manga) DiskUtil.createNoMediaFile(tmpDir, context) From 0d264026cf840d4a77ecdf0110db43ce80f9b21a Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 7 Dec 2024 13:36:01 +0700 Subject: [PATCH 037/166] docs: Sync changelog and add fixme note --- CHANGELOG.md | 1 + .../main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3db16f13d6..1caa607a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Fixed Keyboard is covering web page inputs - Increased `tryToSetForeground` delay to fix potential crashes (@nonproto) - Update dependency org.conscrypt:conscrypt-android to v2.5.3 +- Port upstream's download cache system ## [1.8.5.13] diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index 3ff36e5a1e..2be641e573 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -199,6 +199,7 @@ class DownloadCache( _isInitializing.emit(true) } + // FIXME: Wait for SourceManager to be initialized val sources = getSources() val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } From c9eb3023edaa6e5a351e510226654e5fd44944fa Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 7 Dec 2024 15:15:48 +0700 Subject: [PATCH 038/166] revert(reader): Revert setMaxTileSize to use GL's max texture size --- .../kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index 9f1aa9cb56..8730e25d50 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.data.coil.customDecoder import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView import eu.kanade.tachiyomi.util.system.DeviceUtil +import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.animatorDurationScale import okio.BufferedSource @@ -126,7 +127,7 @@ open class ReaderPageImageView @JvmOverloads constructor( } else { SubsamplingScaleImageView(context) }.apply { - setMaxTileSize(ImageUtil.hardwareBitmapThreshold) + setMaxTileSize(GLUtil.DEVICE_TEXTURE_LIMIT) setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER) setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) setMinimumTileDpi(180) From 8bc22fec28dfe9d50f378f424d13a76397362511 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 7 Dec 2024 15:21:26 +0700 Subject: [PATCH 039/166] revert: "revert(reader): Revert setMaxTileSize to use GL's max texture size" This reverts commit c9eb3023edaa6e5a351e510226654e5fd44944fa. --- .../kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index 8730e25d50..9f1aa9cb56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -36,7 +36,6 @@ import eu.kanade.tachiyomi.data.coil.customDecoder import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView import eu.kanade.tachiyomi.util.system.DeviceUtil -import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.animatorDurationScale import okio.BufferedSource @@ -127,7 +126,7 @@ open class ReaderPageImageView @JvmOverloads constructor( } else { SubsamplingScaleImageView(context) }.apply { - setMaxTileSize(GLUtil.DEVICE_TEXTURE_LIMIT) + setMaxTileSize(ImageUtil.hardwareBitmapThreshold) setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER) setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) setMinimumTileDpi(180) From a4ab7f11e22eb70657e7ed21a9f39e0895120c02 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 8 Dec 2024 06:47:02 +0700 Subject: [PATCH 040/166] fix(reader): Prevent potential NPE --- .../ui/reader/viewer/pager/PagerPageHolder.kt | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index e2d17dec75..e6fce6720f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -301,34 +301,31 @@ class PagerPageHolder( private fun SubsamplingScaleImageView.landscapeZoom(forward: Boolean?) { forward ?: return if (viewer.config.landscapeZoom && viewer.config.imageScaleType == SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE && sWidth > sHeight && scale == minScale) { - handler.postDelayed( - { - val point = when (viewer.config.imageZoomType) { - ZoomType.Left -> if (forward) PointF(0F, 0F) else PointF(sWidth.toFloat(), 0F) - ZoomType.Right -> if (forward) PointF(sWidth.toFloat(), 0F) else PointF(0F, 0F) - ZoomType.Center -> center.also { it?.y = 0F } - } + handler.postDelayed(500) { + val point = when (viewer.config.imageZoomType) { + ZoomType.Left -> if (forward) PointF(0F, 0F) else PointF(sWidth.toFloat(), 0F) + ZoomType.Right -> if (forward) PointF(sWidth.toFloat(), 0F) else PointF(0F, 0F) + ZoomType.Center -> center.also { it?.y = 0F } + } - val rootInsets = viewer.activity.window.decorView.rootWindowInsets - val topInsets = if (viewer.activity.isSplitScreen) { - 0f - } else { - rootInsets?.topCutoutInset()?.toFloat() ?: 0f - } - val bottomInsets = if (viewer.activity.isSplitScreen) { - 0f - } else { - rootInsets?.bottomCutoutInset()?.toFloat() ?: 0f - } - val targetScale = (height.toFloat() - topInsets - bottomInsets) / sHeight.toFloat() - animateScaleAndCenter(min(targetScale, minScale * 2), point)!! - .withDuration(500) - .withEasing(SubsamplingScaleImageView.EASE_IN_OUT_QUAD) - .withInterruptible(true) - .start() - }, - 500, - ) + val rootInsets = viewer.activity.window.decorView.rootWindowInsets + val topInsets = if (viewer.activity.isSplitScreen) { + 0f + } else { + rootInsets?.topCutoutInset()?.toFloat() ?: 0f + } + val bottomInsets = if (viewer.activity.isSplitScreen) { + 0f + } else { + rootInsets?.bottomCutoutInset()?.toFloat() ?: 0f + } + val targetScale = (height.toFloat() - topInsets - bottomInsets) / sHeight.toFloat() + (animateScaleAndCenter(min(targetScale, minScale * 2), point) ?: return@postDelayed) + .withDuration(500) + .withEasing(SubsamplingScaleImageView.EASE_IN_OUT_QUAD) + .withInterruptible(true) + .start() + } } } From f114320123f1cbed97af984edf8ae44e09dfaf31 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 8 Dec 2024 07:03:13 +0700 Subject: [PATCH 041/166] fix: Missing import --- .../kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index e6fce6720f..8dc4dcc000 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -11,6 +11,7 @@ import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.os.Build import android.view.LayoutInflater +import androidx.core.os.postDelayed import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import co.touchlab.kermit.Logger From 2f8ae26a838d6e002b8b3df1f57520f3a2cfd36c Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 8 Dec 2024 07:53:06 +0700 Subject: [PATCH 042/166] refactor(manga): Removing runBlocking --- .../tachiyomi/ui/library/LibraryController.kt | 11 +- .../ui/manga/MangaDetailsController.kt | 65 +++-- .../source/browse/BrowseSourceController.kt | 39 +-- .../globalsearch/GlobalSearchController.kt | 61 ++-- .../kanade/tachiyomi/util/MangaExtensions.kt | 266 +++++++++--------- 5 files changed, 233 insertions(+), 209 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 77a468109c..792549b73b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -101,6 +101,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceDrawable import eu.kanade.tachiyomi.util.system.ignoredSystemInsets import eu.kanade.tachiyomi.util.system.isImeVisible +import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.openInBrowser @@ -128,7 +129,7 @@ import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.text import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.widget.EmptyView -import java.util.* +import java.util.Locale import kotlin.math.abs import kotlin.math.max import kotlin.math.roundToInt @@ -2191,9 +2192,11 @@ open class LibraryController( */ private fun showChangeMangaCategoriesSheet() { val activity = activity ?: return - selectedMangas.toList().moveCategories(activity) { - presenter.getLibrary() - destroyActionModeIfNeeded() + viewScope.launchIO { + selectedMangas.toList().moveCategories(activity) { + presenter.getLibrary() + destroyActionModeIfNeeded() + } } } 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 4c3552985a..856f2a2894 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 @@ -111,12 +111,14 @@ import eu.kanade.tachiyomi.util.system.isLandscape import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isPromptChecked import eu.kanade.tachiyomi.util.system.isTablet +import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat import eu.kanade.tachiyomi.util.system.setCustomTitleAndMessage import eu.kanade.tachiyomi.util.system.timeSpanFromNow import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.copyToClipboard import eu.kanade.tachiyomi.util.view.findChild @@ -1633,10 +1635,12 @@ class MangaDetailsController : private fun showCategoriesSheet() { val adding = !presenter.manga.favorite - presenter.manga.moveCategories(activity!!, adding) { - updateHeader() - if (adding) { - showAddedSnack() + viewScope.launchIO { + presenter.manga.moveCategories(activity!!, adding) { + updateHeader() + if (adding) { + showAddedSnack() + } } } } @@ -1644,32 +1648,37 @@ class MangaDetailsController : private fun toggleMangaFavorite() { val view = view ?: return val activity = activity ?: return - snack?.dismiss() - snack = presenter.manga.addOrRemoveToFavorites( - presenter.preferences, - view, - activity, - presenter.sourceManager, - this, - onMangaAdded = { migrationInfo -> - migrationInfo?.let { + viewScope.launchIO { + withUIContext { snack?.dismiss() } + snack = presenter.manga.addOrRemoveToFavorites( + presenter.preferences, + view, + activity, + presenter.sourceManager, + this@MangaDetailsController, + onMangaAdded = { migrationInfo -> + migrationInfo?.let { + presenter.fetchChapters(andTracking = true) + } + updateHeader() + showAddedSnack() + }, + onMangaMoved = { + updateHeader() presenter.fetchChapters(andTracking = true) + }, + onMangaDeleted = { + updateHeader() + presenter.confirmDeletion() + }, + scope = viewScope, + ) + if (snack?.duration == Snackbar.LENGTH_INDEFINITE) { + withUIContext { + val favButton = getHeader()?.binding?.favoriteButton + (activity as? MainActivity)?.setUndoSnackBar(snack, favButton) } - updateHeader() - showAddedSnack() - }, - onMangaMoved = { - updateHeader() - presenter.fetchChapters(andTracking = true) - }, - onMangaDeleted = { - updateHeader() - presenter.confirmDeletion() - }, - ) - if (snack?.duration == Snackbar.LENGTH_INDEFINITE) { - val favButton = getHeader()?.binding?.favoriteButton - (activity as? MainActivity)?.setUndoSnackBar(snack, favButton) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index 5a36e21dde..f113d9242b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -43,8 +43,10 @@ import eu.kanade.tachiyomi.util.addOrRemoveToFavorites import eu.kanade.tachiyomi.util.system.connectivityManager import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.e +import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.applyBottomAnimatedInsets import eu.kanade.tachiyomi.util.view.fullAppBarHeight @@ -766,22 +768,27 @@ open class BrowseSourceController(bundle: Bundle) : val manga = (adapter?.getItem(position) as? BrowseSourceItem?)?.manga ?: return val view = view ?: return val activity = activity ?: return - snack?.dismiss() - snack = manga.addOrRemoveToFavorites( - preferences, - view, - activity, - presenter.sourceManager, - this, - onMangaAdded = { - adapter?.notifyItemChanged(position) - snack = view.snack(MR.strings.added_to_library) - }, - onMangaMoved = { adapter?.notifyItemChanged(position) }, - onMangaDeleted = { presenter.confirmDeletion(manga) }, - ) - if (snack?.duration == Snackbar.LENGTH_INDEFINITE) { - (activity as? MainActivity)?.setUndoSnackBar(snack) + viewScope.launchIO { + withUIContext { snack?.dismiss() } + snack = manga.addOrRemoveToFavorites( + preferences, + view, + activity, + presenter.sourceManager, + this@BrowseSourceController, + onMangaAdded = { + adapter?.notifyItemChanged(position) + snack = view.snack(MR.strings.added_to_library) + }, + onMangaMoved = { adapter?.notifyItemChanged(position) }, + onMangaDeleted = { presenter.confirmDeletion(manga) }, + scope = viewScope, + ) + if (snack?.duration == Snackbar.LENGTH_INDEFINITE) { + withUIContext { + (activity as? MainActivity)?.setUndoSnackBar(snack) + } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchController.kt index 5c3d1bf81b..9cde0eca8c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchController.kt @@ -24,7 +24,9 @@ import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController import eu.kanade.tachiyomi.util.addOrRemoveToFavorites import eu.kanade.tachiyomi.util.system.extensionIntentForText +import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat +import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.isControllerVisible import eu.kanade.tachiyomi.util.view.scrollViewWith @@ -114,34 +116,39 @@ open class GlobalSearchController( val view = view ?: return val activity = activity ?: return - snack?.dismiss() - snack = manga.addOrRemoveToFavorites( - preferences, - view, - activity, - presenter.sourceManager, - this, - onMangaAdded = { migrationInfo -> - migrationInfo?.let { (source, stillFaved) -> - val index = this.adapter - ?.currentItems?.indexOfFirst { it.source.id == source } ?: return@let - val item = this.adapter?.getItem(index) ?: return@let - val oldMangaIndex = item.results?.indexOfFirst { - it.manga.title.lowercase() == manga.title.lowercase() - } ?: return@let - val oldMangaItem = item.results.getOrNull(oldMangaIndex) - oldMangaItem?.manga?.favorite = stillFaved - val holder = binding.recycler.findViewHolderForAdapterPosition(index) as? GlobalSearchHolder - holder?.updateManga(oldMangaIndex) + viewScope.launchIO { + withUIContext { snack?.dismiss() } + snack = manga.addOrRemoveToFavorites( + preferences, + view, + activity, + presenter.sourceManager, + this@GlobalSearchController, + onMangaAdded = { migrationInfo -> + migrationInfo?.let { (source, stillFaved) -> + val index = this@GlobalSearchController.adapter + ?.currentItems?.indexOfFirst { it.source.id == source } ?: return@let + val item = this@GlobalSearchController.adapter?.getItem(index) ?: return@let + val oldMangaIndex = item.results?.indexOfFirst { + it.manga.title.lowercase() == manga.title.lowercase() + } ?: return@let + val oldMangaItem = item.results.getOrNull(oldMangaIndex) + oldMangaItem?.manga?.favorite = stillFaved + val holder = binding.recycler.findViewHolderForAdapterPosition(index) as? GlobalSearchHolder + holder?.updateManga(oldMangaIndex) + } + adapter.notifyItemChanged(position) + snack = view.snack(MR.strings.added_to_library) + }, + onMangaMoved = { adapter.notifyItemChanged(position) }, + onMangaDeleted = { presenter.confirmDeletion(manga) }, + scope = viewScope, + ) + if (snack?.duration == Snackbar.LENGTH_INDEFINITE) { + withUIContext { + (activity as? MainActivity)?.setUndoSnackBar(snack) } - adapter.notifyItemChanged(position) - snack = view.snack(MR.strings.added_to_library) - }, - onMangaMoved = { adapter.notifyItemChanged(position) }, - onMangaDeleted = { presenter.confirmDeletion(manga) }, - ) - if (snack?.duration == Snackbar.LENGTH_INDEFINITE) { - (activity as? MainActivity)?.setUndoSnackBar(snack) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 279a8be209..9a820b2927 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -41,8 +41,9 @@ import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.widget.TriStateCheckBox import java.util.Date import java.util.Locale -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import yokai.domain.category.interactor.GetCategories @@ -84,69 +85,69 @@ suspend fun Manga.shouldDownloadNewChapters(prefs: PreferencesHelper, getCategor return categoriesForManga.any { it in includedCategories } } -fun Manga.moveCategories(activity: Activity, onMangaMoved: () -> Unit) { +suspend fun Manga.moveCategories(activity: Activity, onMangaMoved: () -> Unit) { moveCategories(activity, false, onMangaMoved) } -fun Manga.moveCategories( +suspend fun Manga.moveCategories( activity: Activity, addingToLibrary: Boolean, onMangaMoved: () -> Unit, ) { val getCategories: GetCategories = Injekt.get() - // FIXME: Don't do blocking - val categories = runBlocking { getCategories.await() } - val categoriesForManga = runBlocking { - this@moveCategories.id?.let { mangaId -> getCategories.awaitByMangaId(mangaId) } - .orEmpty() - } + val categories = getCategories.await() + val categoriesForManga = this.id?.let { mangaId -> getCategories.awaitByMangaId(mangaId) }.orEmpty() val ids = categoriesForManga.mapNotNull { it.id }.toTypedArray() - SetCategoriesSheet( - activity, - this, - categories.toMutableList(), - ids, - addingToLibrary, - ) { - onMangaMoved() - if (addingToLibrary) { - autoAddTrack(onMangaMoved) - } - }.show() + withUIContext { + SetCategoriesSheet( + activity, + this@moveCategories, + categories.toMutableList(), + ids, + addingToLibrary, + ) { + onMangaMoved() + if (addingToLibrary) { + autoAddTrack(onMangaMoved) + } + }.show() + } } -fun List.moveCategories( +suspend fun List.moveCategories( activity: Activity, onMangaMoved: () -> Unit, ) { if (this.isEmpty()) return val getCategories: GetCategories = Injekt.get() - // FIXME: Don't do blocking - val categories = runBlocking { getCategories.await() } + val categories = getCategories.await() val mangaCategories = map { manga -> - manga.id?.let { mangaId -> runBlocking { getCategories.awaitByMangaId(mangaId) } }.orEmpty() + manga.id?.let { mangaId -> getCategories.awaitByMangaId(mangaId) }.orEmpty() } val commonCategories = mangaCategories.reduce { set1, set2 -> set1.intersect(set2.toSet()).toMutableList() }.toSet() val mixedCategories = mangaCategories.flatten().distinct().subtract(commonCategories).toMutableList() - SetCategoriesSheet( - activity, - this, - categories.toMutableList(), - categories.map { - when (it) { - in commonCategories -> TriStateCheckBox.State.CHECKED - in mixedCategories -> TriStateCheckBox.State.IGNORE - else -> TriStateCheckBox.State.UNCHECKED - } - }.toTypedArray(), - false, - ) { - onMangaMoved() - }.show() + + withUIContext { + SetCategoriesSheet( + activity, + this@moveCategories, + categories.toMutableList(), + categories.map { + when (it) { + in commonCategories -> TriStateCheckBox.State.CHECKED + in mixedCategories -> TriStateCheckBox.State.IGNORE + else -> TriStateCheckBox.State.UNCHECKED + } + }.toTypedArray(), + false, + ) { + onMangaMoved() + }.show() + } } -fun Manga.addOrRemoveToFavorites( +suspend fun Manga.addOrRemoveToFavorites( preferences: PreferencesHelper, view: View, activity: Activity, @@ -160,15 +161,12 @@ fun Manga.addOrRemoveToFavorites( setMangaCategories: SetMangaCategories = Injekt.get(), getManga: GetManga = Injekt.get(), updateManga: UpdateManga = Injekt.get(), + @OptIn(DelicateCoroutinesApi::class) + scope: CoroutineScope = GlobalScope, ): Snackbar? { if (!favorite) { if (checkForDupes) { - val duplicateManga = runBlocking(Dispatchers.IO) { - getManga.awaitDuplicateFavorite( - this@addOrRemoveToFavorites.title, - this@addOrRemoveToFavorites.source, - ) - } + val duplicateManga = getManga.awaitDuplicateFavorite(this.title, this.source) if (duplicateManga != null) { showAddDuplicateDialog( this, @@ -187,6 +185,7 @@ fun Manga.addOrRemoveToFavorites( onMangaAdded, onMangaMoved, onMangaDeleted, + scope = scope, ) }, migrateManga = { source, faved -> @@ -197,8 +196,7 @@ fun Manga.addOrRemoveToFavorites( } } - // FIXME: Don't do blocking - val categories = runBlocking { getCategories.await() } + val categories = getCategories.await() val defaultCategoryId = preferences.defaultCategory().get() val defaultCategory = categories.find { it.id == defaultCategoryId } val lastUsedCategories = Category.lastCategoriesAddedTo.mapNotNull { catId -> @@ -209,22 +207,23 @@ fun Manga.addOrRemoveToFavorites( favorite = true date_added = Date().time autoAddTrack(onMangaMoved) - // FIXME: Don't do blocking - runBlocking { - updateManga.await( - MangaUpdate( - id = this@addOrRemoveToFavorites.id!!, - favorite = true, - dateAdded = this@addOrRemoveToFavorites.date_added, - ) + updateManga.await( + MangaUpdate( + id = this@addOrRemoveToFavorites.id!!, + favorite = true, + dateAdded = this@addOrRemoveToFavorites.date_added, ) - setMangaCategories.await(this@addOrRemoveToFavorites.id!!, listOf(defaultCategory.id!!.toLong())) - } + ) + setMangaCategories.await(this@addOrRemoveToFavorites.id!!, listOf(defaultCategory.id!!.toLong())) (activity as? MainActivity)?.showNotificationPermissionPrompt() onMangaMoved() - return view.snack(activity.getString(MR.strings.added_to_, defaultCategory.name)) { - setAction(MR.strings.change) { - moveCategories(activity, onMangaMoved) + return withUIContext { + view.snack(activity.getString(MR.strings.added_to_, defaultCategory.name)) { + setAction(MR.strings.change) { + scope.launchIO { + moveCategories(activity, onMangaMoved) + } + } } } } @@ -235,35 +234,36 @@ fun Manga.addOrRemoveToFavorites( favorite = true date_added = Date().time autoAddTrack(onMangaMoved) - // FIXME: Don't do blocking - runBlocking { - updateManga.await( - MangaUpdate( - id = this@addOrRemoveToFavorites.id!!, - favorite = true, - dateAdded = this@addOrRemoveToFavorites.date_added, - ) + updateManga.await( + MangaUpdate( + id = this@addOrRemoveToFavorites.id!!, + favorite = true, + dateAdded = this@addOrRemoveToFavorites.date_added, ) - setMangaCategories.await(this@addOrRemoveToFavorites.id!!, lastUsedCategories.map { it.id!!.toLong() }) - } + ) + setMangaCategories.await(this@addOrRemoveToFavorites.id!!, lastUsedCategories.map { it.id!!.toLong() }) (activity as? MainActivity)?.showNotificationPermissionPrompt() onMangaMoved() - return view.snack( - activity.getString( - MR.strings.added_to_, - when (lastUsedCategories.size) { - 0 -> activity.getString(MR.strings.default_category).lowercase(Locale.ROOT) - 1 -> lastUsedCategories.firstOrNull()?.name ?: "" - else -> activity.getString( - MR.plurals.category_plural, - lastUsedCategories.size, - lastUsedCategories.size, - ) - }, - ), - ) { - setAction(MR.strings.change) { - moveCategories(activity, onMangaMoved) + return withUIContext { + view.snack( + activity.getString( + MR.strings.added_to_, + when (lastUsedCategories.size) { + 0 -> activity.getString(MR.strings.default_category).lowercase(Locale.ROOT) + 1 -> lastUsedCategories.firstOrNull()?.name ?: "" + else -> activity.getString( + MR.plurals.category_plural, + lastUsedCategories.size, + lastUsedCategories.size, + ) + }, + ), + ) { + setAction(MR.strings.change) { + scope.launchIO { + moveCategories(activity, onMangaMoved) + } + } } } } @@ -271,27 +271,28 @@ fun Manga.addOrRemoveToFavorites( favorite = true date_added = Date().time autoAddTrack(onMangaMoved) - // FIXME: Don't do blocking - runBlocking { - updateManga.await( - MangaUpdate( - id = this@addOrRemoveToFavorites.id!!, - favorite = true, - dateAdded = this@addOrRemoveToFavorites.date_added, - ) + updateManga.await( + MangaUpdate( + id = this@addOrRemoveToFavorites.id!!, + favorite = true, + dateAdded = this@addOrRemoveToFavorites.date_added, ) - setMangaCategories.await(this@addOrRemoveToFavorites.id!!, emptyList()) - } + ) + setMangaCategories.await(this@addOrRemoveToFavorites.id!!, emptyList()) onMangaMoved() (activity as? MainActivity)?.showNotificationPermissionPrompt() - return if (categories.isNotEmpty()) { - view.snack(activity.getString(MR.strings.added_to_, activity.getString(MR.strings.default_value))) { - setAction(MR.strings.change) { - moveCategories(activity, onMangaMoved) + return withUIContext { + if (categories.isNotEmpty()) { + view.snack(activity.getString(MR.strings.added_to_, activity.getString(MR.strings.default_value))) { + setAction(MR.strings.change) { + scope.launchIO { + moveCategories(activity, onMangaMoved) + } + } } + } else { + view.snack(MR.strings.added_to_library) } - } else { - view.snack(MR.strings.added_to_library) } } else -> { // Always ask @@ -302,23 +303,19 @@ fun Manga.addOrRemoveToFavorites( val lastAddedDate = date_added favorite = false date_added = 0 - // FIXME: Don't do blocking - runBlocking { - updateManga.await( - MangaUpdate( - id = this@addOrRemoveToFavorites.id!!, - favorite = false, - dateAdded = 0, - ) + updateManga.await( + MangaUpdate( + id = this@addOrRemoveToFavorites.id!!, + favorite = false, + dateAdded = 0, ) - } + ) onMangaMoved() return view.snack(view.context.getString(MR.strings.removed_from_library), Snackbar.LENGTH_INDEFINITE) { setAction(MR.strings.undo) { favorite = true date_added = lastAddedDate - // FIXME: Don't do blocking - runBlocking { + scope.launchIO { updateManga.await( MangaUpdate( id = this@addOrRemoveToFavorites.id!!, @@ -344,39 +341,40 @@ fun Manga.addOrRemoveToFavorites( return null } -private fun Manga.showSetCategoriesSheet( +private suspend fun Manga.showSetCategoriesSheet( activity: Activity, categories: List, onMangaAdded: (Pair?) -> Unit, onMangaMoved: () -> Unit, getCategories: GetCategories = Injekt.get(), ) { - // FIXME: Don't do blocking - val categoriesForManga = runBlocking { getCategories.awaitByMangaId(this@showSetCategoriesSheet.id!!) } + val categoriesForManga = getCategories.awaitByMangaId(this.id!!) val ids = categoriesForManga.mapNotNull { it.id }.toTypedArray() - SetCategoriesSheet( - activity, - this, - categories.toMutableList(), - ids, - true, - ) { - (activity as? MainActivity)?.showNotificationPermissionPrompt() - onMangaAdded(null) - autoAddTrack(onMangaMoved) - }.show() + withUIContext { + SetCategoriesSheet( + activity, + this@showSetCategoriesSheet, + categories.toMutableList(), + ids, + true, + ) { + (activity as? MainActivity)?.showNotificationPermissionPrompt() + onMangaAdded(null) + autoAddTrack(onMangaMoved) + }.show() + } } -private fun showAddDuplicateDialog( +private suspend fun showAddDuplicateDialog( newManga: Manga, libraryManga: Manga, activity: Activity, sourceManager: SourceManager, controller: Controller, - addManga: () -> Unit, + addManga: suspend () -> Unit, migrateManga: (Long, Boolean) -> Unit, -) { +) = withUIContext { val source = sourceManager.getOrStub(libraryManga.source) val titles by lazy { MigrationFlags.titles(activity, libraryManga) } @@ -415,7 +413,7 @@ private fun showAddDuplicateDialog( MangaDetailsController(libraryManga) .withFadeTransaction(), ) - 1 -> addManga() + 1 -> launchIO { addManga() } 2 -> { if (!newManga.initialized) { activity.toast(MR.strings.must_view_details_before_migration, Toast.LENGTH_LONG) From 28cbf0b988e27999359ebd69aba2d66a33053baa Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 8 Dec 2024 08:07:26 +0700 Subject: [PATCH 043/166] fix(manga): Missing "withUIContext" --- .../kanade/tachiyomi/util/MangaExtensions.kt | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 9a820b2927..49f19a1fef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -311,31 +311,33 @@ suspend fun Manga.addOrRemoveToFavorites( ) ) onMangaMoved() - return view.snack(view.context.getString(MR.strings.removed_from_library), Snackbar.LENGTH_INDEFINITE) { - setAction(MR.strings.undo) { - favorite = true - date_added = lastAddedDate - scope.launchIO { - updateManga.await( - MangaUpdate( - id = this@addOrRemoveToFavorites.id!!, - favorite = true, - dateAdded = lastAddedDate, + return withUIContext { + view.snack(view.context.getString(MR.strings.removed_from_library), Snackbar.LENGTH_INDEFINITE) { + setAction(MR.strings.undo) { + favorite = true + date_added = lastAddedDate + scope.launchIO { + updateManga.await( + MangaUpdate( + id = this@addOrRemoveToFavorites.id!!, + favorite = true, + dateAdded = lastAddedDate, + ) ) - ) - } - onMangaMoved() - } - addCallback( - object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (!favorite) { - onMangaDeleted() - } } - }, - ) + onMangaMoved() + } + addCallback( + object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (!favorite) { + onMangaDeleted() + } + } + }, + ) + } } } return null From 823860a56fe0657d41cc96023c20525daacc74b3 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 8 Dec 2024 08:09:37 +0700 Subject: [PATCH 044/166] chore(manga): Try not to use GlobalScope as much as possible --- .../main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 49f19a1fef..5680c5f2d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -191,6 +191,7 @@ suspend fun Manga.addOrRemoveToFavorites( migrateManga = { source, faved -> onMangaAdded(source to faved) }, + scope = scope, ) return null } @@ -376,6 +377,8 @@ private suspend fun showAddDuplicateDialog( controller: Controller, addManga: suspend () -> Unit, migrateManga: (Long, Boolean) -> Unit, + @OptIn(DelicateCoroutinesApi::class) + scope: CoroutineScope = GlobalScope, ) = withUIContext { val source = sourceManager.getOrStub(libraryManga.source) @@ -385,7 +388,7 @@ private suspend fun showAddDuplicateDialog( val enabled = titles.indices.map { listView.isItemChecked(it) }.toTypedArray() val flags = MigrationFlags.getFlagsFromPositions(enabled, libraryManga) val enhancedServices by lazy { Injekt.get().services.filterIsInstance() } - launchUI { + scope.launchUI { MigrationProcessAdapter.migrateMangaInternal( flags, enhancedServices, @@ -415,7 +418,7 @@ private suspend fun showAddDuplicateDialog( MangaDetailsController(libraryManga) .withFadeTransaction(), ) - 1 -> launchIO { addManga() } + 1 -> scope.launchIO { addManga() } 2 -> { if (!newManga.initialized) { activity.toast(MR.strings.must_view_details_before_migration, Toast.LENGTH_LONG) From d7c3aa6b45175e410e904aae1574e4514202a87c Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 8 Dec 2024 08:28:20 +0700 Subject: [PATCH 045/166] fix(manga): Move more stuff to UI thread --- .../eu/kanade/tachiyomi/util/MangaExtensions.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 5680c5f2d9..48f4b19818 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -216,9 +216,9 @@ suspend fun Manga.addOrRemoveToFavorites( ) ) setMangaCategories.await(this@addOrRemoveToFavorites.id!!, listOf(defaultCategory.id!!.toLong())) - (activity as? MainActivity)?.showNotificationPermissionPrompt() - onMangaMoved() return withUIContext { + onMangaMoved() + (activity as? MainActivity)?.showNotificationPermissionPrompt() view.snack(activity.getString(MR.strings.added_to_, defaultCategory.name)) { setAction(MR.strings.change) { scope.launchIO { @@ -243,9 +243,9 @@ suspend fun Manga.addOrRemoveToFavorites( ) ) setMangaCategories.await(this@addOrRemoveToFavorites.id!!, lastUsedCategories.map { it.id!!.toLong() }) - (activity as? MainActivity)?.showNotificationPermissionPrompt() - onMangaMoved() return withUIContext { + onMangaMoved() + (activity as? MainActivity)?.showNotificationPermissionPrompt() view.snack( activity.getString( MR.strings.added_to_, @@ -280,9 +280,9 @@ suspend fun Manga.addOrRemoveToFavorites( ) ) setMangaCategories.await(this@addOrRemoveToFavorites.id!!, emptyList()) - onMangaMoved() - (activity as? MainActivity)?.showNotificationPermissionPrompt() return withUIContext { + onMangaMoved() + (activity as? MainActivity)?.showNotificationPermissionPrompt() if (categories.isNotEmpty()) { view.snack(activity.getString(MR.strings.added_to_, activity.getString(MR.strings.default_value))) { setAction(MR.strings.change) { @@ -311,8 +311,8 @@ suspend fun Manga.addOrRemoveToFavorites( dateAdded = 0, ) ) - onMangaMoved() return withUIContext { + onMangaMoved() view.snack(view.context.getString(MR.strings.removed_from_library), Snackbar.LENGTH_INDEFINITE) { setAction(MR.strings.undo) { favorite = true From 349b9c181aa2dccbff10701a11b022861691b6ef Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 8 Dec 2024 09:37:10 +0700 Subject: [PATCH 046/166] fix(library): Include default category in allCategories --- .../kanade/tachiyomi/ui/library/LibraryPresenter.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 34e43533c1..46d4d0c457 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 @@ -818,16 +818,20 @@ class LibraryPresenter( getPreferencesFlow(), preferences.removeArticles().changes(), fetchLibrary - ) { allCategories, libraryMangaList, prefs, removeArticles, _ -> + ) { dbCategories, libraryMangaList, prefs, removeArticles, _ -> groupType = prefs.groupType + val defaultCategory = createDefaultCategory() + val allCategories = listOf(defaultCategory) + dbCategories + val (items, categories, hiddenItems) = if (groupType <= BY_DEFAULT || !libraryIsGrouped) { getLibraryItems( - allCategories, + dbCategories, libraryMangaList, prefs.sortingMode, prefs.sortAscending, prefs.showAllCategories, + defaultCategory, ) } else { getDynamicLibraryItems( @@ -854,6 +858,7 @@ class LibraryPresenter( sortingMode: Int, isAscending: Boolean, showAll: Boolean, + defaultCategory: Category, ): Triple, List, List> { val categories = allCategories.toMutableList() val hiddenItems = mutableListOf() @@ -898,7 +903,7 @@ class LibraryPresenter( preferences.collapsedCategories().get().mapNotNull { it.toIntOrNull() }.toSet() } - if (categorySet.contains(0)) categories.add(0, createDefaultCategory()) + if (categorySet.contains(0)) categories.add(0, defaultCategory) if (libraryIsGrouped) { categories.forEach { category -> val catId = category.id ?: return@forEach From 1d200f426c81002a6e1946fe29552ead5a2246a8 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 8 Dec 2024 12:24:51 +0700 Subject: [PATCH 047/166] fix(manga): Compress custom cover to not exceed 4092px --- .../kanade/tachiyomi/data/cache/CoverCache.kt | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt index b1f094c553..c1a969f0d4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt @@ -1,6 +1,9 @@ package eu.kanade.tachiyomi.data.cache import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Build import android.text.format.Formatter import co.touchlab.kermit.Logger import coil3.imageLoader @@ -171,8 +174,37 @@ class CoverCache(val context: Context) { */ @Throws(IOException::class) fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) { + val maxTextureSize = 4096f + var bitmap = BitmapFactory.decodeStream(inputStream) + if (maxOf(bitmap.width, bitmap.height) > maxTextureSize) { + val widthRatio = bitmap.width / maxTextureSize + val heightRatio = bitmap.height / maxTextureSize + + val targetWidth: Float + val targetHeight: Float + + if (widthRatio >= heightRatio) { + targetWidth = maxTextureSize + targetHeight = (targetWidth / bitmap.width) * bitmap.height + } else { + targetHeight = maxTextureSize + targetWidth = (targetHeight / bitmap.height) * bitmap.width + } + + val scaledBitmap = Bitmap.createScaledBitmap(bitmap, targetWidth.toInt(), targetHeight.toInt(), true) + bitmap.recycle() + bitmap = scaledBitmap + } getCustomCoverFile(manga).outputStream().use { - inputStream.copyTo(it) + @Suppress("DEPRECATION") + bitmap.compress( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + Bitmap.CompressFormat.WEBP_LOSSLESS + else + Bitmap.CompressFormat.WEBP, + 100, + it + ) } } From 264942525961607a4a7a11f33d4a0b8f295b4598 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 8 Dec 2024 13:46:10 +0700 Subject: [PATCH 048/166] chore(coil): Remove commented code [skip ci] It's used to compress custom cover instead --- .../data/coil/TachiyomiImageDecoder.kt | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt index acadf2ce5f..27bc94de36 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt @@ -59,29 +59,6 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti } } - /* - val maxTextureSize = 4096f - if (maxOf(bitmap.width, bitmap.height) > maxTextureSize) { - val widthRatio = bitmap.width / maxTextureSize - val heightRatio = bitmap.height / maxTextureSize - - val targetWidth: Float - val targetHeight: Float - - if (widthRatio >= heightRatio) { - targetWidth = maxTextureSize - targetHeight = (targetWidth / bitmap.width) * bitmap.height - } else { - targetHeight = maxTextureSize - targetWidth = (targetHeight / bitmap.height) * bitmap.width - } - - val scaledBitmap = Bitmap.createScaledBitmap(bitmap, targetWidth.toInt(), targetHeight.toInt(), true) - bitmap.recycle() - bitmap = scaledBitmap - } - */ - return DecodeResult( image = bitmap.asImage(), isSampled = sampleSize > 1, From 6bd2f0ab9aecdeedf35e6ca271f18e4f54078611 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 06:29:56 +0700 Subject: [PATCH 049/166] chore(deps): Update moko to v0.24.4 (#203) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6bd44d2ef4..78bee4df5f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ aboutlibraries = "11.2.3" chucker = "3.5.2" flexible-adapter = "c8013533" fast_adapter = "5.6.0" -moko = "0.24.2" +moko = "0.24.4" okhttp = "5.0.0-alpha.14" shizuku = "13.1.5" # FIXME: Uncomment once SQLDelight support KMP AndroidX SQLiteDriver From 4c30881e91215e51b8348a62dcb40e8a68c7ea64 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 9 Dec 2024 06:30:08 +0700 Subject: [PATCH 050/166] docs: Sync changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1caa607a8c..bb11f4cb61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Simplify network helper code - Fully migrated from StorIO to SQLDelight - Update dependency com.android.tools:desugar_jdk_libs to v2.1.3 -- Update moko to v0.24.2 +- Update moko to v0.24.4 - Refactor trackers to use DTOs (@MajorTanya) - Fix AniList `ALSearchItem.status` nullibility (@Secozzi) - Replace Injekt with Koin From 06c7cc7d17468bc1f9ff2f31416e7a54cf18c182 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 9 Dec 2024 08:09:19 +0700 Subject: [PATCH 051/166] chore(release): Sync changelog for v1.9.0 release --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb11f4cb61..5836a0ed4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +## [1.9.0] + ### Additions - Sync DoH provider list with upstream (added Mullvad, Control D, Njalla, and Shecan) - Add option to enable verbose logging @@ -28,6 +30,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Bangumi search now shows the score and summary of a search result (@MajorTanya) - Logs are now written to a file for easier debugging - Bump default user agent (@AntsyLich) +- Custom cover is now compressed to WebP to prevent OOM crashes ### Fixes - Fix only few DoH provider is actually being used (Cloudflare, Google, AdGuard, and Quad9) @@ -41,6 +44,8 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Fix browser not opening in some cases in Honor devices (@MajorTanya) - Fix "ConcurrentModificationException" crashes - Fix Komga unread badge, again +- Fix default category can't be updated manually +- Fix crashes trying to load Library caused by cover being too large ### Other - Simplify network helper code From c357f6f65853fcafdc70f350daa8b1b5733ef575 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 9 Dec 2024 08:31:26 +0700 Subject: [PATCH 052/166] chore: Sync project --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/ISSUE_TEMPLATE/issue_report.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 646bdb9e4f..241ffa0f67 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -35,7 +35,7 @@ body: required: true - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. required: true - - label: I have updated the app to version **[1.8.5.13](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.0](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have checked through the app settings for my feature. required: true diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index 212bdee2b7..c0b9135137 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -100,7 +100,7 @@ body: required: true - label: I have tried the [troubleshooting guide](https://mihon.app/help/). required: true - - label: I have updated the app to version **[1.8.5.13](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.0](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have updated all installed extensions. required: true From 6c111a1247bf9c4652d1b3ede6333a38c26504ea Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 9 Dec 2024 08:34:27 +0700 Subject: [PATCH 053/166] docs(changelog): Fix typo [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5836a0ed4f..b3a0c519de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,7 +76,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Update dependency io.mockk:mockk to v1.13.13 - Update shizuku to v13.1.5 - Use reflection to fix shizuku breaking changes (@Jobobby04) -- Bump comple sdk to 35 +- Bump compile sdk to 35 - Handle Android SDK 35 API collision (@AntsyLich) - Update kotlin monorepo to v2.0.21 - Update dependency androidx.work:work-runtime-ktx to v2.10.0 From 07ed81454fac21109e826012b76312bac720168a Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 9 Dec 2024 19:03:25 +0700 Subject: [PATCH 054/166] refactor(reader): Remove runBlocking usage from ViewModel --- .../tachiyomi/ui/reader/ReaderViewModel.kt | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 5cf637a3ee..9632fc4c64 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -68,7 +68,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import rx.Completable import rx.schedulers.Schedulers @@ -158,22 +157,7 @@ class ReaderViewModel( private var finished = false private var chapterToDownload: Download? = null - /** - * Chapter list for the active manga. It's retrieved lazily and should be accessed for the first - * time in a background thread to avoid blocking the UI. - */ - private val chapterList by lazy { - val manga = manga!! - val dbChapters = runBlocking { getChapter.awaitAll(manga) } - - val selectedChapter = dbChapters.find { it.id == chapterId } - ?: error("Requested chapter of id $chapterId not found in chapter list") - - val chaptersForReader = - chapterFilter.filterChaptersForReader(dbChapters, manga, selectedChapter) - val chapterSort = ChapterSort(manga, chapterFilter, preferences) - chaptersForReader.sortedWith(chapterSort.sortComparator(true)).map(::ReaderChapter) - } + private var chapterList = emptyList() private var chapterItems = emptyList() @@ -261,6 +245,7 @@ class ReaderViewModel( val context = Injekt.get() loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) + chapterList = getChapterList() loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id }) Result.success(true) } else { @@ -276,11 +261,24 @@ class ReaderViewModel( } } + private suspend fun getChapterList(): List { + val manga = manga!! + val dbChapters = getChapter.awaitAll(manga.id!!, true) + + val selectedChapter = dbChapters.find { it.id == chapterId } + ?: error("Requested chapter of id $chapterId not found in chapter list") + + val chaptersForReader = + chapterFilter.filterChaptersForReader(dbChapters, manga, selectedChapter) + val chapterSort = ChapterSort(manga, chapterFilter, preferences) + return chaptersForReader.sortedWith(chapterSort.sortComparator(true)).map(::ReaderChapter) + } + suspend fun getChapters(): List { val manga = manga ?: return emptyList() chapterItems = withContext(Dispatchers.IO) { val chapterSort = ChapterSort(manga, chapterFilter, preferences) - val dbChapters = runBlocking { getChapter.awaitAll(manga) } + val dbChapters = getChapter.awaitAll(manga) chapterSort.getChaptersSorted( dbChapters, filterForReader = true, @@ -544,26 +542,28 @@ class ReaderViewModel( private fun downloadNextChapters() { val manga = manga ?: return - if (getCurrentChapter()?.pageLoader !is DownloadPageLoader) return - val nextChapter = state.value.viewerChapters?.nextChapter?.chapter ?: return - val chaptersNumberToDownload = preferences.autoDownloadWhileReading().get() - if (chaptersNumberToDownload == 0 || !manga.favorite) return - val isNextChapterDownloaded = downloadManager.isChapterDownloaded(nextChapter, manga) - if (isNextChapterDownloaded) { - downloadAutoNextChapters(chaptersNumberToDownload, nextChapter.id) + viewModelScope.launchNonCancellableIO { + if (getCurrentChapter()?.pageLoader !is DownloadPageLoader) return@launchNonCancellableIO + val nextChapter = state.value.viewerChapters?.nextChapter?.chapter ?: return@launchNonCancellableIO + val chaptersNumberToDownload = preferences.autoDownloadWhileReading().get() + if (chaptersNumberToDownload == 0 || !manga.favorite) return@launchNonCancellableIO + val isNextChapterDownloaded = downloadManager.isChapterDownloaded(nextChapter, manga) + if (isNextChapterDownloaded) { + downloadAutoNextChapters(chaptersNumberToDownload, nextChapter.id) + } } } - private fun downloadAutoNextChapters(choice: Int, nextChapterId: Long?) { + private suspend fun downloadAutoNextChapters(choice: Int, nextChapterId: Long?) { val chaptersToDownload = getNextUnreadChaptersSorted(nextChapterId).take(choice - 1) if (chaptersToDownload.isNotEmpty()) { downloadChapters(chaptersToDownload) } } - private fun getNextUnreadChaptersSorted(nextChapterId: Long?): List { + private suspend fun getNextUnreadChaptersSorted(nextChapterId: Long?): List { val chapterSort = ChapterSort(manga!!, chapterFilter, preferences) - return chapterList.map { ChapterItem(it.chapter, manga!!) } + return getChapterList().map { ChapterItem(it.chapter, manga!!) } .filter { !it.read || it.id == nextChapterId } .sortedWith(chapterSort.sortComparator(true)) .takeLastWhile { it.id != nextChapterId } @@ -594,6 +594,7 @@ class ReaderViewModel( */ private fun deleteChapterIfNeeded(currentChapter: ReaderChapter) { viewModelScope.launchNonCancellableIO { + val chapterList = getChapterList() // Determine which chapter should be deleted and enqueue val currentChapterPosition = chapterList.indexOf(currentChapter) val removeAfterReadSlots = preferences.removeAfterReadSlots().get() From 22978ab8bf70fe031e3ed3157b5159c07eeb28cc Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 9 Dec 2024 20:20:12 +0700 Subject: [PATCH 055/166] refactor(recents): Some adjustments --- .../tachiyomi/data/database/models/History.kt | 4 +- .../database/models/MangaChapterHistory.kt | 58 ++++++------- .../tachiyomi/ui/reader/ReaderViewModel.kt | 12 +-- .../sqldelight/tachiyomi/data/history.sq | 84 +++++++++---------- 4 files changed, 80 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/History.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/History.kt index ce2b07f966..11ec2e6c2b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/History.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/History.kt @@ -49,8 +49,8 @@ interface History : Serializable { ): History = HistoryImpl().apply { this.id = id this.chapter_id = chapterId - this.last_read = lastRead ?: 0L - this.time_read = timeRead ?: 0L + lastRead?.let { this.last_read = it } + timeRead?.let { this.time_read = it } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt index 109817670f..afe855bff3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.kt @@ -38,7 +38,7 @@ data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val histo coverLastModified: Long, // chapter chapterId: Long?, - _mangaId: Long?, + chapterMangaId: Long?, chapterUrl: String?, name: String?, scanlator: String?, @@ -80,36 +80,38 @@ data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val histo ) val chapter = try { - chapterId?.let { - Chapter.mapper( - id = chapterId, - mangaId = _mangaId ?: mangaId, - url = chapterUrl!!, - name = name!!, - scanlator = scanlator, - read = read!!, - bookmark = bookmark!!, - lastPageRead = lastPageRead!!, - pagesLeft = pagesLeft!!, - chapterNumber = chapterNumber!!, - sourceOrder = sourceOrder!!, - dateFetch = dateFetch!!, - dateUpload = dateUpload!!, - ) - } - } catch (_: NullPointerException) { null } ?: Chapter.create() + Chapter.mapper( + id = chapterId!!, + mangaId = chapterMangaId!!, + url = chapterUrl!!, + name = name!!, + scanlator = scanlator, + read = read!!, + bookmark = bookmark!!, + lastPageRead = lastPageRead!!, + pagesLeft = pagesLeft!!, + chapterNumber = chapterNumber!!, + sourceOrder = sourceOrder!!, + dateFetch = dateFetch!!, + dateUpload = dateUpload!!, + ) + } catch (_: NullPointerException) { + ChapterImpl() + } val history = try { - historyId?.let { - History.mapper( - id = historyId, - chapterId = historyChapterId ?: chapterId ?: 0L, - lastRead = historyLastRead, - timeRead = historyTimeRead, - ) + History.mapper( + id = historyId!!, + chapterId = historyChapterId!!, + lastRead = historyLastRead, + timeRead = historyTimeRead, + ) + } catch (_: NullPointerException) { + HistoryImpl().apply { + historyChapterId?.let { chapter_id = it } + historyLastRead?.let { last_read = it } + historyTimeRead?.let { time_read = it } } - } catch (_: NullPointerException) { null } ?: History.create().apply { - historyLastRead?.let { last_read = it } } return MangaChapterHistory(manga, chapter, history) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 9632fc4c64..21c860dc8c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -157,7 +157,7 @@ class ReaderViewModel( private var finished = false private var chapterToDownload: Download? = null - private var chapterList = emptyList() + private var chapterList: List? = null private var chapterItems = emptyList() @@ -215,7 +215,7 @@ class ReaderViewModel( * Whether this presenter is initialized yet. */ fun needsInit(): Boolean { - return manga == null + return manga == null || chapterList == null } /** @@ -246,7 +246,7 @@ class ReaderViewModel( loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) chapterList = getChapterList() - loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id }) + loadChapter(loader!!, chapterList!!.first { chapterId == it.chapter.id }) Result.success(true) } else { // Unlikely but okay @@ -402,11 +402,11 @@ class ReaderViewModel( ): ViewerChapters { loader.loadChapter(chapter) - val chapterPos = chapterList.indexOf(chapter) + val chapterPos = chapterList?.indexOf(chapter) ?: -1 val newChapters = ViewerChapters( chapter, - chapterList.getOrNull(chapterPos - 1), - chapterList.getOrNull(chapterPos + 1), + chapterList?.getOrNull(chapterPos - 1), + chapterList?.getOrNull(chapterPos + 1), ) withUIContext { diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq index cf4a9bb91b..4a5c245a4c 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq @@ -69,7 +69,7 @@ ON C._id = H.history_chapter_id AND H.history_last_read > 0 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 +AND C.scanlator = S.name WHERE lower(M.title) LIKE '%' || :search || '%' AND ( :apply_filter = 0 OR S.name IS NULL @@ -101,7 +101,7 @@ AND max_last_read.history_chapter_id = H.history_chapter_id AND max_last_read.history_last_read > 0 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 +AND C.scanlator = S.name WHERE lower(M.title) LIKE '%' || :search || '%' AND ( :apply_filter = 0 OR S.name IS NULL @@ -110,10 +110,10 @@ ORDER BY max_last_read.history_last_read DESC LIMIT :limit OFFSET :offset; getRecentsAll: -SELECT R.* FROM ( +SELECT R1.* FROM ( SELECT M.*, - chapters.*, + C.*, history.history_id AS history_id, history.history_chapter_id AS history_chapter_id, history.history_last_read AS history_last_read, @@ -122,71 +122,71 @@ FROM ( SELECT M2.* FROM mangas AS M2 LEFT JOIN ( - SELECT manga_id, COUNT(*) AS unread + SELECT manga_id, COUNT(*) AS value FROM chapters WHERE read = 0 GROUP BY manga_id - ) AS C - ON M2._id = C.manga_id + ) AS unread + ON M2._id = unread.manga_id WHERE ( - :include_read = 0 OR C.unread > 0 + :include_read = 0 OR unread.value > 0 ) GROUP BY M2._id ORDER BY title ) AS M -JOIN chapters -ON M._id = chapters.manga_id +JOIN chapters AS C +ON M._id = C.manga_id JOIN history -ON chapters._id = history.history_chapter_id +ON C._id = history.history_chapter_id JOIN ( SELECT - chapters.manga_id AS manga_id, - chapters._id AS history_chapter_id, - MAX(history.history_last_read) AS history_last_read - FROM chapters JOIN history - ON chapters._id = history.history_chapter_id - GROUP BY chapters.manga_id + C2.manga_id AS manga_id, + C2._id AS history_chapter_id, + MAX(H2.history_last_read) AS history_last_read + FROM chapters AS C2 JOIN history AS H2 + ON C2._id = H2.history_chapter_id + GROUP BY C2.manga_id ) AS max_last_read -ON chapters.manga_id = max_last_read.manga_id +ON C.manga_id = max_last_read.manga_id AND max_last_read.history_chapter_id = history.history_chapter_id AND max_last_read.history_last_read > 0 LEFT JOIN scanlators_view AS S -ON chapters.manga_id = S.manga_id -AND ifnull(chapters.scanlator, 'N/A') = ifnull(S.name, '//') -- I assume if it's N/A it shouldn't be filtered +ON C.manga_id = S.manga_id +AND C.scanlator = S.name WHERE lower(title) LIKE '%' || :search || '%' AND ( :apply_filter = 0 OR S.name IS NULL ) -) AS R +) AS R1 -UNION -- +UNION -- Newly added chapter -SELECT R.* FROM ( +SELECT R2.* FROM ( SELECT M.*, - chapters.*, + C.*, NULL AS history_id, NULL AS history_chapter_id, - chapters.date_fetch AS history_last_read, + C.date_fetch AS history_last_read, NULL AS history_time_read FROM mangas AS M -JOIN chapters -ON M._id = chapters.manga_id +JOIN chapters AS C +ON M._id = C.manga_id JOIN history -ON chapters._id = history.history_chapter_id +ON C._id = history.history_chapter_id JOIN ( SELECT - manga_id, - chapters._id AS history_chapter_id, + C2.manga_id, + C2._id AS history_chapter_id, max(date_upload) - FROM chapters JOIN mangas AS M2 - ON M2._id = manga_id - WHERE read = 0 - GROUP BY manga_id + FROM chapters AS C2 JOIN mangas AS M2 + ON M2._id = C2.manga_id + WHERE C2.read = 0 + GROUP BY C2.manga_id ) AS newest_chapter LEFT JOIN scanlators_view AS S -ON chapters.manga_id = S.manga_id -AND ifnull(chapters.scanlator, 'N/A') = ifnull(S.name, '//') -- I assume if it's N/A it shouldn't be filtered +ON C.manga_id = S.manga_id +AND C.scanlator = S.name WHERE favorite = 1 AND newest_chapter.history_chapter_id = history.history_chapter_id AND date_fetch > date_added @@ -194,14 +194,14 @@ AND lower(title) LIKE '%' || :search || '%' AND ( :apply_filter = 0 OR S.name IS NULL ) -) AS R +) AS R2 -UNION -- +UNION -- Newly added manga -SELECT R.* FROM ( +SELECT R3.* FROM ( SELECT M.*, - chapters.*, + C.*, NULL AS history_id, NULL AS history_chapter_id, M.date_added AS history_last_read, @@ -222,9 +222,9 @@ JOIN ( NULL AS pages_left, NULL AS chapter_number, NULL AS source_order -) AS chapters +) AS C WHERE favorite = 1 AND lower(title) LIKE '%' || :search || '%' -) AS R +) AS R3 ORDER BY history_last_read DESC LIMIT :limit OFFSET :offset; From d3c98fb89785193ab3a7300d548c7950d6ec45a6 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 9 Dec 2024 21:57:17 +0700 Subject: [PATCH 056/166] fix(recents): Can't open chapters from Grouped and All id column name for mangas and chapter is both _id causing it conflict when doing 'Rn.*'. In fact, 'Rn.*' is not even needed for union, it just needs to be on the same order, same type, and have the same number of columns. --- CHANGELOG.md | 3 +++ .../eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt | 8 +++++--- data/src/commonMain/sqldelight/tachiyomi/data/history.sq | 7 +------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a0c519de..bc8b4d0f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +### Fixes +- Fix chapters cannot be opened from `Recents > Grouped` and `Recents > All` + ## [1.9.0] ### Additions diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index 644535f45a..c07d9213dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -179,7 +179,7 @@ class RecentsPresenter( RecentsViewType.GroupedAll, RecentsViewType.UngroupedAll -> { getRecents.awaitAll( showRead, - true, + false, isEndless, !updatePageCount && !isOnFirstPage, query, @@ -466,12 +466,14 @@ class RecentsPresenter( } private suspend fun getNextChapter(manga: Manga): Chapter? { - val chapters = getChapter.awaitAll(manga) + val mangaId = manga.id ?: return null + val chapters = getChapter.awaitAll(mangaId, true) return ChapterSort(manga, chapterFilter, preferences).getNextUnreadChapter(chapters, false) } private suspend fun getFirstUpdatedChapter(manga: Manga, chapter: Chapter): Chapter? { - val chapters = getChapter.awaitAll(manga) + val mangaId = manga.id ?: return null + val chapters = getChapter.awaitAll(mangaId, true) return chapters .sortedWith(ChapterSort(manga, chapterFilter, preferences).sortComparator(true)).find { !it.read && abs(it.date_fetch - chapter.date_fetch) <= TimeUnit.HOURS.toMillis(12) diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq index 4a5c245a4c..95ebbe6c57 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq @@ -110,7 +110,6 @@ ORDER BY max_last_read.history_last_read DESC LIMIT :limit OFFSET :offset; getRecentsAll: -SELECT R1.* FROM ( SELECT M.*, C.*, @@ -157,11 +156,9 @@ WHERE lower(title) LIKE '%' || :search || '%' AND ( :apply_filter = 0 OR S.name IS NULL ) -) AS R1 UNION -- Newly added chapter -SELECT R2.* FROM ( SELECT M.*, C.*, @@ -194,11 +191,9 @@ AND lower(title) LIKE '%' || :search || '%' AND ( :apply_filter = 0 OR S.name IS NULL ) -) AS R2 UNION -- Newly added manga -SELECT R3.* FROM ( SELECT M.*, C.*, @@ -225,6 +220,6 @@ JOIN ( ) AS C WHERE favorite = 1 AND lower(title) LIKE '%' || :search || '%' -) AS R3 + ORDER BY history_last_read DESC LIMIT :limit OFFSET :offset; From e45baf6ab477656ab7765cbc130d3efd1dbd4462 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 9 Dec 2024 22:16:35 +0700 Subject: [PATCH 057/166] revert(recents): Filter scanlator I forgot to turn this back on --- .../java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index c07d9213dd..5c8dc92eb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -179,7 +179,7 @@ class RecentsPresenter( RecentsViewType.GroupedAll, RecentsViewType.UngroupedAll -> { getRecents.awaitAll( showRead, - false, + true, isEndless, !updatePageCount && !isOnFirstPage, query, From fe59d7f4ec722d647f85f6cd6d53dbe3ea6d9737 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 07:43:12 +0700 Subject: [PATCH 058/166] chore(deps): Add leakcanary to debug memleaks --- app/build.gradle.kts | 5 +++++ gradle/libs.versions.toml | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a6326ad233..b8eda7304c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -269,6 +269,11 @@ dependencies { testRuntimeOnly(libs.bundles.test.runtime) androidTestImplementation(libs.bundles.test.android) testImplementation(kotlinx.coroutines.test) + + // For detecting memory leaks + // REF: https://square.github.io/leakcanary/ + // debugImplementation(libs.leakcanary.android) + implementation(libs.leakcanary.plumber) } tasks { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 78bee4df5f..cc94fa7fb5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ sqldelight = "2.0.2" junit = "5.11.3" kermit = "2.0.5" koin = "4.0.0" +leakcanary = "2.14" voyager = "1.1.0-beta02" [libraries] @@ -50,6 +51,9 @@ koin-injekt = { module = "com.github.null2264:injekt-koin", version = "aad18b614 kotest-assertions = { module = "io.kotest:kotest-assertions-core", version = "5.9.1" } +leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } +leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" } + libarchive = { module = "me.zhanghai.android.libarchive:library", version = "1.1.4" } material = { module = "com.google.android.material:material", version = "1.12.0" } From 6935ae545c0b0b7439ed15d7116fbe4714ef92c2 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 07:54:53 +0700 Subject: [PATCH 059/166] chore(deps): Uncomment leakcanary on debug build --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b8eda7304c..f7ca95f70b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -272,7 +272,7 @@ dependencies { // For detecting memory leaks // REF: https://square.github.io/leakcanary/ - // debugImplementation(libs.leakcanary.android) + debugImplementation(libs.leakcanary.android) implementation(libs.leakcanary.plumber) } From 41319660f698e84f2b3e7f9b2b8aab4f0ba4aa64 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:06:32 +0700 Subject: [PATCH 060/166] fix(deps): Update dependency io.github.kevinnzou:compose-webview to v0.33.6 (#280) * fix(deps): Update dependency io.github.kevinnzou:compose-webview to v0.33.6 * fix: Namespace change --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ahmad Ansori Palembani --- .../yokai/presentation/webview/WebViewScreenContent.kt | 10 +++++----- gradle/compose.versions.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/yokai/presentation/webview/WebViewScreenContent.kt b/app/src/main/java/yokai/presentation/webview/WebViewScreenContent.kt index 3e3e3cacb9..8bd4bb4cb6 100644 --- a/app/src/main/java/yokai/presentation/webview/WebViewScreenContent.kt +++ b/app/src/main/java/yokai/presentation/webview/WebViewScreenContent.kt @@ -33,11 +33,11 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp -import com.kevinnzou.accompanist.web.AccompanistWebViewClient -import com.kevinnzou.accompanist.web.LoadingState -import com.kevinnzou.accompanist.web.WebView -import com.kevinnzou.accompanist.web.rememberWebViewNavigator -import com.kevinnzou.accompanist.web.rememberWebViewState +import com.kevinnzou.web.AccompanistWebViewClient +import com.kevinnzou.web.LoadingState +import com.kevinnzou.web.WebView +import com.kevinnzou.web.rememberWebViewNavigator +import com.kevinnzou.web.rememberWebViewState import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.util.system.extensionIntentForText diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 02dec18450..30249349c2 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -10,7 +10,7 @@ material-motion = { module = "io.github.fornewid:material-motion-compose-core", ui-tooling = { module = "androidx.compose.ui:ui-tooling" } ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } icons = { module = "androidx.compose.material:material-icons-extended" } -webview = { module = "io.github.kevinnzou:compose-webview", version = "0.33.3" } +webview = { module = "io.github.kevinnzou:compose-webview", version = "0.33.6" } [bundles] compose = [ "animation", "foundation", "material3", "material-motion", "ui-tooling-preview", "icons" ] From c6f6718d30fed1f75d639f404661464f70d14ea0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:06:56 +0700 Subject: [PATCH 061/166] fix(deps): Update dependency org.jsoup:jsoup to v1.18.3 (#281) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cc94fa7fb5..db9e114140 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ material = { module = "com.google.android.material:material", version = "1.12.0" markwon = { module = "io.noties.markwon:core", version = "4.6.2" } mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version = "v3.1.0" } java-nat-sort = { module = "com.github.gpanther:java-nat-sort", version = "natural-comparator-1.1" } -jsoup = { module = "org.jsoup:jsoup", version = "1.18.1" } +jsoup = { module = "org.jsoup:jsoup", version = "1.18.3" } junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-android = { module = "androidx.test.ext:junit", version = "1.2.1" } From e1bf13f1d9570cd2a5c705daee34136ed3e306c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:07:13 +0700 Subject: [PATCH 062/166] fix(deps): Update voyager to v1.1.0-beta03 (#282) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index db9e114140..1a6e7d5a1f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ junit = "5.11.3" kermit = "2.0.5" koin = "4.0.0" leakcanary = "2.14" -voyager = "1.1.0-beta02" +voyager = "1.1.0-beta03" [libraries] aboutlibraries = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" } From 1504d94f52f5c0fb937275df9a85a079cdb35bb7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:07:42 +0700 Subject: [PATCH 063/166] fix(deps): Update dependency androidx.annotation:annotation to v1.9.1 (#283) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/androidx.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 79c411f324..950b5cb7ef 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -6,7 +6,7 @@ lifecycle = "2.8.7" [libraries] activity = { module = "androidx.activity:activity-ktx", version.ref = "activity" } activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } -annotation = { module = "androidx.annotation:annotation", version = "1.8.2" } +annotation = { module = "androidx.annotation:annotation", version = "1.9.1" } appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0" } browser = { module = "androidx.browser:browser", version = "1.8.0" } biometric = { module = "androidx.biometric:biometric", version = "1.1.0" } From 4cdcf62351f3aa33a2734770c5100202dd0f2e92 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:08:05 +0700 Subject: [PATCH 064/166] fix(deps): Update dependency androidx.constraintlayout:constraintlayout to v2.2.0 (#284) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/androidx.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 950b5cb7ef..aaf608b3e9 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -14,7 +14,7 @@ cardview = { module = "androidx.cardview:cardview", version = "1.0.0" } core = { module = "androidx.core:core-ktx", version = "1.15.0" } core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" } glance-appwidget = { module = "androidx.glance:glance-appwidget", version = "1.0.0" } -layout-constraint = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.4" } +layout-constraint = { module = "androidx.constraintlayout:constraintlayout", version = "2.2.0" } layout-swiperefresh = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version = "1.1.0" } lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle" } lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } From ee52e6ecf787a13d639a75a787dc6d321ee0f333 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:08:45 +0700 Subject: [PATCH 065/166] fix(deps): Update dependency com.google.firebase:firebase-bom to v33.7.0 (#286) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a6e7d5a1f..f9379bebc3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ directionalviewpager = { module = "com.github.tachiyomiorg:DirectionalViewPager" disklrucache = { module = "com.jakewharton:disklrucache", version = "2.0.2" } fastadapter-extensions-binding = { module = "com.mikepenz:fastadapter-extensions-binding", version.ref = "fast_adapter" } fastadapter = { module = "com.mikepenz:fastadapter", version.ref = "fast_adapter" } -firebase = { module = "com.google.firebase:firebase-bom", version = "33.6.0" } +firebase = { module = "com.google.firebase:firebase-bom", version = "33.7.0" } firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } flexbox = { module = "com.google.android.flexbox:flexbox", version = "3.0.0" } From d780d6ceb13591e440cb824912e08a6436f403c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:09:32 +0700 Subject: [PATCH 066/166] fix(deps): Update dependency androidx.glance:glance-appwidget to v1.1.1 (#285) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/androidx.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index aaf608b3e9..c65c235f27 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -13,7 +13,7 @@ biometric = { module = "androidx.biometric:biometric", version = "1.1.0" } cardview = { module = "androidx.cardview:cardview", version = "1.0.0" } core = { module = "androidx.core:core-ktx", version = "1.15.0" } core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" } -glance-appwidget = { module = "androidx.glance:glance-appwidget", version = "1.0.0" } +glance-appwidget = { module = "androidx.glance:glance-appwidget", version = "1.1.1" } layout-constraint = { module = "androidx.constraintlayout:constraintlayout", version = "2.2.0" } layout-swiperefresh = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version = "1.1.0" } lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle" } From 9918c407c85432a1934f587129c4276523ccd903 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:09:55 +0700 Subject: [PATCH 067/166] fix(deps): Update fast.adapter to v5.7.0 (#287) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9379bebc3..25ca20a78c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ aboutlibraries = "11.2.3" chucker = "3.5.2" flexible-adapter = "c8013533" -fast_adapter = "5.6.0" +fast_adapter = "5.7.0" moko = "0.24.4" okhttp = "5.0.0-alpha.14" shizuku = "13.1.5" From 618109c80ed2f5ddb2d4ee5fc537737665f7854b Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 08:10:07 +0700 Subject: [PATCH 068/166] docs: Sync changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc8b4d0f91..41ee5d072e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,16 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ### Fixes - Fix chapters cannot be opened from `Recents > Grouped` and `Recents > All` +### Other +- Update dependency io.github.kevinnzou:compose-webview to v0.33.6 +- Update dependency org.jsoup:jsoup to v1.18.3 +- Update voyager to v1.1.0-beta03 +- Update dependency androidx.annotation:annotation to v1.9.1 +- Update dependency androidx.constraintlayout:constraintlayout to v2.2.0 +- Update dependency androidx.glance:glance-appwidget to v1.1.1 +- Update dependency com.google.firebase:firebase-bom to v33.7.0 +- Update fast.adapter to v5.7.0 + ## [1.9.0] ### Additions From 160a7109da2949759fd1fd0a9bf91c5ceec45a88 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 09:30:07 +0700 Subject: [PATCH 069/166] ci: Don't leave changelog empty --- .github/workflows/build_push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index e26e30d509..720bc4771a 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -69,7 +69,7 @@ jobs: VERSION_FORMAT='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?(-[0-9A-Za-z\.-]+)?(\+[0-9A-Za-z\.-]+)?$|^Unreleased$' { echo "CHANGELOG<> "$GITHUB_OUTPUT" 2> /dev/null From 1cb635999ef5003508eda3fae743e4012659d0b9 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 09:34:36 +0700 Subject: [PATCH 070/166] docs(sql): Documentate this insanity of a query [skip ci] --- data/src/commonMain/sqldelight/tachiyomi/data/history.sq | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq index 95ebbe6c57..35f29d6ecd 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq @@ -110,14 +110,14 @@ ORDER BY max_last_read.history_last_read DESC LIMIT :limit OFFSET :offset; getRecentsAll: -SELECT +SELECT -- Recently read manga M.*, C.*, history.history_id AS history_id, history.history_chapter_id AS history_chapter_id, history.history_last_read AS history_last_read, history.history_time_read AS history_time_read -FROM ( +FROM ( -- Check if there's any unread chapters and whether to include read chapters SELECT M2.* FROM mangas AS M2 LEFT JOIN ( @@ -145,7 +145,7 @@ JOIN ( FROM chapters AS C2 JOIN history AS H2 ON C2._id = H2.history_chapter_id GROUP BY C2.manga_id -) AS max_last_read +) AS max_last_read -- Most recent chapters ON C.manga_id = max_last_read.manga_id AND max_last_read.history_chapter_id = history.history_chapter_id AND max_last_read.history_last_read > 0 @@ -157,7 +157,7 @@ AND ( :apply_filter = 0 OR S.name IS NULL ) -UNION -- Newly added chapter +UNION -- Newly fetched chapter SELECT M.*, From 119b1c64b27a5a0bf1048ce25ea85017e5bf1412 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 10:29:28 +0700 Subject: [PATCH 071/166] fix(source/local): Don't crash trying to get manga language XML is pain --- .../java/eu/kanade/tachiyomi/source/LocalSource.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index 6844263359..c766b813a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -69,11 +69,18 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour .filter { !it.isDirectory } .firstOrNull { it.name == COMIC_INFO_FILE } - return if (localDetails != null) { - decodeComicInfo(localDetails.openInputStream()).language?.value ?: "other" + val lang = if (localDetails != null) { + try { + decodeComicInfo(localDetails.openInputStream()).language?.value + } catch (e: Exception) { + Logger.e(e) { "Unable to retrieve manga language" } + null + } } else { - "other" + null } + + return lang ?: "other" } } From 158049d4f44e26656d2740940dec7320c8fd99ba Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 10:31:09 +0700 Subject: [PATCH 072/166] docs: Sync changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ee5d072e..ae5b2a2f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ### Fixes - Fix chapters cannot be opened from `Recents > Grouped` and `Recents > All` +- Fix crashes caused by malformed XML ### Other - Update dependency io.github.kevinnzou:compose-webview to v0.33.6 From 05b20e00e0aea9ff68b68b1edaa074659291277b Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 12:05:55 +0700 Subject: [PATCH 073/166] fix(deps): Downgrade dependency org.conscrypt:conscrypt-android to v2.5.2 REF: https://github.com/google/conscrypt/issues/1268 --- CHANGELOG.md | 1 + gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae5b2a2f50..c2d5f2f938 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Update dependency androidx.glance:glance-appwidget to v1.1.1 - Update dependency com.google.firebase:firebase-bom to v33.7.0 - Update fast.adapter to v5.7.0 +- Downgrade dependency org.conscrypt:conscrypt-android to v2.5.2 ## [1.9.0] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 25ca20a78c..f7413613b8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ coil3-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" } compose-theme-adapter3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version = "0.33.2-alpha" } conductor = { module = "com.bluelinelabs:conductor", version = "4.0.0-preview-4" } conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version = "3.0.0" } -conscrypt = { module = "org.conscrypt:conscrypt-android", version = "2.5.3" } +conscrypt = { module = "org.conscrypt:conscrypt-android", version = "2.5.2" } desugar = { module = "com.android.tools:desugar_jdk_libs", version = "2.1.3" } directionalviewpager = { module = "com.github.tachiyomiorg:DirectionalViewPager", version = "1.0.0" } disklrucache = { module = "com.jakewharton:disklrucache", version = "2.0.2" } From 53f8a37c8e67b497302ae33748c2dc79596f829c Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 12:14:36 +0700 Subject: [PATCH 074/166] chore: Bump version to v1.9.1 for beta and nightly --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f7ca95f70b..b19167dcc5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ fun runCommand(command: String): String { return String(byteOut.toByteArray()).trim() } -val _versionName = "1.9.0" +val _versionName = "1.9.1" val betaCount by lazy { val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") From 88959d956fa7a4e1a3b7d3438bd23a185236255f Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 12:52:51 +0700 Subject: [PATCH 075/166] fix(cover): Recycle bitmap after compression --- app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt index c1a969f0d4..6157819c27 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt @@ -205,6 +205,7 @@ class CoverCache(val context: Context) { 100, it ) + bitmap.recycle() } } From 8c8b2f9634c29686da7ee12b35e72c51a8ecb712 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 13:28:22 +0700 Subject: [PATCH 076/166] chore(release): v1.9.1 --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/ISSUE_TEMPLATE/issue_report.yml | 2 +- CHANGELOG.md | 3 +++ app/build.gradle.kts | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 241ffa0f67..5bda7296a3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -35,7 +35,7 @@ body: required: true - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. required: true - - label: I have updated the app to version **[1.9.0](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.1](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have checked through the app settings for my feature. required: true diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index c0b9135137..b8aab2e172 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -100,7 +100,7 @@ body: required: true - label: I have tried the [troubleshooting guide](https://mihon.app/help/). required: true - - label: I have updated the app to version **[1.9.0](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.1](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have updated all installed extensions. required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d5f2f938..29850768e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,12 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +## [1.9.1] + ### Fixes - Fix chapters cannot be opened from `Recents > Grouped` and `Recents > All` - Fix crashes caused by malformed XML +- Fix potential memory leak ### Other - Update dependency io.github.kevinnzou:compose-webview to v0.33.6 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b19167dcc5..21d0600ac0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 150 + versionCode = 151 versionName = _versionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true From 87c13b44ab01bfdfdb46a8a6c0cc058f01cb1aaa Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 10 Dec 2024 13:44:23 +0700 Subject: [PATCH 077/166] chore: Bump version to v1.9.2 for beta and nightly [skip ci] --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 21d0600ac0..331209962d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ fun runCommand(command: String): String { return String(byteOut.toByteArray()).trim() } -val _versionName = "1.9.1" +val _versionName = "1.9.2" val betaCount by lazy { val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") From 8e7f5d889712fcb5571e5181e2883f8c60f67c5d Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 11 Dec 2024 11:22:42 +0700 Subject: [PATCH 078/166] style(chapter): Adjust contrast --- CHANGELOG.md | 3 +++ .../eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29850768e1..381e6fcb95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +### Changes +- Adjust chapter title-details contrast + ## [1.9.1] ### Fixes diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt index e1176d7f57..d5bd0ac97c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterUtil.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.util.chapter import android.content.Context import android.content.res.ColorStateList import android.widget.TextView +import androidx.core.graphics.ColorUtils import androidx.core.widget.TextViewCompat import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import eu.kanade.tachiyomi.R @@ -102,12 +103,11 @@ class ChapterUtil { private fun readColor(context: Context): Int = context.contextCompatColor(R.color.read_chapter) - private fun unreadColor(context: Context, secondary: Boolean = false): Int = - if (!secondary) { - context.getResourceColor(R.attr.colorOnBackground) - } else { - context.getResourceColor(android.R.attr.textColorSecondary) - } + private fun unreadColor(context: Context, secondary: Boolean = false): Int { + val color = context.getResourceColor(R.attr.colorOnSurface) + // 78% alpha for chapter details, 100% for chapter number/title + return ColorUtils.setAlphaComponent(color, if (secondary) 198 else 255) + } private fun bookmarkedColor(context: Context): Int = context.getResourceColor(R.attr.colorSecondary) From b4e3dcfddae51ac8f808609ab7d95e44f936f7d5 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 11 Dec 2024 19:21:41 +0700 Subject: [PATCH 079/166] refactor(reader): Replace rx with kotlin coroutine --- .../tachiyomi/ui/reader/ReaderViewModel.kt | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 21c860dc8c..88374bddb0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -69,8 +69,6 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import rx.Completable -import rx.schedulers.Schedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -1012,13 +1010,9 @@ class ReaderViewModel( if (!chapter.chapter.read) return val manga = manga ?: return - Completable - .fromCallable { - downloadManager.enqueueDeleteChapters(listOf(chapter.chapter), manga) - } - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + viewModelScope.launchNonCancellableIO { + downloadManager.enqueueDeleteChapters(listOf(chapter.chapter), manga) + } } /** @@ -1026,10 +1020,9 @@ class ReaderViewModel( * are ignored. */ private fun deletePendingChapters() { - Completable.fromCallable { downloadManager.deletePendingChapters() } - .onErrorComplete() - .subscribeOn(Schedulers.io()) - .subscribe() + viewModelScope.launchNonCancellableIO { + downloadManager.deletePendingChapters() + } } data class State( From dbf5a7efcdf9b3c04083d6b38140c921cbd88128 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 11 Dec 2024 19:31:53 +0700 Subject: [PATCH 080/166] fix(reader): Some desync issue causing download's remove after read to not work properly --- .../kanade/tachiyomi/ui/reader/ReaderViewModel.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 88374bddb0..cf0a856541 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -155,7 +155,7 @@ class ReaderViewModel( private var finished = false private var chapterToDownload: Download? = null - private var chapterList: List? = null + private lateinit var chapterList: List private var chapterItems = emptyList() @@ -213,7 +213,7 @@ class ReaderViewModel( * Whether this presenter is initialized yet. */ fun needsInit(): Boolean { - return manga == null || chapterList == null + return manga == null || !this::chapterList.isInitialized } /** @@ -400,11 +400,11 @@ class ReaderViewModel( ): ViewerChapters { loader.loadChapter(chapter) - val chapterPos = chapterList?.indexOf(chapter) ?: -1 + val chapterPos = chapterList.indexOf(chapter) ?: -1 val newChapters = ViewerChapters( chapter, - chapterList?.getOrNull(chapterPos - 1), - chapterList?.getOrNull(chapterPos + 1), + chapterList.getOrNull(chapterPos - 1), + chapterList.getOrNull(chapterPos + 1), ) withUIContext { @@ -561,7 +561,7 @@ class ReaderViewModel( private suspend fun getNextUnreadChaptersSorted(nextChapterId: Long?): List { val chapterSort = ChapterSort(manga!!, chapterFilter, preferences) - return getChapterList().map { ChapterItem(it.chapter, manga!!) } + return chapterList.map { ChapterItem(it.chapter, manga!!) } .filter { !it.read || it.id == nextChapterId } .sortedWith(chapterSort.sortComparator(true)) .takeLastWhile { it.id != nextChapterId } @@ -592,7 +592,6 @@ class ReaderViewModel( */ private fun deleteChapterIfNeeded(currentChapter: ReaderChapter) { viewModelScope.launchNonCancellableIO { - val chapterList = getChapterList() // Determine which chapter should be deleted and enqueue val currentChapterPosition = chapterList.indexOf(currentChapter) val removeAfterReadSlots = preferences.removeAfterReadSlots().get() From eeb572740ac90017efb77808becbd8a46003b55a Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 11 Dec 2024 19:53:34 +0700 Subject: [PATCH 081/166] style(AppUpdateNotifier): Fix consistency --- CHANGELOG.md | 4 ++++ .../eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 381e6fcb95..54c4f1db1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ### Changes - Adjust chapter title-details contrast +- Make app updater notification consistent with other notifications + +### Fixes +- Fix "Remove from read" not working properly ## [1.9.1] diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt index 68780b5942..9cc2042f15 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt @@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationManager import yokai.i18n.MR import yokai.util.lang.getString @@ -30,7 +31,7 @@ internal class AppUpdateNotifier(private val context: Context) { * Builder to manage notifications. */ val notificationBuilder by lazy { - NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).apply { + context.notificationBuilder(Notifications.CHANNEL_COMMON).apply { setSmallIcon(AR.drawable.stat_sys_download) setContentTitle(context.getString(MR.strings.app_name)) } From 1bc107f26b46d130a44d1efd2e262b4ed74daa44 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 11 Dec 2024 20:16:22 +0700 Subject: [PATCH 082/166] chore(release): v1.9.2 --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/ISSUE_TEMPLATE/issue_report.yml | 2 +- CHANGELOG.md | 2 ++ app/build.gradle.kts | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 5bda7296a3..0894b9d7d6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -35,7 +35,7 @@ body: required: true - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. required: true - - label: I have updated the app to version **[1.9.1](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.2](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have checked through the app settings for my feature. required: true diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index b8aab2e172..3fb191542a 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -100,7 +100,7 @@ body: required: true - label: I have tried the [troubleshooting guide](https://mihon.app/help/). required: true - - label: I have updated the app to version **[1.9.1](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.2](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have updated all installed extensions. required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 54c4f1db1c..eeff61d9d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +## [1.9.2] + ### Changes - Adjust chapter title-details contrast - Make app updater notification consistent with other notifications diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 331209962d..31dc91f31c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 151 + versionCode = 152 versionName = _versionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true From 365875590f9267f076daec61311e6667be76260e Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 11 Dec 2024 20:23:42 +0700 Subject: [PATCH 083/166] chore: Bump version to v1.9.3 for beta and nightly --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31dc91f31c..896e486ae6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ fun runCommand(command: String): String { return String(byteOut.toByteArray()).trim() } -val _versionName = "1.9.2" +val _versionName = "1.9.3" val betaCount by lazy { val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") From ea0496858144b3ede830aec640ca6b669c3ec158 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 12 Dec 2024 11:00:46 +0700 Subject: [PATCH 084/166] refactor(ChapterSourceSync): Simplify code and insert new chapters in bulk --- .../tachiyomi/data/database/models/Chapter.kt | 4 + .../util/chapter/ChapterSourceSync.kt | 92 +++++++++---------- .../data/chapter/ChapterRepositoryImpl.kt | 17 ++-- .../yokai/domain/chapter/ChapterRepository.kt | 4 +- .../chapter/interactor/DeleteChapter.kt | 2 +- 5 files changed, 60 insertions(+), 59 deletions(-) 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 d1ed846c03..1773e214eb 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 @@ -90,4 +90,8 @@ interface Chapter : SChapter, Serializable { source_order = other.source_order copyFrom(other as SChapter) } + + fun copy() = ChapterImpl().apply { + copyFrom(this@Chapter) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 3930be9bb8..bda3a024bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -117,53 +117,58 @@ suspend fun syncChaptersWithSource( // Return if there's nothing to add, delete or change, avoid unnecessary db transactions. if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { val newestDate = dbChapters.maxOfOrNull { it.date_upload } ?: 0L - if (newestDate != 0L && newestDate != manga.last_update) { + if (newestDate != 0L && newestDate > manga.last_update) { manga.last_update = newestDate - val update = MangaUpdate(manga.id!!, lastUpdate = manga.last_update) + val update = MangaUpdate(manga.id!!, lastUpdate = newestDate) updateManga.await(update) } return Pair(emptyList(), emptyList()) } - val readded = mutableListOf() + val reAdded = mutableListOf() val deletedChapterNumbers = TreeSet() val deletedReadChapterNumbers = TreeSet() - if (toDelete.isNotEmpty()) { - for (c in toDelete) { - if (c.read) { - deletedReadChapterNumbers.add(c.chapter_number) - } - deletedChapterNumbers.add(c.chapter_number) - } - deleteChapter.awaitAll(toDelete) + val deletedBookmarkedChapterNumbers = TreeSet() + toDelete.forEach { + if (it.read) deletedReadChapterNumbers.add(it.chapter_number) + if (it.bookmark) deletedBookmarkedChapterNumbers.add(it.chapter_number) + deletedChapterNumbers.add(it.chapter_number) } - if (toAdd.isNotEmpty()) { - // Set the date fetch for new items in reverse order to allow another sorting method. - // Sources MUST return the chapters from most to less recent, which is common. - var now = Date().time + val now = Date().time - for (i in toAdd.indices.reversed()) { - val chapter = toAdd[i] - chapter.date_fetch = now++ - if (chapter.isRecognizedNumber && chapter.chapter_number in deletedChapterNumbers) { - // Try to mark already read chapters as read when the source deletes them - if (chapter.chapter_number in deletedReadChapterNumbers) { - chapter.read = true - } - // Try to to use the fetch date it originally had to not pollute 'Updates' tab - toDelete.filter { it.chapter_number == chapter.chapter_number } - .minByOrNull { it.date_fetch }?.let { - chapter.date_fetch = it.date_fetch - } + // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones + // Sources MUST return the chapters from most to less recent, which is common. + var itemCount = toAdd.size + var updatedToAdd = toAdd.map { toAddItem -> + val chapter: Chapter = toAddItem.copy() - readded.add(chapter) + chapter.date_fetch = now + itemCount-- + + if (!chapter.isRecognizedNumber || chapter.chapter_number !in deletedChapterNumbers) return@map chapter + + chapter.read = chapter.chapter_number in deletedReadChapterNumbers + chapter.bookmark = chapter.chapter_number in deletedBookmarkedChapterNumbers + + // Try to use the fetch date it originally had to not pollute 'Updates' tab + toDelete.filter { it.chapter_number == chapter.chapter_number } + .minByOrNull { it.date_fetch }?.let { + chapter.date_fetch = it.date_fetch } - } - toAdd.forEach { chapter -> - chapter.id = insertChapter.await(chapter) - } + + reAdded.add(chapter) + + chapter + } + + if (toDelete.isNotEmpty()) { + val idsToDelete = toDelete.mapNotNull { it.id } + deleteChapter.awaitAllById(idsToDelete) + } + + if (updatedToAdd.isNotEmpty()) { + updatedToAdd = insertChapter.awaitBulk(toAdd) } if (toChange.isNotEmpty()) { @@ -182,24 +187,15 @@ suspend fun syncChaptersWithSource( } } - var mangaUpdate: MangaUpdate? = null // Set this manga as updated since chapters were changed - val newestChapterDate = getChapter.awaitAll(manga, false) - .maxOfOrNull { it.date_upload } ?: 0L - if (newestChapterDate == 0L) { - if (toAdd.isNotEmpty()) { - manga.last_update = Date().time - mangaUpdate = MangaUpdate(manga.id!!, lastUpdate = manga.last_update) - } - } else { - manga.last_update = newestChapterDate - mangaUpdate = MangaUpdate(manga.id!!, lastUpdate = manga.last_update) - } - mangaUpdate?.let { updateManga.await(it) } + // Note that last_update actually represents last time the chapter list changed at all + // Those changes already checked beforehand, so we can proceed to updating the manga + manga.last_update = Date().time + updateManga.await(MangaUpdate(manga.id!!, lastUpdate = manga.last_update)) - val reAddedSet = readded.toSet() + val reAddedSet = reAdded.toSet() return Pair( - toAdd.subtract(reAddedSet).toList().filterChaptersByScanlators(manga), + updatedToAdd.subtract(reAddedSet).toList().filterChaptersByScanlators(manga), toDelete - reAddedSet, ) } diff --git a/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt b/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt index feabecc904..1b38b71a9f 100644 --- a/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt +++ b/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt @@ -54,27 +54,26 @@ class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepos override suspend fun delete(chapter: Chapter) = try { - partialDelete(chapter) + partialDelete(chapter.id!!) true } catch (e: Exception) { Logger.e(e) { "Failed to delete chapter with id '${chapter.id}'" } false } - override suspend fun deleteAll(chapters: List) = + override suspend fun deleteAllById(chapters: List) = try { - partialDelete(*chapters.toTypedArray()) + partialDelete(*chapters.toLongArray()) true } catch (e: Exception) { Logger.e(e) { "Failed to bulk delete chapters" } false } - private suspend fun partialDelete(vararg chapters: Chapter) { + private suspend fun partialDelete(vararg chapterIds: Long) { handler.await(inTransaction = true) { - chapters.forEach { chapter -> - if (chapter.id == null) return@forEach - chaptersQueries.delete(chapter.id!!) + chapterIds.forEach { chapterId -> + chaptersQueries.delete(chapterId) } } } @@ -143,7 +142,7 @@ class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepos override suspend fun insertBulk(chapters: List) = handler.await(true) { - chapters.forEach { chapter -> + chapters.map { chapter -> chaptersQueries.insert( mangaId = chapter.manga_id!!, url = chapter.url, @@ -158,6 +157,8 @@ class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepos dateFetch = chapter.date_fetch, dateUpload = chapter.date_upload, ) + val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne() + chapter.copy().apply { id = lastInsertId } } } } diff --git a/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt b/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt index dbeae5b66a..dd39b017a1 100644 --- a/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt +++ b/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt @@ -23,11 +23,11 @@ interface ChapterRepository { fun getScanlatorsByChapterAsFlow(mangaId: Long): Flow> suspend fun delete(chapter: Chapter): Boolean - suspend fun deleteAll(chapters: List): Boolean + suspend fun deleteAllById(chapters: List): Boolean suspend fun update(update: ChapterUpdate): Boolean suspend fun updateAll(updates: List): Boolean suspend fun insert(chapter: Chapter): Long? - suspend fun insertBulk(chapters: List) + suspend fun insertBulk(chapters: List): List } diff --git a/app/src/main/java/yokai/domain/chapter/interactor/DeleteChapter.kt b/app/src/main/java/yokai/domain/chapter/interactor/DeleteChapter.kt index d3c09abbc9..9e71f51dc5 100644 --- a/app/src/main/java/yokai/domain/chapter/interactor/DeleteChapter.kt +++ b/app/src/main/java/yokai/domain/chapter/interactor/DeleteChapter.kt @@ -7,5 +7,5 @@ class DeleteChapter( private val chapterRepository: ChapterRepository, ) { suspend fun await(chapter: Chapter) = chapterRepository.delete(chapter) - suspend fun awaitAll(chapters: List) = chapterRepository.deleteAll(chapters) + suspend fun awaitAllById(chapterIds: List) = chapterRepository.deleteAllById(chapterIds) } From 1c996f9a591290f3ae8e1788a85784ac8dafdf6d Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 12 Dec 2024 11:20:23 +0700 Subject: [PATCH 085/166] docs: Sync changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeff61d9d9..5e2d8d831e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +### Fixes +- Fix slow chapter load +- Fix chapter bookmark state is not persistent + ## [1.9.2] ### Changes From 39775ea3086319136d3b1d2d4bcd95cf6b9da4ff Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 12 Dec 2024 21:40:33 +0700 Subject: [PATCH 086/166] fix(sql): Use UNION ALL instead of UNION --- data/src/commonMain/sqldelight/tachiyomi/data/history.sq | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq index 35f29d6ecd..48bcc04ff6 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq @@ -157,7 +157,7 @@ AND ( :apply_filter = 0 OR S.name IS NULL ) -UNION -- Newly fetched chapter +UNION ALL -- Newly fetched chapter SELECT M.*, @@ -192,7 +192,7 @@ AND ( :apply_filter = 0 OR S.name IS NULL ) -UNION -- Newly added manga +UNION ALL -- Newly added manga SELECT M.*, From 37535d3bcf4fef0497a15ff143bebcc1b6f9692e Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 12 Dec 2024 21:53:53 +0700 Subject: [PATCH 087/166] revert: "fix(sql): Use UNION ALL instead of UNION" This reverts commit 39775ea3086319136d3b1d2d4bcd95cf6b9da4ff. --- data/src/commonMain/sqldelight/tachiyomi/data/history.sq | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq index 48bcc04ff6..35f29d6ecd 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq @@ -157,7 +157,7 @@ AND ( :apply_filter = 0 OR S.name IS NULL ) -UNION ALL -- Newly fetched chapter +UNION -- Newly fetched chapter SELECT M.*, @@ -192,7 +192,7 @@ AND ( :apply_filter = 0 OR S.name IS NULL ) -UNION ALL -- Newly added manga +UNION -- Newly added manga SELECT M.*, From 16316d810b8bb19fc98b1a1948ced859c1333d39 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani <46041660+null2264@users.noreply.github.com> Date: Sat, 14 Dec 2024 09:17:50 +0700 Subject: [PATCH 088/166] refactor: Replace DownloadQueue with Flow (#301) * refactor: Replace DownloadQueue with Flow * fix: Remove DownloadQueue leftover * fix: Download progress not progressing * chore: Remove unnecessary downloadStatusChange trigger Already handled by MainActivity * fix: Chapter download state stuck in CHECKED * fix: Chapter download state stuck in QUEUE on deletion * fix: A regression, download progress not progressing * refactor: Remove rx usage * docs: Sync changelog --- CHANGELOG.md | 5 + .../tachiyomi/data/download/DownloadJob.kt | 23 +- .../data/download/DownloadManager.kt | 136 ++++---- .../tachiyomi/data/download/DownloadStore.kt | 6 + .../tachiyomi/data/download/Downloader.kt | 312 ++++++++++-------- .../tachiyomi/data/download/model/Download.kt | 59 ++-- .../data/download/model/DownloadQueue.kt | 177 ++++------ .../data/notification/NotificationReceiver.kt | 2 +- .../base/presenter/BaseCoroutinePresenter.kt | 4 +- .../ui/download/DownloadBottomPresenter.kt | 41 ++- .../ui/download/DownloadBottomSheet.kt | 19 +- .../tachiyomi/ui/download/DownloadHolder.kt | 2 +- .../ui/extension/ExtensionBottomPresenter.kt | 14 +- .../ui/extension/ExtensionBottomSheet.kt | 4 - .../tachiyomi/ui/library/LibraryController.kt | 6 - .../tachiyomi/ui/library/LibraryPresenter.kt | 14 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 11 +- .../ui/manga/MangaDetailsController.kt | 3 +- .../ui/manga/MangaDetailsPresenter.kt | 107 +++--- .../tachiyomi/ui/recents/RecentsController.kt | 9 +- .../tachiyomi/ui/recents/RecentsPresenter.kt | 75 +++-- 21 files changed, 551 insertions(+), 478 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e2d8d831e..4c30c705ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Fix slow chapter load - Fix chapter bookmark state is not persistent +## Other +- Refactor downloader + - Replace RxJava usage with Kotlin coroutines + - Replace DownloadQueue with Flow to hopefully fix ConcurrentModificationException entirely + ## [1.9.2] ### Changes diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt index 4f761983d7..11d95942a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadJob.kt @@ -22,8 +22,9 @@ import eu.kanade.tachiyomi.util.system.workManager import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.map import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import yokai.i18n.MR @@ -39,7 +40,7 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout private val preferences: PreferencesHelper = Injekt.get() override suspend fun getForegroundInfo(): ForegroundInfo { - val firstDL = downloadManager.queue.firstOrNull() + val firstDL = downloadManager.queueState.value.firstOrNull() val notification = DownloadNotifier(context).setPlaceholder(firstDL).build() val id = Notifications.ID_DOWNLOAD_CHAPTER return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -70,7 +71,6 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout } catch (_: CancellationException) { Result.success() } finally { - callListeners(false, downloadManager) if (runExtJobAfter) { ExtensionUpdateJob.runJobAgain(applicationContext, NetworkType.CONNECTED) } @@ -96,12 +96,6 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout private const val TAG = "Downloader" private const val START_EXT_JOB_AFTER = "StartExtJobAfter" - private val downloadChannel = MutableSharedFlow( - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - val downloadFlow = downloadChannel.asSharedFlow() - fun start(context: Context, alsoStartExtJob: Boolean = false) { val request = OneTimeWorkRequestBuilder() .addTag(TAG) @@ -118,16 +112,17 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout context.workManager.cancelUniqueWork(TAG) } - fun callListeners(downloading: Boolean? = null, downloadManager: DownloadManager? = null) { - val dManager by lazy { downloadManager ?: Injekt.get() } - downloadChannel.tryEmit(downloading ?: !dManager.isPaused()) - } - fun isRunning(context: Context): Boolean { return context.workManager .getWorkInfosForUniqueWork(TAG) .get() .let { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 } } + + fun isRunningFlow(context: Context): Flow { + return context.workManager + .getWorkInfosForUniqueWorkFlow(TAG) + .map { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index ac0521a940..26bb0404a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -5,7 +5,6 @@ import co.touchlab.kermit.Logger import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.source.Source @@ -13,10 +12,14 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.launchIO -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart import uy.kohesive.injekt.injectLazy import yokai.domain.download.DownloadPreferences import yokai.i18n.MR @@ -65,8 +68,11 @@ class DownloadManager(val context: Context) { /** * Downloads queue, where the pending chapters are stored. */ - val queue: DownloadQueue - get() = downloader.queue + val queueState + get() = downloader.queueState + + val isDownloaderRunning + get() = DownloadJob.isRunningFlow(context) /** * Tells the downloader to begin downloads. @@ -75,7 +81,6 @@ class DownloadManager(val context: Context) { */ fun startDownloads(): Boolean { val hasStarted = downloader.start() - DownloadJob.callListeners(downloadManager = this) return hasStarted } @@ -99,22 +104,21 @@ class DownloadManager(val context: Context) { * * @param isNotification value that determines if status is set (needed for view updates) */ - fun clearQueue(isNotification: Boolean = false) { - deletePendingDownloads(*downloader.queue.toTypedArray()) - downloader.removeFromQueue(isNotification) - DownloadJob.callListeners(false, this) + fun clearQueue() { + deletePendingDownloads(*queueState.value.toTypedArray()) + downloader.clearQueue() + downloader.stop() } fun startDownloadNow(chapter: Chapter) { - val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return - val queue = downloader.queue.toMutableList() + val download = queueState.value.find { it.chapter.id == chapter.id } ?: return + val queue = queueState.value.toMutableList() queue.remove(download) queue.add(0, download) reorderQueue(queue) if (isPaused()) { if (DownloadJob.isRunning(context)) { downloader.start() - DownloadJob.callListeners(true, this) } else { DownloadJob.start(context) } @@ -127,24 +131,12 @@ class DownloadManager(val context: Context) { * @param downloads value to set the download queue to */ fun reorderQueue(downloads: List) { - val wasPaused = isPaused() - if (downloads.isEmpty()) { - DownloadJob.stop(context) - downloader.queue.clear() - return - } - downloader.pause() - downloader.queue.clear() - downloader.queue.addAll(downloads) - if (!wasPaused) { - downloader.start() - DownloadJob.callListeners(true, this) - } + downloader.updateQueue(downloads) } fun isPaused() = !downloader.isRunning - fun hasQueue() = downloader.queue.isNotEmpty() + fun hasQueue() = queueState.value.isNotEmpty() /** * Tells the downloader to enqueue the given list of chapters. @@ -164,10 +156,7 @@ class DownloadManager(val context: Context) { */ fun addDownloadsToStartOfQueue(downloads: List) { if (downloads.isEmpty()) return - queue.toMutableList().apply { - addAll(0, downloads) - reorderQueue(this) - } + reorderQueue(downloads + queueState.value) if (!DownloadJob.isRunning(context)) DownloadJob.start(context) } @@ -212,7 +201,7 @@ class DownloadManager(val context: Context) { * @param chapter the chapter to check. */ fun getChapterDownloadOrNull(chapter: Chapter): Download? { - return downloader.queue + return queueState.value .firstOrNull { it.chapter.id == chapter.id && it.chapter.manga_id == chapter.manga_id } } @@ -249,27 +238,15 @@ class DownloadManager(val context: Context) { * @param manga the manga of the chapters. * @param source the source of the chapters. */ - @OptIn(DelicateCoroutinesApi::class) fun deleteChapters(chapters: List, manga: Manga, source: Source, force: Boolean = false) { - val filteredChapters = if (force) chapters else getChaptersToDelete(chapters, manga) - GlobalScope.launch(Dispatchers.IO) { - val wasPaused = isPaused() + launchIO { + val filteredChapters = if (force) chapters else getChaptersToDelete(chapters, manga) if (filteredChapters.isEmpty()) { - return@launch + return@launchIO } - downloader.pause() - downloader.queue.remove(filteredChapters) - if (!wasPaused && downloader.queue.isNotEmpty()) { - downloader.start() - DownloadJob.callListeners(true) - } else if (downloader.queue.isEmpty() && DownloadJob.isRunning(context)) { - DownloadJob.callListeners(false) - DownloadJob.stop(context) - } else if (downloader.queue.isEmpty()) { - DownloadJob.callListeners(false) - downloader.stop() - } - queue.remove(filteredChapters) + + removeFromDownloadQueue(filteredChapters) + val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source) + provider.findTempChapterDirs( filteredChapters, @@ -278,10 +255,27 @@ class DownloadManager(val context: Context) { ) chapterDirs.forEach { it.delete() } cache.removeChapters(filteredChapters, manga) + if (cache.getDownloadCount(manga, true) == 0) { // Delete manga directory if empty chapterDirs.firstOrNull()?.parentFile?.delete() } - queue.updateListeners() + } + } + + private fun removeFromDownloadQueue(chapters: List) { + val wasRunning = downloader.isRunning + if (wasRunning) { + downloader.pause() + } + + downloader.removeFromQueue(chapters) + + if (wasRunning) { + if (queueState.value.isEmpty()) { + downloader.stop() + } else if (queueState.value.isNotEmpty()) { + downloader.start() + } } } @@ -345,9 +339,7 @@ class DownloadManager(val context: Context) { fun deleteManga(manga: Manga, source: Source, removeQueued: Boolean = true) { launchIO { if (removeQueued) { - downloader.removeFromQueue(manga, true) - queue.remove(manga) - queue.updateListeners() + downloader.removeFromQueue(manga) } provider.findMangaDir(manga, source)?.delete() cache.removeManga(manga) @@ -418,9 +410,6 @@ class DownloadManager(val context: Context) { cache.forceRenewCache() } - fun addListener(listener: DownloadQueue.DownloadListener) = queue.addListener(listener) - fun removeListener(listener: DownloadQueue.DownloadListener) = queue.removeListener(listener) - private fun getChaptersToDelete(chapters: List, manga: Manga): List { // Retrieve the categories that are set to exclude from being deleted on read return if (!preferences.removeBookmarkedChapters().get()) { @@ -429,4 +418,33 @@ class DownloadManager(val context: Context) { chapters } } + + fun statusFlow(): Flow = queueState + .flatMapLatest { downloads -> + downloads + .map { download -> + download.statusFlow.drop(1).map { download } + } + .merge() + } + .onStart { + emitAll( + queueState.value.filter { download -> download.status == Download.State.DOWNLOADING }.asFlow(), + ) + } + + fun progressFlow(): Flow = queueState + .flatMapLatest { downloads -> + downloads + .map { download -> + download.progressFlow.drop(1).map { download } + } + .merge() + } + .onStart { + emitAll( + queueState.value.filter { download -> download.status == Download.State.DOWNLOADING } + .asFlow(), + ) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt index 85dabb16c1..3a32addd1a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt @@ -59,6 +59,12 @@ class DownloadStore( } } + fun removeAll(downloads: List) { + preferences.edit { + downloads.forEach { remove(getKey(it)) } + } + } + /** * Removes all the downloads from the store. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index f66d79cfe3..b074eb8421 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -5,11 +5,9 @@ import android.os.Handler import android.os.Looper import co.touchlab.kermit.Logger import com.hippo.unifile.UniFile -import com.jakewharton.rxrelay.PublishRelay import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.domain.manga.models.Manga @@ -32,22 +30,31 @@ import java.io.File import java.util.* import java.util.zip.* import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.retryWhen +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.supervisorScope import nl.adaptivity.xmlutil.serialization.XML import okhttp3.Response -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers import uy.kohesive.injekt.injectLazy import yokai.core.archive.ZipWriter import yokai.core.metadata.COMIC_INFO_FILE @@ -61,16 +68,7 @@ import yokai.util.lang.getString /** * This class is the one in charge of downloading chapters. * - * Its [queue] contains the list of chapters to download. In order to download them, the downloader - * subscriptions must be running and the list of chapters must be sent to them by [downloadsRelay]. - * - * The queue manipulation must be done in one thread (currently the main thread) to avoid unexpected - * behavior, but it's safe to read it from multiple threads. - * - * @param context the application context. - * @param provider the downloads directory provider. - * @param cache the downloads cache, used to add the downloads to the cache after their completion. - * @param sourceManager the source manager. + * Its queue contains the list of chapters to download. */ class Downloader( private val context: Context, @@ -92,7 +90,8 @@ class Downloader( /** * Queue where active downloads are kept. */ - val queue = DownloadQueue(store) + private val _queueState = MutableStateFlow>(emptyList()) + val queueState = _queueState.asStateFlow() private val handler = Handler(Looper.getMainLooper()) @@ -101,21 +100,14 @@ class Downloader( */ private val notifier by lazy { DownloadNotifier(context) } - /** - * Downloader subscription. - */ - private var subscription: Subscription? = null - - /** - * Relay to send a list of downloads to the downloader. - */ - private val downloadsRelay = PublishRelay.create>() + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var downloaderJob: Job? = null /** * Whether the downloader is running. */ val isRunning: Boolean - get() = subscription != null + get() = downloaderJob?.isActive ?: false /** * Whether the downloader is paused @@ -126,8 +118,7 @@ class Downloader( init { launchNow { val chapters = async { store.restore() } - queue.addAll(chapters.await()) - DownloadJob.callListeners() + addAllToQueue(chapters.await()) } } @@ -138,17 +129,17 @@ class Downloader( * @return true if the downloader is started, false otherwise. */ fun start(): Boolean { - if (subscription != null || queue.isEmpty()) { + if (isRunning || queueState.value.isEmpty()) { return false } - initializeSubscription() - val pending = queue.filter { it.status != Download.State.DOWNLOADED } + val pending = queueState.value.filter { it.status != Download.State.DOWNLOADED } pending.forEach { if (it.status != Download.State.QUEUE) it.status = Download.State.QUEUE } isPaused = false - downloadsRelay.call(pending) + launchDownloaderJob() + return pending.isNotEmpty() } @@ -156,8 +147,8 @@ class Downloader( * Stops the downloader. */ fun stop(reason: String? = null) { - destroySubscription() - queue + cancelDownloaderJob() + queueState.value .filter { it.status == Download.State.DOWNLOADING } .forEach { it.status = Download.State.ERROR } @@ -166,104 +157,109 @@ class Downloader( return } - DownloadJob.stop(context) - if (isPaused && queue.isNotEmpty()) { + if (isPaused && queueState.value.isNotEmpty()) { handler.postDelayed({ notifier.onDownloadPaused() }, 150) } else { notifier.dismiss() } - DownloadJob.callListeners(false) + isPaused = false + + DownloadJob.stop(context) } /** * Pauses the downloader */ fun pause() { - destroySubscription() - queue + cancelDownloaderJob() + queueState.value .filter { it.status == Download.State.DOWNLOADING } .forEach { it.status = Download.State.QUEUE } isPaused = true } - /** - * Removes everything from the queue. - * - * @param isNotification value that determines if status is set (needed for view updates) - */ - fun removeFromQueue(isNotification: Boolean = false) { - destroySubscription() + fun clearQueue() { + cancelDownloaderJob() - // Needed to update the chapter view - if (isNotification) { - queue - .filter { it.status == Download.State.QUEUE } - .forEach { it.status = Download.State.NOT_DOWNLOADED } - } - queue.clear() + internalClearQueue() notifier.dismiss() } - /** - * Removes everything from the queue for a certain manga - * - * @param isNotification value that determines if status is set (needed for view updates) - */ - fun removeFromQueue(manga: Manga, isNotification: Boolean = false) { - // Needed to update the chapter view - if (isNotification) { - queue.filter { it.status == Download.State.QUEUE && it.manga.id == manga.id } - .forEach { it.status = Download.State.NOT_DOWNLOADED } - } - queue.remove(manga) - if (queue.isEmpty()) { - if (DownloadJob.isRunning(context)) DownloadJob.stop(context) - stop() - } - notifier.dismiss() - } - - /** - * Prepares the subscriptions to start downloading. - */ - private fun initializeSubscription() { + private fun launchDownloaderJob() { if (isRunning) return - subscription = downloadsRelay.concatMapIterable { it } - // Concurrently download from 5 different sources - .groupBy { it.source } - .flatMap( - { bySource -> - bySource.concatMap { download -> - Observable.fromCallable { - runBlocking { downloadChapter(download) } - download - }.subscribeOn(Schedulers.io()) + downloaderJob = scope.launch { + val activeDownloadsFlow = queueState.transformLatest { queue -> + while (true) { + val activeDownloads = queue.asSequence() + // Ignore completed downloads, leave them in the queue + .filter { + val statusValue = it.status.value + Download.State.NOT_DOWNLOADED.value <= statusValue && statusValue <= Download.State.DOWNLOADING.value + } + .groupBy { it.source } + .toList() + // Concurrently download from 5 different sources + .take(5) + .map { (_, downloads) -> downloads.first() } + emit(activeDownloads) + + if (activeDownloads.isEmpty()) break + // Suspend until a download enters the ERROR state + val activeDownloadsErroredFlow = + combine(activeDownloads.map(Download::statusFlow)) { states -> + states.contains(Download.State.ERROR) + }.filter { it } + activeDownloadsErroredFlow.first() + } + }.distinctUntilChanged() + + // Use supervisorScope to cancel child jobs when the downloader job is cancelled + supervisorScope { + val downloadJobs = mutableMapOf() + + activeDownloadsFlow.collectLatest { activeDownloads -> + val downloadJobsToStop = downloadJobs.filter { it.key !in activeDownloads } + downloadJobsToStop.forEach { (download, job) -> + job.cancel() + downloadJobs.remove(download) } - }, - 5, - ) - .onBackpressureLatest() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - completeDownload(it) - }, - { error -> - Logger.e(error) - notifier.onError(error.message) - stop() - }, - ) + + val downloadsToStart = activeDownloads.filter { it !in downloadJobs } + downloadsToStart.forEach { download -> + downloadJobs[download] = launchDownloadJob(download) + } + } + } + } + } + + private fun CoroutineScope.launchDownloadJob(download: Download) = launchIO { + try { + downloadChapter(download) + + // Remove successful download from queue + if (download.status == Download.State.DOWNLOADED) { + removeFromQueue(download) + } + if (areAllDownloadsFinished()) { + stop() + } + } catch (e: Throwable) { + if (e is CancellationException) throw e + Logger.e(e) + notifier.onError(e.message) + stop() + } } /** * Destroys the downloader subscriptions. */ - private fun destroySubscription() { - subscription?.unsubscribe() - subscription = null + private fun cancelDownloaderJob() { + downloaderJob?.cancel() + downloaderJob = null } /** @@ -279,7 +275,7 @@ class Downloader( } val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO - val wasEmpty = queue.isEmpty() + val wasEmpty = queueState.value.isEmpty() // Called in background thread, the operation can be slow with SAF. val chaptersWithoutDir = async { chapters @@ -292,22 +288,17 @@ class Downloader( // Runs in main thread (synchronization needed). val chaptersToQueue = chaptersWithoutDir.await() // Filter out those already enqueued. - .filter { chapter -> queue.none { it.chapter.id == chapter.id } } + .filter { chapter -> queueState.value.none { it.chapter.id == chapter.id } } // Create a download for each one. .map { Download(source, manga, it) } if (chaptersToQueue.isNotEmpty()) { - queue.addAll(chaptersToQueue) - - if (isRunning) { - // Send the list of downloads to the downloader. - downloadsRelay.call(chaptersToQueue) - } + addAllToQueue(chaptersToQueue) // Start downloader if needed if (autoStart && wasEmpty) { - val queuedDownloads = queue.count { it.source !is UnmeteredSource } - val maxDownloadsFromSource = queue + val queuedDownloads = queueState.value.count { it.source !is UnmeteredSource } + val maxDownloadsFromSource = queueState.value .groupBy { it.source } .filterKeys { it !is UnmeteredSource } .maxOfOrNull { it.value.size } ?: 0 @@ -670,25 +661,86 @@ class Downloader( dir.createFile(COMIC_INFO_FILE)?.writeText(xml.encodeToString(ComicInfo.serializer(), comicInfo)) } - /** - * Completes a download. This method is called in the main thread. - */ - private fun completeDownload(download: Download) { - // Delete successful downloads from queue - if (download.status == Download.State.DOWNLOADED) { - // Remove downloaded chapter from queue - queue.remove(download) - } - if (areAllDownloadsFinished()) { - stop() - } - } - /** * Returns true if all the queued downloads are in DOWNLOADED or ERROR state. */ private fun areAllDownloadsFinished(): Boolean { - return queue.none { it.status <= Download.State.DOWNLOADING } + return queueState.value.none { it.status <= Download.State.DOWNLOADING } + } + + private fun addAllToQueue(downloads: List) { + _queueState.update { + downloads.forEach { download -> + download.status = Download.State.QUEUE + } + store.addAll(downloads) + it + downloads + } + } + + fun removeFromQueue(download: Download) { + _queueState.update { + store.remove(download) + if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) { + download.status = Download.State.NOT_DOWNLOADED + } + it - download + } + } + + private inline fun removeFromQueueIf(predicate: (Download) -> Boolean) { + _queueState.update { queue -> + val downloads = queue.filter { predicate(it) } + store.removeAll(downloads) + downloads.forEach { download -> + if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) { + download.status = Download.State.NOT_DOWNLOADED + } + } + queue - downloads + } + } + + fun removeFromQueue(chapter: Chapter) { + removeFromQueueIf { it.chapter.id == chapter.id } + } + + fun removeFromQueue(chapters: List) { + removeFromQueueIf { it.chapter.id in chapters.map { it.id } } + } + + fun removeFromQueue(manga: Manga) { + removeFromQueueIf { it.manga.id == manga.id } + } + + private fun internalClearQueue() { + _queueState.update { + it.forEach { download -> + if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) { + download.status = Download.State.NOT_DOWNLOADED + } + } + store.clear() + emptyList() + } + } + + fun updateQueue(downloads: List) { + val wasRunning = isRunning + + if (downloads.isEmpty()) { + clearQueue() + DownloadJob.stop(context) + return + } + + pause() + internalClearQueue() + addAllToQueue(downloads) + + if (wasRunning) { + start() + } } companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt index d2daedf1ea..de3264b16c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt @@ -4,8 +4,15 @@ import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource -import rx.subjects.PublishSubject import kotlin.math.roundToInt +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { @@ -17,17 +24,31 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { val downloadedImages: Int get() = pages?.count { it.status == Page.State.READY } ?: 0 - @Volatile @Transient - var status: State = State.default + @Transient + private val _statusFlow = MutableStateFlow(State.NOT_DOWNLOADED) + + @Transient + val statusFlow = _statusFlow.asStateFlow() + var status: State + get() = _statusFlow.value set(status) { - field = status - statusSubject?.onNext(this) - statusCallback?.invoke(this) + _statusFlow.value = status } - @Transient private var statusSubject: PublishSubject? = null + @Transient + val progressFlow = flow { + if (pages == null) { + emit(0) + while (pages == null) { + delay(50) + } + } - @Transient private var statusCallback: ((Download) -> Unit)? = null + val progressFlows = pages!!.map(Page::progressFlow) + emitAll(combine(progressFlows) { it.average().roundToInt() }) + } + .distinctUntilChanged() + .debounce(50) val pageProgress: Int get() { @@ -41,21 +62,13 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { return pages.map(Page::progress).average().roundToInt() } - fun setStatusSubject(subject: PublishSubject?) { - statusSubject = subject - } - - fun setStatusCallback(f: ((Download) -> Unit)?) { - statusCallback = f - } - - enum class State { - CHECKED, - NOT_DOWNLOADED, - QUEUE, - DOWNLOADING, - DOWNLOADED, - ERROR, + enum class State(val value: Int) { + CHECKED(-1), + NOT_DOWNLOADED(0), + QUEUE(1), + DOWNLOADING(2), + DOWNLOADED(3), + ERROR(4), ; companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt index f11d1e7d22..1bcea75984 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt @@ -1,131 +1,84 @@ package eu.kanade.tachiyomi.data.download.model -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.download.DownloadStore -import eu.kanade.tachiyomi.domain.manga.models.Manga -import kotlinx.coroutines.MainScope +import androidx.annotation.CallSuper +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.util.system.launchUI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import rx.subjects.PublishSubject -import java.util.concurrent.* +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged -class DownloadQueue( - private val store: DownloadStore, - private val queue: MutableList = CopyOnWriteArrayList(), -) : - List by queue { +sealed class DownloadQueue { + interface Listener { + val progressJobs: MutableMap - private val statusSubject = PublishSubject.create() + // Override with presenterScope or viewScope + val queueListenerScope: CoroutineScope - private val updatedRelay = PublishRelay.create() - - private val downloadListeners: MutableList = CopyOnWriteArrayList() - - private var scope = MainScope() - - fun addAll(downloads: List) { - downloads.forEach { download -> - download.setStatusSubject(statusSubject) - download.setStatusCallback(::setPagesFor) - download.status = Download.State.QUEUE + fun onPageProgressUpdate(download: Download) { + onProgressUpdate(download) } - queue.addAll(downloads) - store.addAll(downloads) - updatedRelay.call(Unit) - } + fun onProgressUpdate(download: Download) + fun onQueueUpdate(download: Download) - fun remove(download: Download) { - val removed = queue.remove(download) - store.remove(download) - download.setStatusSubject(null) - download.setStatusCallback(null) - if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) { - download.status = Download.State.NOT_DOWNLOADED - } - callListeners(download) - if (removed) { - updatedRelay.call(Unit) - } - } + // Subscribe on presenter/controller creation on UI thread + @CallSuper + fun onStatusChange(download: Download) { + when (download.status) { + Download.State.DOWNLOADING -> { + launchProgressJob(download) + // Initial update of the downloaded pages + onQueueUpdate(download) + } + Download.State.DOWNLOADED -> { + cancelProgressJob(download) - fun updateListeners() { - val listeners = downloadListeners.toList() - listeners.forEach { it.updateDownloads() } - } - - fun remove(chapter: Chapter) { - find { it.chapter.id == chapter.id }?.let { remove(it) } - } - - fun remove(chapters: List) { - for (chapter in chapters) { remove(chapter) } - } - - fun remove(manga: Manga) { - filter { it.manga.id == manga.id }.forEach { remove(it) } - } - - fun clear() { - queue.forEach { download -> - download.setStatusSubject(null) - download.setStatusCallback(null) - if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) { - download.status = Download.State.NOT_DOWNLOADED + onProgressUpdate(download) + onQueueUpdate(download) + } + Download.State.ERROR -> cancelProgressJob(download) + else -> { + /* unused */ + } } - callListeners(download) } - queue.clear() - store.clear() - updatedRelay.call(Unit) - } - private fun setPagesFor(download: Download) { - if (download.status == Download.State.DOWNLOADING) { - if (download.pages != null) { - for (page in download.pages!!) - scope.launch { - page.statusFlow.collectLatest { - callListeners(download) - } + /** + * Observe the progress of a download and notify the view. + * + * @param download the download to observe its progress. + */ + private fun launchProgressJob(download: Download) { + val job = queueListenerScope.launchUI { + while (download.pages == null) { + delay(50) + } + + val progressFlows = download.pages!!.map(Page::progressFlow) + combine(progressFlows, Array::sum) + .distinctUntilChanged() + .debounce(50) + .collectLatest { + onPageProgressUpdate(download) } } - callListeners(download) - } else if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) { -// setPagesSubject(download.pages, null) - if (download.status == Download.State.ERROR) { - callListeners(download) - } - } else { - callListeners(download) + + // Avoid leaking jobs + progressJobs.remove(download)?.cancel() + + progressJobs[download] = job } - } - private fun callListeners(download: Download) { - val iterator = downloadListeners.iterator() - while (iterator.hasNext()) { - iterator.next().updateDownload(download) + /** + * Unsubscribes the given download from the progress subscriptions. + * + * @param download the download to unsubscribe. + */ + private fun cancelProgressJob(download: Download) { + progressJobs.remove(download)?.cancel() } } - -// private fun setPagesSubject(pages: List?, subject: PublishSubject?) { -// if (pages != null) { -// for (page in pages) { -// page.setStatusSubject(subject) -// } -// } -// } - - fun addListener(listener: DownloadListener) { - downloadListeners.add(listener) - } - - fun removeListener(listener: DownloadListener) { - downloadListeners.remove(listener) - } - - interface DownloadListener { - fun updateDownload(download: Download) - fun updateDownloads() - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 1bc04e65bc..c5a59b07eb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -64,7 +64,7 @@ class NotificationReceiver : BroadcastReceiver() { downloadManager.pauseDownloads() } // Clear the download queue - ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true) + ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue() // Delete image from path and dismiss notification ACTION_DELETE_IMAGE -> deleteImage( context, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BaseCoroutinePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BaseCoroutinePresenter.kt index 6a9515583f..cbeb64cc73 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BaseCoroutinePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BaseCoroutinePresenter.kt @@ -1,10 +1,11 @@ package eu.kanade.tachiyomi.ui.base.presenter +import androidx.annotation.CallSuper +import java.lang.ref.WeakReference import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import java.lang.ref.WeakReference open class BaseCoroutinePresenter { var presenterScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -24,6 +25,7 @@ open class BaseCoroutinePresenter { open fun onCreate() { } + @CallSuper open fun onDestroy() { presenterScope.cancel() weakView = null diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomPresenter.kt index 227fb619ad..e4fa9eaa7c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomPresenter.kt @@ -4,7 +4,9 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter +import eu.kanade.tachiyomi.util.system.launchUI import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uy.kohesive.injekt.injectLazy @@ -12,7 +14,8 @@ import uy.kohesive.injekt.injectLazy /** * Presenter of [DownloadBottomSheet]. */ -class DownloadBottomPresenter : BaseCoroutinePresenter() { +class DownloadBottomPresenter : BaseCoroutinePresenter(), + DownloadQueue.Listener { /** * Download manager. @@ -20,15 +23,27 @@ class DownloadBottomPresenter : BaseCoroutinePresenter() { val downloadManager: DownloadManager by injectLazy() var items = listOf() + override val progressJobs = mutableMapOf() + override val queueListenerScope get() = presenterScope + /** * Property to get the queue from the download manager. */ - val downloadQueue: DownloadQueue - get() = downloadManager.queue + val downloadQueueState + get() = downloadManager.queueState + + override fun onCreate() { + presenterScope.launchUI { + downloadManager.statusFlow().collect(::onStatusChange) + } + presenterScope.launchUI { + downloadManager.progressFlow().collect(::onPageProgressUpdate) + } + } fun getItems() { presenterScope.launch { - val items = downloadQueue + val items = downloadQueueState.value .groupBy { it.source } .map { entry -> DownloadHeaderItem(entry.key.id, entry.key.name, entry.value.size).apply { @@ -85,4 +100,22 @@ class DownloadBottomPresenter : BaseCoroutinePresenter() { fun cancelDownloads(downloads: List) { downloadManager.deletePendingDownloads(*downloads.toTypedArray()) } + + override fun onStatusChange(download: Download) { + super.onStatusChange(download) + view?.update(downloadManager.isRunning) + } + + override fun onQueueUpdate(download: Download) { + view?.onUpdateDownloadedPages(download) + } + + override fun onProgressUpdate(download: Download) { + view?.onUpdateProgress(download) + } + + override fun onPageProgressUpdate(download: Download) { + super.onPageProgressUpdate(download) + view?.onUpdateDownloadedPages(download) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt index b0e354cbb3..ef9a074b29 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt @@ -117,14 +117,14 @@ class DownloadBottomSheet @JvmOverloads constructor( fun update(isRunning: Boolean) { presenter.getItems() onQueueStatusChange(isRunning) - if (binding.downloadFab.isInvisible != presenter.downloadQueue.isEmpty()) { - binding.downloadFab.isInvisible = presenter.downloadQueue.isEmpty() + if (binding.downloadFab.isInvisible != presenter.downloadQueueState.value.isEmpty()) { + binding.downloadFab.isInvisible = presenter.downloadQueueState.value.isEmpty() } prepareMenu() } private fun updateDLTitle() { - val extCount = presenter.downloadQueue.firstOrNull() + val extCount = presenter.downloadQueueState.value.firstOrNull() binding.titleText.text = if (extCount != null) { context.getString( MR.strings.downloading_, @@ -143,8 +143,8 @@ class DownloadBottomSheet @JvmOverloads constructor( private fun onQueueStatusChange(running: Boolean) { val oldRunning = isRunning isRunning = running - if (binding.downloadFab.isInvisible != presenter.downloadQueue.isEmpty()) { - binding.downloadFab.isInvisible = presenter.downloadQueue.isEmpty() + if (binding.downloadFab.isInvisible != presenter.downloadQueueState.value.isEmpty()) { + binding.downloadFab.isInvisible = presenter.downloadQueueState.value.isEmpty() } updateFab() if (oldRunning != running) { @@ -210,7 +210,7 @@ class DownloadBottomSheet @JvmOverloads constructor( private fun setInformationView() { updateDLTitle() setBottomSheet() - if (presenter.downloadQueue.isEmpty()) { + if (presenter.downloadQueueState.value.isEmpty()) { binding.emptyView.show( R.drawable.ic_download_off_24dp, MR.strings.nothing_is_downloading, @@ -224,10 +224,10 @@ class DownloadBottomSheet @JvmOverloads constructor( val menu = binding.sheetToolbar.menu updateFab() // Set clear button visibility. - menu.findItem(R.id.clear_queue)?.isVisible = !presenter.downloadQueue.isEmpty() + menu.findItem(R.id.clear_queue)?.isVisible = presenter.downloadQueueState.value.isNotEmpty() // Set reorder button visibility. - menu.findItem(R.id.reorder)?.isVisible = !presenter.downloadQueue.isEmpty() + menu.findItem(R.id.reorder)?.isVisible = presenter.downloadQueueState.value.isNotEmpty() } private fun updateFab() { @@ -274,7 +274,7 @@ class DownloadBottomSheet @JvmOverloads constructor( } private fun setBottomSheet() { - val hasQueue = presenter.downloadQueue.isNotEmpty() + val hasQueue = presenter.downloadQueueState.value.isNotEmpty() if (hasQueue) { sheetBehavior?.skipCollapsed = !hasQueue if (sheetBehavior.isHidden()) sheetBehavior?.collapse() @@ -320,7 +320,6 @@ class DownloadBottomSheet @JvmOverloads constructor( } } presenter.reorder(downloads) - controller?.updateChapterDownload(download, false) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt index 27c42fdb2b..f7e1ce7a5d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt @@ -68,7 +68,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : if (binding.downloadProgress.max == 1) { binding.downloadProgress.max = pages.size * 100 } - binding.downloadProgress.progress = download.pageProgress + binding.downloadProgress.setProgressCompat(download.pageProgress, true) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt index b55158122e..f82ca90be3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt @@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.ui.extension import android.content.pm.PackageInstaller import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.extension.ExtensionInstallerJob import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.Extension @@ -12,7 +10,6 @@ import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.ui.migration.BaseMigrationPresenter import eu.kanade.tachiyomi.util.system.LocaleHelper -import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.withUIContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -31,7 +28,7 @@ typealias ExtensionIntallInfo = Pair /** * Presenter of [ExtensionBottomSheet]. */ -class ExtensionBottomPresenter : BaseMigrationPresenter(), DownloadQueue.DownloadListener { +class ExtensionBottomPresenter : BaseMigrationPresenter() { private var extensions = emptyList() @@ -43,7 +40,7 @@ class ExtensionBottomPresenter : BaseMigrationPresenter(), override fun onCreate() { super.onCreate() - downloadManager.addListener(this) + presenterScope.launch { val extensionJob = async { extensionManager.findAvailableExtensions() @@ -289,11 +286,4 @@ class ExtensionBottomPresenter : BaseMigrationPresenter(), extensionManager.trust(pkgName, versionCode, signatureHash) } } - - override fun updateDownload(download: Download) = updateDownloads() - override fun updateDownloads() { - presenterScope.launchUI { - view?.updateDownloadStatus(!downloadManager.isPaused()) - } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt index cda08da10c..cf92de7949 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt @@ -521,8 +521,4 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At return if (index == -1) POSITION_NONE else index } } - - fun updateDownloadStatus(isRunning: Boolean) { - (controller.activity as? MainActivity)?.downloadStatusChanged(isRunning) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 792549b73b..52acb5855d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -61,7 +61,6 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.core.preference.Preference import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.LibraryManga -import eu.kanade.tachiyomi.data.download.DownloadJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications @@ -1059,7 +1058,6 @@ open class LibraryController( presenter.getLibrary() isPoppingIn = true } - DownloadJob.callListeners() binding.recyclerCover.isClickable = false binding.recyclerCover.isFocusable = false singleCategory = presenter.categories.size <= 1 @@ -2199,8 +2197,4 @@ open class LibraryController( } } } - - fun updateDownloadStatus(isRunning: Boolean) { - (activity as? MainActivity)?.downloadStatusChanged(isRunning) - } } 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 46d4d0c457..b38686e07e 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 @@ -11,8 +11,6 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.removeCover import eu.kanade.tachiyomi.data.database.models.seriesType import eu.kanade.tachiyomi.data.download.DownloadManager -import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.preference.DelayedLibrarySuggestionsJob import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager @@ -44,7 +42,6 @@ import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import eu.kanade.tachiyomi.util.mapStatus import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchNonCancellableIO -import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withUIContext import java.util.* @@ -95,7 +92,7 @@ class LibraryPresenter( private val downloadManager: DownloadManager = Injekt.get(), private val chapterFilter: ChapterFilter = Injekt.get(), private val trackManager: TrackManager = Injekt.get(), -) : BaseCoroutinePresenter(), DownloadQueue.DownloadListener { +) : BaseCoroutinePresenter() { private val getCategories: GetCategories by injectLazy() private val setMangaCategories: SetMangaCategories by injectLazy() private val updateCategories: UpdateCategories by injectLazy() @@ -189,7 +186,7 @@ class LibraryPresenter( override fun onCreate() { super.onCreate() - downloadManager.addListener(this) + if (!controllerIsSubClass) { lastLibraryItems?.let { libraryItems = it } lastCategories?.let { categories = it } @@ -1640,13 +1637,6 @@ class LibraryPresenter( } } - override fun updateDownload(download: Download) = updateDownloads() - override fun updateDownloads() { - presenterScope.launchUI { - view?.updateDownloadStatus(!downloadManager.isPaused()) - } - } - data class ItemPreferences( val filterDownloaded: Int, val filterUnread: Int, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 2feabdb339..2aea06713d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -62,12 +62,12 @@ import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.Router import com.getkeepsafe.taptargetview.TapTarget import com.getkeepsafe.taptargetview.TapTargetView +import com.google.android.material.badge.BadgeDrawable import com.google.android.material.navigation.NavigationBarView import com.google.android.material.snackbar.Snackbar import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.download.DownloadJob import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.notification.NotificationReceiver @@ -458,7 +458,7 @@ open class MainActivity : BaseActivity() { } } - DownloadJob.downloadFlow.onEach(::downloadStatusChanged).launchIn(lifecycleScope) + downloadManager.isDownloaderRunning.onEach(::downloadStatusChanged).launchIn(lifecycleScope) lifecycleScope WindowCompat.setDecorFitsSystemWindows(window, false) setSupportActionBar(binding.toolbar) @@ -947,7 +947,6 @@ open class MainActivity : BaseActivity() { extensionManager.getExtensionUpdates(false) } setExtensionsBadge() - DownloadJob.callListeners(downloadManager = downloadManager) showDLQueueTutorial() reEnableBackPressedCallBack() } @@ -1504,12 +1503,16 @@ open class MainActivity : BaseActivity() { } } + fun BadgeDrawable.updateQueueSize(queueSize: Int) { + number = queueSize + } + fun downloadStatusChanged(downloading: Boolean) { lifecycleScope.launchUI { val hasQueue = downloading || downloadManager.hasQueue() if (hasQueue) { val badge = nav.getOrCreateBadge(R.id.nav_recents) - badge.number = downloadManager.queue.size + badge.updateQueueSize(downloadManager.queueState.value.size) if (downloading) badge.backgroundColor = -870219 else badge.backgroundColor = Color.GRAY showDLQueueTutorial() } else { 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 856f2a2894..01a980fa31 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 @@ -792,7 +792,6 @@ class MangaDetailsController : binding.swipeRefresh.isRefreshing = enabled } - //region Recycler methods fun updateChapterDownload(download: Download) { getHolder(download.chapter)?.notifyStatus( download.status, @@ -1802,7 +1801,7 @@ class MangaDetailsController : override fun onDestroyActionMode(mode: ActionMode?) { actionMode = null setStatusBarAndToolbar() - if (startingRangeChapterPos != null && rangeMode == RangeMode.Download) { + if (startingRangeChapterPos != null && rangeMode in setOf(RangeMode.Download, RangeMode.RemoveDownload)) { val item = adapter?.getItem(startingRangeChapterPos!!) as? ChapterItem (binding.recycler.findViewHolderForAdapterPosition(startingRangeChapterPos!!) as? ChapterHolder)?.notifyStatus( item?.status ?: Download.State.NOT_DOWNLOADED, 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 5ed928c9bd..67ed320378 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 @@ -60,6 +60,7 @@ import eu.kanade.tachiyomi.util.manga.MangaUtil import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.ImageUtil +import eu.kanade.tachiyomi.util.system.e import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchNonCancellableIO import eu.kanade.tachiyomi.util.system.launchNow @@ -72,8 +73,11 @@ import java.io.FileOutputStream import java.io.OutputStream import java.util.Locale import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -108,7 +112,8 @@ class MangaDetailsPresenter( private val downloadManager: DownloadManager = Injekt.get(), private val chapterFilter: ChapterFilter = Injekt.get(), private val storageManager: StorageManager = Injekt.get(), -) : BaseCoroutinePresenter(), DownloadQueue.DownloadListener { +) : BaseCoroutinePresenter(), + DownloadQueue.Listener { private val getAvailableScanlators: GetAvailableScanlators by injectLazy() private val getCategories: GetCategories by injectLazy() private val getChapter: GetChapter by injectLazy() @@ -174,6 +179,9 @@ class MangaDetailsPresenter( var allChapterScanlators: Set = emptySet() + override val progressJobs: MutableMap = mutableMapOf() + override val queueListenerScope get() = presenterScope + override fun onCreate() { val controller = view ?: return @@ -181,10 +189,24 @@ class MangaDetailsPresenter( if (!::manga.isInitialized) runBlocking { refreshMangaFromDb() } syncData() - downloadManager.addListener(this) + presenterScope.launchUI { + downloadManager.statusFlow() + .filter { it.manga.id == mangaId } + .catch { error -> Logger.e(error) } + .collect(::onStatusChange) + } + presenterScope.launchUI { + downloadManager.progressFlow() + .filter { it.manga.id == mangaId } + .catch { error -> Logger.e(error) } + .collect(::onQueueUpdate) + } + presenterScope.launchIO { + downloadManager.queueState.collectLatest(::onQueueUpdate) + } runBlocking { - tracks = getTrack.awaitAllByMangaId(manga.id!!) + tracks = getTrack.awaitAllByMangaId(mangaId) } } @@ -219,11 +241,6 @@ class MangaDetailsPresenter( refreshTracking(false) } - override fun onDestroy() { - super.onDestroy() - downloadManager.removeListener(this) - } - fun fetchChapters(andTracking: Boolean = true) { presenterScope.launch { getChapters() @@ -252,12 +269,12 @@ class MangaDetailsPresenter( return chapters } - private suspend fun getChapters() { + private suspend fun getChapters(queue: List = downloadManager.queueState.value) { val chapters = getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } // Find downloaded chapters - setDownloadedChapters(chapters) + setDownloadedChapters(chapters, queue) allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() this.chapters = applyChapterFilters(chapters) @@ -274,33 +291,17 @@ class MangaDetailsPresenter( * * @param chapters the list of chapter from the database. */ - private fun setDownloadedChapters(chapters: List) { + private fun setDownloadedChapters(chapters: List, queue: List) { for (chapter in chapters) { if (downloadManager.isChapterDownloaded(chapter, manga)) { chapter.status = Download.State.DOWNLOADED - } else if (downloadManager.hasQueue()) { - chapter.status = downloadManager.queue.find { it.chapter.id == chapter.id } + } else if (queue.isNotEmpty()) { + chapter.status = queue.find { it.chapter.id == chapter.id } ?.status ?: Download.State.default } } } - override fun updateDownload(download: Download) { - chapters.find { it.id == download.chapter.id }?.download = download - presenterScope.launchUI { - view?.updateChapterDownload(download) - } - } - - override fun updateDownloads() { - presenterScope.launch(Dispatchers.Default) { - getChapters() - withContext(Dispatchers.Main) { - view?.updateChapters(chapters) - } - } - } - /** * Converts a chapter from the database to an extended model, allowing to store new fields. */ @@ -310,7 +311,7 @@ class MangaDetailsPresenter( model.isLocked = isLockedFromSearch // Find an active download for this chapter. - val download = downloadManager.queue.find { it.chapter.id == id } + val download = downloadManager.queueState.value.find { it.chapter.id == id } if (download != null) { // If there's an active download, assign it. @@ -387,14 +388,15 @@ class MangaDetailsPresenter( * @param chapter the chapter to delete. */ fun deleteChapter(chapter: ChapterItem) { - downloadManager.deleteChapters(listOf(chapter), manga, source, true) this.chapters.find { it.id == chapter.id }?.apply { if (chapter.chapter.bookmark && !preferences.removeBookmarkedChapters().get()) return@apply - status = Download.State.QUEUE + status = Download.State.NOT_DOWNLOADED download = null } view?.updateChapters(this.chapters) + + downloadManager.deleteChapters(listOf(chapter), manga, source, true) } /** @@ -402,22 +404,21 @@ class MangaDetailsPresenter( * @param chapters the list of chapters to delete. */ fun deleteChapters(chapters: List, update: Boolean = true, isEverything: Boolean = false) { - launchIO { - if (isEverything) { - downloadManager.deleteManga(manga, source) - } else { - downloadManager.deleteChapters(chapters, manga, source) - } - } chapters.forEach { chapter -> this.chapters.find { it.id == chapter.id }?.apply { if (chapter.chapter.bookmark && !preferences.removeBookmarkedChapters().get() && !isEverything) return@apply - status = Download.State.QUEUE + status = Download.State.NOT_DOWNLOADED download = null } } if (update) view?.updateChapters(this.chapters) + + if (isEverything) { + downloadManager.deleteManga(manga, source) + } else { + downloadManager.deleteChapters(chapters, manga, source) + } } suspend fun refreshMangaFromDb(): Manga { @@ -1150,6 +1151,32 @@ class MangaDetailsPresenter( return if (date <= 0L) null else date } + override fun onStatusChange(download: Download) { + super.onStatusChange(download) + chapters.find { it.id == download.chapter.id }?.status = download.status + onPageProgressUpdate(download) + } + + private suspend fun onQueueUpdate(queue: List) = withIOContext { + getChapters(queue) + withUIContext { + view?.updateChapters(chapters) + } + } + + override fun onQueueUpdate(download: Download) { + // already handled by onStatusChange + } + + override fun onProgressUpdate(download: Download) { + // already handled by onStatusChange + } + + override fun onPageProgressUpdate(download: Download) { + chapters.find { it.id == download.chapter.id }?.download = download + view?.updateChapterDownload(download) + } + companion object { const val MULTIPLE_VOLUMES = 1 const val TENS_OF_CHAPTERS = 2 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt index 85244ca08c..e787f4d6ad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt @@ -629,13 +629,8 @@ class RecentsController(bundle: Bundle? = null) : } } - fun updateChapterDownload(download: Download, updateDLSheet: Boolean = true) { - if (view == null) return - if (updateDLSheet) { - binding.downloadBottomSheet.dlBottomSheet.update(!presenter.downloadManager.isPaused()) - binding.downloadBottomSheet.dlBottomSheet.onUpdateProgress(download) - binding.downloadBottomSheet.dlBottomSheet.onUpdateDownloadedPages(download) - } + fun updateChapterDownload(download: Download) { + if (view == null || !this::adapter.isInitialized) return val id = download.chapter.id ?: return val item = adapter.getItemByChapterId(id) ?: return val holder = binding.recycler.findViewHolderForItemId(item.id!!) as? RecentMangaHolder ?: return diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index 5c8dc92eb9..56b6baba1c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.data.database.models.ChapterHistory import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.HistoryImpl import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.data.download.DownloadJob import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.DownloadQueue @@ -31,6 +30,7 @@ import kotlin.math.abs import kotlin.math.roundToInt import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -55,7 +55,8 @@ class RecentsPresenter( val preferences: PreferencesHelper = Injekt.get(), val downloadManager: DownloadManager = Injekt.get(), private val chapterFilter: ChapterFilter = Injekt.get(), -) : BaseCoroutinePresenter(), DownloadQueue.DownloadListener { +) : BaseCoroutinePresenter(), + DownloadQueue.Listener { private val handler: DatabaseHandler by injectLazy() private val getChapter: GetChapter by injectLazy() @@ -99,10 +100,27 @@ class RecentsPresenter( private val isOnFirstPage: Boolean get() = pageOffset == 0 + override val progressJobs = mutableMapOf() + override val queueListenerScope get() = presenterScope + override fun onCreate() { super.onCreate() - downloadManager.addListener(this) - DownloadJob.downloadFlow.onEach(::downloadStatusChanged).launchIn(presenterScope) + presenterScope.launchUI { + downloadManager.statusFlow().collect(::onStatusChange) + } + presenterScope.launchUI { + downloadManager.progressFlow().collect(::onProgressUpdate) + } + presenterScope.launchIO { + downloadManager.queueState.collectLatest { + setDownloadedChapters(recentItems, it) + withUIContext { + view?.showLists(recentItems, true) + view?.updateDownloadStatus(!downloadManager.isPaused()) + } + } + } + downloadManager.isDownloaderRunning.onEach(::downloadStatusChanged).launchIn(presenterScope) LibraryUpdateJob.updateFlow.onEach(::onUpdateManga).launchIn(presenterScope) if (lastRecents != null) { if (recentItems.isEmpty()) { @@ -482,7 +500,6 @@ class RecentsPresenter( override fun onDestroy() { super.onDestroy() - downloadManager.removeListener(this) lastRecents = recentItems } @@ -500,12 +517,12 @@ class RecentsPresenter( * * @param chapters the list of chapter from the database. */ - private fun setDownloadedChapters(chapters: List) { + private fun setDownloadedChapters(chapters: List, queue: List = downloadManager.queueState.value) { for (item in chapters.filter { it.chapter.id != null }) { if (downloadManager.isChapterDownloaded(item.chapter, item.mch.manga)) { item.status = Download.State.DOWNLOADED - } else if (downloadManager.hasQueue()) { - item.download = downloadManager.queue.find { it.chapter.id == item.chapter.id } + } else if (queue.isNotEmpty()) { + item.download = queue.find { it.chapter.id == item.chapter.id } item.status = item.download?.status ?: Download.State.default } @@ -514,8 +531,8 @@ class RecentsPresenter( downloadInfo.chapterId = chapter.id if (downloadManager.isChapterDownloaded(chapter, item.mch.manga)) { downloadInfo.status = Download.State.DOWNLOADED - } else if (downloadManager.hasQueue()) { - downloadInfo.download = downloadManager.queue.find { it.chapter.id == chapter.id } + } else if (queue.isNotEmpty()) { + downloadInfo.download = queue.find { it.chapter.id == chapter.id } downloadInfo.status = downloadInfo.download?.status ?: Download.State.default } downloadInfo @@ -523,32 +540,6 @@ class RecentsPresenter( } } - override fun updateDownload(download: Download) { - recentItems.find { - download.chapter.id == it.chapter.id || - download.chapter.id in it.mch.extraChapters.map { ch -> ch.id } - }?.apply { - if (chapter.id != download.chapter.id) { - val downloadInfo = downloadInfo.find { it.chapterId == download.chapter.id } - ?: return@apply - downloadInfo.download = download - } else { - this.download = download - } - } - presenterScope.launchUI { view?.updateChapterDownload(download) } - } - - override fun updateDownloads() { - presenterScope.launch { - setDownloadedChapters(recentItems) - withContext(Dispatchers.Main) { - view?.showLists(recentItems, true) - view?.updateDownloadStatus(!downloadManager.isPaused()) - } - } - } - private fun downloadStatusChanged(downloading: Boolean) { presenterScope.launchUI { view?.updateDownloadStatus(downloading) @@ -704,6 +695,18 @@ class RecentsPresenter( } } + override fun onProgressUpdate(download: Download) { + // don't do anything + } + + override fun onQueueUpdate(download: Download) { + view?.updateChapterDownload(download) + } + + override fun onPageProgressUpdate(download: Download) { + view?.updateChapterDownload(download) + } + enum class GroupType { BySeries, ByWeek, From d61052485f616fdd5536ae561a2d51c6184c8062 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 09:29:28 +0700 Subject: [PATCH 089/166] fix: Use collect instead of collectLatest --- .../java/eu/kanade/tachiyomi/ui/library/LibraryController.kt | 5 ++--- .../java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 52acb5855d..b1a556c242 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -135,7 +135,6 @@ import kotlin.math.roundToInt import kotlin.random.Random import kotlin.random.nextInt import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -616,9 +615,9 @@ open class LibraryController( setPreferenceFlows() LibraryUpdateJob.updateFlow.onEach(::onUpdateManga).launchIn(viewScope) viewScope.launchUI { - LibraryUpdateJob.isRunningFlow(view.context).collectLatest { + LibraryUpdateJob.isRunningFlow(view.context).collect { val holder = if (mAdapter != null) visibleHeaderHolder() else null - val category = holder?.category ?: return@collectLatest + val category = holder?.category ?: return@collect holder.notifyStatus(LibraryUpdateJob.categoryInQueue(category.id), category) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt index e787f4d6ad..022d491fd4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt @@ -92,7 +92,6 @@ import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset import java.util.Locale import kotlin.math.max -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import yokai.i18n.MR import yokai.util.lang.getString @@ -390,7 +389,7 @@ class RecentsController(bundle: Bundle? = null) : }, ) viewScope.launch { - LibraryUpdateJob.isRunningFlow(view.context).collectLatest { + LibraryUpdateJob.isRunningFlow(view.context).collect { binding.swipeRefresh.isRefreshing = it } } From 336579bd35a53ba49136270a90bc9e3c2102ee0a Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 09:51:43 +0700 Subject: [PATCH 090/166] docs: Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c30c705ef..b5f2a8f90f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Fix slow chapter load - Fix chapter bookmark state is not persistent -## Other +### Other - Refactor downloader - Replace RxJava usage with Kotlin coroutines - Replace DownloadQueue with Flow to hopefully fix ConcurrentModificationException entirely From 5b637fae8fec154fe408d0880a516aabcf70ea3c Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 09:55:36 +0700 Subject: [PATCH 091/166] chore(release): v1.9.3 --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/ISSUE_TEMPLATE/issue_report.yml | 2 +- CHANGELOG.md | 2 ++ app/build.gradle.kts | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 0894b9d7d6..745c064999 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -35,7 +35,7 @@ body: required: true - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. required: true - - label: I have updated the app to version **[1.9.2](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.3](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have checked through the app settings for my feature. required: true diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index 3fb191542a..7fe226401c 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -100,7 +100,7 @@ body: required: true - label: I have tried the [troubleshooting guide](https://mihon.app/help/). required: true - - label: I have updated the app to version **[1.9.2](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.3](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have updated all installed extensions. required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index b5f2a8f90f..8670b71d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +## [1.9.3] + ### Fixes - Fix slow chapter load - Fix chapter bookmark state is not persistent diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 896e486ae6..8a132f8020 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 152 + versionCode = 153 versionName = _versionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true From 5fe72d4cb5dbeea0db90ae6eb142d6c7db0ca255 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 10:26:52 +0700 Subject: [PATCH 092/166] chore: Bump version to 1.9.4 for nightly and beta releases [skip ci] --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8a132f8020..198439bf97 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ fun runCommand(command: String): String { return String(byteOut.toByteArray()).trim() } -val _versionName = "1.9.3" +val _versionName = "1.9.4" val betaCount by lazy { val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") From 09621111bff3c24e71f47295c93cefb2060a249b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 10:43:32 +0700 Subject: [PATCH 093/166] fix(deps): Update dependency androidx.compose:compose-bom to v2024.12.01 (#296) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/compose.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 30249349c2..eb7c047db8 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,5 +1,5 @@ [versions] -compose = "2024.11.00" +compose = "2024.12.01" [libraries] bom = { module = "androidx.compose:compose-bom", version.ref = "compose" } From 8f45148a9ed3c09a5d5150144170002b6bab0162 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 10:43:38 +0700 Subject: [PATCH 094/166] docs: Sync changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8670b71d15..f7a194d0f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +### Other +- Update dependency androidx.compose:compose-bom to v2024.12.01 + ## [1.9.3] ### Fixes From 86f5e743e1b1a0bb1e3dc754038b69ec6ac3f837 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 11:05:26 +0700 Subject: [PATCH 095/166] chore(deps): Update plugin kotlinter to v5 (#304) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7413613b8..b80d6ef577 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -109,7 +109,7 @@ aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "abo firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.2" } google-services = { id = "com.google.gms.google-services", version = "4.4.2" } gradle-versions = { id = "com.github.ben-manes.versions", version = "0.42.0" } -kotlinter = { id = "org.jmailen.kotlinter", version = "4.1.1" } +kotlinter = { id = "org.jmailen.kotlinter", version = "5.0.1" } moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } From fbe97606164b1c3e75dbc69f22610017ecdeacee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 11:06:18 +0700 Subject: [PATCH 096/166] chore(deps): Update plugin gradle-versions to v0.51.0 (#303) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b80d6ef577..7d3fc9ec4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -108,7 +108,7 @@ voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", vers aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.2" } google-services = { id = "com.google.gms.google-services", version = "4.4.2" } -gradle-versions = { id = "com.github.ben-manes.versions", version = "0.42.0" } +gradle-versions = { id = "com.github.ben-manes.versions", version = "0.51.0" } kotlinter = { id = "org.jmailen.kotlinter", version = "5.0.1" } moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } From bfee1de3b18892ea446c367d1b48c853b072b204 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 11:06:27 +0700 Subject: [PATCH 097/166] docs: Sync changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a194d0f9..5c70ecd1e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ### Other - Update dependency androidx.compose:compose-bom to v2024.12.01 +- Update plugin kotlinter to v5 +- Update plugin gradle-versions to v0.51.0 ## [1.9.3] From 00aa93d1895dd12e245bd3edf3d2e9573683ad5c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 11:49:47 +0700 Subject: [PATCH 098/166] chore(deps): Update kotlin monorepo to v2.1.0 (#291) * chore(deps): Update kotlin monorepo to v2.1.0 * docs: Sync changelog --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ahmad Ansori Palembani --- CHANGELOG.md | 1 + gradle/kotlinx.versions.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c70ecd1e0..758c9c2881 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Update dependency androidx.compose:compose-bom to v2024.12.01 - Update plugin kotlinter to v5 - Update plugin gradle-versions to v0.51.0 +- Update kotlin monorepo to v2.1.0 ## [1.9.3] diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 2b8d124a3e..2384d6c058 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.0.21" +kotlin = "2.1.0" serialization = "1.7.3" xml_serialization = "0.90.3" From 8c5b54df5f567f391d1493aa0db02b801936dba9 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 13:29:18 +0700 Subject: [PATCH 099/166] fix(library): Handle multiple header --- .../eu/kanade/tachiyomi/ui/library/LibraryController.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index b1a556c242..2493614196 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -616,9 +616,11 @@ open class LibraryController( LibraryUpdateJob.updateFlow.onEach(::onUpdateManga).launchIn(viewScope) viewScope.launchUI { LibraryUpdateJob.isRunningFlow(view.context).collect { - val holder = if (mAdapter != null) visibleHeaderHolder() else null - val category = holder?.category ?: return@collect - holder.notifyStatus(LibraryUpdateJob.categoryInQueue(category.id), category) + adapter.getHeaderPositions().forEach { + val holder = (binding.libraryGridRecycler.recycler.findViewHolderForAdapterPosition(it) as? LibraryHeaderHolder) ?: return@forEach + val category = holder.category ?: return@forEach + holder.notifyStatus(LibraryUpdateJob.categoryInQueue(category.id), category) + } } } From ee2acf8b98d3bdca58db12787e4a9878c817eb62 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 14:29:10 +0700 Subject: [PATCH 100/166] fix: Delete duplicate chapters --- .../kanade/tachiyomi/util/chapter/ChapterSourceSync.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index bda3a024bd..66e73646a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -107,12 +107,17 @@ suspend fun syncChaptersWithSource( it.chapter_number = ChapterRecognition.parseChapterNumber(it.name, manga.title, it.chapter_number) } - // Chapters from the db not in the source. - val toDelete = dbChapters.filterNot { dbChapter -> + val duplicates = dbChapters.groupBy { it.url } + .filter { it.value.size > 1 } + .flatMap { (_, chapters) -> + chapters.drop(1) + } + val notInSource = dbChapters.filterNot { dbChapter -> sourceChapters.any { sourceChapter -> dbChapter.url == sourceChapter.url } } + val toDelete = duplicates + notInSource // Return if there's nothing to add, delete or change, avoid unnecessary db transactions. if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { From 62e26d1f38c916db41fece73143fda191b8f82ca Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 18:46:49 +0700 Subject: [PATCH 101/166] refactor: Simplify code and move stuff around --- .../util/chapter/ChapterSourceSync.kt | 93 ++++++++----------- 1 file changed, 40 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 66e73646a9..743052c1ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -65,48 +65,6 @@ suspend fun syncChaptersWithSource( // Chapters whose metadata have changed. val toChange = mutableListOf() - for (sourceChapter in sourceChapters) { - val dbChapter = dbChapters.find { it.url == sourceChapter.url } - - // Add the chapter if not in db already, or update if the metadata changed. - if (dbChapter == null) { - toAdd.add(sourceChapter) - } else { - // this forces metadata update for the main viewable things in the chapter list - if (source is HttpSource) { - source.prepareNewChapter(sourceChapter, manga) - } - - sourceChapter.chapter_number = - ChapterRecognition.parseChapterNumber(sourceChapter.name, manga.title, sourceChapter.chapter_number) - - if (shouldUpdateDbChapter(dbChapter, sourceChapter)) { - if ((dbChapter.name != sourceChapter.name || dbChapter.scanlator != sourceChapter.scanlator) && - downloadManager.isChapterDownloaded(dbChapter, manga) - ) { - downloadManager.renameChapter(source, manga, dbChapter, sourceChapter) - } - val update = ChapterUpdate( - dbChapter.id!!, - scanlator = sourceChapter.scanlator, - name = sourceChapter.name, - dateUpload = sourceChapter.date_upload, - chapterNumber = sourceChapter.chapter_number.toDouble(), - sourceOrder = sourceChapter.source_order.toLong(), - ) - toChange.add(update) - } - } - } - - // Recognize number for new chapters. - toAdd.forEach { - if (source is HttpSource) { - source.prepareNewChapter(it, manga) - } - it.chapter_number = ChapterRecognition.parseChapterNumber(it.name, manga.title, it.chapter_number) - } - val duplicates = dbChapters.groupBy { it.url } .filter { it.value.size > 1 } .flatMap { (_, chapters) -> @@ -119,6 +77,39 @@ suspend fun syncChaptersWithSource( } val toDelete = duplicates + notInSource + for (sourceChapter in sourceChapters) { + val chapter = sourceChapter + + if (source is HttpSource) { + source.prepareNewChapter(chapter, manga) + } + chapter.chapter_number = ChapterRecognition.parseChapterNumber(chapter.name, manga.title, chapter.chapter_number) + + val dbChapter = dbChapters.find { it.url == chapter.url } + + // Add the chapter if not in db already, or update if the metadata changed. + if (dbChapter == null) { + toAdd.add(chapter) + } else { + if (shouldUpdateDbChapter(dbChapter, chapter)) { + if ((dbChapter.name != chapter.name || dbChapter.scanlator != chapter.scanlator) && + downloadManager.isChapterDownloaded(dbChapter, manga) + ) { + downloadManager.renameChapter(source, manga, dbChapter, chapter) + } + val update = ChapterUpdate( + dbChapter.id!!, + scanlator = chapter.scanlator, + name = chapter.name, + dateUpload = chapter.date_upload, + chapterNumber = chapter.chapter_number.toDouble(), + sourceOrder = chapter.source_order.toLong(), + ) + toChange.add(update) + } + } + } + // Return if there's nothing to add, delete or change, avoid unnecessary db transactions. if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { val newestDate = dbChapters.maxOfOrNull { it.date_upload } ?: 0L @@ -198,21 +189,17 @@ suspend fun syncChaptersWithSource( manga.last_update = Date().time updateManga.await(MangaUpdate(manga.id!!, lastUpdate = manga.last_update)) - val reAddedSet = reAdded.toSet() + val reAddedUrls = reAdded.map { it.url }.toHashSet() + val filteredScanlators = ChapterUtil.getScanlators(manga.filtered_scanlators).toHashSet() + return Pair( - updatedToAdd.subtract(reAddedSet).toList().filterChaptersByScanlators(manga), - toDelete - reAddedSet, + updatedToAdd.filterNot { + it.url in reAddedUrls || it.scanlator in filteredScanlators + }, + toDelete.filterNot { it.url in reAddedUrls }, ) } -private fun List.filterChaptersByScanlators(manga: Manga): List { - if (manga.filtered_scanlators.isNullOrBlank()) return this - - return this.filter { chapter -> - !ChapterUtil.getScanlators(manga.filtered_scanlators).contains(chapter.scanlator) - } -} - // checks if the chapter in db needs updated private fun shouldUpdateDbChapter(dbChapter: Chapter, sourceChapter: Chapter): Boolean { return dbChapter.scanlator != sourceChapter.scanlator || From e604c951ed08de095de17a5b654417d72e4bd49c Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 19:02:10 +0700 Subject: [PATCH 102/166] refactor: ifnull no longer needed --- data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq b/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq index f5d5e36b9b..636236ae58 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq @@ -26,7 +26,7 @@ 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 +AND C.scanlator = S.name WHERE C.manga_id = :manga_id AND ( :apply_filter = 0 OR S.name IS NULL @@ -41,7 +41,7 @@ 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 +AND C.scanlator = S.name WHERE C.url = :url AND ( :apply_filter = 0 OR S.name IS NULL @@ -52,7 +52,7 @@ 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 +AND C.scanlator = S.name WHERE C.url = :url AND C.manga_id = :manga_id AND ( :apply_filter = 0 OR S.name IS NULL @@ -67,7 +67,7 @@ JOIN chapters AS C ON M._id = C.manga_id 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 +AND C.scanlator = S.name WHERE M.favorite = 1 AND C.date_fetch > M.date_added AND lower(M.title) LIKE '%' || :search || '%' From abcf06b92174a66983f19faf56c4b638f1ec1fa8 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 19:20:23 +0700 Subject: [PATCH 103/166] chore: Partial revert --- .../java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 743052c1ca..e729b8ef5a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -113,7 +113,7 @@ suspend fun syncChaptersWithSource( // Return if there's nothing to add, delete or change, avoid unnecessary db transactions. if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { val newestDate = dbChapters.maxOfOrNull { it.date_upload } ?: 0L - if (newestDate != 0L && newestDate > manga.last_update) { + if (newestDate != 0L && newestDate != manga.last_update) { manga.last_update = newestDate val update = MangaUpdate(manga.id!!, lastUpdate = newestDate) updateManga.await(update) From 71a9e2493bdb985f450ad9ef626fd6b2c75e11dc Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 19:39:13 +0700 Subject: [PATCH 104/166] fix(chapter): A mishap --- .../java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index e729b8ef5a..1cbf81d063 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -164,7 +164,7 @@ suspend fun syncChaptersWithSource( } if (updatedToAdd.isNotEmpty()) { - updatedToAdd = insertChapter.awaitBulk(toAdd) + updatedToAdd = insertChapter.awaitBulk(updatedToAdd) } if (toChange.isNotEmpty()) { From 82b73bce76d9545e83a82638d36cfd82d793d330 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 19:39:13 +0700 Subject: [PATCH 105/166] fix(chapter): A mishap --- .../java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index bda3a024bd..f3c955dbe0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -168,7 +168,7 @@ suspend fun syncChaptersWithSource( } if (updatedToAdd.isNotEmpty()) { - updatedToAdd = insertChapter.awaitBulk(toAdd) + updatedToAdd = insertChapter.awaitBulk(updatedToAdd) } if (toChange.isNotEmpty()) { From 316bc87a5c430bfd9ff5db300ad4ef3630949995 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 19:44:31 +0700 Subject: [PATCH 106/166] chore(release): v1.9.4 --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/ISSUE_TEMPLATE/issue_report.yml | 2 +- CHANGELOG.md | 5 +++++ app/build.gradle.kts | 4 ++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 745c064999..ae6a49a881 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -35,7 +35,7 @@ body: required: true - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. required: true - - label: I have updated the app to version **[1.9.3](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.4](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have checked through the app settings for my feature. required: true diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index 7fe226401c..ea2e865021 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -100,7 +100,7 @@ body: required: true - label: I have tried the [troubleshooting guide](https://mihon.app/help/). required: true - - label: I have updated the app to version **[1.9.3](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.4](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have updated all installed extensions. required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 8670b71d15..c83b2564f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +## [1.9.4] + +### Fixes +- Fix chapter date fetch always null causing it to not appear on Updates tab + ## [1.9.3] ### Fixes diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8a132f8020..55c1939605 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ fun runCommand(command: String): String { return String(byteOut.toByteArray()).trim() } -val _versionName = "1.9.3" +val _versionName = "1.9.4" val betaCount by lazy { val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") @@ -54,7 +54,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 153 + versionCode = 154 versionName = _versionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true From 93e5effaaf32121a8cc6498bcaecbef643dd1105 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 19:55:09 +0700 Subject: [PATCH 107/166] chore: Bump version to 1.9.5 for nightly and beta --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 55c1939605..af09d6225c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ fun runCommand(command: String): String { return String(byteOut.toByteArray()).trim() } -val _versionName = "1.9.4" +val _versionName = "1.9.5" val betaCount by lazy { val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") From 59a8bfc7aa035ff3e54af1160916927f761bc741 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 14 Dec 2024 20:05:55 +0700 Subject: [PATCH 108/166] revert: "chore: Partial revert" This reverts commit abcf06b92174a66983f19faf56c4b638f1ec1fa8. --- .../java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 1cbf81d063..ab67e3aa40 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -113,7 +113,7 @@ suspend fun syncChaptersWithSource( // Return if there's nothing to add, delete or change, avoid unnecessary db transactions. if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { val newestDate = dbChapters.maxOfOrNull { it.date_upload } ?: 0L - if (newestDate != 0L && newestDate != manga.last_update) { + if (newestDate != 0L && newestDate > manga.last_update) { manga.last_update = newestDate val update = MangaUpdate(manga.id!!, lastUpdate = newestDate) updateManga.await(update) From 1bbfd97b3044dc42f614cb94ab6cb4e0a0cab0d2 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 15 Dec 2024 09:16:17 +0700 Subject: [PATCH 109/166] fix: Downgrade dependency me.zhanghai.android.libarchive:library to v1.1.2 Causing segmentation fault on some devices --- CHANGELOG.md | 1 + gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc4b2a579a..b37fa9cf3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Update plugin kotlinter to v5 - Update plugin gradle-versions to v0.51.0 - Update kotlin monorepo to v2.1.0 +- Downgrade dependency me.zhanghai.android.libarchive:library to v1.1.2 ## [1.9.4] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d3fc9ec4e..0f38e62902 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,7 +54,7 @@ kotest-assertions = { module = "io.kotest:kotest-assertions-core", version = "5. leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" } -libarchive = { module = "me.zhanghai.android.libarchive:library", version = "1.1.4" } +libarchive = { module = "me.zhanghai.android.libarchive:library", version = "1.1.2" } material = { module = "com.google.android.material:material", version = "1.12.0" } markwon = { module = "io.noties.markwon:core", version = "4.6.2" } From 834958a8198b2d4c90f9fae150179e49b9be99ab Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 15 Dec 2024 09:22:16 +0700 Subject: [PATCH 110/166] fix(chapter): Distinct by url again --- .../java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index ab67e3aa40..5c340064e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -137,7 +137,7 @@ suspend fun syncChaptersWithSource( // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones // Sources MUST return the chapters from most to less recent, which is common. var itemCount = toAdd.size - var updatedToAdd = toAdd.map { toAddItem -> + var updatedToAdd = toAdd.distinctBy { it.url }.map { toAddItem -> val chapter: Chapter = toAddItem.copy() chapter.date_fetch = now + itemCount-- From d99476f9bfdcd8d2adb9906151e00fea449c9ab6 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 15 Dec 2024 10:29:01 +0700 Subject: [PATCH 111/166] fix(chapter): Check if url already managed or not --- .../eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 5c340064e7..f105fcbb2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -77,9 +77,13 @@ suspend fun syncChaptersWithSource( } val toDelete = duplicates + notInSource + val managedUrls = mutableListOf() + for (sourceChapter in sourceChapters) { val chapter = sourceChapter + if (chapter.url in managedUrls) continue + if (source is HttpSource) { source.prepareNewChapter(chapter, manga) } @@ -108,6 +112,8 @@ suspend fun syncChaptersWithSource( toChange.add(update) } } + + managedUrls.add(chapter.url) } // Return if there's nothing to add, delete or change, avoid unnecessary db transactions. @@ -137,7 +143,7 @@ suspend fun syncChaptersWithSource( // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones // Sources MUST return the chapters from most to less recent, which is common. var itemCount = toAdd.size - var updatedToAdd = toAdd.distinctBy { it.url }.map { toAddItem -> + var updatedToAdd = toAdd.map { toAddItem -> val chapter: Chapter = toAddItem.copy() chapter.date_fetch = now + itemCount-- From c7b6e8ee00376812ac5be586789f76d807c086b3 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 15 Dec 2024 19:53:43 +0700 Subject: [PATCH 112/166] revert: "revert: "fix(sql): Use UNION ALL instead of UNION"" This reverts commit 37535d3bcf4fef0497a15ff143bebcc1b6f9692e. --- data/src/commonMain/sqldelight/tachiyomi/data/history.sq | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq index 35f29d6ecd..48bcc04ff6 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq @@ -157,7 +157,7 @@ AND ( :apply_filter = 0 OR S.name IS NULL ) -UNION -- Newly fetched chapter +UNION ALL -- Newly fetched chapter SELECT M.*, @@ -192,7 +192,7 @@ AND ( :apply_filter = 0 OR S.name IS NULL ) -UNION -- Newly added manga +UNION ALL -- Newly added manga SELECT M.*, From 247ed3bca77b309bfa6c11352ca9928c5bdf8a9f Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 15 Dec 2024 20:24:26 +0700 Subject: [PATCH 113/166] revert: "revert: "revert: "fix(sql): Use UNION ALL instead of UNION""" That only made it worse... --- data/src/commonMain/sqldelight/tachiyomi/data/history.sq | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq index 48bcc04ff6..35f29d6ecd 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq @@ -157,7 +157,7 @@ AND ( :apply_filter = 0 OR S.name IS NULL ) -UNION ALL -- Newly fetched chapter +UNION -- Newly fetched chapter SELECT M.*, @@ -192,7 +192,7 @@ AND ( :apply_filter = 0 OR S.name IS NULL ) -UNION ALL -- Newly added manga +UNION -- Newly added manga SELECT M.*, From 6c1d8d5011f2273a14ff5a6448c8fa93ef93fe31 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 15 Dec 2024 21:19:28 +0700 Subject: [PATCH 114/166] revert "refactor: Some multiplatform bs" --- CHANGELOG.md | 1 - core/build.gradle.kts | 1 - .../kotlin/yokai/core/archive/ArchiveEntry.kt | 0 .../kotlin/yokai/core/archive/ArchiveInputStream.kt | 2 +- .../src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt | 2 +- .../commonMain/kotlin/yokai/core/archive/ArchiveInputStream.kt | 3 --- core/src/commonMain/kotlin/yokai/core/archive/ArchiveReader.kt | 3 --- .../iosMain/kotlin/yokai/core/archive/ArchiveInputStream.kt | 3 --- core/src/iosMain/kotlin/yokai/core/archive/ArchiveReader.kt | 3 --- gradle/libs.versions.toml | 2 +- 10 files changed, 3 insertions(+), 17 deletions(-) rename core/src/{commonMain => androidMain}/kotlin/yokai/core/archive/ArchiveEntry.kt (100%) delete mode 100644 core/src/commonMain/kotlin/yokai/core/archive/ArchiveInputStream.kt delete mode 100644 core/src/commonMain/kotlin/yokai/core/archive/ArchiveReader.kt delete mode 100644 core/src/iosMain/kotlin/yokai/core/archive/ArchiveInputStream.kt delete mode 100644 core/src/iosMain/kotlin/yokai/core/archive/ArchiveReader.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index b37fa9cf3f..fc4b2a579a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,6 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Update plugin kotlinter to v5 - Update plugin gradle-versions to v0.51.0 - Update kotlin monorepo to v2.1.0 -- Downgrade dependency me.zhanghai.android.libarchive:library to v1.1.2 ## [1.9.4] diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 937662ecbe..ddac21ff77 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -66,7 +66,6 @@ android { tasks { withType { compilerOptions.freeCompilerArgs.addAll( - "-Xexpect-actual-classes", "-Xcontext-receivers", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", diff --git a/core/src/commonMain/kotlin/yokai/core/archive/ArchiveEntry.kt b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveEntry.kt similarity index 100% rename from core/src/commonMain/kotlin/yokai/core/archive/ArchiveEntry.kt rename to core/src/androidMain/kotlin/yokai/core/archive/ArchiveEntry.kt diff --git a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt index c805145fed..1da5acfe4a 100644 --- a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt +++ b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt @@ -3,6 +3,6 @@ package yokai.core.archive import java.io.InputStream // TODO: Use Okio's Source -actual abstract class ArchiveInputStream : InputStream() { +abstract class ArchiveInputStream : InputStream() { abstract fun getNextEntry(): ArchiveEntry? } diff --git a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt index 8b7e1462bc..0eb3669d49 100644 --- a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt +++ b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt @@ -3,7 +3,7 @@ package yokai.core.archive import java.io.Closeable import java.io.InputStream -actual abstract class ArchiveReader : Closeable { +abstract class ArchiveReader : Closeable { abstract val address: Long abstract val size: Long diff --git a/core/src/commonMain/kotlin/yokai/core/archive/ArchiveInputStream.kt b/core/src/commonMain/kotlin/yokai/core/archive/ArchiveInputStream.kt deleted file mode 100644 index c4a5ff540e..0000000000 --- a/core/src/commonMain/kotlin/yokai/core/archive/ArchiveInputStream.kt +++ /dev/null @@ -1,3 +0,0 @@ -package yokai.core.archive - -expect abstract class ArchiveInputStream diff --git a/core/src/commonMain/kotlin/yokai/core/archive/ArchiveReader.kt b/core/src/commonMain/kotlin/yokai/core/archive/ArchiveReader.kt deleted file mode 100644 index 111e11def4..0000000000 --- a/core/src/commonMain/kotlin/yokai/core/archive/ArchiveReader.kt +++ /dev/null @@ -1,3 +0,0 @@ -package yokai.core.archive - -expect abstract class ArchiveReader diff --git a/core/src/iosMain/kotlin/yokai/core/archive/ArchiveInputStream.kt b/core/src/iosMain/kotlin/yokai/core/archive/ArchiveInputStream.kt deleted file mode 100644 index 9d85374f50..0000000000 --- a/core/src/iosMain/kotlin/yokai/core/archive/ArchiveInputStream.kt +++ /dev/null @@ -1,3 +0,0 @@ -package yokai.core.archive - -actual abstract class ArchiveInputStream diff --git a/core/src/iosMain/kotlin/yokai/core/archive/ArchiveReader.kt b/core/src/iosMain/kotlin/yokai/core/archive/ArchiveReader.kt deleted file mode 100644 index b06f6c26d8..0000000000 --- a/core/src/iosMain/kotlin/yokai/core/archive/ArchiveReader.kt +++ /dev/null @@ -1,3 +0,0 @@ -package yokai.core.archive - -actual abstract class ArchiveReader diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f38e62902..7d3fc9ec4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,7 +54,7 @@ kotest-assertions = { module = "io.kotest:kotest-assertions-core", version = "5. leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" } -libarchive = { module = "me.zhanghai.android.libarchive:library", version = "1.1.2" } +libarchive = { module = "me.zhanghai.android.libarchive:library", version = "1.1.4" } material = { module = "com.google.android.material:material", version = "1.12.0" } markwon = { module = "io.noties.markwon:core", version = "4.6.2" } From 263603616e7a9b8f855a6be172f9506dbca0b809 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sun, 15 Dec 2024 21:22:46 +0700 Subject: [PATCH 115/166] fix: Don't target iOS We're not doing this anytime soon --- core/build.gradle.kts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index ddac21ff77..e169090b2b 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -8,9 +8,9 @@ plugins { kotlin { androidTarget() - iosX64() - iosArm64() - iosSimulatorArm64() + // iosX64() + // iosArm64() + // iosSimulatorArm64() sourceSets { commonMain { dependencies { @@ -52,10 +52,10 @@ kotlin { implementation(libs.libarchive) } } - iosMain { - dependencies { - } - } + // iosMain { + // dependencies { + // } + // } } } From e5b8ed9e9db3517eb08d1f9d8057d94ec97fb84c Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 16 Dec 2024 08:27:17 +0700 Subject: [PATCH 116/166] fix(recents): Find unread from SQL instead of code --- .../kanade/tachiyomi/ui/recents/RecentsPresenter.kt | 13 ++++++------- .../eu/kanade/tachiyomi/util/chapter/ChapterSort.kt | 8 ++++++++ .../yokai/data/chapter/ChapterRepositoryImpl.kt | 5 +++++ .../java/yokai/domain/chapter/ChapterRepository.kt | 1 + .../yokai/domain/chapter/interactor/GetChapter.kt | 3 +++ .../sqldelight/tachiyomi/data/chapters.sq | 11 +++++++++++ 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index 56b6baba1c..138df111a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -485,17 +485,16 @@ class RecentsPresenter( private suspend fun getNextChapter(manga: Manga): Chapter? { val mangaId = manga.id ?: return null - val chapters = getChapter.awaitAll(mangaId, true) - return ChapterSort(manga, chapterFilter, preferences).getNextUnreadChapter(chapters, false) + val chapters = getChapter.awaitUnread(mangaId, true) + return ChapterSort(manga, chapterFilter, preferences).getNextChapter(chapters, false) } private suspend fun getFirstUpdatedChapter(manga: Manga, chapter: Chapter): Chapter? { val mangaId = manga.id ?: return null - val chapters = getChapter.awaitAll(mangaId, true) - return chapters - .sortedWith(ChapterSort(manga, chapterFilter, preferences).sortComparator(true)).find { - !it.read && abs(it.date_fetch - chapter.date_fetch) <= TimeUnit.HOURS.toMillis(12) - } + val chapters = getChapter.awaitUnread(mangaId, true) + return chapters.sortedWith(ChapterSort(manga, chapterFilter, preferences).sortComparator(true)).find { + abs(it.date_fetch - chapter.date_fetch) <= TimeUnit.HOURS.toMillis(12) + } } override fun onDestroy() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSort.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSort.kt index dd118ac77c..4e2075e5e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSort.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSort.kt @@ -30,6 +30,14 @@ class ChapterSort(val manga: Manga, val chapterFilter: ChapterFilter = Injekt.ge return chapters.sortedWith(sortComparator()) } + fun getNextChapter(rawChapters: List, andFiltered: Boolean = true): T? { + val chapters = when { + andFiltered -> chapterFilter.filterChapters(rawChapters, manga) + else -> rawChapters + } + return chapters.sortedWith(sortComparator(true)).firstOrNull() + } + fun getNextUnreadChapter(rawChapters: List, andFiltered: Boolean = true): T? { val chapters = when { andFiltered -> chapterFilter.filterChapters(rawChapters, manga) diff --git a/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt b/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt index 1b38b71a9f..868e0748e8 100644 --- a/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt +++ b/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt @@ -43,6 +43,11 @@ class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepos chaptersQueries.getChaptersByUrlAndMangaId(url, mangaId, filterScanlators.toInt().toLong(), Chapter::mapper) } + override suspend fun getUnread(mangaId: Long, filterScanlators: Boolean): List = + handler.awaitList { + chaptersQueries.findUnreadByMangaId(mangaId, filterScanlators.toInt().toLong(), Chapter::mapper) + } + override suspend fun getRecents(filterScanlators: Boolean, search: String, limit: Long, offset: Long): List = handler.awaitList { chaptersQueries.getRecents(search, filterScanlators.toInt().toLong(), limit, offset, MangaChapter::mapper) } diff --git a/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt b/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt index dd39b017a1..eb061a809d 100644 --- a/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt +++ b/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt @@ -16,6 +16,7 @@ interface ChapterRepository { suspend fun getChaptersByUrlAndMangaId(url: String, mangaId: Long, filterScanlators: Boolean): List suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long, filterScanlators: Boolean): Chapter? + suspend fun getUnread(mangaId: Long, filterScanlators: Boolean): List suspend fun getRecents(filterScanlators: Boolean, search: String = "", limit: Long = 25L, offset: Long = 0L): List diff --git a/app/src/main/java/yokai/domain/chapter/interactor/GetChapter.kt b/app/src/main/java/yokai/domain/chapter/interactor/GetChapter.kt index 01d3b8b42a..648614506d 100644 --- a/app/src/main/java/yokai/domain/chapter/interactor/GetChapter.kt +++ b/app/src/main/java/yokai/domain/chapter/interactor/GetChapter.kt @@ -11,6 +11,9 @@ class GetChapter( suspend fun awaitAll(manga: Manga, filterScanlators: Boolean? = null) = awaitAll(manga.id!!, filterScanlators ?: (manga.filtered_scanlators?.isNotEmpty() == true)) + suspend fun awaitUnread(mangaId: Long, filterScanlators: Boolean) = + chapterRepository.getUnread(mangaId, filterScanlators) + suspend fun awaitById(id: Long) = chapterRepository.getChapterById(id) suspend fun awaitAllByUrl(chapterUrl: String, filterScanlators: Boolean) = diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq b/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq index 636236ae58..329fa4a8d4 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq @@ -58,6 +58,17 @@ AND ( :apply_filter = 0 OR S.name IS NULL ); +findUnreadByMangaId: +SELECT C.* +FROM chapters AS C +LEFT JOIN scanlators_view AS S +ON C.manga_id = S.manga_id +AND C.scanlator = S.name +WHERE C.manga_id = :manga_id AND C.read = 0 +AND ( + :apply_filter = 0 OR S.name IS NULL +); + getRecents: SELECT M.*, From 4fc18b49134e5bdef77bb40ee02c48f476f42a52 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 16 Dec 2024 09:00:40 +0700 Subject: [PATCH 117/166] fix(recents): Missing `ON` statement --- data/src/commonMain/sqldelight/tachiyomi/data/history.sq | 1 + 1 file changed, 1 insertion(+) diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq index 35f29d6ecd..268b9c7cb3 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq @@ -181,6 +181,7 @@ JOIN ( WHERE C2.read = 0 GROUP BY C2.manga_id ) AS newest_chapter +ON C.manga_id = newest_chapter.manga_id LEFT JOIN scanlators_view AS S ON C.manga_id = S.manga_id AND C.scanlator = S.name From 301acb9f4d155b7a9cb20cd86bf451c1f35ba545 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 16 Dec 2024 09:12:49 +0700 Subject: [PATCH 118/166] fix(history): Remove unnecessary JOIN statement --- data/src/commonMain/sqldelight/tachiyomi/data/history.sq | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq index 268b9c7cb3..db218836a3 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/history.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/history.sq @@ -169,12 +169,10 @@ SELECT FROM mangas AS M JOIN chapters AS C ON M._id = C.manga_id -JOIN history -ON C._id = history.history_chapter_id JOIN ( SELECT C2.manga_id, - C2._id AS history_chapter_id, + C2._id, max(date_upload) FROM chapters AS C2 JOIN mangas AS M2 ON M2._id = C2.manga_id @@ -186,8 +184,8 @@ LEFT JOIN scanlators_view AS S ON C.manga_id = S.manga_id AND C.scanlator = S.name WHERE favorite = 1 -AND newest_chapter.history_chapter_id = history.history_chapter_id -AND date_fetch > date_added +AND C._id = newest_chapter._id +AND C.date_fetch > M.date_added AND lower(title) LIKE '%' || :search || '%' AND ( :apply_filter = 0 OR S.name IS NULL From 2461b93af5a5b4d87f00d0db3e1318c493fd22e4 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 16 Dec 2024 09:44:01 +0700 Subject: [PATCH 119/166] docs: Sync changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc4b2a579a..ad932c65ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +### Fixes +- Fix new chapters not showing up in `Recents > Grouped` +- Add potential workaround for duplicate chapter bug + ### Other - Update dependency androidx.compose:compose-bom to v2024.12.01 - Update plugin kotlinter to v5 From 3651c2a853ff769de4d48d5d21116f05a97c0564 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 16 Dec 2024 11:17:29 +0700 Subject: [PATCH 120/166] fix(browse): Update favorite state in real time --- .../ui/source/browse/BrowseSourceItem.kt | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt index 8e51800910..1f5c844ada 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt @@ -18,15 +18,31 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.setBGAndFG import eu.kanade.tachiyomi.widget.AutofitRecyclerView +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import uy.kohesive.injekt.injectLazy +import yokai.domain.manga.interactor.GetManga +// FIXME: Migrate to compose class BrowseSourceItem( - val manga: Manga, + initialManga: Manga, private val catalogueAsList: Preference, private val catalogueListType: Preference, private val outlineOnCovers: Preference, ) : AbstractFlexibleItem() { + private val getManga: GetManga by injectLazy() + + val mangaId: Long = initialManga.id!! + var manga: Manga = initialManga + private set + // TODO: Could potentially cause memleak, test it with leakcanary before deploying to stable! + private val scope = MainScope() + private var job: Job? = null + override fun getLayoutRes(): Int { return if (catalogueAsList.get()) { R.layout.manga_list_item @@ -76,18 +92,34 @@ class BrowseSourceItem( position: Int, payloads: MutableList?, ) { - holder.onSetValues(manga) + if (job == null) holder.onSetValues(manga) + job?.cancel() + job = scope.launch { + getManga.subscribeByUrlAndSource(manga.url, manga.source).collectLatest { + manga = it ?: return@collectLatest + holder.onSetValues(manga) + } + } + } + + override fun unbindViewHolder( + adapter: FlexibleAdapter>?, + holder: BrowseSourceHolder?, + position: Int + ) { + job?.cancel() + job = null } override fun equals(other: Any?): Boolean { if (this === other) return true if (other is BrowseSourceItem) { - return manga.id!! == other.manga.id!! + return mangaId == other.mangaId } return false } override fun hashCode(): Int { - return manga.id!!.hashCode() + return mangaId.hashCode() } } From 8ec29bf755df3e61939cb589fa9abc66d5e932ed Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 16 Dec 2024 11:40:53 +0700 Subject: [PATCH 121/166] fix(globalsearch): Update favorite state in real time --- CHANGELOG.md | 1 + .../globalsearch/GlobalSearchMangaItem.kt | 37 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad932c65ee..20dc0fc6f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ### Fixes - Fix new chapters not showing up in `Recents > Grouped` - Add potential workaround for duplicate chapter bug +- Fix favorite state is not being updated when browsing source ### Other - Update dependency androidx.compose:compose-bom to v2024.12.01 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt index 387bc3d96a..ab9763b203 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt @@ -7,8 +7,25 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.domain.manga.models.Manga +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import uy.kohesive.injekt.injectLazy +import yokai.domain.manga.interactor.GetManga -class GlobalSearchMangaItem(val manga: Manga) : AbstractFlexibleItem() { +// FIXME: Migrate to compose +class GlobalSearchMangaItem( + initialManga: Manga +) : AbstractFlexibleItem() { + + private val getManga: GetManga by injectLazy() + + var manga: Manga = initialManga + private set + // TODO: Could potentially cause memleak, test it with leakcanary before deploying to stable! + private val scope = MainScope() + private var job: Job? = null override fun getLayoutRes(): Int { return R.layout.source_global_search_controller_card_item @@ -24,7 +41,23 @@ class GlobalSearchMangaItem(val manga: Manga) : AbstractFlexibleItem?, ) { - holder.bind(manga) + if (job == null) holder.bind(manga) + job?.cancel() + job = scope.launch { + getManga.subscribeByUrlAndSource(manga.url, manga.source).collectLatest { + manga = it ?: return@collectLatest + holder.bind(manga) + } + } + } + + override fun unbindViewHolder( + adapter: FlexibleAdapter>?, + holder: GlobalSearchMangaHolder?, + position: Int + ) { + job?.cancel() + job = null } override fun equals(other: Any?): Boolean { From 50cad86c0cb32f84eb57695872f9d26bef82f701 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 16 Dec 2024 12:22:21 +0700 Subject: [PATCH 122/166] chore(globalsearch): Save initial mangaId just in case --- .../ui/source/globalsearch/GlobalSearchMangaItem.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt index ab9763b203..503d2778e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt @@ -21,6 +21,7 @@ class GlobalSearchMangaItem( private val getManga: GetManga by injectLazy() + val mangaId: Long? = initialManga.id var manga: Manga = initialManga private set // TODO: Could potentially cause memleak, test it with leakcanary before deploying to stable! @@ -62,12 +63,12 @@ class GlobalSearchMangaItem( override fun equals(other: Any?): Boolean { if (other is GlobalSearchMangaItem) { - return manga.id == other.manga.id + return mangaId == other.mangaId } return false } override fun hashCode(): Int { - return manga.id?.toInt() ?: 0 + return mangaId?.toInt() ?: 0 } } From 2ef1195a90fcbe20a2ed759f34bd4d278dd27842 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 16 Dec 2024 20:43:37 +0700 Subject: [PATCH 123/166] refactor(manga): Slowly using flow --- .../ui/manga/MangaDetailsController.kt | 12 +- .../ui/manga/MangaDetailsPresenter.kt | 244 +++++++++--------- .../tachiyomi/domain/manga/models/Manga.kt | 23 ++ 3 files changed, 145 insertions(+), 134 deletions(-) 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 01a980fa31..f5dfe23e52 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 @@ -194,7 +194,7 @@ class MangaDetailsController : } } - private val manga: Manga? get() = if (presenter.isMangaLateInitInitialized()) presenter.manga else null + private val manga: Manga? get() = presenter.currentManga.value private var colorAnimator: ValueAnimator? = null override val presenter: MangaDetailsPresenter private var coverColor: Int? = null @@ -674,7 +674,7 @@ class MangaDetailsController : returningFromReader = false runBlocking { val itemAnimator = binding.recycler.itemAnimator - val chapters = withTimeoutOrNull(1000) { presenter.getChaptersNow() } ?: return@runBlocking + val chapters = withTimeoutOrNull(1000) { presenter.setAndGetChapters() } ?: return@runBlocking binding.recycler.itemAnimator = null tabletAdapter?.notifyItemChanged(0) adapter?.setChapters(chapters) @@ -821,15 +821,15 @@ class MangaDetailsController : updateMenuVisibility(activityBinding?.toolbar?.menu) } - fun updateChapters(chapters: List) { + fun updateChapters(fetchFromSource: Boolean = false) { view ?: return binding.swipeRefresh.isRefreshing = presenter.isLoading - if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) { + if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested && fetchFromSource) { launchUI { binding.swipeRefresh.isRefreshing = true } - presenter.fetchChaptersFromSource() + presenter.refreshChapters() } tabletAdapter?.notifyItemChanged(0) - adapter?.setChapters(chapters) + adapter?.setChapters(presenter.chapters) addMangaHeader() colorToolbar(binding.recycler.canScrollVertically(-1)) updateMenuVisibility(activityBinding?.toolbar?.menu) 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 67ed320378..f49dfd10f7 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 @@ -34,6 +34,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.domain.manga.models.Manga +import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source @@ -76,11 +77,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -127,11 +132,15 @@ class MangaDetailsPresenter( private val networkPreferences: NetworkPreferences by injectLazy() -// private val currentMangaInternal: MutableStateFlow = MutableStateFlow(null) -// val currentManga get() = currentMangaInternal.asStateFlow() + private val currentMangaInternal = MutableStateFlow(null) + val currentManga = currentMangaInternal.asStateFlow() - lateinit var manga: Manga - fun isMangaLateInitInitialized() = ::manga.isInitialized + /** + * Unsafe, call only after currentManga is no longer null + */ + var manga: Manga + get() = currentManga.value!! + set(value) { currentMangaInternal.value = value } private val customMangaManager: CustomMangaManager by injectLazy() private val mangaShortcutManager: MangaShortcutManager by injectLazy() @@ -151,8 +160,12 @@ class MangaDetailsPresenter( var trackList: List = emptyList() - var chapters: List = emptyList() - private set + private val currentChaptersInternal = MutableStateFlow>(emptyList()) + val currentChapters = currentChaptersInternal.asStateFlow() + + var chapters: List + get() = currentChapters.value + private set(value) { currentChaptersInternal.value = value } var allChapters: List = emptyList() private set @@ -186,7 +199,7 @@ class MangaDetailsPresenter( val controller = view ?: return isLockedFromSearch = controller.shouldLockIfNeeded && SecureActivityDelegate.shouldBeLocked() - if (!::manga.isInitialized) runBlocking { refreshMangaFromDb() } + if (currentManga.value == null) runBlocking { refreshMangaFromDb() } syncData() presenterScope.launchUI { @@ -204,6 +217,22 @@ class MangaDetailsPresenter( presenterScope.launchIO { downloadManager.queueState.collectLatest(::onQueueUpdate) } + presenterScope.launchUI { + currentManga.collectLatest { + if (it == null) return@collectLatest + } + } + presenterScope.launchIO { + currentChapters.collectLatest { chapters -> + allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } + + allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() + + withUIContext { + controller.updateChapters(allChapters.isEmpty()) + } + } + } runBlocking { tracks = getTrack.awaitAllByMangaId(mangaId) @@ -229,8 +258,7 @@ class MangaDetailsPresenter( controller.updateHeader() refreshAll() } else { - runBlocking { getChapters() } - controller.updateChapters(this.chapters) + runBlocking { chapters = getChapters() } getHistory() } @@ -243,16 +271,14 @@ class MangaDetailsPresenter( fun fetchChapters(andTracking: Boolean = true) { presenterScope.launch { - getChapters() + setCurrentChapters(getChapters()) if (andTracking) fetchTracks() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } getHistory() } } fun setCurrentManga(manga: Manga?) { -// currentMangaInternal.update { manga } - this.manga = manga!! + currentMangaInternal.update { manga } } // TODO: Use flow to "sync" data instead @@ -264,20 +290,18 @@ class MangaDetailsPresenter( } } - suspend fun getChaptersNow(): List { - getChapters() - return chapters + // TODO: Use getChapter.subscribe() flow instead + suspend fun setAndGetChapters(): List { + return currentChaptersInternal.updateAndGet { getChapters() } } - private suspend fun getChapters(queue: List = downloadManager.queueState.value) { - val chapters = getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } - allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } + // TODO: Use getChapter.subscribe() flow instead + private fun setCurrentChapters(chapters: List) { + currentChaptersInternal.update { chapters } + } - // Find downloaded chapters - setDownloadedChapters(chapters, queue) - allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() - - this.chapters = applyChapterFilters(chapters) + private suspend fun getChapters(): List { + return getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } } private fun getHistory() { @@ -394,7 +418,7 @@ class MangaDetailsPresenter( download = null } - view?.updateChapters(this.chapters) + view?.updateChapters() downloadManager.deleteChapters(listOf(chapter), manga, source, true) } @@ -412,7 +436,7 @@ class MangaDetailsPresenter( } } - if (update) view?.updateChapters(this.chapters) + if (update) view?.updateChapters() if (isEverything) { downloadManager.deleteManga(manga, source) @@ -432,126 +456,93 @@ class MangaDetailsPresenter( if (view?.isNotOnline() == true && !manga.isLocal()) return presenterScope.launch { isLoading = true - var mangaError: java.lang.Exception? = null - var chapterError: java.lang.Exception? = null - val chapters = async(Dispatchers.IO) { - try { - source.getChapterList(manga.copy()) - } catch (e: Exception) { - chapterError = e - emptyList() - } - } - val nManga = async(Dispatchers.IO) { - try { - source.getMangaDetails(manga.copy()) - } catch (e: java.lang.Exception) { - mangaError = e - null - } - } + val tasks = listOf( + async { fetchMangaFromSource() }, + async { fetchChaptersFromSource() }, + ) + tasks.awaitAll() + isLoading = false + } + } - val networkManga = nManga.await() - if (networkManga != null) { - manga.prepareCoverUpdate(coverCache, networkManga, false) - manga.copyFrom(networkManga) - manga.initialized = true + private suspend fun fetchMangaFromSource() { + try { + val manga = manga.copy() + val networkManga = source.getMangaDetails(manga) - updateManga.await(manga.toMangaUpdate()) + manga.prepareCoverUpdate(coverCache, networkManga, false) + manga.copyFrom(networkManga) + manga.initialized = true - launchIO { - val request = - ImageRequest.Builder(preferences.context).data(manga.cover()) - .memoryCachePolicy(CachePolicy.DISABLED) - .diskCachePolicy(CachePolicy.WRITE_ONLY) - .build() + updateManga.await(manga.toMangaUpdate()) - if (preferences.context.imageLoader.execute(request) is SuccessResult) { - withContext(Dispatchers.Main) { - view?.setPaletteColor() - } + setCurrentManga(manga) + + presenterScope.launchNonCancellableIO { + val request = + ImageRequest.Builder(preferences.context).data(manga.cover()) + .memoryCachePolicy(CachePolicy.DISABLED) + .diskCachePolicy(CachePolicy.WRITE_ONLY) + .build() + + if (preferences.context.imageLoader.execute(request) is SuccessResult) { + withContext(Dispatchers.Main) { + view?.setPaletteColor() } } } - val finChapters = chapters.await() - if (finChapters.isNotEmpty()) { - val newChapters = withIOContext { syncChaptersWithSource(finChapters, manga, source) } - if (newChapters.first.isNotEmpty()) { + } catch (e: Exception) { + if (e is HttpException && e.code == 103) return + + withUIContext { + view?.showError(trimException(e)) + } + } + } + + private suspend fun fetchChaptersFromSource(manualFetch: Boolean = true) { + try { + withIOContext { + val chapters = source.getChapterList(manga.copy()) + val (added, removed) = syncChaptersWithSource(chapters, manga, source) + if (added.isNotEmpty() && manualFetch) { if (manga.shouldDownloadNewChapters(preferences)) { - downloadChapters( - newChapters.first.sortedBy { it.chapter_number } - .map { it.toModel() }, - ) + downloadChapters(added.sortedBy { it.chapter_number }.map { it.toModel() }) + } + withUIContext { + view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } } - view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } } - if (newChapters.second.isNotEmpty()) { - val removedChaptersId = newChapters.second.map { it.id } + if (removed.isNotEmpty() && manualFetch) { + val removedChaptersId = removed.map { it.id } val removedChapters = this@MangaDetailsPresenter.chapters.filter { it.id in removedChaptersId && it.isDownloaded } if (removedChapters.isNotEmpty()) { - withContext(Dispatchers.Main) { - view?.showChaptersRemovedPopup( - removedChapters, - ) + withUIContext { + view?.showChaptersRemovedPopup(removedChapters) } } } - getChapters() + setCurrentChapters(getChapters()) + getHistory() } - isLoading = false - if (chapterError == null) { - withContext(Dispatchers.Main) { - view?.updateChapters(this@MangaDetailsPresenter.chapters) - } + } catch (e: Exception) { + withUIContext { + view?.showError(trimException(e)) } - if (chapterError != null) { - withContext(Dispatchers.Main) { - view?.showError( - trimException(chapterError!!), - ) - } - return@launch - } else if (mangaError != null) { - withContext(Dispatchers.Main) { - view?.showError( - trimException(mangaError!!), - ) - } - } - getHistory() } } /** * Requests an updated list of chapters from the source. */ - fun fetchChaptersFromSource() { - hasRequested = true - isLoading = true - - presenterScope.launch(Dispatchers.IO) { - val chapters = try { - source.getChapterList(manga.copy()) - } catch (e: Exception) { - withContext(Dispatchers.Main) { view?.showError(trimException(e)) } - return@launch - } + fun refreshChapters() { + presenterScope.launchUI { + hasRequested = true + isLoading = true + fetchChaptersFromSource(true) isLoading = false - try { - syncChaptersWithSource(chapters, manga, source) - - getChapters() - withContext(Dispatchers.Main) { - view?.updateChapters(this@MangaDetailsPresenter.chapters) - } - getHistory() - } catch (e: java.lang.Exception) { - withContext(Dispatchers.Main) { - view?.showError(trimException(e)) - } - } } } @@ -579,8 +570,7 @@ class MangaDetailsPresenter( it.toProgressUpdate() } updateChapter.awaitAll(updates) - getChapters() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } + setCurrentChapters(getChapters()) } } @@ -609,8 +599,7 @@ class MangaDetailsPresenter( if (read && deleteNow && preferences.removeAfterMarkedAsRead().get()) { deleteChapters(selectedChapters, false) } - getChapters() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } + setCurrentChapters(getChapters()) if (read && deleteNow) { val latestReadChapter = selectedChapters.maxByOrNull { it.chapter_number.toInt() }?.chapter updateTrackChapterMarkedAsRead(preferences, latestReadChapter, manga.id) { @@ -741,8 +730,7 @@ class MangaDetailsPresenter( private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags)) - getChapters() - withUIContext { view?.updateChapters(chapters) } + setCurrentChapters(getChapters()) } private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true @@ -1158,9 +1146,9 @@ class MangaDetailsPresenter( } private suspend fun onQueueUpdate(queue: List) = withIOContext { - getChapters(queue) + setDownloadedChapters(chapters, queue) withUIContext { - view?.updateChapters(chapters) + view?.updateChapters() } } diff --git a/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt b/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt index da8d99f67b..58e8edcc13 100644 --- a/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt +++ b/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt @@ -35,6 +35,29 @@ interface Manga : SManga { var cover_last_modified: Long + override fun copy(): Manga { + return (super.copy() as Manga).also { + it.id = this.id + it.source = this.source + it.favorite = this.favorite + it.last_update = this.last_update + it.date_added = this.date_added + it.viewer_flags = this.viewer_flags + it.chapter_flags = this.chapter_flags + it.hide_title = this.hide_title + it.filtered_scanlators = this.filtered_scanlators + + it.ogTitle = this.ogTitle + it.ogAuthor = this.ogAuthor + it.ogArtist = this.ogArtist + it.ogDesc = this.ogDesc + it.ogGenre = this.ogGenre + it.ogStatus = this.ogStatus + + it.cover_last_modified = this.cover_last_modified + } + } + @Deprecated("Use ogTitle directly instead", ReplaceWith("ogTitle")) val originalTitle: String get() = ogTitle From f8d74a6b2f832199720a8a7cd5a29d6766953172 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 16 Dec 2024 20:55:15 +0700 Subject: [PATCH 124/166] revert: "refactor(manga): Slowly using flow" I'll just redo this in the morning --- .../ui/manga/MangaDetailsController.kt | 12 +- .../ui/manga/MangaDetailsPresenter.kt | 244 +++++++++--------- .../tachiyomi/domain/manga/models/Manga.kt | 23 -- 3 files changed, 134 insertions(+), 145 deletions(-) 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 f5dfe23e52..01a980fa31 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 @@ -194,7 +194,7 @@ class MangaDetailsController : } } - private val manga: Manga? get() = presenter.currentManga.value + private val manga: Manga? get() = if (presenter.isMangaLateInitInitialized()) presenter.manga else null private var colorAnimator: ValueAnimator? = null override val presenter: MangaDetailsPresenter private var coverColor: Int? = null @@ -674,7 +674,7 @@ class MangaDetailsController : returningFromReader = false runBlocking { val itemAnimator = binding.recycler.itemAnimator - val chapters = withTimeoutOrNull(1000) { presenter.setAndGetChapters() } ?: return@runBlocking + val chapters = withTimeoutOrNull(1000) { presenter.getChaptersNow() } ?: return@runBlocking binding.recycler.itemAnimator = null tabletAdapter?.notifyItemChanged(0) adapter?.setChapters(chapters) @@ -821,15 +821,15 @@ class MangaDetailsController : updateMenuVisibility(activityBinding?.toolbar?.menu) } - fun updateChapters(fetchFromSource: Boolean = false) { + fun updateChapters(chapters: List) { view ?: return binding.swipeRefresh.isRefreshing = presenter.isLoading - if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested && fetchFromSource) { + if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) { launchUI { binding.swipeRefresh.isRefreshing = true } - presenter.refreshChapters() + presenter.fetchChaptersFromSource() } tabletAdapter?.notifyItemChanged(0) - adapter?.setChapters(presenter.chapters) + adapter?.setChapters(chapters) addMangaHeader() colorToolbar(binding.recycler.canScrollVertically(-1)) updateMenuVisibility(activityBinding?.toolbar?.menu) 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 f49dfd10f7..67ed320378 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 @@ -34,7 +34,6 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.domain.manga.models.Manga -import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source @@ -77,15 +76,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -132,15 +127,11 @@ class MangaDetailsPresenter( private val networkPreferences: NetworkPreferences by injectLazy() - private val currentMangaInternal = MutableStateFlow(null) - val currentManga = currentMangaInternal.asStateFlow() +// private val currentMangaInternal: MutableStateFlow = MutableStateFlow(null) +// val currentManga get() = currentMangaInternal.asStateFlow() - /** - * Unsafe, call only after currentManga is no longer null - */ - var manga: Manga - get() = currentManga.value!! - set(value) { currentMangaInternal.value = value } + lateinit var manga: Manga + fun isMangaLateInitInitialized() = ::manga.isInitialized private val customMangaManager: CustomMangaManager by injectLazy() private val mangaShortcutManager: MangaShortcutManager by injectLazy() @@ -160,12 +151,8 @@ class MangaDetailsPresenter( var trackList: List = emptyList() - private val currentChaptersInternal = MutableStateFlow>(emptyList()) - val currentChapters = currentChaptersInternal.asStateFlow() - - var chapters: List - get() = currentChapters.value - private set(value) { currentChaptersInternal.value = value } + var chapters: List = emptyList() + private set var allChapters: List = emptyList() private set @@ -199,7 +186,7 @@ class MangaDetailsPresenter( val controller = view ?: return isLockedFromSearch = controller.shouldLockIfNeeded && SecureActivityDelegate.shouldBeLocked() - if (currentManga.value == null) runBlocking { refreshMangaFromDb() } + if (!::manga.isInitialized) runBlocking { refreshMangaFromDb() } syncData() presenterScope.launchUI { @@ -217,22 +204,6 @@ class MangaDetailsPresenter( presenterScope.launchIO { downloadManager.queueState.collectLatest(::onQueueUpdate) } - presenterScope.launchUI { - currentManga.collectLatest { - if (it == null) return@collectLatest - } - } - presenterScope.launchIO { - currentChapters.collectLatest { chapters -> - allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } - - allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() - - withUIContext { - controller.updateChapters(allChapters.isEmpty()) - } - } - } runBlocking { tracks = getTrack.awaitAllByMangaId(mangaId) @@ -258,7 +229,8 @@ class MangaDetailsPresenter( controller.updateHeader() refreshAll() } else { - runBlocking { chapters = getChapters() } + runBlocking { getChapters() } + controller.updateChapters(this.chapters) getHistory() } @@ -271,14 +243,16 @@ class MangaDetailsPresenter( fun fetchChapters(andTracking: Boolean = true) { presenterScope.launch { - setCurrentChapters(getChapters()) + getChapters() if (andTracking) fetchTracks() + withContext(Dispatchers.Main) { view?.updateChapters(chapters) } getHistory() } } fun setCurrentManga(manga: Manga?) { - currentMangaInternal.update { manga } +// currentMangaInternal.update { manga } + this.manga = manga!! } // TODO: Use flow to "sync" data instead @@ -290,18 +264,20 @@ class MangaDetailsPresenter( } } - // TODO: Use getChapter.subscribe() flow instead - suspend fun setAndGetChapters(): List { - return currentChaptersInternal.updateAndGet { getChapters() } + suspend fun getChaptersNow(): List { + getChapters() + return chapters } - // TODO: Use getChapter.subscribe() flow instead - private fun setCurrentChapters(chapters: List) { - currentChaptersInternal.update { chapters } - } + private suspend fun getChapters(queue: List = downloadManager.queueState.value) { + val chapters = getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } + allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } - private suspend fun getChapters(): List { - return getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } + // Find downloaded chapters + setDownloadedChapters(chapters, queue) + allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() + + this.chapters = applyChapterFilters(chapters) } private fun getHistory() { @@ -418,7 +394,7 @@ class MangaDetailsPresenter( download = null } - view?.updateChapters() + view?.updateChapters(this.chapters) downloadManager.deleteChapters(listOf(chapter), manga, source, true) } @@ -436,7 +412,7 @@ class MangaDetailsPresenter( } } - if (update) view?.updateChapters() + if (update) view?.updateChapters(this.chapters) if (isEverything) { downloadManager.deleteManga(manga, source) @@ -456,93 +432,126 @@ class MangaDetailsPresenter( if (view?.isNotOnline() == true && !manga.isLocal()) return presenterScope.launch { isLoading = true - val tasks = listOf( - async { fetchMangaFromSource() }, - async { fetchChaptersFromSource() }, - ) - tasks.awaitAll() - isLoading = false - } - } + var mangaError: java.lang.Exception? = null + var chapterError: java.lang.Exception? = null + val chapters = async(Dispatchers.IO) { + try { + source.getChapterList(manga.copy()) + } catch (e: Exception) { + chapterError = e + emptyList() + } + } + val nManga = async(Dispatchers.IO) { + try { + source.getMangaDetails(manga.copy()) + } catch (e: java.lang.Exception) { + mangaError = e + null + } + } - private suspend fun fetchMangaFromSource() { - try { - val manga = manga.copy() - val networkManga = source.getMangaDetails(manga) + val networkManga = nManga.await() + if (networkManga != null) { + manga.prepareCoverUpdate(coverCache, networkManga, false) + manga.copyFrom(networkManga) + manga.initialized = true - manga.prepareCoverUpdate(coverCache, networkManga, false) - manga.copyFrom(networkManga) - manga.initialized = true + updateManga.await(manga.toMangaUpdate()) - updateManga.await(manga.toMangaUpdate()) + launchIO { + val request = + ImageRequest.Builder(preferences.context).data(manga.cover()) + .memoryCachePolicy(CachePolicy.DISABLED) + .diskCachePolicy(CachePolicy.WRITE_ONLY) + .build() - setCurrentManga(manga) - - presenterScope.launchNonCancellableIO { - val request = - ImageRequest.Builder(preferences.context).data(manga.cover()) - .memoryCachePolicy(CachePolicy.DISABLED) - .diskCachePolicy(CachePolicy.WRITE_ONLY) - .build() - - if (preferences.context.imageLoader.execute(request) is SuccessResult) { - withContext(Dispatchers.Main) { - view?.setPaletteColor() + if (preferences.context.imageLoader.execute(request) is SuccessResult) { + withContext(Dispatchers.Main) { + view?.setPaletteColor() + } } } } - } catch (e: Exception) { - if (e is HttpException && e.code == 103) return - - withUIContext { - view?.showError(trimException(e)) - } - } - } - - private suspend fun fetchChaptersFromSource(manualFetch: Boolean = true) { - try { - withIOContext { - val chapters = source.getChapterList(manga.copy()) - val (added, removed) = syncChaptersWithSource(chapters, manga, source) - if (added.isNotEmpty() && manualFetch) { + val finChapters = chapters.await() + if (finChapters.isNotEmpty()) { + val newChapters = withIOContext { syncChaptersWithSource(finChapters, manga, source) } + if (newChapters.first.isNotEmpty()) { if (manga.shouldDownloadNewChapters(preferences)) { - downloadChapters(added.sortedBy { it.chapter_number }.map { it.toModel() }) - } - withUIContext { - view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } + downloadChapters( + newChapters.first.sortedBy { it.chapter_number } + .map { it.toModel() }, + ) } + view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } } - if (removed.isNotEmpty() && manualFetch) { - val removedChaptersId = removed.map { it.id } + if (newChapters.second.isNotEmpty()) { + val removedChaptersId = newChapters.second.map { it.id } val removedChapters = this@MangaDetailsPresenter.chapters.filter { it.id in removedChaptersId && it.isDownloaded } if (removedChapters.isNotEmpty()) { - withUIContext { - view?.showChaptersRemovedPopup(removedChapters) + withContext(Dispatchers.Main) { + view?.showChaptersRemovedPopup( + removedChapters, + ) } } } - setCurrentChapters(getChapters()) - getHistory() + getChapters() } - } catch (e: Exception) { - withUIContext { - view?.showError(trimException(e)) + isLoading = false + if (chapterError == null) { + withContext(Dispatchers.Main) { + view?.updateChapters(this@MangaDetailsPresenter.chapters) + } } + if (chapterError != null) { + withContext(Dispatchers.Main) { + view?.showError( + trimException(chapterError!!), + ) + } + return@launch + } else if (mangaError != null) { + withContext(Dispatchers.Main) { + view?.showError( + trimException(mangaError!!), + ) + } + } + getHistory() } } /** * Requests an updated list of chapters from the source. */ - fun refreshChapters() { - presenterScope.launchUI { - hasRequested = true - isLoading = true - fetchChaptersFromSource(true) + fun fetchChaptersFromSource() { + hasRequested = true + isLoading = true + + presenterScope.launch(Dispatchers.IO) { + val chapters = try { + source.getChapterList(manga.copy()) + } catch (e: Exception) { + withContext(Dispatchers.Main) { view?.showError(trimException(e)) } + return@launch + } isLoading = false + try { + syncChaptersWithSource(chapters, manga, source) + + getChapters() + withContext(Dispatchers.Main) { + view?.updateChapters(this@MangaDetailsPresenter.chapters) + } + getHistory() + } catch (e: java.lang.Exception) { + withContext(Dispatchers.Main) { + view?.showError(trimException(e)) + } + } } } @@ -570,7 +579,8 @@ class MangaDetailsPresenter( it.toProgressUpdate() } updateChapter.awaitAll(updates) - setCurrentChapters(getChapters()) + getChapters() + withContext(Dispatchers.Main) { view?.updateChapters(chapters) } } } @@ -599,7 +609,8 @@ class MangaDetailsPresenter( if (read && deleteNow && preferences.removeAfterMarkedAsRead().get()) { deleteChapters(selectedChapters, false) } - setCurrentChapters(getChapters()) + getChapters() + withContext(Dispatchers.Main) { view?.updateChapters(chapters) } if (read && deleteNow) { val latestReadChapter = selectedChapters.maxByOrNull { it.chapter_number.toInt() }?.chapter updateTrackChapterMarkedAsRead(preferences, latestReadChapter, manga.id) { @@ -730,7 +741,8 @@ class MangaDetailsPresenter( private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags)) - setCurrentChapters(getChapters()) + getChapters() + withUIContext { view?.updateChapters(chapters) } } private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true @@ -1146,9 +1158,9 @@ class MangaDetailsPresenter( } private suspend fun onQueueUpdate(queue: List) = withIOContext { - setDownloadedChapters(chapters, queue) + getChapters(queue) withUIContext { - view?.updateChapters() + view?.updateChapters(chapters) } } diff --git a/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt b/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt index 58e8edcc13..da8d99f67b 100644 --- a/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt +++ b/domain/src/commonMain/kotlin/eu/kanade/tachiyomi/domain/manga/models/Manga.kt @@ -35,29 +35,6 @@ interface Manga : SManga { var cover_last_modified: Long - override fun copy(): Manga { - return (super.copy() as Manga).also { - it.id = this.id - it.source = this.source - it.favorite = this.favorite - it.last_update = this.last_update - it.date_added = this.date_added - it.viewer_flags = this.viewer_flags - it.chapter_flags = this.chapter_flags - it.hide_title = this.hide_title - it.filtered_scanlators = this.filtered_scanlators - - it.ogTitle = this.ogTitle - it.ogAuthor = this.ogAuthor - it.ogArtist = this.ogArtist - it.ogDesc = this.ogDesc - it.ogGenre = this.ogGenre - it.ogStatus = this.ogStatus - - it.cover_last_modified = this.cover_last_modified - } - } - @Deprecated("Use ogTitle directly instead", ReplaceWith("ogTitle")) val originalTitle: String get() = ogTitle From f81be429dfd50bf88e36a5575bb0a22a9493d5cc Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 07:33:24 +0700 Subject: [PATCH 125/166] refactor(manga): Slowly using flow attempt 2 --- .../tachiyomi/data/database/models/Manga.kt | 31 ++ .../ui/manga/MangaDetailsController.kt | 12 +- .../ui/manga/MangaDetailsPresenter.kt | 309 +++++++++--------- 3 files changed, 183 insertions(+), 169 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index fb560da49c..29aedbd6e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -182,6 +182,37 @@ var Manga.vibrantCoverColor: Int? id?.let { MangaCoverMetadata.setVibrantColor(it, value) } } +fun Manga.copyDomain(): Manga = MangaImpl().also { other -> + other.url = this.url + other.title = this.title + other.artist = this.artist + other.author = this.author + other.description = this.description + other.genre = this.genre + other.status = this.status + other.thumbnail_url = this.thumbnail_url + other.initialized = this.initialized + + other.id = this.id + other.source = this.source + other.favorite = this.favorite + other.last_update = this.last_update + other.date_added = this.date_added + other.viewer_flags = this.viewer_flags + other.chapter_flags = this.chapter_flags + other.hide_title = this.hide_title + other.filtered_scanlators = this.filtered_scanlators + + other.ogTitle = this.ogTitle + other.ogAuthor = this.ogAuthor + other.ogArtist = this.ogArtist + other.ogDesc = this.ogDesc + other.ogGenre = this.ogGenre + other.ogStatus = this.ogStatus + + other.cover_last_modified = this.cover_last_modified +} + fun Manga.Companion.create(source: Long) = MangaImpl().apply { this.source = source } 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 01a980fa31..b382f47097 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 @@ -194,7 +194,7 @@ class MangaDetailsController : } } - private val manga: Manga? get() = if (presenter.isMangaLateInitInitialized()) presenter.manga else null + private val manga: Manga? get() = presenter.currentManga.value private var colorAnimator: ValueAnimator? = null override val presenter: MangaDetailsPresenter private var coverColor: Int? = null @@ -674,7 +674,7 @@ class MangaDetailsController : returningFromReader = false runBlocking { val itemAnimator = binding.recycler.itemAnimator - val chapters = withTimeoutOrNull(1000) { presenter.getChaptersNow() } ?: return@runBlocking + val chapters = withTimeoutOrNull(1000) { presenter.setAndGetChapters() } ?: return@runBlocking binding.recycler.itemAnimator = null tabletAdapter?.notifyItemChanged(0) adapter?.setChapters(chapters) @@ -821,15 +821,11 @@ class MangaDetailsController : updateMenuVisibility(activityBinding?.toolbar?.menu) } - fun updateChapters(chapters: List) { + fun updateChapters() { view ?: return binding.swipeRefresh.isRefreshing = presenter.isLoading - if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) { - launchUI { binding.swipeRefresh.isRefreshing = true } - presenter.fetchChaptersFromSource() - } + adapter?.setChapters(presenter.chapters) tabletAdapter?.notifyItemChanged(0) - adapter?.setChapters(chapters) addMangaHeader() colorToolbar(binding.recycler.canScrollVertically(-1)) updateMenuVisibility(activityBinding?.toolbar?.menu) 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 67ed320378..a2630bcb9f 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 @@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.bookmarkedFilter import eu.kanade.tachiyomi.data.database.models.chapterOrder +import eu.kanade.tachiyomi.data.database.models.copyDomain import eu.kanade.tachiyomi.data.database.models.downloadedFilter import eu.kanade.tachiyomi.data.database.models.prepareCoverUpdate import eu.kanade.tachiyomi.data.database.models.readFilter @@ -34,6 +35,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.domain.manga.models.Manga +import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source @@ -76,11 +78,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -127,11 +133,15 @@ class MangaDetailsPresenter( private val networkPreferences: NetworkPreferences by injectLazy() -// private val currentMangaInternal: MutableStateFlow = MutableStateFlow(null) -// val currentManga get() = currentMangaInternal.asStateFlow() + private val currentMangaInternal = MutableStateFlow(null) + val currentManga = currentMangaInternal.asStateFlow() - lateinit var manga: Manga - fun isMangaLateInitInitialized() = ::manga.isInitialized + /** + * Unsafe, call only after currentManga is no longer null + */ + var manga: Manga + get() = currentManga.value!! + set(value) { currentMangaInternal.value = value } private val customMangaManager: CustomMangaManager by injectLazy() private val mangaShortcutManager: MangaShortcutManager by injectLazy() @@ -151,8 +161,12 @@ class MangaDetailsPresenter( var trackList: List = emptyList() - var chapters: List = emptyList() - private set + private val currentChaptersInternal = MutableStateFlow>(emptyList()) + val currentChapters = currentChaptersInternal.asStateFlow() + + var chapters: List + get() = currentChapters.value + private set(value) { currentChaptersInternal.value = value } var allChapters: List = emptyList() private set @@ -186,7 +200,7 @@ class MangaDetailsPresenter( val controller = view ?: return isLockedFromSearch = controller.shouldLockIfNeeded && SecureActivityDelegate.shouldBeLocked() - if (!::manga.isInitialized) runBlocking { refreshMangaFromDb() } + if (currentManga.value == null) runBlocking { refreshMangaFromDb() } syncData() presenterScope.launchUI { @@ -204,6 +218,26 @@ class MangaDetailsPresenter( presenterScope.launchIO { downloadManager.queueState.collectLatest(::onQueueUpdate) } + presenterScope.launchUI { + currentManga.collectLatest { + if (it == null) return@collectLatest + + controller.updateHeader() + } + } + presenterScope.launchIO { + currentChapters.collectLatest { chapters -> + allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } + + allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() + + getHistory() + + withUIContext { + controller.updateChapters() + } + } + } runBlocking { tracks = getTrack.awaitAllByMangaId(mangaId) @@ -216,25 +250,24 @@ class MangaDetailsPresenter( fun onCreateLate() { val controller = view ?: return + isLoading = true + controller.setRefresh(true) // FIXME: Use progress indicator instead + LibraryUpdateJob.updateFlow .filter { it == mangaId } .onEach { onUpdateManga() } .launchIn(presenterScope) - if (manga.isLocal()) { - refreshAll() - } else if (!manga.initialized) { - isLoading = true - controller.setRefresh(true) - controller.updateHeader() - refreshAll() - } else { - runBlocking { getChapters() } - controller.updateChapters(this.chapters) - getHistory() - } + val updateMangaNeeded = !manga.initialized + val updateChaptersNeeded = runBlocking { setAndGetChapters() }.isEmpty() presenterScope.launch { + val tasks = listOf( + async { if (updateMangaNeeded) fetchMangaFromSource() }, + async { if (updateChaptersNeeded) fetchChaptersFromSource(false) }, + ) + tasks.awaitAll() + setTrackItems() } @@ -243,16 +276,14 @@ class MangaDetailsPresenter( fun fetchChapters(andTracking: Boolean = true) { presenterScope.launch { - getChapters() + setCurrentChapters(getChapters()) if (andTracking) fetchTracks() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } getHistory() } } fun setCurrentManga(manga: Manga?) { -// currentMangaInternal.update { manga } - this.manga = manga!! + currentMangaInternal.update { manga } } // TODO: Use flow to "sync" data instead @@ -264,20 +295,18 @@ class MangaDetailsPresenter( } } - suspend fun getChaptersNow(): List { - getChapters() - return chapters + // TODO: Use getChapter.subscribe() flow instead + suspend fun setAndGetChapters(): List { + return currentChaptersInternal.updateAndGet { getChapters().applyChapterFilters() } } - private suspend fun getChapters(queue: List = downloadManager.queueState.value) { - val chapters = getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } - allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } + // TODO: Use getChapter.subscribe() flow instead + private fun setCurrentChapters(chapters: List) { + currentChaptersInternal.update { chapters.applyChapterFilters() } + } - // Find downloaded chapters - setDownloadedChapters(chapters, queue) - allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() - - this.chapters = applyChapterFilters(chapters) + private suspend fun getChapters(): List { + return getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } } private fun getHistory() { @@ -291,14 +320,17 @@ class MangaDetailsPresenter( * * @param chapters the list of chapter from the database. */ - private fun setDownloadedChapters(chapters: List, queue: List) { - for (chapter in chapters) { - if (downloadManager.isChapterDownloaded(chapter, manga)) { - chapter.status = Download.State.DOWNLOADED - } else if (queue.isNotEmpty()) { - chapter.status = queue.find { it.chapter.id == chapter.id } - ?.status ?: Download.State.default + private fun setDownloadedChapters(queue: List) { + currentChaptersInternal.update { chapters -> + for (chapter in chapters) { + if (downloadManager.isChapterDownloaded(chapter, manga)) { + chapter.status = Download.State.DOWNLOADED + } else if (queue.isNotEmpty()) { + chapter.status = queue.find { it.chapter.id == chapter.id } + ?.status ?: Download.State.default + } } + chapters } } @@ -332,12 +364,12 @@ class MangaDetailsPresenter( * @param chapterList the list of chapters from the database * @return an observable of the list of chapters filtered and sorted. */ - private fun applyChapterFilters(chapterList: List): List { + private fun List.applyChapterFilters(): List { if (isLockedFromSearch) { - return chapterList + return this } - getScrollType(chapterList) - return chapterSort.getChaptersSorted(chapterList) + getScrollType(this) + return chapterSort.getChaptersSorted(this) } fun getChapterUrl(chapter: Chapter): String? { @@ -394,7 +426,7 @@ class MangaDetailsPresenter( download = null } - view?.updateChapters(this.chapters) + view?.updateChapters() downloadManager.deleteChapters(listOf(chapter), manga, source, true) } @@ -412,7 +444,7 @@ class MangaDetailsPresenter( } } - if (update) view?.updateChapters(this.chapters) + if (update) view?.updateChapters() if (isEverything) { downloadManager.deleteManga(manga, source) @@ -432,125 +464,80 @@ class MangaDetailsPresenter( if (view?.isNotOnline() == true && !manga.isLocal()) return presenterScope.launch { isLoading = true - var mangaError: java.lang.Exception? = null - var chapterError: java.lang.Exception? = null - val chapters = async(Dispatchers.IO) { - try { - source.getChapterList(manga.copy()) - } catch (e: Exception) { - chapterError = e - emptyList() - } - } - val nManga = async(Dispatchers.IO) { - try { - source.getMangaDetails(manga.copy()) - } catch (e: java.lang.Exception) { - mangaError = e - null - } - } + val tasks = listOf( + async { fetchMangaFromSource() }, + async { fetchChaptersFromSource() }, + ) + tasks.awaitAll() + isLoading = false + } + } - val networkManga = nManga.await() - if (networkManga != null) { - manga.prepareCoverUpdate(coverCache, networkManga, false) - manga.copyFrom(networkManga) - manga.initialized = true + private suspend fun fetchMangaFromSource() { + try { + val manga = manga.copyDomain() + val networkManga = source.getMangaDetails(manga.copy()) - updateManga.await(manga.toMangaUpdate()) + manga.prepareCoverUpdate(coverCache, networkManga, false) + manga.copyFrom(networkManga) + manga.initialized = true - launchIO { - val request = - ImageRequest.Builder(preferences.context).data(manga.cover()) - .memoryCachePolicy(CachePolicy.DISABLED) - .diskCachePolicy(CachePolicy.WRITE_ONLY) - .build() + updateManga.await(manga.toMangaUpdate()) - if (preferences.context.imageLoader.execute(request) is SuccessResult) { - withContext(Dispatchers.Main) { - view?.setPaletteColor() - } + setCurrentManga(manga) + + presenterScope.launchNonCancellableIO { + val request = + ImageRequest.Builder(preferences.context).data(manga.cover()) + .memoryCachePolicy(CachePolicy.DISABLED) + .diskCachePolicy(CachePolicy.WRITE_ONLY) + .build() + + if (preferences.context.imageLoader.execute(request) is SuccessResult) { + withContext(Dispatchers.Main) { + view?.setPaletteColor() } } } - val finChapters = chapters.await() - if (finChapters.isNotEmpty()) { - val newChapters = withIOContext { syncChaptersWithSource(finChapters, manga, source) } - if (newChapters.first.isNotEmpty()) { + } catch (e: Exception) { + if (e is HttpException && e.code == 103) return + + withUIContext { + view?.showError(trimException(e)) + } + } + } + + private suspend fun fetchChaptersFromSource(manualFetch: Boolean = true) { + try { + withIOContext { + val chapters = source.getChapterList(manga.copy()) + val (added, removed) = syncChaptersWithSource(chapters, manga, source) + if (added.isNotEmpty() && manualFetch) { if (manga.shouldDownloadNewChapters(preferences)) { - downloadChapters( - newChapters.first.sortedBy { it.chapter_number } - .map { it.toModel() }, - ) + downloadChapters(added.sortedBy { it.chapter_number }.map { it.toModel() }) + } + withUIContext { + view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } } - view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } } - if (newChapters.second.isNotEmpty()) { - val removedChaptersId = newChapters.second.map { it.id } + if (removed.isNotEmpty() && manualFetch) { + val removedChaptersId = removed.map { it.id } val removedChapters = this@MangaDetailsPresenter.chapters.filter { it.id in removedChaptersId && it.isDownloaded } if (removedChapters.isNotEmpty()) { - withContext(Dispatchers.Main) { - view?.showChaptersRemovedPopup( - removedChapters, - ) + withUIContext { + view?.showChaptersRemovedPopup(removedChapters) } } } - getChapters() - } - isLoading = false - if (chapterError == null) { - withContext(Dispatchers.Main) { - view?.updateChapters(this@MangaDetailsPresenter.chapters) - } - } - if (chapterError != null) { - withContext(Dispatchers.Main) { - view?.showError( - trimException(chapterError!!), - ) - } - return@launch - } else if (mangaError != null) { - withContext(Dispatchers.Main) { - view?.showError( - trimException(mangaError!!), - ) - } - } - getHistory() - } - } - - /** - * Requests an updated list of chapters from the source. - */ - fun fetchChaptersFromSource() { - hasRequested = true - isLoading = true - - presenterScope.launch(Dispatchers.IO) { - val chapters = try { - source.getChapterList(manga.copy()) - } catch (e: Exception) { - withContext(Dispatchers.Main) { view?.showError(trimException(e)) } - return@launch - } - isLoading = false - try { - syncChaptersWithSource(chapters, manga, source) - - getChapters() - withContext(Dispatchers.Main) { - view?.updateChapters(this@MangaDetailsPresenter.chapters) - } + setCurrentChapters(getChapters()) getHistory() - } catch (e: java.lang.Exception) { - withContext(Dispatchers.Main) { - view?.showError(trimException(e)) - } + } + } catch (e: Exception) { + withUIContext { + view?.showError(trimException(e)) } } } @@ -579,8 +566,7 @@ class MangaDetailsPresenter( it.toProgressUpdate() } updateChapter.awaitAll(updates) - getChapters() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } + setCurrentChapters(getChapters()) } } @@ -609,8 +595,7 @@ class MangaDetailsPresenter( if (read && deleteNow && preferences.removeAfterMarkedAsRead().get()) { deleteChapters(selectedChapters, false) } - getChapters() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } + setCurrentChapters(getChapters()) if (read && deleteNow) { val latestReadChapter = selectedChapters.maxByOrNull { it.chapter_number.toInt() }?.chapter updateTrackChapterMarkedAsRead(preferences, latestReadChapter, manga.id) { @@ -741,8 +726,7 @@ class MangaDetailsPresenter( private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags)) - getChapters() - withUIContext { view?.updateChapters(chapters) } + setCurrentChapters(getChapters()) } private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true @@ -811,7 +795,7 @@ class MangaDetailsPresenter( withUIContext { view?.shareManga(uri.uri.toFile()) } - } catch (_: java.lang.Exception) { + } catch (_: Exception) { } } } @@ -1153,15 +1137,15 @@ class MangaDetailsPresenter( override fun onStatusChange(download: Download) { super.onStatusChange(download) - chapters.find { it.id == download.chapter.id }?.status = download.status + currentChaptersInternal.update { chapters -> + chapters.find { it.id == download.chapter.id }?.status = download.status + chapters + } onPageProgressUpdate(download) } private suspend fun onQueueUpdate(queue: List) = withIOContext { - getChapters(queue) - withUIContext { - view?.updateChapters(chapters) - } + setDownloadedChapters(queue) } override fun onQueueUpdate(download: Download) { @@ -1173,7 +1157,10 @@ class MangaDetailsPresenter( } override fun onPageProgressUpdate(download: Download) { - chapters.find { it.id == download.chapter.id }?.download = download + currentChaptersInternal.update { chapters -> + chapters.find { it.id == download.chapter.id }?.download = download + chapters + } view?.updateChapterDownload(download) } From 60fe907cc0b65230ad9ee094192e52c1eda419e1 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 07:56:36 +0700 Subject: [PATCH 126/166] fix(manga): Loading state --- .../eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 a2630bcb9f..49dbb4e6e2 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 @@ -174,7 +174,7 @@ class MangaDetailsPresenter( var allHistory: List = emptyList() private set - val headerItem: MangaHeaderItem by lazy { MangaHeaderItem(mangaId, view?.fromCatalogue == true)} + val headerItem: MangaHeaderItem get() = MangaHeaderItem(mangaId, view?.fromCatalogue == true) var tabletChapterHeaderItem: MangaHeaderItem? = null get() { when (view?.isTablet) { @@ -262,11 +262,13 @@ class MangaDetailsPresenter( val updateChaptersNeeded = runBlocking { setAndGetChapters() }.isEmpty() presenterScope.launch { + isLoading = true val tasks = listOf( async { if (updateMangaNeeded) fetchMangaFromSource() }, async { if (updateChaptersNeeded) fetchChaptersFromSource(false) }, ) tasks.awaitAll() + isLoading = false setTrackItems() } From 4a0f57821180df2e3dbd18bbd91378dfbb0600de Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 08:09:09 +0700 Subject: [PATCH 127/166] fix(manga): Group currentManga setup together --- .../ui/manga/MangaDetailsPresenter.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) 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 49dbb4e6e2..9311865417 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 @@ -200,6 +200,14 @@ class MangaDetailsPresenter( val controller = view ?: return isLockedFromSearch = controller.shouldLockIfNeeded && SecureActivityDelegate.shouldBeLocked() + + presenterScope.launchUI { + currentManga.collectLatest { + if (it == null) return@collectLatest + + controller.updateHeader() + } + } if (currentManga.value == null) runBlocking { refreshMangaFromDb() } syncData() @@ -218,13 +226,6 @@ class MangaDetailsPresenter( presenterScope.launchIO { downloadManager.queueState.collectLatest(::onQueueUpdate) } - presenterScope.launchUI { - currentManga.collectLatest { - if (it == null) return@collectLatest - - controller.updateHeader() - } - } presenterScope.launchIO { currentChapters.collectLatest { chapters -> allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } @@ -258,7 +259,7 @@ class MangaDetailsPresenter( .onEach { onUpdateManga() } .launchIn(presenterScope) - val updateMangaNeeded = !manga.initialized + val updateMangaNeeded = currentManga.value?.initialized != true val updateChaptersNeeded = runBlocking { setAndGetChapters() }.isEmpty() presenterScope.launch { @@ -496,7 +497,7 @@ class MangaDetailsPresenter( .build() if (preferences.context.imageLoader.execute(request) is SuccessResult) { - withContext(Dispatchers.Main) { + withUIContext { view?.setPaletteColor() } } From 4ffb0ad8ee2180150071889a090ed6687e63bf80 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 08:18:01 +0700 Subject: [PATCH 128/166] revert: Revert flow usage commits Too much headache... --- .../tachiyomi/data/database/models/Manga.kt | 31 -- .../ui/manga/MangaDetailsController.kt | 12 +- .../ui/manga/MangaDetailsPresenter.kt | 314 +++++++++--------- 3 files changed, 170 insertions(+), 187 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index 29aedbd6e0..fb560da49c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -182,37 +182,6 @@ var Manga.vibrantCoverColor: Int? id?.let { MangaCoverMetadata.setVibrantColor(it, value) } } -fun Manga.copyDomain(): Manga = MangaImpl().also { other -> - other.url = this.url - other.title = this.title - other.artist = this.artist - other.author = this.author - other.description = this.description - other.genre = this.genre - other.status = this.status - other.thumbnail_url = this.thumbnail_url - other.initialized = this.initialized - - other.id = this.id - other.source = this.source - other.favorite = this.favorite - other.last_update = this.last_update - other.date_added = this.date_added - other.viewer_flags = this.viewer_flags - other.chapter_flags = this.chapter_flags - other.hide_title = this.hide_title - other.filtered_scanlators = this.filtered_scanlators - - other.ogTitle = this.ogTitle - other.ogAuthor = this.ogAuthor - other.ogArtist = this.ogArtist - other.ogDesc = this.ogDesc - other.ogGenre = this.ogGenre - other.ogStatus = this.ogStatus - - other.cover_last_modified = this.cover_last_modified -} - fun Manga.Companion.create(source: Long) = MangaImpl().apply { this.source = source } 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 b382f47097..01a980fa31 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 @@ -194,7 +194,7 @@ class MangaDetailsController : } } - private val manga: Manga? get() = presenter.currentManga.value + private val manga: Manga? get() = if (presenter.isMangaLateInitInitialized()) presenter.manga else null private var colorAnimator: ValueAnimator? = null override val presenter: MangaDetailsPresenter private var coverColor: Int? = null @@ -674,7 +674,7 @@ class MangaDetailsController : returningFromReader = false runBlocking { val itemAnimator = binding.recycler.itemAnimator - val chapters = withTimeoutOrNull(1000) { presenter.setAndGetChapters() } ?: return@runBlocking + val chapters = withTimeoutOrNull(1000) { presenter.getChaptersNow() } ?: return@runBlocking binding.recycler.itemAnimator = null tabletAdapter?.notifyItemChanged(0) adapter?.setChapters(chapters) @@ -821,11 +821,15 @@ class MangaDetailsController : updateMenuVisibility(activityBinding?.toolbar?.menu) } - fun updateChapters() { + fun updateChapters(chapters: List) { view ?: return binding.swipeRefresh.isRefreshing = presenter.isLoading - adapter?.setChapters(presenter.chapters) + if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) { + launchUI { binding.swipeRefresh.isRefreshing = true } + presenter.fetchChaptersFromSource() + } tabletAdapter?.notifyItemChanged(0) + adapter?.setChapters(chapters) addMangaHeader() colorToolbar(binding.recycler.canScrollVertically(-1)) updateMenuVisibility(activityBinding?.toolbar?.menu) 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 9311865417..67ed320378 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 @@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.bookmarkedFilter import eu.kanade.tachiyomi.data.database.models.chapterOrder -import eu.kanade.tachiyomi.data.database.models.copyDomain import eu.kanade.tachiyomi.data.database.models.downloadedFilter import eu.kanade.tachiyomi.data.database.models.prepareCoverUpdate import eu.kanade.tachiyomi.data.database.models.readFilter @@ -35,7 +34,6 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.domain.manga.models.Manga -import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source @@ -78,15 +76,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -133,15 +127,11 @@ class MangaDetailsPresenter( private val networkPreferences: NetworkPreferences by injectLazy() - private val currentMangaInternal = MutableStateFlow(null) - val currentManga = currentMangaInternal.asStateFlow() +// private val currentMangaInternal: MutableStateFlow = MutableStateFlow(null) +// val currentManga get() = currentMangaInternal.asStateFlow() - /** - * Unsafe, call only after currentManga is no longer null - */ - var manga: Manga - get() = currentManga.value!! - set(value) { currentMangaInternal.value = value } + lateinit var manga: Manga + fun isMangaLateInitInitialized() = ::manga.isInitialized private val customMangaManager: CustomMangaManager by injectLazy() private val mangaShortcutManager: MangaShortcutManager by injectLazy() @@ -161,12 +151,8 @@ class MangaDetailsPresenter( var trackList: List = emptyList() - private val currentChaptersInternal = MutableStateFlow>(emptyList()) - val currentChapters = currentChaptersInternal.asStateFlow() - - var chapters: List - get() = currentChapters.value - private set(value) { currentChaptersInternal.value = value } + var chapters: List = emptyList() + private set var allChapters: List = emptyList() private set @@ -174,7 +160,7 @@ class MangaDetailsPresenter( var allHistory: List = emptyList() private set - val headerItem: MangaHeaderItem get() = MangaHeaderItem(mangaId, view?.fromCatalogue == true) + val headerItem: MangaHeaderItem by lazy { MangaHeaderItem(mangaId, view?.fromCatalogue == true)} var tabletChapterHeaderItem: MangaHeaderItem? = null get() { when (view?.isTablet) { @@ -200,15 +186,7 @@ class MangaDetailsPresenter( val controller = view ?: return isLockedFromSearch = controller.shouldLockIfNeeded && SecureActivityDelegate.shouldBeLocked() - - presenterScope.launchUI { - currentManga.collectLatest { - if (it == null) return@collectLatest - - controller.updateHeader() - } - } - if (currentManga.value == null) runBlocking { refreshMangaFromDb() } + if (!::manga.isInitialized) runBlocking { refreshMangaFromDb() } syncData() presenterScope.launchUI { @@ -226,19 +204,6 @@ class MangaDetailsPresenter( presenterScope.launchIO { downloadManager.queueState.collectLatest(::onQueueUpdate) } - presenterScope.launchIO { - currentChapters.collectLatest { chapters -> - allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } - - allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() - - getHistory() - - withUIContext { - controller.updateChapters() - } - } - } runBlocking { tracks = getTrack.awaitAllByMangaId(mangaId) @@ -251,26 +216,25 @@ class MangaDetailsPresenter( fun onCreateLate() { val controller = view ?: return - isLoading = true - controller.setRefresh(true) // FIXME: Use progress indicator instead - LibraryUpdateJob.updateFlow .filter { it == mangaId } .onEach { onUpdateManga() } .launchIn(presenterScope) - val updateMangaNeeded = currentManga.value?.initialized != true - val updateChaptersNeeded = runBlocking { setAndGetChapters() }.isEmpty() + if (manga.isLocal()) { + refreshAll() + } else if (!manga.initialized) { + isLoading = true + controller.setRefresh(true) + controller.updateHeader() + refreshAll() + } else { + runBlocking { getChapters() } + controller.updateChapters(this.chapters) + getHistory() + } presenterScope.launch { - isLoading = true - val tasks = listOf( - async { if (updateMangaNeeded) fetchMangaFromSource() }, - async { if (updateChaptersNeeded) fetchChaptersFromSource(false) }, - ) - tasks.awaitAll() - isLoading = false - setTrackItems() } @@ -279,14 +243,16 @@ class MangaDetailsPresenter( fun fetchChapters(andTracking: Boolean = true) { presenterScope.launch { - setCurrentChapters(getChapters()) + getChapters() if (andTracking) fetchTracks() + withContext(Dispatchers.Main) { view?.updateChapters(chapters) } getHistory() } } fun setCurrentManga(manga: Manga?) { - currentMangaInternal.update { manga } +// currentMangaInternal.update { manga } + this.manga = manga!! } // TODO: Use flow to "sync" data instead @@ -298,18 +264,20 @@ class MangaDetailsPresenter( } } - // TODO: Use getChapter.subscribe() flow instead - suspend fun setAndGetChapters(): List { - return currentChaptersInternal.updateAndGet { getChapters().applyChapterFilters() } + suspend fun getChaptersNow(): List { + getChapters() + return chapters } - // TODO: Use getChapter.subscribe() flow instead - private fun setCurrentChapters(chapters: List) { - currentChaptersInternal.update { chapters.applyChapterFilters() } - } + private suspend fun getChapters(queue: List = downloadManager.queueState.value) { + val chapters = getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } + allChapters = if (!isScanlatorFiltered()) chapters else getChapter.awaitAll(mangaId, false).map { it.toModel() } - private suspend fun getChapters(): List { - return getChapter.awaitAll(mangaId, isScanlatorFiltered()).map { it.toModel() } + // Find downloaded chapters + setDownloadedChapters(chapters, queue) + allChapterScanlators = allChapters.mapNotNull { it.chapter.scanlator }.toSet() + + this.chapters = applyChapterFilters(chapters) } private fun getHistory() { @@ -323,17 +291,14 @@ class MangaDetailsPresenter( * * @param chapters the list of chapter from the database. */ - private fun setDownloadedChapters(queue: List) { - currentChaptersInternal.update { chapters -> - for (chapter in chapters) { - if (downloadManager.isChapterDownloaded(chapter, manga)) { - chapter.status = Download.State.DOWNLOADED - } else if (queue.isNotEmpty()) { - chapter.status = queue.find { it.chapter.id == chapter.id } - ?.status ?: Download.State.default - } + private fun setDownloadedChapters(chapters: List, queue: List) { + for (chapter in chapters) { + if (downloadManager.isChapterDownloaded(chapter, manga)) { + chapter.status = Download.State.DOWNLOADED + } else if (queue.isNotEmpty()) { + chapter.status = queue.find { it.chapter.id == chapter.id } + ?.status ?: Download.State.default } - chapters } } @@ -367,12 +332,12 @@ class MangaDetailsPresenter( * @param chapterList the list of chapters from the database * @return an observable of the list of chapters filtered and sorted. */ - private fun List.applyChapterFilters(): List { + private fun applyChapterFilters(chapterList: List): List { if (isLockedFromSearch) { - return this + return chapterList } - getScrollType(this) - return chapterSort.getChaptersSorted(this) + getScrollType(chapterList) + return chapterSort.getChaptersSorted(chapterList) } fun getChapterUrl(chapter: Chapter): String? { @@ -429,7 +394,7 @@ class MangaDetailsPresenter( download = null } - view?.updateChapters() + view?.updateChapters(this.chapters) downloadManager.deleteChapters(listOf(chapter), manga, source, true) } @@ -447,7 +412,7 @@ class MangaDetailsPresenter( } } - if (update) view?.updateChapters() + if (update) view?.updateChapters(this.chapters) if (isEverything) { downloadManager.deleteManga(manga, source) @@ -467,80 +432,125 @@ class MangaDetailsPresenter( if (view?.isNotOnline() == true && !manga.isLocal()) return presenterScope.launch { isLoading = true - val tasks = listOf( - async { fetchMangaFromSource() }, - async { fetchChaptersFromSource() }, - ) - tasks.awaitAll() - isLoading = false - } - } + var mangaError: java.lang.Exception? = null + var chapterError: java.lang.Exception? = null + val chapters = async(Dispatchers.IO) { + try { + source.getChapterList(manga.copy()) + } catch (e: Exception) { + chapterError = e + emptyList() + } + } + val nManga = async(Dispatchers.IO) { + try { + source.getMangaDetails(manga.copy()) + } catch (e: java.lang.Exception) { + mangaError = e + null + } + } - private suspend fun fetchMangaFromSource() { - try { - val manga = manga.copyDomain() - val networkManga = source.getMangaDetails(manga.copy()) + val networkManga = nManga.await() + if (networkManga != null) { + manga.prepareCoverUpdate(coverCache, networkManga, false) + manga.copyFrom(networkManga) + manga.initialized = true - manga.prepareCoverUpdate(coverCache, networkManga, false) - manga.copyFrom(networkManga) - manga.initialized = true + updateManga.await(manga.toMangaUpdate()) - updateManga.await(manga.toMangaUpdate()) + launchIO { + val request = + ImageRequest.Builder(preferences.context).data(manga.cover()) + .memoryCachePolicy(CachePolicy.DISABLED) + .diskCachePolicy(CachePolicy.WRITE_ONLY) + .build() - setCurrentManga(manga) - - presenterScope.launchNonCancellableIO { - val request = - ImageRequest.Builder(preferences.context).data(manga.cover()) - .memoryCachePolicy(CachePolicy.DISABLED) - .diskCachePolicy(CachePolicy.WRITE_ONLY) - .build() - - if (preferences.context.imageLoader.execute(request) is SuccessResult) { - withUIContext { - view?.setPaletteColor() + if (preferences.context.imageLoader.execute(request) is SuccessResult) { + withContext(Dispatchers.Main) { + view?.setPaletteColor() + } } } } - } catch (e: Exception) { - if (e is HttpException && e.code == 103) return - - withUIContext { - view?.showError(trimException(e)) - } - } - } - - private suspend fun fetchChaptersFromSource(manualFetch: Boolean = true) { - try { - withIOContext { - val chapters = source.getChapterList(manga.copy()) - val (added, removed) = syncChaptersWithSource(chapters, manga, source) - if (added.isNotEmpty() && manualFetch) { + val finChapters = chapters.await() + if (finChapters.isNotEmpty()) { + val newChapters = withIOContext { syncChaptersWithSource(finChapters, manga, source) } + if (newChapters.first.isNotEmpty()) { if (manga.shouldDownloadNewChapters(preferences)) { - downloadChapters(added.sortedBy { it.chapter_number }.map { it.toModel() }) - } - withUIContext { - view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } + downloadChapters( + newChapters.first.sortedBy { it.chapter_number } + .map { it.toModel() }, + ) } + view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } } - if (removed.isNotEmpty() && manualFetch) { - val removedChaptersId = removed.map { it.id } + if (newChapters.second.isNotEmpty()) { + val removedChaptersId = newChapters.second.map { it.id } val removedChapters = this@MangaDetailsPresenter.chapters.filter { it.id in removedChaptersId && it.isDownloaded } if (removedChapters.isNotEmpty()) { - withUIContext { - view?.showChaptersRemovedPopup(removedChapters) + withContext(Dispatchers.Main) { + view?.showChaptersRemovedPopup( + removedChapters, + ) } } } - setCurrentChapters(getChapters()) - getHistory() + getChapters() } - } catch (e: Exception) { - withUIContext { - view?.showError(trimException(e)) + isLoading = false + if (chapterError == null) { + withContext(Dispatchers.Main) { + view?.updateChapters(this@MangaDetailsPresenter.chapters) + } + } + if (chapterError != null) { + withContext(Dispatchers.Main) { + view?.showError( + trimException(chapterError!!), + ) + } + return@launch + } else if (mangaError != null) { + withContext(Dispatchers.Main) { + view?.showError( + trimException(mangaError!!), + ) + } + } + getHistory() + } + } + + /** + * Requests an updated list of chapters from the source. + */ + fun fetchChaptersFromSource() { + hasRequested = true + isLoading = true + + presenterScope.launch(Dispatchers.IO) { + val chapters = try { + source.getChapterList(manga.copy()) + } catch (e: Exception) { + withContext(Dispatchers.Main) { view?.showError(trimException(e)) } + return@launch + } + isLoading = false + try { + syncChaptersWithSource(chapters, manga, source) + + getChapters() + withContext(Dispatchers.Main) { + view?.updateChapters(this@MangaDetailsPresenter.chapters) + } + getHistory() + } catch (e: java.lang.Exception) { + withContext(Dispatchers.Main) { + view?.showError(trimException(e)) + } } } } @@ -569,7 +579,8 @@ class MangaDetailsPresenter( it.toProgressUpdate() } updateChapter.awaitAll(updates) - setCurrentChapters(getChapters()) + getChapters() + withContext(Dispatchers.Main) { view?.updateChapters(chapters) } } } @@ -598,7 +609,8 @@ class MangaDetailsPresenter( if (read && deleteNow && preferences.removeAfterMarkedAsRead().get()) { deleteChapters(selectedChapters, false) } - setCurrentChapters(getChapters()) + getChapters() + withContext(Dispatchers.Main) { view?.updateChapters(chapters) } if (read && deleteNow) { val latestReadChapter = selectedChapters.maxByOrNull { it.chapter_number.toInt() }?.chapter updateTrackChapterMarkedAsRead(preferences, latestReadChapter, manga.id) { @@ -729,7 +741,8 @@ class MangaDetailsPresenter( private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags)) - setCurrentChapters(getChapters()) + getChapters() + withUIContext { view?.updateChapters(chapters) } } private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true @@ -798,7 +811,7 @@ class MangaDetailsPresenter( withUIContext { view?.shareManga(uri.uri.toFile()) } - } catch (_: Exception) { + } catch (_: java.lang.Exception) { } } } @@ -1140,15 +1153,15 @@ class MangaDetailsPresenter( override fun onStatusChange(download: Download) { super.onStatusChange(download) - currentChaptersInternal.update { chapters -> - chapters.find { it.id == download.chapter.id }?.status = download.status - chapters - } + chapters.find { it.id == download.chapter.id }?.status = download.status onPageProgressUpdate(download) } private suspend fun onQueueUpdate(queue: List) = withIOContext { - setDownloadedChapters(queue) + getChapters(queue) + withUIContext { + view?.updateChapters(chapters) + } } override fun onQueueUpdate(download: Download) { @@ -1160,10 +1173,7 @@ class MangaDetailsPresenter( } override fun onPageProgressUpdate(download: Download) { - currentChaptersInternal.update { chapters -> - chapters.find { it.id == download.chapter.id }?.download = download - chapters - } + chapters.find { it.id == download.chapter.id }?.download = download view?.updateChapterDownload(download) } From 2dfc1e3451804636a8e15c96eee0481c113d4fee Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 08:47:42 +0700 Subject: [PATCH 129/166] refactor(manga): Split refreshAll to fetchMangaFromSource and fetchChaptersFromSource --- .../ui/manga/MangaDetailsController.kt | 8 +- .../ui/manga/MangaDetailsPresenter.kt | 169 +++++++----------- 2 files changed, 66 insertions(+), 111 deletions(-) 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 01a980fa31..6a06ec5673 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 @@ -821,15 +821,11 @@ class MangaDetailsController : updateMenuVisibility(activityBinding?.toolbar?.menu) } - fun updateChapters(chapters: List) { + fun updateChapters() { view ?: return binding.swipeRefresh.isRefreshing = presenter.isLoading - if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) { - launchUI { binding.swipeRefresh.isRefreshing = true } - presenter.fetchChaptersFromSource() - } tabletAdapter?.notifyItemChanged(0) - adapter?.setChapters(chapters) + adapter?.setChapters(presenter.chapters) addMangaHeader() colorToolbar(binding.recycler.canScrollVertically(-1)) updateMenuVisibility(activityBinding?.toolbar?.menu) 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 67ed320378..9a9c69e2ea 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 @@ -34,6 +34,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.domain.manga.models.Manga +import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source @@ -221,20 +222,22 @@ class MangaDetailsPresenter( .onEach { onUpdateManga() } .launchIn(presenterScope) - if (manga.isLocal()) { - refreshAll() - } else if (!manga.initialized) { - isLoading = true - controller.setRefresh(true) - controller.updateHeader() - refreshAll() - } else { - runBlocking { getChapters() } - controller.updateChapters(this.chapters) - getHistory() - } + val fetchMangaNeeded = !manga.initialized + val fetchChaptersNeeded = runBlocking { getChaptersNow() }.isEmpty() presenterScope.launch { + isLoading = true + controller.updateHeader() + val tasks = listOf( + async { if (fetchMangaNeeded) fetchMangaFromSource() }, + async { if (fetchChaptersNeeded) fetchChaptersFromSource(false) }, + ) + tasks.awaitAll() + isLoading = false + withUIContext { + controller.updateChapters() + } + setTrackItems() } @@ -245,7 +248,7 @@ class MangaDetailsPresenter( presenterScope.launch { getChapters() if (andTracking) fetchTracks() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } + withUIContext { view?.updateChapters() } getHistory() } } @@ -394,7 +397,7 @@ class MangaDetailsPresenter( download = null } - view?.updateChapters(this.chapters) + view?.updateChapters() downloadManager.deleteChapters(listOf(chapter), manga, source, true) } @@ -412,7 +415,7 @@ class MangaDetailsPresenter( } } - if (update) view?.updateChapters(this.chapters) + if (update) view?.updateChapters() if (isEverything) { downloadManager.deleteManga(manga, source) @@ -427,39 +430,17 @@ class MangaDetailsPresenter( return dbManga } - /** Refresh Manga Info and Chapter List (not tracking) */ - fun refreshAll() { - if (view?.isNotOnline() == true && !manga.isLocal()) return - presenterScope.launch { - isLoading = true - var mangaError: java.lang.Exception? = null - var chapterError: java.lang.Exception? = null - val chapters = async(Dispatchers.IO) { - try { - source.getChapterList(manga.copy()) - } catch (e: Exception) { - chapterError = e - emptyList() - } - } - val nManga = async(Dispatchers.IO) { - try { - source.getMangaDetails(manga.copy()) - } catch (e: java.lang.Exception) { - mangaError = e - null - } - } - - val networkManga = nManga.await() - if (networkManga != null) { + private suspend fun fetchMangaFromSource() { + try { + withIOContext { + val networkManga = source.getMangaDetails(manga.copy()) manga.prepareCoverUpdate(coverCache, networkManga, false) manga.copyFrom(networkManga) manga.initialized = true updateManga.await(manga.toMangaUpdate()) - launchIO { + presenterScope.launchNonCancellableIO { val request = ImageRequest.Builder(preferences.context).data(manga.cover()) .memoryCachePolicy(CachePolicy.DISABLED) @@ -467,90 +448,68 @@ class MangaDetailsPresenter( .build() if (preferences.context.imageLoader.execute(request) is SuccessResult) { - withContext(Dispatchers.Main) { + withUIContext { view?.setPaletteColor() } } } } - val finChapters = chapters.await() - if (finChapters.isNotEmpty()) { - val newChapters = withIOContext { syncChaptersWithSource(finChapters, manga, source) } - if (newChapters.first.isNotEmpty()) { - if (manga.shouldDownloadNewChapters(preferences)) { + } catch (e: Exception) { + if (e is HttpException && e.code == 103) return + withUIContext { + view?.showError(trimException(e)) + } + } + } + + private suspend fun fetchChaptersFromSource(manualFetch: Boolean = true) { + try { + withIOContext { + val chapters = source.getChapterList(manga.copy()) + val (added, removed) = withIOContext { syncChaptersWithSource(chapters, manga, source) } + if (added.isNotEmpty()) { + if (manga.shouldDownloadNewChapters(preferences) && manualFetch) { downloadChapters( - newChapters.first.sortedBy { it.chapter_number } + added.sortedBy { it.chapter_number } .map { it.toModel() }, ) } view?.view?.context?.let { mangaShortcutManager.updateShortcuts(it) } } - if (newChapters.second.isNotEmpty()) { - val removedChaptersId = newChapters.second.map { it.id } + if (removed.isNotEmpty()) { + val removedChaptersId = removed.map { it.id } val removedChapters = this@MangaDetailsPresenter.chapters.filter { it.id in removedChaptersId && it.isDownloaded } if (removedChapters.isNotEmpty()) { - withContext(Dispatchers.Main) { - view?.showChaptersRemovedPopup( - removedChapters, - ) + withUIContext { + view?.showChaptersRemovedPopup(removedChapters) } } } getChapters() + getHistory() } - isLoading = false - if (chapterError == null) { - withContext(Dispatchers.Main) { - view?.updateChapters(this@MangaDetailsPresenter.chapters) - } + } catch (e: Exception) { + withUIContext { + view?.showError(trimException(e)) } - if (chapterError != null) { - withContext(Dispatchers.Main) { - view?.showError( - trimException(chapterError!!), - ) - } - return@launch - } else if (mangaError != null) { - withContext(Dispatchers.Main) { - view?.showError( - trimException(mangaError!!), - ) - } - } - getHistory() } } - /** - * Requests an updated list of chapters from the source. - */ - fun fetchChaptersFromSource() { - hasRequested = true - isLoading = true - - presenterScope.launch(Dispatchers.IO) { - val chapters = try { - source.getChapterList(manga.copy()) - } catch (e: Exception) { - withContext(Dispatchers.Main) { view?.showError(trimException(e)) } - return@launch - } + /** Refresh Manga Info and Chapter List (not tracking) */ + fun refreshAll() { + if (view?.isNotOnline() == true && !manga.isLocal()) return + presenterScope.launch { + isLoading = true + val tasks = listOf( + async { fetchMangaFromSource() }, + async { fetchChaptersFromSource() }, + ) + tasks.awaitAll() isLoading = false - try { - syncChaptersWithSource(chapters, manga, source) - - getChapters() - withContext(Dispatchers.Main) { - view?.updateChapters(this@MangaDetailsPresenter.chapters) - } - getHistory() - } catch (e: java.lang.Exception) { - withContext(Dispatchers.Main) { - view?.showError(trimException(e)) - } + withUIContext { + view?.updateChapters() } } } @@ -580,7 +539,7 @@ class MangaDetailsPresenter( } updateChapter.awaitAll(updates) getChapters() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } + withContext(Dispatchers.Main) { view?.updateChapters() } } } @@ -610,7 +569,7 @@ class MangaDetailsPresenter( deleteChapters(selectedChapters, false) } getChapters() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } + withUIContext { view?.updateChapters() } if (read && deleteNow) { val latestReadChapter = selectedChapters.maxByOrNull { it.chapter_number.toInt() }?.chapter updateTrackChapterMarkedAsRead(preferences, latestReadChapter, manga.id) { @@ -742,7 +701,7 @@ class MangaDetailsPresenter( private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags)) getChapters() - withUIContext { view?.updateChapters(chapters) } + withUIContext { view?.updateChapters() } } private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true @@ -1160,7 +1119,7 @@ class MangaDetailsPresenter( private suspend fun onQueueUpdate(queue: List) = withIOContext { getChapters(queue) withUIContext { - view?.updateChapters(chapters) + view?.updateChapters() } } From 8c9208c3b6c1a8e4c6d7b8f754fd9f3c2d7240c2 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 09:03:44 +0700 Subject: [PATCH 130/166] chore(manga): Always try to refresh if it's a local entry --- .../eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9a9c69e2ea..b9ccbff7c4 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 @@ -222,8 +222,8 @@ class MangaDetailsPresenter( .onEach { onUpdateManga() } .launchIn(presenterScope) - val fetchMangaNeeded = !manga.initialized - val fetchChaptersNeeded = runBlocking { getChaptersNow() }.isEmpty() + val fetchMangaNeeded = !manga.initialized || manga.isLocal() + val fetchChaptersNeeded = runBlocking { getChaptersNow() }.isEmpty() || manga.isLocal() presenterScope.launch { isLoading = true From a404eb2c83730f5822949ff58165103d22efcb17 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 09:06:00 +0700 Subject: [PATCH 131/166] docs: Sync changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20dc0fc6f3..f2cdd60970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,12 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +### Changes +- Entries from local source now behaves similar to entries from online sources + ### Fixes - Fix new chapters not showing up in `Recents > Grouped` -- Add potential workaround for duplicate chapter bug +- Add potential workarounds for duplicate chapter bug - Fix favorite state is not being updated when browsing source ### Other From 8020f6591c12056da63506af1884da8f156298aa Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 11:28:22 +0700 Subject: [PATCH 132/166] docs: Remove todo note Doesn't seem to have any leaks... --- .../eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt | 1 - .../tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt index 1f5c844ada..818d75894e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt @@ -39,7 +39,6 @@ class BrowseSourceItem( val mangaId: Long = initialManga.id!! var manga: Manga = initialManga private set - // TODO: Could potentially cause memleak, test it with leakcanary before deploying to stable! private val scope = MainScope() private var job: Job? = null diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt index 503d2778e7..425d186748 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt @@ -24,7 +24,6 @@ class GlobalSearchMangaItem( val mangaId: Long? = initialManga.id var manga: Manga = initialManga private set - // TODO: Could potentially cause memleak, test it with leakcanary before deploying to stable! private val scope = MainScope() private var job: Job? = null From bbfa8c8bc48887053fe2287698cc7cca526ba06e Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 11:32:46 +0700 Subject: [PATCH 133/166] chore(release): v1.9.5 --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/ISSUE_TEMPLATE/issue_report.yml | 2 +- CHANGELOG.md | 2 ++ app/build.gradle.kts | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index ae6a49a881..b7937a9d4d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -35,7 +35,7 @@ body: required: true - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. required: true - - label: I have updated the app to version **[1.9.4](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.5](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have checked through the app settings for my feature. required: true diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index ea2e865021..d4a48fcf60 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -100,7 +100,7 @@ body: required: true - label: I have tried the [troubleshooting guide](https://mihon.app/help/). required: true - - label: I have updated the app to version **[1.9.4](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.5](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have updated all installed extensions. required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index f2cdd60970..a77348b012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +## [1.9.5] + ### Changes - Entries from local source now behaves similar to entries from online sources diff --git a/app/build.gradle.kts b/app/build.gradle.kts index af09d6225c..b02e3feb11 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 154 + versionCode = 155 versionName = _versionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true From 5fad2c015430b776ec87ae6490a6017d0c123d02 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 11:43:30 +0700 Subject: [PATCH 134/166] chore: Bump version to 1.9.6 for nightly and beta --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b02e3feb11..35d5695ccb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ fun runCommand(command: String): String { return String(byteOut.toByteArray()).trim() } -val _versionName = "1.9.5" +val _versionName = "1.9.6" val betaCount by lazy { val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") From fcaff92db18bf41bcbf985027b6ef5d0b84bcf1e Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 19:18:17 +0700 Subject: [PATCH 135/166] refactor(browse): Setup flow from presenter --- .../tachiyomi/ui/source/browse/BrowseSourceItem.kt | 8 +++----- .../ui/source/browse/BrowseSourcePresenter.kt | 1 + .../ui/source/globalsearch/GlobalSearchMangaItem.kt | 10 ++++------ .../ui/source/globalsearch/GlobalSearchPresenter.kt | 7 ++++++- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt index 818d75894e..92fdcf1713 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt @@ -20,22 +20,20 @@ import eu.kanade.tachiyomi.ui.library.setBGAndFG import eu.kanade.tachiyomi.widget.AutofitRecyclerView import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import uy.kohesive.injekt.injectLazy -import yokai.domain.manga.interactor.GetManga // FIXME: Migrate to compose class BrowseSourceItem( initialManga: Manga, + private val mangaFlow: Flow, private val catalogueAsList: Preference, private val catalogueListType: Preference, private val outlineOnCovers: Preference, ) : AbstractFlexibleItem() { - private val getManga: GetManga by injectLazy() - val mangaId: Long = initialManga.id!! var manga: Manga = initialManga private set @@ -94,7 +92,7 @@ class BrowseSourceItem( if (job == null) holder.onSetValues(manga) job?.cancel() job = scope.launch { - getManga.subscribeByUrlAndSource(manga.url, manga.source).collectLatest { + mangaFlow.collectLatest { manga = it ?: return@collectLatest holder.onSetValues(manga) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index 1685a1b883..53d27e2951 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -185,6 +185,7 @@ open class BrowseSourcePresenter( first to second.map { BrowseSourceItem( it, + getManga.subscribeByUrlAndSource(it.url, it.source), browseAsList, sourceListType, outlineCovers, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt index 425d186748..da932ecda8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaItem.kt @@ -9,18 +9,16 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.domain.manga.models.Manga import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import uy.kohesive.injekt.injectLazy -import yokai.domain.manga.interactor.GetManga // FIXME: Migrate to compose class GlobalSearchMangaItem( - initialManga: Manga + initialManga: Manga, + private val mangaFlow: Flow, ) : AbstractFlexibleItem() { - private val getManga: GetManga by injectLazy() - val mangaId: Long? = initialManga.id var manga: Manga = initialManga private set @@ -44,7 +42,7 @@ class GlobalSearchMangaItem( if (job == null) holder.bind(manga) job?.cancel() job = scope.launch { - getManga.subscribeByUrlAndSource(manga.url, manga.source).collectLatest { + mangaFlow.collectLatest { manga = it ?: return@collectLatest holder.bind(manga) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt index 6fe2a49d1d..b544928f31 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt @@ -192,7 +192,12 @@ open class GlobalSearchPresenter( } val result = createCatalogueSearchItem( source, - mangas.map { GlobalSearchMangaItem(it) }, + mangas.map { + GlobalSearchMangaItem( + it, + getManga.subscribeByUrlAndSource(it.url, it.source), + ) + }, ) items = items .map { item -> if (item.source == result.source) result else item } From 0d205f4a6d2f402b689f4badae7750c24e52e76e Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 19:34:35 +0700 Subject: [PATCH 136/166] fix(browse): Recycle on adapter clear --- .../ui/source/browse/BrowseSourceAdapter.kt | 24 +++++++++++++++++++ .../source/browse/BrowseSourceController.kt | 2 +- .../ui/source/browse/BrowseSourceItem.kt | 4 ++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt new file mode 100644 index 0000000000..563e7220a5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt @@ -0,0 +1,24 @@ +package eu.kanade.tachiyomi.ui.source.browse + +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible + +class BrowseSourceAdapter : FlexibleAdapter>(null, null) { + private fun clearItems() { + allBoundViewHolders.forEach { holder -> + val item = getItem(holder.flexibleAdapterPosition) as? BrowseSourceItem ?: return@forEach + item.recycle() + } + } + + override fun clear() { + clearItems() + super.clear() + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + clearItems() + super.onDetachedFromRecyclerView(recyclerView) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index f113d9242b..d655bba3da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -165,7 +165,7 @@ open class BrowseSourceController(bundle: Bundle) : super.onViewCreated(view) // Initialize adapter, scroll listener and recycler views - adapter = FlexibleAdapter(null, this) + adapter = BrowseSourceAdapter() setupRecycler(view) binding.fab.isVisible = presenter.sourceFilters.isNotEmpty() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt index 92fdcf1713..ea6389951c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt @@ -104,6 +104,10 @@ class BrowseSourceItem( holder: BrowseSourceHolder?, position: Int ) { + recycle() + } + + fun recycle() { job?.cancel() job = null } From cba97eb94d7c08a5fff45d8875625128d3a3317a Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 20:13:09 +0700 Subject: [PATCH 137/166] revert: "fix(browse): Recycle on adapter clear" Nvm, that caused the item to be unclickable :^) --- .../ui/source/browse/BrowseSourceAdapter.kt | 24 ------------------- .../source/browse/BrowseSourceController.kt | 2 +- .../ui/source/browse/BrowseSourceItem.kt | 4 ---- 3 files changed, 1 insertion(+), 29 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt deleted file mode 100644 index 563e7220a5..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.kanade.tachiyomi.ui.source.browse - -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible - -class BrowseSourceAdapter : FlexibleAdapter>(null, null) { - private fun clearItems() { - allBoundViewHolders.forEach { holder -> - val item = getItem(holder.flexibleAdapterPosition) as? BrowseSourceItem ?: return@forEach - item.recycle() - } - } - - override fun clear() { - clearItems() - super.clear() - } - - override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { - clearItems() - super.onDetachedFromRecyclerView(recyclerView) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index d655bba3da..f113d9242b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -165,7 +165,7 @@ open class BrowseSourceController(bundle: Bundle) : super.onViewCreated(view) // Initialize adapter, scroll listener and recycler views - adapter = BrowseSourceAdapter() + adapter = FlexibleAdapter(null, this) setupRecycler(view) binding.fab.isVisible = presenter.sourceFilters.isNotEmpty() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt index ea6389951c..92fdcf1713 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt @@ -104,10 +104,6 @@ class BrowseSourceItem( holder: BrowseSourceHolder?, position: Int ) { - recycle() - } - - fun recycle() { job?.cancel() job = null } From 18c5a689818cf6da4cc1b35e416a42f0dfa0ecc7 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 18 Dec 2024 06:00:27 +0700 Subject: [PATCH 138/166] fix(manga): Fix crashes --- CHANGELOG.md | 4 ++++ .../eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a77348b012..beefb81194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] +### Fixes +- Fix weird flickering when browsing sources +- Fix some crashes + ## [1.9.5] ### Changes 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 b9ccbff7c4..8b855b5517 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 @@ -227,7 +227,9 @@ class MangaDetailsPresenter( presenterScope.launch { isLoading = true - controller.updateHeader() + withUIContext { + controller.updateHeader() + } val tasks = listOf( async { if (fetchMangaNeeded) fetchMangaFromSource() }, async { if (fetchChaptersNeeded) fetchChaptersFromSource(false) }, @@ -539,7 +541,7 @@ class MangaDetailsPresenter( } updateChapter.awaitAll(updates) getChapters() - withContext(Dispatchers.Main) { view?.updateChapters() } + withUIContext { view?.updateChapters() } } } From 6c40fe92bedc798c92433f3443df8cbb8cc5c549 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 18 Dec 2024 06:03:14 +0700 Subject: [PATCH 139/166] chore(release): v1.9.6 --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/ISSUE_TEMPLATE/issue_report.yml | 2 +- CHANGELOG.md | 2 +- app/build.gradle.kts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index b7937a9d4d..9d202df605 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -35,7 +35,7 @@ body: required: true - label: If this is an issue with an extension, or a request for an extension, I should be contacting the extensions repository's maintainer/support for help. required: true - - label: I have updated the app to version **[1.9.5](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.6](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have checked through the app settings for my feature. required: true diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index d4a48fcf60..294bc46041 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -100,7 +100,7 @@ body: required: true - label: I have tried the [troubleshooting guide](https://mihon.app/help/). required: true - - label: I have updated the app to version **[1.9.5](https://github.com/null2264/yokai/releases/latest)**. + - label: I have updated the app to version **[1.9.6](https://github.com/null2264/yokai/releases/latest)**. required: true - label: I have updated all installed extensions. required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index beefb81194..68853a583a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - `Translation` - Translation changes/updates - `Other` - Technical changes/updates -## [Unreleased] +## [1.9.6] ### Fixes - Fix weird flickering when browsing sources diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 35d5695ccb..dea43d83e2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,7 @@ val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 155 + versionCode = 156 versionName = _versionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true From b3e69bdb2898749c99c1d90436f0aa3a5c58a38f Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 18 Dec 2024 11:22:23 +0700 Subject: [PATCH 140/166] fix(library): Sort by latest chapter is not working properly `last_update` is now when entry chapter list is changed instead of when is the latest entry uploaded. It is now replaced by LibraryManga's latestUpdate Fixes GH-309 --- CHANGELOG.md | 6 +++++- .../java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68853a583a..71019e9628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,14 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - `Translation` - Translation changes/updates - `Other` - Technical changes/updates +## [Unreleased] + +### Fixes +- Fix sorting by latest chapter is not working properly + ## [1.9.6] ### Fixes -- Fix weird flickering when browsing sources - Fix some crashes ## [1.9.5] 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 b38686e07e..77ed4ca035 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 @@ -658,7 +658,7 @@ class LibraryPresenter( category.mangaSort != null -> { var sort = when (category.sortingMode() ?: LibrarySort.Title) { LibrarySort.Title -> sortAlphabetical(i1, i2) - LibrarySort.LatestChapter -> i2.manga.last_update.compareTo(i1.manga.last_update) + LibrarySort.LatestChapter -> i2.manga.latestUpdate.compareTo(i1.manga.latestUpdate) LibrarySort.Unread -> when { i1.manga.unread == i2.manga.unread -> 0 i1.manga.unread == 0 -> if (category.isAscending()) 1 else -1 From 02296985d65950f908ea3fcbc7668e2efec0931f Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 18 Dec 2024 11:29:55 +0700 Subject: [PATCH 141/166] chore(chapter): No longer set last_update as newest chapter's date_upload Grab this info from SQL instead --- .../kanade/tachiyomi/util/chapter/ChapterSourceSync.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index f105fcbb2e..987d76c91b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -118,12 +118,7 @@ suspend fun syncChaptersWithSource( // Return if there's nothing to add, delete or change, avoid unnecessary db transactions. if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { - val newestDate = dbChapters.maxOfOrNull { it.date_upload } ?: 0L - if (newestDate != 0L && newestDate > manga.last_update) { - manga.last_update = newestDate - val update = MangaUpdate(manga.id!!, lastUpdate = newestDate) - updateManga.await(update) - } + // TODO: Predict when the next chapter gonna release return Pair(emptyList(), emptyList()) } @@ -189,6 +184,8 @@ suspend fun syncChaptersWithSource( } } + // TODO: Predict when the next chapter gonna release + // Set this manga as updated since chapters were changed // Note that last_update actually represents last time the chapter list changed at all // Those changes already checked beforehand, so we can proceed to updating the manga From 659ba892181a2a72fae9b939c5222b98df5328c2 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Tue, 17 Dec 2024 19:34:35 +0700 Subject: [PATCH 142/166] fix(browse): Recycle on adapter clear Take 2, but this time don't override onDetachFromRecyclerView --- .../ui/source/browse/BrowseSourceAdapter.kt | 16 ++++++++++++++++ .../ui/source/browse/BrowseSourceController.kt | 2 +- .../ui/source/browse/BrowseSourceItem.kt | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt new file mode 100644 index 0000000000..559dd8c8ff --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt @@ -0,0 +1,16 @@ +package eu.kanade.tachiyomi.ui.source.browse + +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible + +class BrowseSourceAdapter : FlexibleAdapter>(null, null) { + override fun clear() { + allBoundViewHolders.forEach { holder -> + val item = getItem(holder.flexibleAdapterPosition) as? BrowseSourceItem ?: return@forEach + item.recycle() + } + + super.clear() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index f113d9242b..d655bba3da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -165,7 +165,7 @@ open class BrowseSourceController(bundle: Bundle) : super.onViewCreated(view) // Initialize adapter, scroll listener and recycler views - adapter = FlexibleAdapter(null, this) + adapter = BrowseSourceAdapter() setupRecycler(view) binding.fab.isVisible = presenter.sourceFilters.isNotEmpty() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt index 92fdcf1713..ea6389951c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt @@ -104,6 +104,10 @@ class BrowseSourceItem( holder: BrowseSourceHolder?, position: Int ) { + recycle() + } + + fun recycle() { job?.cancel() job = null } From 33332110f1fc8a3a9b52693c5a63a203cdbe86c2 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 18 Dec 2024 12:07:03 +0700 Subject: [PATCH 143/166] revert: "fix(browse): Recycle on adapter clear" Still unclickable --- .../ui/source/browse/BrowseSourceAdapter.kt | 16 ---------------- .../ui/source/browse/BrowseSourceController.kt | 2 +- .../ui/source/browse/BrowseSourceItem.kt | 4 ---- 3 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt deleted file mode 100644 index 559dd8c8ff..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceAdapter.kt +++ /dev/null @@ -1,16 +0,0 @@ -package eu.kanade.tachiyomi.ui.source.browse - -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible - -class BrowseSourceAdapter : FlexibleAdapter>(null, null) { - override fun clear() { - allBoundViewHolders.forEach { holder -> - val item = getItem(holder.flexibleAdapterPosition) as? BrowseSourceItem ?: return@forEach - item.recycle() - } - - super.clear() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index d655bba3da..f113d9242b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -165,7 +165,7 @@ open class BrowseSourceController(bundle: Bundle) : super.onViewCreated(view) // Initialize adapter, scroll listener and recycler views - adapter = BrowseSourceAdapter() + adapter = FlexibleAdapter(null, this) setupRecycler(view) binding.fab.isVisible = presenter.sourceFilters.isNotEmpty() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt index ea6389951c..92fdcf1713 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt @@ -104,10 +104,6 @@ class BrowseSourceItem( holder: BrowseSourceHolder?, position: Int ) { - recycle() - } - - fun recycle() { job?.cancel() job = null } From 17eec5f6aabcf4c8ed90830bf0dc8cc3997f0a2b Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 19 Dec 2024 07:18:14 +0700 Subject: [PATCH 144/166] revert: "refactor(archive): Move stuff around" This reverts commit e19d048bb157898553f7f256cd4110140fb13301. --- .../core/archive/AndroidArchiveInputStream.kt | 2 +- .../yokai/core/archive/AndroidArchiveReader.kt | 14 ++++++-------- .../yokai/core/archive/ArchiveInputStream.kt | 4 +--- .../kotlin/yokai/core/archive/ArchiveReader.kt | 13 +++---------- 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveInputStream.kt b/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveInputStream.kt index bb382a346e..fb5426f46a 100644 --- a/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveInputStream.kt +++ b/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveInputStream.kt @@ -53,7 +53,7 @@ class AndroidArchiveInputStream(buffer: Long, size: Long) : ArchiveInputStream() Archive.readFree(archive) } - override fun getNextEntry() = Archive.readNextHeader(archive).takeUnless { it == 0L }?.let { entry -> + fun getNextEntry() = Archive.readNextHeader(archive).takeUnless { it == 0L }?.let { entry -> val name = ArchiveEntry.pathnameUtf8(entry) ?: ArchiveEntry.pathname(entry)?.decodeToString() ?: return null val isFile = ArchiveEntry.filetype(entry) == ArchiveEntry.AE_IFREG ArchiveEntry(name, isFile) diff --git a/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveReader.kt b/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveReader.kt index f9194e53c6..8309e9b1a6 100644 --- a/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveReader.kt +++ b/core/src/androidMain/kotlin/yokai/core/archive/AndroidArchiveReader.kt @@ -9,17 +9,15 @@ import eu.kanade.tachiyomi.util.system.openFileDescriptor import me.zhanghai.android.libarchive.ArchiveException import java.io.InputStream -class AndroidArchiveReader(pfd: ParcelFileDescriptor) : ArchiveReader() { - override val size = - pfd.statSize - override val address = - Os.mmap(0, size, OsConstants.PROT_READ, OsConstants.MAP_PRIVATE, pfd.fileDescriptor, 0) +class AndroidArchiveReader(pfd: ParcelFileDescriptor) : ArchiveReader { + val size = pfd.statSize + val address = Os.mmap(0, size, OsConstants.PROT_READ, OsConstants.MAP_PRIVATE, pfd.fileDescriptor, 0) - override fun createInputStream(address: Long, size: Long): ArchiveInputStream = - AndroidArchiveInputStream(address, size) + override fun useEntries(block: (Sequence) -> T): T = + AndroidArchiveInputStream(address, size).use { block(generateSequence { it.getNextEntry() }) } override fun getInputStream(entryName: String): InputStream? { - val archive = createInputStream(address, size) + val archive = AndroidArchiveInputStream(address, size) try { while (true) { val entry = archive.getNextEntry() ?: break diff --git a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt index 1da5acfe4a..6a9cd0185b 100644 --- a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt +++ b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveInputStream.kt @@ -3,6 +3,4 @@ package yokai.core.archive import java.io.InputStream // TODO: Use Okio's Source -abstract class ArchiveInputStream : InputStream() { - abstract fun getNextEntry(): ArchiveEntry? -} +abstract class ArchiveInputStream : InputStream() diff --git a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt index 0eb3669d49..00d646f3a7 100644 --- a/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt +++ b/core/src/androidMain/kotlin/yokai/core/archive/ArchiveReader.kt @@ -3,14 +3,7 @@ package yokai.core.archive import java.io.Closeable import java.io.InputStream -abstract class ArchiveReader : Closeable { - abstract val address: Long - abstract val size: Long - - abstract fun createInputStream(address: Long, size: Long): ArchiveInputStream - - inline fun useEntries(block: (Sequence) -> T): T = - createInputStream(address, size).use { block(generateSequence { it.getNextEntry() }) } - - abstract fun getInputStream(entryName: String): InputStream? +interface ArchiveReader : Closeable { + fun useEntries(block: (Sequence) -> T): T + fun getInputStream(entryName: String): InputStream? } From 985ac6d7a826594274f940c03a840d1ae6b6cd59 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 19 Dec 2024 07:56:41 +0700 Subject: [PATCH 145/166] refactor(download): Move .show() inside `with` block --- .../tachiyomi/data/download/DownloadNotifier.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt index ae8260b386..82131a1e7e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt @@ -155,9 +155,10 @@ internal class DownloadNotifier(private val context: Context) { } setStyle(null) setProgress(download.pages!!.size, download.downloadedImages, false) + + // Displays the progress bar on notification + show() } - // Displays the progress bar on notification - notification.show() } /** @@ -212,8 +213,9 @@ internal class DownloadNotifier(private val context: Context) { clearActions() setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setProgress(0, 0, false) + + show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) } - notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) // Reset download information isDownloading = false @@ -291,8 +293,9 @@ internal class DownloadNotifier(private val context: Context) { } color = ContextCompat.getColor(context, R.color.secondaryTachiyomi) setProgress(0, 0, false) + + show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) } - notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) // Reset download information errorThrown = true From cd8ff6f898223bace5e5aa9ea58ad877c265a020 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 19 Dec 2024 08:08:59 +0700 Subject: [PATCH 146/166] refactor(downloader): Extract `ensureSuccessfulDownload` Also double-bang archiveChapter, since exceptions already being catch by `downloadChapter` --- .../tachiyomi/data/download/Downloader.kt | 106 +++++++++--------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index b074eb8421..ad65c6ece6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -389,7 +389,30 @@ class Downloader( } // Do after download completes - ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) + + if (!isDownloadSuccessful(download, tmpDir)) { + download.status = Download.State.ERROR + return + } + + createComicInfoFile( + tmpDir, + download.manga, + download.chapter, + download.source, + ) + + // Only rename the directory if it's downloaded + if (preferences.saveChaptersAsCBZ().get()) { + archiveChapter(mangaDir, chapterDirname, tmpDir) + } else { + tmpDir.renameTo(chapterDirname) + } + cache.addChapter(chapterDirname, mangaDir, download.manga) + + DiskUtil.createNoMediaFile(tmpDir, context) + + download.status = Download.State.DOWNLOADED } catch (error: Throwable) { if (error is CancellationException) throw error // If the page list threw, it will resume here @@ -399,6 +422,31 @@ class Downloader( } } + private fun isDownloadSuccessful( + download: Download, + tmpDir: UniFile, + ): Boolean { + // Page list hasn't been initialized + val downloadPageCount = download.pages?.size ?: return false + + // Ensure that all pages has been downloaded + if (download.downloadedImages != downloadPageCount) return false + + // Ensure that the chapter folder has all the pages + val downloadedImagesCount = tmpDir.listFiles().orEmpty().count { + val fileName = it.name.orEmpty() + when { + fileName in listOf(COMIC_INFO_FILE, NOMEDIA_FILE) -> false + fileName.endsWith(".tmp") -> false + // Only count the first split page and not the others + fileName.contains("__") && !fileName.endsWith("__001.jpg") -> false + else -> true + } + } + + return downloadedImagesCount == downloadPageCount + } + /** * Returns the observable which gets the image from the filesystem if it exists or downloads it * otherwise. @@ -549,60 +597,6 @@ class Downloader( } } - /** - * Checks if the download was successful. - * - * @param download the download to check. - * @param mangaDir the manga directory of the download. - * @param tmpDir the directory where the download is currently stored. - * @param dirname the real (non temporary) directory name of the download. - */ - private suspend fun ensureSuccessfulDownload( - download: Download, - mangaDir: UniFile, - tmpDir: UniFile, - dirname: String, - ) { - // Page list hasn't been initialized - val downloadPageCount = download.pages?.size ?: return - // Ensure that all pages has been downloaded - if (download.downloadedImages < downloadPageCount) return - // Ensure that the chapter folder has all the pages - val downloadedImagesCount = tmpDir.listFiles().orEmpty().count { - val fileName = it.name.orEmpty() - when { - fileName in listOf(COMIC_INFO_FILE, NOMEDIA_FILE) -> false - fileName.endsWith(".tmp") -> false - // Only count the first split page and not the others - fileName.contains("__") && !fileName.endsWith("__001.jpg") -> false - else -> true - } - } - - download.status = if (downloadedImagesCount == downloadPageCount) { - createComicInfoFile( - tmpDir, - download.manga, - download.chapter, - download.source, - ) - - // Only rename the directory if it's downloaded - if (preferences.saveChaptersAsCBZ().get()) { - archiveChapter(mangaDir, dirname, tmpDir) - } else { - tmpDir.renameTo(dirname) - } - cache.addChapter(dirname, mangaDir, download.manga) - - DiskUtil.createNoMediaFile(tmpDir, context) - - Download.State.DOWNLOADED - } else { - Download.State.ERROR - } - } - /** * Archive the chapter pages as a CBZ. */ @@ -611,7 +605,7 @@ class Downloader( dirname: String, tmpDir: UniFile, ) { - val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX") ?: return + val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!! ZipWriter(context, zip).use { writer -> tmpDir.listFiles()?.forEach { file -> writer.write(file) From 1571678ddbb1441d00af6a327391e735b358701c Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 19 Dec 2024 10:35:08 +0700 Subject: [PATCH 147/166] chore(deps): Update NDK to v27.2.12479018 --- CHANGELOG.md | 3 +++ buildSrc/src/main/kotlin/AndroidConfig.kt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71019e9628..8d4b3b4ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ### Fixes - Fix sorting by latest chapter is not working properly +### Other +- Update NDK to v27.2.12479018 + ## [1.9.6] ### Fixes diff --git a/buildSrc/src/main/kotlin/AndroidConfig.kt b/buildSrc/src/main/kotlin/AndroidConfig.kt index fe8b3f85d7..742ae66247 100644 --- a/buildSrc/src/main/kotlin/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/AndroidConfig.kt @@ -2,5 +2,5 @@ object AndroidConfig { const val compileSdk = 35 const val minSdk = 23 const val targetSdk = 35 - const val ndk = "23.1.7779620" + const val ndk = "27.2.12479018" } From bf8eccc1864afcff906c63c454a8ec069dea2d00 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 19 Dec 2024 10:43:02 +0700 Subject: [PATCH 148/166] ci: Update Android SDK tools to v35.0.0 --- .github/workflows/build_check.yml | 2 +- .github/workflows/build_push.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_check.yml b/.github/workflows/build_check.yml index f3aabd7add..abb7c971a7 100644 --- a/.github/workflows/build_check.yml +++ b/.github/workflows/build_check.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Android SDK run: | - ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3" + ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0" - name: Setup Gradle uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29 diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 720bc4771a..c81a5504cf 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -40,7 +40,7 @@ jobs: - name: Setup Android SDK run: | - ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;34.0.0" + ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0" - name: Setup Gradle uses: null2264/actions/gradle-setup@a4d662095a2f2af1ed24f1228eb6e55b0f9f1f29 From 2466d3a493d4e42726bf7e3cdb57e0ec22cbbfb2 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 19 Dec 2024 10:56:29 +0700 Subject: [PATCH 149/166] ci: Upload APK to artifact --- .github/workflows/build_push.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index c81a5504cf..f4e04b9288 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -170,6 +170,7 @@ jobs: set -e dir="app/build/outputs/apk/standard/${{ steps.version_stage.outputs.STAGE }}" + echo "APK_DIR=$dir" >> $GITHUB_ENV mv $dir/app-standard-universal-*-signed.apk yokai-${{ env.VERSION_TAG }}.apk sha=`sha256sum yokai-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` @@ -191,6 +192,12 @@ jobs: sha=`sha256sum yokai-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV + - name: Upload to artifact + if: env.VERSION_TAG != '' + uses: actions/upload-artifact@v4 + with: + path: ${{ env.APK_DIR }}/yokai-* + - name: Create Release if: startsWith(env.VERSION_TAG, 'v') uses: softprops/action-gh-release@v2 From eea87eede42efed5b6b65ed8862e9dd70d1f6384 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 19 Dec 2024 10:59:00 +0700 Subject: [PATCH 150/166] chore: Bump version to v1.9.7 for beta and nightly --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dea43d83e2..e28e5c0a71 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ fun runCommand(command: String): String { return String(byteOut.toByteArray()).trim() } -val _versionName = "1.9.6" +val _versionName = "1.9.7" val betaCount by lazy { val betaTags = runCommand("git tag -l --sort=refname v${_versionName}-b*") From 12002f62cd5151456c00fa991ccdd4066fc2954f Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 19 Dec 2024 11:09:26 +0700 Subject: [PATCH 151/166] ci: Fix upload to artifact --- .github/workflows/build_push.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index f4e04b9288..47431af25c 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -170,7 +170,6 @@ jobs: set -e dir="app/build/outputs/apk/standard/${{ steps.version_stage.outputs.STAGE }}" - echo "APK_DIR=$dir" >> $GITHUB_ENV mv $dir/app-standard-universal-*-signed.apk yokai-${{ env.VERSION_TAG }}.apk sha=`sha256sum yokai-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` @@ -196,7 +195,7 @@ jobs: if: env.VERSION_TAG != '' uses: actions/upload-artifact@v4 with: - path: ${{ env.APK_DIR }}/yokai-* + path: yokai-* - name: Create Release if: startsWith(env.VERSION_TAG, 'v') From f985ad6daaa87a7353437e4d1e23e3babe8ab25b Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 20 Dec 2024 08:05:19 +0700 Subject: [PATCH 152/166] fix(download): Don't delay pause notification --- .../java/eu/kanade/tachiyomi/data/download/Downloader.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index ad65c6ece6..1b2c925611 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.download import android.content.Context -import android.os.Handler import android.os.Looper import co.touchlab.kermit.Logger import com.hippo.unifile.UniFile @@ -93,8 +92,6 @@ class Downloader( private val _queueState = MutableStateFlow>(emptyList()) val queueState = _queueState.asStateFlow() - private val handler = Handler(Looper.getMainLooper()) - /** * Notifier for the downloader state and progress. */ @@ -158,7 +155,7 @@ class Downloader( } if (isPaused && queueState.value.isNotEmpty()) { - handler.postDelayed({ notifier.onDownloadPaused() }, 150) + notifier.onDownloadPaused() } else { notifier.dismiss() } From 2299aaac63257a8056167d256569e4bd27b668f3 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 20 Dec 2024 08:30:33 +0700 Subject: [PATCH 153/166] fix(manga): Explicitly check if tablet header is not null to avoid NPE --- CHANGELOG.md | 1 + .../eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4b3b4ea4..685abfd0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ### Fixes - Fix sorting by latest chapter is not working properly +- Prevent some NPE crashes ### Other - Update NDK to v27.2.12479018 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 6a06ec5673..9b288c450c 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 @@ -832,12 +832,13 @@ class MangaDetailsController : } private fun addMangaHeader() { - if (tabletAdapter?.scrollableHeaders?.isEmpty() == true) { + val tabletHeader = presenter.tabletChapterHeaderItem + if (tabletHeader != null && tabletAdapter?.scrollableHeaders?.isEmpty() == true) { tabletAdapter?.removeAllScrollableHeaders() tabletAdapter?.addScrollableHeader(presenter.headerItem) adapter?.removeAllScrollableHeaders() - adapter?.addScrollableHeader(presenter.tabletChapterHeaderItem!!) - } else if (!isTablet && adapter?.scrollableHeaders?.isEmpty() == true) { + adapter?.addScrollableHeader(tabletHeader) + } else if (adapter?.scrollableHeaders?.isEmpty() == true) { adapter?.removeAllScrollableHeaders() adapter?.addScrollableHeader(presenter.headerItem) } From c1cb7a20664dae555ad3a4fb26684f9751054ee5 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 20 Dec 2024 08:37:00 +0700 Subject: [PATCH 154/166] fix(source/local): Don't use double-bang --- app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index c766b813a3..f7040a5c66 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -286,8 +286,9 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour if (!directory.exists()) return lang?.let { langMap[manga.url] = it } - val file = directory.createFile(COMIC_INFO_FILE)!! - file.writeText(xml.encodeToString(ComicInfo.serializer(), manga.toComicInfo(lang = lang))) + directory.createFile(COMIC_INFO_FILE)?.let { file -> + file.writeText(xml.encodeToString(ComicInfo.serializer(), manga.toComicInfo(lang = lang))) + } } @Serializable From 3d2e2b277468508704723128661e9c89bb86cdca Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 20 Dec 2024 08:46:18 +0700 Subject: [PATCH 155/166] fix(library): Don't use double-bang --- .../kanade/tachiyomi/ui/library/LibraryHeaderHolder.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderHolder.kt index 387f4688a6..9ef0bd1db5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderHolder.kt @@ -260,24 +260,24 @@ class LibraryHeaderHolder(val view: View, val adapter: LibraryCategoryAdapter) : } private fun showCatSortOptions() { - if (category == null) return + val cat = category ?: return adapter.controller?.activity?.let { activity -> - val items = LibrarySort.entries.map { it.menuSheetItem(category!!.isDynamic) } - val sortingMode = category!!.sortingMode(true) + val items = LibrarySort.entries.map { it.menuSheetItem(cat.isDynamic) } + val sortingMode = cat.sortingMode(true) val sheet = MaterialMenuSheet( activity, items, activity.getString(MR.strings.sort_by), sortingMode?.mainValue, ) { sheet, item -> - onCatSortClicked(category!!, item) + onCatSortClicked(cat, item) val nCategory = (adapter.getItem(flexibleAdapterPosition) as? LibraryHeaderItem)?.category val isAscending = nCategory?.isAscending() ?: false val drawableRes = getSortRes(item, isAscending) sheet.setDrawable(item, drawableRes) false } - val isAscending = category!!.isAscending() + val isAscending = cat.isAscending() val drawableRes = getSortRes(sortingMode, isAscending) sheet.setDrawable(sortingMode?.mainValue ?: -1, drawableRes) sheet.show() From 778bd05e21a0de91b6cb7acb67bafa7599ac334b Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 20 Dec 2024 08:47:25 +0700 Subject: [PATCH 156/166] ci: Remove upload to artifact No longer needed --- .github/workflows/build_push.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 47431af25c..c81a5504cf 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -191,12 +191,6 @@ jobs: sha=`sha256sum yokai-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV - - name: Upload to artifact - if: env.VERSION_TAG != '' - uses: actions/upload-artifact@v4 - with: - path: yokai-* - - name: Create Release if: startsWith(env.VERSION_TAG, 'v') uses: softprops/action-gh-release@v2 From 8ac818797777b626fbe2fd81cde9f0be3b1cce88 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 20 Dec 2024 09:29:05 +0700 Subject: [PATCH 157/166] fix(browse): Enable stable id --- .../kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index f113d9242b..fa40d5714d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -165,7 +165,7 @@ open class BrowseSourceController(bundle: Bundle) : super.onViewCreated(view) // Initialize adapter, scroll listener and recycler views - adapter = FlexibleAdapter(null, this) + adapter = FlexibleAdapter(null, this, true) setupRecycler(view) binding.fab.isVisible = presenter.sourceFilters.isNotEmpty() From b974eff32038ea87014cb782df5bf5eeaa780970 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 20 Dec 2024 13:19:36 +0700 Subject: [PATCH 158/166] debug: Verbose logging for FlexibleAdapter --- app/src/main/java/eu/kanade/tachiyomi/App.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index f34d8205a9..5121e6fd6b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -34,6 +34,8 @@ import coil3.util.DebugLogger import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import com.hippo.unifile.UniFile +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.utils.Log.Level import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager import eu.kanade.tachiyomi.core.preference.Preference import eu.kanade.tachiyomi.core.preference.PreferenceStore @@ -139,6 +141,12 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F .onEach { ImageUtil.hardwareBitmapThreshold = it } .launchIn(scope) + networkPreferences.verboseLogging().changes() + .onEach { enabled -> + FlexibleAdapter.enableLogs(if (enabled) Level.VERBOSE else Level.SUPPRESS) + } + .launchIn(scope) + scope.launchIO { with(TachiyomiWidgetManager()) { this@App.init() } } From c73d0f843f761a9c88eacf5ff0a1667d6336c57d Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 20 Dec 2024 20:12:09 +0700 Subject: [PATCH 159/166] fix(browse): A different approach --- .../source/browse/BrowseSourceController.kt | 34 +++++++++++++++-- .../ui/source/browse/BrowseSourceItem.kt | 38 ++++++------------- .../ui/source/browse/BrowseSourcePresenter.kt | 1 - 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index fa40d5714d..3b0014d977 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -61,7 +61,12 @@ import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.EmptyView import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset import kotlin.math.roundToInt +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import uy.kohesive.injekt.injectLazy +import yokai.domain.manga.interactor.GetManga import yokai.i18n.MR import yokai.util.lang.getString @@ -101,6 +106,8 @@ open class BrowseSourceController(bundle: Bundle) : }, ) + private val getManga: GetManga by injectLazy() + /** * Preferences helper. */ @@ -133,6 +140,9 @@ open class BrowseSourceController(bundle: Bundle) : private val isBehindGlobalSearch: Boolean get() = router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller is GlobalSearchController + /** Watch for manga data changes */ + private var watchJob: Job? = null + init { setHasOptionsMenu(true) } @@ -655,7 +665,7 @@ open class BrowseSourceController(bundle: Bundle) : * @param manga the manga initialized */ fun onMangaInitialized(manga: Manga) { - getHolder(manga)?.setImage(manga) + getHolder(manga.id!!)?.setImage(manga) } /** @@ -711,12 +721,12 @@ open class BrowseSourceController(bundle: Bundle) : * @param manga the manga to find. * @return the holder of the manga or null if it's not bound. */ - private fun getHolder(manga: Manga): BrowseSourceHolder? { + private fun getHolder(mangaId: Long): BrowseSourceHolder? { val adapter = adapter ?: return null adapter.allBoundViewHolders.forEach { holder -> val item = adapter.getItem(holder.flexibleAdapterPosition) as? BrowseSourceItem - if (item != null && item.manga.id!! == manga.id!!) { + if (item != null && item.mangaId == mangaId) { return holder as BrowseSourceHolder } } @@ -742,6 +752,23 @@ open class BrowseSourceController(bundle: Bundle) : binding.progress.isVisible = false } + /** + * Workaround to fix data state de-sync issues when controller detached, + * and attaching flow directly into Item caused some flickering issues. + * + * FIXME: Could easily be fixed by migrating to Compose. + */ + private fun BrowseSourceItem.subscribe(mangaFlow: Flow) { + watchJob?.cancel() + watchJob = viewScope.launch { + mangaFlow.collectLatest { + if (it == null) return@collectLatest + val holder = getHolder(mangaId) ?: return@collectLatest + updateManga(holder, it) + } + } + } + /** * Called when a manga is clicked. * @@ -750,6 +777,7 @@ open class BrowseSourceController(bundle: Bundle) : */ override fun onItemClick(view: View?, position: Int): Boolean { val item = adapter?.getItem(position) as? BrowseSourceItem ?: return false + item.subscribe(getManga.subscribeByUrlAndSource(item.manga.url, item.manga.source)) router.pushController(MangaDetailsController(item.manga, true).withFadeTransaction()) lastPosition = position return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt index 92fdcf1713..20b4a8ebe0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt @@ -18,16 +18,10 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.setBGAndFG import eu.kanade.tachiyomi.widget.AutofitRecyclerView -import kotlinx.coroutines.Job -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch // FIXME: Migrate to compose class BrowseSourceItem( initialManga: Manga, - private val mangaFlow: Flow, private val catalogueAsList: Preference, private val catalogueListType: Preference, private val outlineOnCovers: Preference, @@ -37,8 +31,6 @@ class BrowseSourceItem( val mangaId: Long = initialManga.id!! var manga: Manga = initialManga private set - private val scope = MainScope() - private var job: Job? = null override fun getLayoutRes(): Int { return if (catalogueAsList.get()) { @@ -83,35 +75,29 @@ class BrowseSourceItem( } } + fun updateManga( + holder: BrowseSourceHolder, + manga: Manga, + ) { + if (manga.id != mangaId) return + + this.manga = manga + holder.onSetValues(manga) + } + override fun bindViewHolder( adapter: FlexibleAdapter>, holder: BrowseSourceHolder, position: Int, payloads: MutableList?, ) { - if (job == null) holder.onSetValues(manga) - job?.cancel() - job = scope.launch { - mangaFlow.collectLatest { - manga = it ?: return@collectLatest - holder.onSetValues(manga) - } - } - } - - override fun unbindViewHolder( - adapter: FlexibleAdapter>?, - holder: BrowseSourceHolder?, - position: Int - ) { - job?.cancel() - job = null + holder.onSetValues(manga) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other is BrowseSourceItem) { - return mangaId == other.mangaId + return this.mangaId == other.mangaId } return false } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index 53d27e2951..1685a1b883 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -185,7 +185,6 @@ open class BrowseSourcePresenter( first to second.map { BrowseSourceItem( it, - getManga.subscribeByUrlAndSource(it.url, it.source), browseAsList, sourceListType, outlineCovers, From 9e5262140e754c672cd32901c123e3ffc142b1ea Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 20 Dec 2024 20:32:03 +0700 Subject: [PATCH 160/166] docs: Sync changelog Also slight code adjustment --- CHANGELOG.md | 1 + .../tachiyomi/ui/source/browse/BrowseSourceController.kt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 685abfd0ea..9e3978ef4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co ### Fixes - Fix sorting by latest chapter is not working properly - Prevent some NPE crashes +- Fix some flickering issues when browsing sources ### Other - Update NDK to v27.2.12479018 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index 3b0014d977..de4ec5547a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -758,10 +758,10 @@ open class BrowseSourceController(bundle: Bundle) : * * FIXME: Could easily be fixed by migrating to Compose. */ - private fun BrowseSourceItem.subscribe(mangaFlow: Flow) { + private fun BrowseSourceItem.subscribe() { watchJob?.cancel() watchJob = viewScope.launch { - mangaFlow.collectLatest { + getManga.subscribeByUrlAndSource(manga.url, manga.source).collectLatest { if (it == null) return@collectLatest val holder = getHolder(mangaId) ?: return@collectLatest updateManga(holder, it) @@ -777,7 +777,7 @@ open class BrowseSourceController(bundle: Bundle) : */ override fun onItemClick(view: View?, position: Int): Boolean { val item = adapter?.getItem(position) as? BrowseSourceItem ?: return false - item.subscribe(getManga.subscribeByUrlAndSource(item.manga.url, item.manga.source)) + item.subscribe() router.pushController(MangaDetailsController(item.manga, true).withFadeTransaction()) lastPosition = position return false From 3606f67dba203c60d4465db0ede261020788dade Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 20 Dec 2024 20:52:45 +0700 Subject: [PATCH 161/166] fix(browse): Unsubscribe before restarting pager --- .../tachiyomi/ui/source/browse/BrowseSourceController.kt | 5 +++++ .../tachiyomi/ui/source/browse/BrowseSourcePresenter.kt | 2 ++ 2 files changed, 7 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index de4ec5547a..8eb367d84b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -752,6 +752,11 @@ open class BrowseSourceController(bundle: Bundle) : binding.progress.isVisible = false } + fun unsubscribe() { + watchJob?.cancel() + watchJob = null + } + /** * Workaround to fix data state de-sync issues when controller detached, * and attaching flow directly into Item caused some flickering issues. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index 1685a1b883..693e04c267 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -171,6 +171,8 @@ open class BrowseSourcePresenter( val sourceListType = preferences.libraryLayout() val outlineCovers = uiPreferences.outlineOnCovers() + view?.unsubscribe() + // Prepare the pager. pagerJob?.cancel() pagerJob = presenterScope.launchIO { From 3787845893c52695558412f3b074efcaf7f86556 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 21 Dec 2024 06:03:23 +0700 Subject: [PATCH 162/166] fix: Download queue count not updating Also, don't use raw integer for download badge colour --- .../kanade/tachiyomi/ui/main/MainActivity.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 2aea06713d..d239eef202 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -102,6 +102,7 @@ import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import eu.kanade.tachiyomi.util.manga.MangaShortcutManager import eu.kanade.tachiyomi.util.showNotificationPermissionPrompt +import eu.kanade.tachiyomi.util.system.contextCompatColor import eu.kanade.tachiyomi.util.system.contextCompatDrawable import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.e @@ -138,6 +139,7 @@ import kotlin.math.min import kotlin.math.roundToLong import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext @@ -458,8 +460,14 @@ open class MainActivity : BaseActivity() { } } - downloadManager.isDownloaderRunning.onEach(::downloadStatusChanged).launchIn(lifecycleScope) - lifecycleScope + combine( + downloadManager.isDownloaderRunning, + downloadManager.queueState, + ) { isDownloading, queueState -> + isDownloading to queueState.size + }.onEach { (isDownloading, queueSize) -> + downloadStatusChanged(isDownloading, queueSize) + }.launchIn(lifecycleScope) WindowCompat.setDecorFitsSystemWindows(window, false) setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayShowCustomEnabled(true) @@ -1503,17 +1511,17 @@ open class MainActivity : BaseActivity() { } } - fun BadgeDrawable.updateQueueSize(queueSize: Int) { + private fun BadgeDrawable.updateQueueSize(queueSize: Int) { number = queueSize } - fun downloadStatusChanged(downloading: Boolean) { + private fun downloadStatusChanged(downloading: Boolean, queueSize: Int) { lifecycleScope.launchUI { val hasQueue = downloading || downloadManager.hasQueue() if (hasQueue) { val badge = nav.getOrCreateBadge(R.id.nav_recents) - badge.updateQueueSize(downloadManager.queueState.value.size) - if (downloading) badge.backgroundColor = -870219 else badge.backgroundColor = Color.GRAY + badge.updateQueueSize(queueSize) + badge.backgroundColor = if (downloading) contextCompatColor(R.attr.colorError) else Color.GRAY showDLQueueTutorial() } else { nav.removeBadge(R.id.nav_recents) From f240fe0dd49f94b8c6b6887449a1459bd9ee62ee Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 21 Dec 2024 06:13:54 +0700 Subject: [PATCH 163/166] fix: Don't use .hasQueue(), check queueSize directly --- app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index d239eef202..0e66ef8010 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -1517,7 +1517,7 @@ open class MainActivity : BaseActivity() { private fun downloadStatusChanged(downloading: Boolean, queueSize: Int) { lifecycleScope.launchUI { - val hasQueue = downloading || downloadManager.hasQueue() + val hasQueue = downloading || queueSize > 0 if (hasQueue) { val badge = nav.getOrCreateBadge(R.id.nav_recents) badge.updateQueueSize(queueSize) From fe666b614f2a59b7870cb34f603bf8280bfd4ddd Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 21 Dec 2024 06:26:40 +0700 Subject: [PATCH 164/166] docs: Sync changelog Also reformat the code slightly --- CHANGELOG.md | 1 + .../main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e3978ef4a..cec05110b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Fix sorting by latest chapter is not working properly - Prevent some NPE crashes - Fix some flickering issues when browsing sources +- Fix download count is not updating ### Other - Update NDK to v27.2.12479018 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 0e66ef8010..141c78406b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -463,11 +463,9 @@ open class MainActivity : BaseActivity() { combine( downloadManager.isDownloaderRunning, downloadManager.queueState, - ) { isDownloading, queueState -> - isDownloading to queueState.size - }.onEach { (isDownloading, queueSize) -> - downloadStatusChanged(isDownloading, queueSize) - }.launchIn(lifecycleScope) + ) { isDownloading, queueState -> isDownloading to queueState.size } + .onEach { downloadStatusChanged(it.first, it.second) } + .launchIn(lifecycleScope) WindowCompat.setDecorFitsSystemWindows(window, false) setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayShowCustomEnabled(true) From 33a84f7e3946c26f3828cdde9755de9d65f1be43 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 21 Dec 2024 11:51:43 +0700 Subject: [PATCH 165/166] fix: Dismiss failed download notification before retrying --- .../tachiyomi/data/notification/NotificationReceiver.kt | 5 +++++ .../eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index c5a59b07eb..649ead87a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -610,6 +610,11 @@ class NotificationReceiver : BroadcastReceiver() { ) } + internal fun dismissFailThenStartAppUpdatePendingJob(context: Context, url: String, notifyOnInstall: Boolean = false): PendingIntent { + dismissNotification(context, Notifications.ID_UPDATER_FAILED) + return startAppUpdatePendingJob(context, url, notifyOnInstall) + } + /** * Returns [PendingIntent] that cancels the download for a Tachiyomi update * diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt index 9cc2042f15..9c00ed71cd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt @@ -233,7 +233,7 @@ internal class AppUpdateNotifier(private val context: Context) { addAction( R.drawable.ic_refresh_24dp, context.getString(MR.strings.retry), - NotificationReceiver.startAppUpdatePendingJob(context, url), + NotificationReceiver.dismissFailThenStartAppUpdatePendingJob(context, url), ) // Cancel action addAction( From bfbbd1b4f35e5699b22e759f63aa81a34ef238f5 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Sat, 21 Dec 2024 12:00:55 +0700 Subject: [PATCH 166/166] fix: Wrong function --- app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 141c78406b..570db5e7ac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -102,7 +102,6 @@ import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import eu.kanade.tachiyomi.util.manga.MangaShortcutManager import eu.kanade.tachiyomi.util.showNotificationPermissionPrompt -import eu.kanade.tachiyomi.util.system.contextCompatColor import eu.kanade.tachiyomi.util.system.contextCompatDrawable import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.e @@ -1519,7 +1518,7 @@ open class MainActivity : BaseActivity() { if (hasQueue) { val badge = nav.getOrCreateBadge(R.id.nav_recents) badge.updateQueueSize(queueSize) - badge.backgroundColor = if (downloading) contextCompatColor(R.attr.colorError) else Color.GRAY + badge.backgroundColor = if (downloading) getResourceColor(R.attr.colorError) else Color.GRAY showDLQueueTutorial() } else { nav.removeBadge(R.id.nav_recents)