Browse Source

Add "Play" button on manga in library (#8218)

* resume manga button in libarary

* work on resume button

* Backup

* work on opening the last read chapter

* backup

* renaming

* fab instead of image

* done with logic

* cleanup

* cleanup

* import cleanup

* cleanup...

* refactoring

* fixing logic

* fixing scopes

* Reworking design

* adding ability to turn on/off the feature

* cleanup

* refactoring, fixing logic, adding filter logic (partial)

* backup

* backup

* logic done

* backup before merge fix

* merge conflict....

* merge conflict...

* reworking ui logic

* removing unnecessary file

* refactoring

* refactoring

* review changes + minor parameter position movement

* commiting suggestion

Co-authored-by: arkon <[email protected]>

* fixing minor mistake

* moving ChapterFilter.kt

Co-authored-by: arkon <[email protected]>
d-najd 2 years ago
parent
commit
ba00d9e5d2

+ 82 - 0
app/src/main/java/eu/kanade/domain/chapter/model/ChapterFilter.kt

@@ -0,0 +1,82 @@
+package eu.kanade.domain.chapter.model
+
+import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.manga.model.TriStateFilter
+import eu.kanade.domain.manga.model.isLocal
+import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.data.download.model.Download
+import eu.kanade.tachiyomi.ui.manga.ChapterItem
+import eu.kanade.tachiyomi.util.chapter.getChapterSort
+
+/**
+ * Applies the view filters to the list of chapters obtained from the database.
+ * @return an observable of the list of chapters filtered and sorted.
+ */
+fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager): List<Chapter> {
+    val isLocalManga = manga.isLocal()
+    val unreadFilter = manga.unreadFilter
+    val downloadedFilter = manga.downloadedFilter
+    val bookmarkedFilter = manga.bookmarkedFilter
+
+    return filter { chapter ->
+        when (unreadFilter) {
+            TriStateFilter.DISABLED -> true
+            TriStateFilter.ENABLED_IS -> !chapter.read
+            TriStateFilter.ENABLED_NOT -> chapter.read
+        }
+    }
+        .filter { chapter ->
+            when (bookmarkedFilter) {
+                TriStateFilter.DISABLED -> true
+                TriStateFilter.ENABLED_IS -> chapter.bookmark
+                TriStateFilter.ENABLED_NOT -> !chapter.bookmark
+            }
+        }
+        .filter { chapter ->
+            val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
+            val downloadState = when {
+                downloaded -> Download.State.DOWNLOADED
+                else -> Download.State.NOT_DOWNLOADED
+            }
+            when (downloadedFilter) {
+                TriStateFilter.DISABLED -> true
+                TriStateFilter.ENABLED_IS -> downloadState == Download.State.DOWNLOADED || isLocalManga
+                TriStateFilter.ENABLED_NOT -> downloadState != Download.State.DOWNLOADED && !isLocalManga
+            }
+        }
+        .sortedWith(getChapterSort(manga))
+}
+
+/**
+ * Applies the view filters to the list of chapters obtained from the database.
+ * @return an observable of the list of chapters filtered and sorted.
+ */
+fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> {
+    val isLocalManga = manga.isLocal()
+    val unreadFilter = manga.unreadFilter
+    val downloadedFilter = manga.downloadedFilter
+    val bookmarkedFilter = manga.bookmarkedFilter
+    return asSequence()
+        .filter { (chapter) ->
+            when (unreadFilter) {
+                TriStateFilter.DISABLED -> true
+                TriStateFilter.ENABLED_IS -> !chapter.read
+                TriStateFilter.ENABLED_NOT -> chapter.read
+            }
+        }
+        .filter { (chapter) ->
+            when (bookmarkedFilter) {
+                TriStateFilter.DISABLED -> true
+                TriStateFilter.ENABLED_IS -> chapter.bookmark
+                TriStateFilter.ENABLED_NOT -> !chapter.bookmark
+            }
+        }
+        .filter {
+            when (downloadedFilter) {
+                TriStateFilter.DISABLED -> true
+                TriStateFilter.ENABLED_IS -> it.isDownloaded || isLocalManga
+                TriStateFilter.ENABLED_NOT -> !it.isDownloaded && !isLocalManga
+            }
+        }
+        .sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) }
+}

+ 2 - 0
app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt

