mirror of
https://github.com/null2264/yokai.git
synced 2025-06-20 10:14:50 +00:00
feat: Mark duplicate read chapters as read
This also refactor how chapters progress are saved. Chapters' progress now save when user "flipped" the page. Closes GH-409 Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
parent
d3050d5799
commit
850151720b
7 changed files with 134 additions and 81 deletions
|
@ -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
|
||||
|
|
|
@ -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<ReaderActivityBinding>() {
|
|||
}
|
||||
}
|
||||
}
|
||||
viewModel.onSaveInstanceState()
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
|
@ -1304,13 +1304,13 @@ class ReaderActivity : BaseActivity<ReaderActivityBinding>() {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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<ReaderChapterItem>()
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Chapter>, List<Chapter>> {
|
||||
if (rawSourceChapters.isEmpty()) {
|
||||
throw Exception("No chapters found")
|
||||
|
@ -122,11 +124,18 @@ suspend fun syncChaptersWithSource(
|
|||
return Pair(emptyList(), emptyList())
|
||||
}
|
||||
|
||||
val reAdded = mutableListOf<Chapter>()
|
||||
val changedOrDuplicateReadUrls = mutableSetOf<String>()
|
||||
|
||||
val deletedChapterNumbers = TreeSet<Float>()
|
||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
||||
|
||||
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 },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,6 +167,10 @@
|
|||
<string name="ungrouped">Ungrouped</string>
|
||||
|
||||
<string name="pref_use_compose_library">Use experimental compose library</string>
|
||||
<string name="pref_behavior">Behavior</string>
|
||||
<string name="pref_mark_as_read_duplicate_read_chapter">Mark duplicate read chapter as read</string>
|
||||
<string name="pref_mark_as_read_duplicate_read_chapter_existing">After reading a chapter</string>
|
||||
<string name="pref_mark_as_read_duplicate_read_chapter_new">After fetching new chapter</string>
|
||||
|
||||
<!-- Library Sort -->
|
||||
<string name="sort_by">Sort by</string>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue