diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bdc76d109..105ff36b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,12 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co - Add random library sort - Add the ability to save search queries - Add toggle to enable/disable hide source on swipe (@Hiirbaf) +- Add the ability to mark duplicate read chapters as read (@AntsyLich) ### Changes - Temporarily disable log file - Categories' header now show filtered count when you search the library when you have "Show number of items" enabled (@LeeSF03) +- Chapter progress now saved everything the page is changed ### Fixes - Allow users to bypass onboarding's permission step if Shizuku is installed 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 da7f99e2b8..c0c9694147 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 @@ -148,6 +148,14 @@ import eu.kanade.tachiyomi.util.view.setMessage import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.widget.doOnEnd import eu.kanade.tachiyomi.widget.doOnStart +import java.io.ByteArrayOutputStream +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.Collections +import java.util.Locale +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -167,13 +175,6 @@ import yokai.domain.ui.settings.ReaderPreferences import yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour import yokai.i18n.MR import yokai.util.lang.getString -import java.io.ByteArrayOutputStream -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols -import java.util.* -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.roundToInt import android.R as AR /** @@ -512,7 +513,6 @@ class ReaderActivity : BaseActivity() { } } } - viewModel.onSaveInstanceState() super.onSaveInstanceState(outState) } @@ -1304,13 +1304,13 @@ class ReaderActivity : BaseActivity() { } override fun onPause() { - viewModel.saveCurrentChapterReadingProgress() + viewModel.flushReadTimer() super.onPause() } override fun onResume() { super.onResume() - viewModel.setReadStartTime() + viewModel.restartReadTimer() } fun reloadChapters(doublePages: Boolean, force: Boolean = false) { 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 d2e6e31b04..960654e072 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 @@ -55,9 +55,7 @@ import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withUIContext import java.util.Date import java.util.concurrent.CancellationException -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -81,6 +79,7 @@ import yokai.domain.chapter.models.ChapterUpdate import yokai.domain.download.DownloadPreferences import yokai.domain.history.interactor.GetHistory import yokai.domain.history.interactor.UpsertHistory +import yokai.domain.library.LibraryPreferences import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.InsertManga import yokai.domain.manga.interactor.UpdateManga @@ -102,6 +101,7 @@ class ReaderViewModel( private val chapterFilter: ChapterFilter = Injekt.get(), private val storageManager: StorageManager = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(), + private val libraryPreferences: LibraryPreferences = Injekt.get(), ) : ViewModel() { private val getCategories: GetCategories by injectLazy() private val getChapter: GetChapter by injectLazy() @@ -160,8 +160,6 @@ class ReaderViewModel( private var chapterItems = emptyList() - private var scope = CoroutineScope(Job() + Dispatchers.Default) - private var hasTrackers: Boolean = false private suspend fun checkTrackers(manga: Manga) = getTrack.awaitAllByMangaId(manga.id).isNotEmpty() @@ -192,24 +190,12 @@ class ReaderViewModel( val currentChapters = state.value.viewerChapters if (currentChapters != null) { currentChapters.unref() - saveReadingProgress(currentChapters.currChapter) chapterToDownload?.let { downloadManager.addDownloadsToStartOfQueue(listOf(it)) } } } - /** - * Called when the activity is saved and not changing configurations. It updates the database - * to persist the current progress of the active chapter. - */ - fun onSaveInstanceState() { - val currentChapter = getCurrentChapter() ?: return - viewModelScope.launchNonCancellableIO { - saveChapterProgress(currentChapter) - } - } - /** * Whether this presenter is initialized yet. */ @@ -375,12 +361,15 @@ class ReaderViewModel( * Called when the user changed to the given [chapter] when changing pages from the viewer. * It's used only to set this chapter as active. */ - private suspend fun loadNewChapter(chapter: ReaderChapter) { + private fun loadNewChapter(chapter: ReaderChapter) { val loader = loader ?: return - Logger.d { "Loading ${chapter.chapter.url}" } + viewModelScope.launchIO { + Logger.d { "Loading ${chapter.chapter.url}" } + + flushReadTimer() + restartReadTimer() - withIOContext { try { loadChapter(loader, chapter) } catch (e: Throwable) { @@ -511,28 +500,15 @@ class ReaderViewModel( val selectedChapter = page.chapter // Save last page read and mark as read if needed - selectedChapter.chapter.last_page_read = page.index - selectedChapter.chapter.pages_left = - (selectedChapter.pages?.size ?: page.index) - page.index - val shouldTrack = !preferences.incognitoMode().get() || hasTrackers - if (shouldTrack && - // For double pages, check if the second to last page is doubled up - ( - (selectedChapter.pages?.lastIndex == page.index && page.firstHalf != true) || - (hasExtraPage && selectedChapter.pages?.lastIndex?.minus(1) == page.index) - ) - ) { - selectedChapter.chapter.read = true - updateTrackChapterAfterReading(selectedChapter) - deleteChapterIfNeeded(selectedChapter) + viewModelScope.launchNonCancellableIO { + saveChapterProgress(selectedChapter, page, hasExtraPage) } if (selectedChapter != currentChapters.currChapter) { Logger.d { "Setting ${selectedChapter.chapter.url} as active" } - saveReadingProgress(currentChapters.currChapter) - setReadStartTime() - scope.launch { loadNewChapter(selectedChapter) } + loadNewChapter(selectedChapter) } + val pages = page.chapter.pages ?: return val inDownloadRange = page.number.toDouble() / pages.size > 0.2 if (inDownloadRange) { @@ -620,28 +596,28 @@ class ReaderViewModel( } } - /** - * Called when reader chapter is changed in reader or when activity is paused. - */ - private fun saveReadingProgress(readerChapter: ReaderChapter) { - viewModelScope.launchNonCancellableIO { - saveChapterProgress(readerChapter) - saveChapterHistory(readerChapter) - } - } - - fun saveCurrentChapterReadingProgress() = getCurrentChapter()?.let { saveReadingProgress(it) } - /** * Saves this [readerChapter]'s progress (last read page and whether it's read). * If incognito mode isn't on or has at least 1 tracker */ - private suspend fun saveChapterProgress(readerChapter: ReaderChapter) { + private suspend fun saveChapterProgress(readerChapter: ReaderChapter, page: ReaderPage, hasExtraPage: Boolean) { readerChapter.requestedPage = readerChapter.chapter.last_page_read getChapter.awaitById(readerChapter.chapter.id!!)?.let { dbChapter -> readerChapter.chapter.bookmark = dbChapter.bookmark } - if (!preferences.incognitoMode().get() || hasTrackers) { + + val shouldTrack = !preferences.incognitoMode().get() || hasTrackers + if (shouldTrack && page.status != Page.State.ERROR) { + readerChapter.chapter.last_page_read = page.index + readerChapter.chapter.pages_left = (readerChapter.pages?.size ?: page.index) - page.index + // For double pages, check if the second to last page is doubled up + if ( + (readerChapter.pages?.lastIndex == page.index && page.firstHalf != true) || + (hasExtraPage && readerChapter.pages?.lastIndex?.minus(1) == page.index) + ) { + onChapterReadComplete(readerChapter) + } + updateChapter.await( ChapterUpdate( id = readerChapter.chapter.id!!, @@ -654,24 +630,57 @@ class ReaderViewModel( } } + private suspend fun onChapterReadComplete(readerChapter: ReaderChapter) { + readerChapter.chapter.read = true + updateTrackChapterAfterReading(readerChapter) + deleteChapterIfNeeded(readerChapter) + + val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get() + .contains(LibraryPreferences.MARK_DUPLICATE_READ_CHAPTER_READ_EXISTING) + if (!markDuplicateAsRead) return + + val duplicateUnreadChapters = chapterList + .mapNotNull { + val chapter = it.chapter + if ( + !chapter.read && + chapter.isRecognizedNumber && + chapter.chapter_number == readerChapter.chapter.chapter_number + ) { + ChapterUpdate(id = chapter.id!!, read = true) + } else { + null + } + } + updateChapter.awaitAll(duplicateUnreadChapters) + } + + fun restartReadTimer() { + chapterReadStartTime = Date().time + } + + fun flushReadTimer() { + getCurrentChapter()?.let { + viewModelScope.launchNonCancellableIO { + saveChapterHistory(it) + } + } + } + /** * Saves this [readerChapter] last read history. */ private suspend fun saveChapterHistory(readerChapter: ReaderChapter) { - if (!preferences.incognitoMode().get()) { - val readAt = Date().time - val sessionReadDuration = chapterReadStartTime?.let { readAt - it } ?: 0 - val history = History.create(readerChapter.chapter).apply { - last_read = readAt - time_read = sessionReadDuration - } - upsertHistory.await(history) - chapterReadStartTime = null - } - } + if (preferences.incognitoMode().get()) return - fun setReadStartTime() { - chapterReadStartTime = Date().time + val endTime = Date().time + val sessionReadDuration = chapterReadStartTime?.let { endTime - it } ?: 0 + val history = History.create(readerChapter.chapter).apply { + last_read = endTime + time_read = sessionReadDuration + } + upsertHistory.await(history) + chapterReadStartTime = null } /** @@ -876,7 +885,7 @@ class ReaderViewModel( } fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { - scope.launch { + viewModelScope.launch { if (firstPage.status != Page.State.READY) return@launch if (secondPage.status != Page.State.READY) return@launch val manga = manga ?: return@launch @@ -925,7 +934,7 @@ class ReaderViewModel( } fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { - scope.launch { + viewModelScope.launch { if (firstPage.status != Page.State.READY) return@launch if (secondPage.status != Page.State.READY) return@launch val manga = manga ?: return@launch diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsLibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsLibraryController.kt index d7d0977e84..e8068a9cb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsLibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsLibraryController.kt @@ -34,6 +34,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import yokai.domain.category.interactor.GetCategories +import yokai.domain.library.LibraryPreferences import yokai.domain.manga.interactor.GetLibraryManga import yokai.domain.ui.UiPreferences import yokai.i18n.MR @@ -48,6 +49,7 @@ class SettingsLibraryController : SettingsLegacyController() { private val getCategories: GetCategories by injectLazy() private val uiPreferences: UiPreferences by injectLazy() + private val libraryPreferences: LibraryPreferences by injectLazy() override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { titleRes = MR.strings.library @@ -212,12 +214,25 @@ class SettingsLibraryController : SettingsLegacyController() { } preferenceCategory { - titleRes = MR.strings.chapters + titleRes = MR.strings.pref_behavior switchPreference { bindTo(uiPreferences.enableChapterSwipeAction()) titleRes = MR.strings.enable_chapter_swipe_action } + multiSelectListPreferenceMat(activity) { + bindTo(preferences.libraryUpdateMangaRestriction()) + titleRes = MR.strings.pref_mark_as_read_duplicate_read_chapter + val entries = mapOf( + MR.strings.pref_mark_as_read_duplicate_read_chapter_existing to + LibraryPreferences.MARK_DUPLICATE_READ_CHAPTER_READ_EXISTING, + MR.strings.pref_mark_as_read_duplicate_read_chapter_new to + LibraryPreferences.MARK_DUPLICATE_READ_CHAPTER_READ_NEW, + ) + entriesRes = entries.keys.toTypedArray() + entryValues = entries.values.toList() + noSelectionRes = MR.strings.none + } } } } 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 987d76c91b..54211507c5 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 @@ -17,6 +17,7 @@ import yokai.domain.chapter.interactor.InsertChapter import yokai.domain.chapter.interactor.UpdateChapter import yokai.domain.chapter.models.ChapterUpdate import yokai.domain.chapter.services.ChapterRecognition +import yokai.domain.library.LibraryPreferences import yokai.domain.manga.interactor.UpdateManga import yokai.domain.manga.models.MangaUpdate @@ -39,6 +40,7 @@ suspend fun syncChaptersWithSource( updateChapter: UpdateChapter = Injekt.get(), updateManga: UpdateManga = Injekt.get(), handler: DatabaseHandler = Injekt.get(), + libraryPreferences: LibraryPreferences = Injekt.get(), ): Pair, List> { if (rawSourceChapters.isEmpty()) { throw Exception("No chapters found") @@ -122,11 +124,18 @@ suspend fun syncChaptersWithSource( return Pair(emptyList(), emptyList()) } - val reAdded = mutableListOf() + val changedOrDuplicateReadUrls = mutableSetOf() val deletedChapterNumbers = TreeSet() val deletedReadChapterNumbers = TreeSet() val deletedBookmarkedChapterNumbers = TreeSet() + + val readChapterNumbers = dbChapters + .asSequence() + .filter { it.read && it.isRecognizedNumber } + .map { it.chapter_number } + .toSet() + toDelete.forEach { if (it.read) deletedReadChapterNumbers.add(it.chapter_number) if (it.bookmark) deletedBookmarkedChapterNumbers.add(it.chapter_number) @@ -135,6 +144,9 @@ suspend fun syncChaptersWithSource( val now = Date().time + val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get() + .contains(LibraryPreferences.MARK_DUPLICATE_READ_CHAPTER_READ_NEW) + // 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 @@ -143,6 +155,11 @@ suspend fun syncChaptersWithSource( chapter.date_fetch = now + itemCount-- + if (chapter.chapter_number in readChapterNumbers && markDuplicateAsRead) { + changedOrDuplicateReadUrls.add(chapter.url) + chapter.read = true + } + if (!chapter.isRecognizedNumber || chapter.chapter_number !in deletedChapterNumbers) return@map chapter chapter.read = chapter.chapter_number in deletedReadChapterNumbers @@ -154,7 +171,7 @@ suspend fun syncChaptersWithSource( chapter.date_fetch = it.date_fetch } - reAdded.add(chapter) + changedOrDuplicateReadUrls.add(chapter.url) chapter } @@ -192,14 +209,13 @@ suspend fun syncChaptersWithSource( manga.last_update = Date().time updateManga.await(MangaUpdate(manga.id!!, lastUpdate = manga.last_update)) - val reAddedUrls = reAdded.map { it.url }.toHashSet() val filteredScanlators = ChapterUtil.getScanlators(manga.filtered_scanlators).toHashSet() return Pair( updatedToAdd.filterNot { - it.url in reAddedUrls || it.scanlator in filteredScanlators + it.url in changedOrDuplicateReadUrls || it.scanlator in filteredScanlators }, - toDelete.filterNot { it.url in reAddedUrls }, + toDelete.filterNot { it.url in changedOrDuplicateReadUrls }, ) } diff --git a/app/src/main/java/yokai/domain/library/LibraryPreferences.kt b/app/src/main/java/yokai/domain/library/LibraryPreferences.kt index e2dc5342f3..ab1094de0f 100644 --- a/app/src/main/java/yokai/domain/library/LibraryPreferences.kt +++ b/app/src/main/java/yokai/domain/library/LibraryPreferences.kt @@ -4,4 +4,11 @@ import eu.kanade.tachiyomi.core.preference.PreferenceStore class LibraryPreferences(private val preferenceStore: PreferenceStore) { fun randomSortSeed() = preferenceStore.getInt("library_random_sort_seed", 0) + + fun markDuplicateReadChapterAsRead() = preferenceStore.getStringSet("mark_duplicate_read_chapter_read", emptySet()) + + companion object { + const val MARK_DUPLICATE_READ_CHAPTER_READ_NEW = "new" + const val MARK_DUPLICATE_READ_CHAPTER_READ_EXISTING = "existing" + } } diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index ef4ade4837..3e52a4026a 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -167,6 +167,10 @@ Ungrouped Use experimental compose library + Behavior + Mark duplicate read chapter as read + After reading a chapter + After fetching new chapter Sort by