@@ -32,6 +32,8 @@ class LibraryPreferences(
 
     fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
 
+    fun showContinueReadingButton() = preferenceStore.getBoolean("display_continue_reading_button", false)
+
     // region Filter
 
     fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)

+ 65 - 3
app/src/main/java/eu/kanade/presentation/components/CommonMangaItem.kt

@@ -12,10 +12,17 @@ import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.material3.FilledIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
 import androidx.compose.material3.LocalContentColor
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
+import androidx.compose.material3.contentColorFor
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Alignment
@@ -41,6 +48,7 @@ object CommonMangaItemDefaults {
     const val BrowseFavoriteCoverAlpha = 0.34f
 }
 
+private val ContinueReadingButtonSize = 38.dp
 private const val GridSelectedCoverAlpha = 0.76f
 
 /**
@@ -55,8 +63,10 @@ fun MangaCompactGridItem(
     coverAlpha: Float = 1f,
     coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
     coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
+    showContinueReadingButton: Boolean = false,
     onLongClick: () -> Unit,
     onClick: () -> Unit,
+    onClickContinueReading: (() -> Unit)? = null,
 ) {
     GridItemSelectable(
         isSelected = isSelected,
@@ -76,7 +86,12 @@ fun MangaCompactGridItem(
             badgesEnd = coverBadgeEnd,
             content = {
                 if (title != null) {
-                    CoverTextOverlay(title = title)
+                    CoverTextOverlay(title = title, showContinueReadingButton)
+                }
+            },
+            continueReadingButton = {
+                if (showContinueReadingButton && onClickContinueReading != null) {
+                    ContinueReadingButton(onClickContinueReading)
                 }
             },
         )
@@ -87,7 +102,10 @@ fun MangaCompactGridItem(
  * Title overlay for [MangaCompactGridItem]
  */
 @Composable
-private fun BoxScope.CoverTextOverlay(title: String) {
+private fun BoxScope.CoverTextOverlay(
+    title: String,
+    showContinueReadingButton: Boolean = false,
+) {
     Box(
         modifier = Modifier
             .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
@@ -101,9 +119,10 @@ private fun BoxScope.CoverTextOverlay(title: String) {
             .fillMaxWidth()
             .align(Alignment.BottomCenter),
     )
+    val endPadding = if (showContinueReadingButton) ContinueReadingButtonSize else 8.dp
     GridItemTitle(
         modifier = Modifier
-            .padding(8.dp)
+            .padding(start = 8.dp, top = 8.dp, end = endPadding, bottom = 8.dp)
             .align(Alignment.BottomStart),
         title = title,
         style = MaterialTheme.typography.titleSmall.copy(
@@ -127,8 +146,10 @@ fun MangaComfortableGridItem(
     coverAlpha: Float = 1f,
     coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
     coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
+    showContinueReadingButton: Boolean = false,
     onLongClick: () -> Unit,
     onClick: () -> Unit,
+    onClickContinueReading: (() -> Unit)? = null,
 ) {
     GridItemSelectable(
         isSelected = isSelected,
@@ -147,6 +168,11 @@ fun MangaComfortableGridItem(
                 },
                 badgesStart = coverBadgeStart,
                 badgesEnd = coverBadgeEnd,
+                continueReadingButton = {
+                    if (showContinueReadingButton && onClickContinueReading != null) {
+                        ContinueReadingButton(onClickContinueReading)
+                    }
+                },
             )
             GridItemTitle(
                 modifier = Modifier.padding(4.dp),
@@ -166,6 +192,7 @@ private fun MangaGridCover(
     cover: @Composable BoxScope.() -> Unit = {},
     badgesStart: (@Composable RowScope.() -> Unit)? = null,
     badgesEnd: (@Composable RowScope.() -> Unit)? = null,
+    continueReadingButton: (@Composable BoxScope.() -> Unit)? = null,
     content: @Composable (BoxScope.() -> Unit)? = null,
 ) {
     Box(
@@ -192,6 +219,7 @@ private fun MangaGridCover(
                 content = badgesEnd,
             )
         }
+        continueReadingButton?.invoke(this)
     }
 }
 
@@ -283,8 +311,10 @@ fun MangaListItem(
     coverData: eu.kanade.domain.manga.model.MangaCover,
     coverAlpha: Float = 1f,
     badge: @Composable RowScope.() -> Unit,
+    showContinueReadingButton: Boolean = false,
     onLongClick: () -> Unit,
     onClick: () -> Unit,
+    onClickContinueReading: (() -> Unit)? = null,
 ) {
     Row(
         modifier = Modifier
@@ -313,5 +343,37 @@ fun MangaListItem(
             style = MaterialTheme.typography.bodyMedium,
         )
         BadgeGroup(content = badge)
+        if (showContinueReadingButton && onClickContinueReading != null) {
+            Box {
+                ContinueReadingButton(onClickContinueReading)
+            }
+        }
+    }
+}
+
+@Composable
+private fun BoxScope.ContinueReadingButton(
+    onClickContinueReading: () -> Unit,
+) {
+    FilledIconButton(
+        onClick = {
+            onClickContinueReading()
+        },
+        modifier = Modifier
+            .size(ContinueReadingButtonSize)
+            .padding(3.dp)
+            .align(Alignment.BottomEnd),
+        shape = MaterialTheme.shapes.small,
+        colors = IconButtonDefaults.filledIconButtonColors(
+            containerColor = MaterialTheme.colorScheme.primaryContainer,
+            contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer),
+        ),
+    ) {
+        Icon(
+            imageVector = Icons.Filled.PlayArrow,
+            contentDescription = "",
+            modifier = Modifier
+                .size(15.dp),
+        )
     }
 }

+ 4 - 0
app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt

@@ -11,6 +11,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.util.fastAll
 import eu.kanade.domain.category.model.Category
+import eu.kanade.domain.library.model.LibraryManga
 import eu.kanade.domain.library.model.display
 import eu.kanade.domain.manga.model.isLocal
 import eu.kanade.presentation.components.EmptyScreen
@@ -29,6 +30,7 @@ import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
 fun LibraryScreen(
     presenter: LibraryPresenter,
     onMangaClicked: (Long) -> Unit,
+    onContinueReadingClicked: (LibraryManga) -> Unit,
     onGlobalSearchClicked: () -> Unit,
     onChangeCategoryClicked: () -> Unit,
     onMarkAsReadClicked: () -> Unit,
@@ -104,6 +106,7 @@ fun LibraryScreen(
             showMangaCount = presenter.mangaCountVisibility,
             onChangeCurrentPage = { presenter.activeCategory = it },
             onMangaClicked = onMangaClicked,
+            onContinueReadingClicked = onContinueReadingClicked,
             onToggleSelection = { presenter.toggleSelection(it) },
             onToggleRangeSelection = {
                 presenter.toggleRangeSelection(it)
@@ -119,6 +122,7 @@ fun LibraryScreen(
             showUnreadBadges = presenter.showUnreadBadges,
             showLocalBadges = presenter.showLocalBadges,
             showLanguageBadges = presenter.showLanguageBadges,
+            showContinueReadingButton = presenter.showContinueReadingButton,
             isIncognitoMode = presenter.isIncognitoMode,
             isDownloadOnly = presenter.isDownloadOnly,
         )

+ 4 - 0
app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt

@@ -18,11 +18,13 @@ fun LibraryComfortableGrid(
     showUnreadBadges: Boolean,
     showLocalBadges: Boolean,
     showLanguageBadges: Boolean,
+    showContinueReadingButton: Boolean,
     columns: Int,
     contentPadding: PaddingValues,
     selection: List<LibraryManga>,
     onClick: (LibraryManga) -> Unit,
     onLongClick: (LibraryManga) -> Unit,
+    onClickContinueReading: (LibraryManga) -> Unit,
     searchQuery: String?,
     onGlobalSearchClicked: () -> Unit,
 ) {
@@ -65,8 +67,10 @@ fun LibraryComfortableGrid(
                         item = libraryItem,
                     )
                 },
+                showContinueReadingButton = showContinueReadingButton,
                 onLongClick = { onLongClick(libraryItem.libraryManga) },
                 onClick = { onClick(libraryItem.libraryManga) },
+                onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) },
             )
         }
     }

+ 4 - 0
app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt

@@ -19,11 +19,13 @@ fun LibraryCompactGrid(
     showUnreadBadges: Boolean,
     showLocalBadges: Boolean,
     showLanguageBadges: Boolean,
+    showContinueReadingButton: Boolean,
     columns: Int,
     contentPadding: PaddingValues,
     selection: List<LibraryManga>,
     onClick: (LibraryManga) -> Unit,
     onLongClick: (LibraryManga) -> Unit,
+    onClickContinueReading: (LibraryManga) -> Unit,
     searchQuery: String?,
     onGlobalSearchClicked: () -> Unit,
 ) {
@@ -66,8 +68,10 @@ fun LibraryCompactGrid(
                         item = libraryItem,
                     )
                 },
+                showContinueReadingButton = showContinueReadingButton,
                 onLongClick = { onLongClick(libraryItem.libraryManga) },
                 onClick = { onClick(libraryItem.libraryManga) },
+                onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) },
             )
         }
     }

+ 7 - 0
app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt

@@ -37,6 +37,7 @@ fun LibraryContent(
     showMangaCount: Boolean,
     onChangeCurrentPage: (Int) -> Unit,
     onMangaClicked: (Long) -> Unit,
+    onContinueReadingClicked: (LibraryManga) -> Unit,
     onToggleSelection: (LibraryManga) -> Unit,
     onToggleRangeSelection: (LibraryManga) -> Unit,
     onRefresh: (Category?) -> Boolean,
@@ -49,6 +50,7 @@ fun LibraryContent(
     showUnreadBadges: Boolean,
     showLocalBadges: Boolean,
     showLanguageBadges: Boolean,
+    showContinueReadingButton: Boolean,
     isDownloadOnly: Boolean,
     isIncognitoMode: Boolean,
 ) {
@@ -88,6 +90,9 @@ fun LibraryContent(
         val onLongClickManga = { manga: LibraryManga ->
             onToggleRangeSelection(manga)
         }
+        val onClickContinueReading = { manga: LibraryManga ->
+            onContinueReadingClicked(manga)
+        }
 
         SwipeRefresh(
             refreshing = isRefreshing,
@@ -115,8 +120,10 @@ fun LibraryContent(
                 showUnreadBadges = showUnreadBadges,
                 showLocalBadges = showLocalBadges,
                 showLanguageBadges = showLanguageBadges,
+                showContinueReadingButton = showContinueReadingButton,
                 onClickManga = onClickManga,
                 onLongClickManga = onLongClickManga,
+                onClickContinueReading = onClickContinueReading,
                 onGlobalSearchClicked = onGlobalSearchClicked,
                 searchQuery = state.searchQuery,
             )

+ 4 - 0
app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt

@@ -27,10 +27,12 @@ fun LibraryList(
     showUnreadBadges: Boolean,
     showLocalBadges: Boolean,
     showLanguageBadges: Boolean,
+    showContinueReadingButton: Boolean,
     contentPadding: PaddingValues,
     selection: List<LibraryManga>,
     onClick: (LibraryManga) -> Unit,
     onLongClick: (LibraryManga) -> Unit,
+    onClickContinueReading: (LibraryManga) -> Unit,
     searchQuery: String?,
     onGlobalSearchClicked: () -> Unit,
 ) {
@@ -72,8 +74,10 @@ fun LibraryList(
                     UnreadBadge(enabled = showUnreadBadges, item = libraryItem)
                     LanguageBadge(showLanguage = showLanguageBadges, showLocal = showLocalBadges, item = libraryItem)
                 },
+                showContinueReadingButton = showContinueReadingButton,
                 onLongClick = { onLongClick(libraryItem.libraryManga) },
                 onClick = { onClick(libraryItem.libraryManga) },
+                onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) },
             )
         }
     }

+ 8 - 0
app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt

@@ -32,8 +32,10 @@ fun LibraryPager(
     showUnreadBadges: Boolean,
     showLocalBadges: Boolean,
     showLanguageBadges: Boolean,
+    showContinueReadingButton: Boolean,
     onClickManga: (LibraryManga) -> Unit,
     onLongClickManga: (LibraryManga) -> Unit,
+    onClickContinueReading: (LibraryManga) -> Unit,
 ) {
     HorizontalPager(
         count = pageCount,
@@ -64,10 +66,12 @@ fun LibraryPager(
                     showUnreadBadges = showUnreadBadges,
                     showLocalBadges = showLocalBadges,
                     showLanguageBadges = showLanguageBadges,
+                    showContinueReadingButton = showContinueReadingButton,
                     contentPadding = contentPadding,
                     selection = selectedManga,
                     onClick = onClickManga,
                     onLongClick = onLongClickManga,
+                    onClickContinueReading = onClickContinueReading,
                     searchQuery = searchQuery,
                     onGlobalSearchClicked = onGlobalSearchClicked,
                 )
@@ -80,11 +84,13 @@ fun LibraryPager(
                     showUnreadBadges = showUnreadBadges,
                     showLocalBadges = showLocalBadges,
                     showLanguageBadges = showLanguageBadges,
+                    showContinueReadingButton = showContinueReadingButton,
                     columns = columns,
                     contentPadding = contentPadding,
                     selection = selectedManga,
                     onClick = onClickManga,
                     onLongClick = onLongClickManga,
+                    onClickContinueReading = onClickContinueReading,
                     searchQuery = searchQuery,
                     onGlobalSearchClicked = onGlobalSearchClicked,
                 )
@@ -96,10 +102,12 @@ fun LibraryPager(
                     showUnreadBadges = showUnreadBadges,
                     showLocalBadges = showLocalBadges,
                     showLanguageBadges = showLanguageBadges,
+                    showContinueReadingButton = showContinueReadingButton,
                     columns = columns,
                     contentPadding = contentPadding,
                     selection = selectedManga,
                     onClick = onClickManga,
+                    onClickContinueReading = onClickContinueReading,
                     onLongClick = onLongClickManga,
                     searchQuery = searchQuery,
                     onGlobalSearchClicked = onGlobalSearchClicked,

+ 17 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -9,6 +9,8 @@ import androidx.compose.ui.platform.LocalContext
 import com.bluelinelabs.conductor.ControllerChangeHandler
 import com.bluelinelabs.conductor.ControllerChangeType
 import eu.kanade.core.prefs.CheckboxState
+import eu.kanade.domain.chapter.model.Chapter
+import eu.kanade.domain.library.model.LibraryManga
 import eu.kanade.domain.manga.model.Manga
 import eu.kanade.domain.manga.model.isLocal
 import eu.kanade.domain.manga.model.toDbManga
@@ -26,6 +28,7 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 import eu.kanade.tachiyomi.ui.category.CategoryController
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.launchUI
 import eu.kanade.tachiyomi.util.system.toast
@@ -50,6 +53,7 @@ class LibraryController(
         LibraryScreen(
             presenter = presenter,
             onMangaClicked = ::openManga,
+            onContinueReadingClicked = ::continueReading,
             onGlobalSearchClicked = {
                 router.pushController(GlobalSearchController(presenter.searchQuery))
             },
@@ -196,6 +200,19 @@ class LibraryController(
         router.pushController(MangaController(mangaId))
     }
 
+    private fun continueReading(libraryManga: LibraryManga) {
+        viewScope.launchIO {
+            val chapter = presenter.getNextUnreadChapter(libraryManga.manga)
+            if (chapter != null) openChapter(chapter)
+        }
+    }
+
+    private fun openChapter(chapter: Chapter) {
+        activity?.run {
+            startActivity(ReaderActivity.newIntent(this, chapter.mangaId, chapter.id))
+        }
+    }
+
     /**
      * Clear all of the manga currently selected, and
      * invalidate the action mode to revert the top toolbar

+ 11 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt

@@ -17,7 +17,9 @@ import eu.kanade.domain.base.BasePreferences
 import eu.kanade.domain.category.interactor.GetCategories
 import eu.kanade.domain.category.interactor.SetMangaCategories
 import eu.kanade.domain.category.model.Category
+import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
 import eu.kanade.domain.chapter.interactor.SetReadStatus
+import eu.kanade.domain.chapter.model.Chapter
 import eu.kanade.domain.chapter.model.toDbChapter
 import eu.kanade.domain.history.interactor.GetNextChapters
 import eu.kanade.domain.library.model.LibraryManga
@@ -44,6 +46,7 @@ import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.source.model.SManga
 import eu.kanade.tachiyomi.source.online.HttpSource
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.util.chapter.getNextUnread
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.launchNonCancellable
 import eu.kanade.tachiyomi.util.lang.withIOContext
@@ -76,9 +79,10 @@ typealias LibraryMap = Map<Long, List<LibraryItem>>
 class LibraryPresenter(
     private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl,
     private val getLibraryManga: GetLibraryManga = Injekt.get(),
-    private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
     private val getCategories: GetCategories = Injekt.get(),
+    private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
     private val getNextChapters: GetNextChapters = Injekt.get(),
+    private val getChaptersByMangaId: GetChapterByMangaId = Injekt.get(),
     private val setReadStatus: SetReadStatus = Injekt.get(),
     private val updateManga: UpdateManga = Injekt.get(),
     private val setMangaCategories: SetMangaCategories = Injekt.get(),
@@ -105,6 +109,8 @@ class LibraryPresenter(
 
     var activeCategory: Int by libraryPreferences.lastUsedCategory().asState()
 
+    val showContinueReadingButton by libraryPreferences.showContinueReadingButton().asState()
+
     val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
     val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
 
@@ -389,6 +395,10 @@ class LibraryPresenter(
             .reduce { set1, set2 -> set1.intersect(set2) }
     }
 
+    suspend fun getNextUnreadChapter(manga: Manga): Chapter? {
+        return getChaptersByMangaId.await(manga.id).getNextUnread(manga, downloadManager)
+    }
+
     /**
      * Returns the mix (non-common) categories for the given list of manga.
      *

+ 25 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt

@@ -282,12 +282,14 @@ class LibrarySettingsSheet(
         private val displayGroup: DisplayGroup
         private val badgeGroup: BadgeGroup
         private val tabsGroup: TabsGroup
+        private val otherGroup: OtherGroup
 
         init {
             displayGroup = DisplayGroup()
             badgeGroup = BadgeGroup()
             tabsGroup = TabsGroup()
-            setGroups(listOf(displayGroup, badgeGroup, tabsGroup))
+            otherGroup = OtherGroup()
+            setGroups(listOf(displayGroup, badgeGroup, tabsGroup, otherGroup))
         }
 
         // Refreshes Display Setting selections
@@ -408,6 +410,28 @@ class LibrarySettingsSheet(
                 adapter.notifyItemChanged(item)
             }
         }
+
+        inner class OtherGroup : Group {
+            private val showContinueReadingButton = Item.CheckboxGroup(R.string.action_display_show_continue_reading_button, this)
+
+            override val header = Item.Header(R.string.other_header)
+            override val items = listOf(showContinueReadingButton)
+            override val footer = null
+
+            override fun initModels() {
+                showContinueReadingButton.checked = libraryPreferences.showContinueReadingButton().get()
+            }
+
+            override fun onItemClicked(item: Item) {
+                item as Item.CheckboxGroup
+                item.checked = !item.checked
+                when (item) {
+                    showContinueReadingButton -> libraryPreferences.showContinueReadingButton().set(item.checked)
+                    else -> {}
+                }
+                adapter.notifyItemChanged(item)
+            }
+        }
     }
 
     open inner class Settings(context: Context, attrs: AttributeSet?) :

+ 3 - 43
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt

@@ -16,6 +16,7 @@ import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
 import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
 import eu.kanade.domain.chapter.interactor.UpdateChapter
 import eu.kanade.domain.chapter.model.ChapterUpdate
+import eu.kanade.domain.chapter.model.applyFilters
 import eu.kanade.domain.chapter.model.toDbChapter
 import eu.kanade.domain.download.service.DownloadPreferences
 import eu.kanade.domain.library.service.LibraryPreferences
@@ -23,8 +24,6 @@ import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
 import eu.kanade.domain.manga.interactor.GetMangaWithChapters
 import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
 import eu.kanade.domain.manga.interactor.UpdateManga
-import eu.kanade.domain.manga.model.TriStateFilter
-import eu.kanade.domain.manga.model.isLocal
 import eu.kanade.domain.manga.model.toDbManga
 import eu.kanade.domain.track.interactor.DeleteTrack
 import eu.kanade.domain.track.interactor.GetTracks
@@ -45,6 +44,7 @@ import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import eu.kanade.tachiyomi.ui.manga.track.TrackItem
 import eu.kanade.tachiyomi.util.chapter.getChapterSort
+import eu.kanade.tachiyomi.util.chapter.getNextUnread
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.launchNonCancellable
 import eu.kanade.tachiyomi.util.lang.toRelativeString
@@ -579,13 +579,7 @@ class MangaPresenter(
      */
     fun getNextUnreadChapter(): DomainChapter? {
         val successState = successState ?: return null
-        return successState.processedChapters.map { it.chapter }.let { chapters ->
-            if (successState.manga.sortDescending()) {
-                chapters.findLast { !it.read }
-            } else {
-                chapters.find { !it.read }
-            }
-        }
+        return successState.chapters.getNextUnread(successState.manga)
     }
 
     fun getUnreadChapters(): List<DomainChapter> {
@@ -1092,40 +1086,6 @@ sealed class MangaScreenState {
 
         val processedChapters: Sequence<ChapterItem>
             get() = chapters.applyFilters(manga)
-
-        /**
-         * Applies the view filters to the list of chapters obtained from the database.
-         * @return an observable of the list of chapters filtered and sorted.
-         */
-        private fun List<ChapterItem>.applyFilters(manga: DomainManga): Sequence<ChapterItem> {
-            val isLocalManga = manga.isLocal()
-            val unreadFilter = manga.unreadFilter
-            val downloadedFilter = manga.downloadedFilter
-            val bookmarkedFilter = manga.bookmarkedFilter
-            return asSequence()
-                .filter { (chapter) ->
-                    when (unreadFilter) {
-                        TriStateFilter.DISABLED -> true
-                        TriStateFilter.ENABLED_IS -> !chapter.read
-                        TriStateFilter.ENABLED_NOT -> chapter.read
-                    }
-                }
-                .filter { (chapter) ->
-                    when (bookmarkedFilter) {
-                        TriStateFilter.DISABLED -> true
-                        TriStateFilter.ENABLED_IS -> chapter.bookmark
-                        TriStateFilter.ENABLED_NOT -> !chapter.bookmark
-                    }
-                }
-                .filter {
-                    when (downloadedFilter) {
-                        TriStateFilter.DISABLED -> true
-                        TriStateFilter.ENABLED_IS -> it.isDownloaded || isLocalManga
-                        TriStateFilter.ENABLED_NOT -> !it.isDownloaded && !isLocalManga
-                    }
-                }
-                .sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) }
-        }
     }
 }
 

+ 33 - 0
app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterGetNextUnread.kt

@@ -0,0 +1,33 @@
+package eu.kanade.tachiyomi.util.chapter
+
+import eu.kanade.domain.chapter.model.Chapter
+import eu.kanade.domain.chapter.model.applyFilters
+import eu.kanade.domain.manga.model.Manga
+import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.ui.manga.ChapterItem
+
+/**
+ * Gets next unread chapter with filters and sorting applied
+ */
+fun List<Chapter>.getNextUnread(manga: Manga, downloadManager: DownloadManager): Chapter? {
+    return applyFilters(manga, downloadManager).let { chapters ->
+        if (manga.sortDescending()) {
+            chapters.findLast { !it.read }
+        } else {
+            chapters.find { !it.read }
+        }
+    }
+}
+
+/**
+ * Gets next unread chapter with filters and sorting applied
+ */
+fun List<ChapterItem>.getNextUnread(manga: Manga): Chapter? {
+    return applyFilters(manga).let { chapters ->
+        if (manga.sortDescending()) {
+            chapters.findLast { !it.chapter.read }
+        } else {
+            chapters.find { !it.chapter.read }
+        }
+    }?.chapter
+}

+ 2 - 0
i18n/src/main/res/values/strings.xml

@@ -108,6 +108,7 @@
     <string name="action_display_language_badge">Language</string>
     <string name="action_display_show_tabs">Show category tabs</string>
     <string name="action_display_show_number_of_items">Show number of items</string>
+    <string name="action_display_show_continue_reading_button">Show continue reading button</string>
     <string name="action_disable">Disable</string>
     <string name="action_pin">Pin</string>
     <string name="action_unpin">Unpin</string>
@@ -579,6 +580,7 @@
     <string name="downloaded_chapters">Downloaded chapters</string>
     <string name="badges_header">Badges</string>
     <string name="tabs_header">Tabs</string>
+    <string name="other_header">Other</string>
 
     <!-- Catalogue fragment -->
     <!-- missing prompt after Compose rewrite #7901 -->