From 3e7dc5c41a1368b5f4050eb2295db58208a376a8 Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+majortanya@users.noreply.github.com> Date: Sat, 17 Jan 2026 11:43:40 +0100 Subject: [PATCH] Add Filters to Updates screen (mihonapp/mihon#2851) * Add Filters to Updates screen Behaves basically like the filters in the library: - Unread: Show/Don't show unread chapters - Downloaded: Show/Don't show downloaded chapters - Started: Show/Don't show chapters that have some progress but aren't fully Read - Bookmarked: Show/Don't show chapters that have been bookmarked Started behaves differently from its Library counterpart because the actual manga data is not available at this point in time and I thought calling getManga for each entry without caching would be a pretty bad idea. I have modelled this closely on the filter control flow in the Library, but I'm sure this can be simplified/adjusted in some way. * Move most filtering logic to SQL Unread, Started, and Bookmarked filters are now part of the SQL query. Download state cannot be filtered in the database so it remains in Kotlin. Because the Downloaded filter has to be run in Kotlin, the combine flow uses the preferences flow twice, once to get the SQL query params and once for the Kotlin filters (only Downloaded at this time). * Add "Hide excluded scanlators" to update filters Based on the work done in mihonapp/mihon#1623 but integrated with the other filters in this PR. Added the user as a co-author for credit. Co-authored-by: Dani <17619547+shabnix@users.noreply.github.com> --------- Co-authored-by: Dani <17619547+shabnix@users.noreply.github.com> --------- * Merge Panorama toggle into Filter diaglog * Make Panorama toggle persistent and share it with History * Implement toggleSwitch method for preference management to avoid create new function on every composition Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> (cherry picked from commit bbe9aa8561360f030072fbc49f79748e71c6535e) --- CHANGELOG.md | 1 + .../presentation/history/HistoryScreen.kt | 22 ++- ....kt => UpdatesDeleteConfirmationDialog.kt} | 0 .../updates/UpdatesFilterDialog.kt | 139 ++++++++++++++++++ .../presentation/updates/UpdatesScreen.kt | 35 ++--- .../kanade/tachiyomi/di/PreferenceModule.kt | 4 + .../kanade/tachiyomi/ui/history/HistoryTab.kt | 7 + .../ui/updates/UpdatesScreenModel.kt | 115 ++++++++++++++- .../ui/updates/UpdatesSettingsScreenModel.kt | 26 ++++ .../kanade/tachiyomi/ui/updates/UpdatesTab.kt | 17 +++ .../data/updates/UpdatesRepositoryImpl.kt | 22 ++- .../sqldelight/tachiyomi/migrations/44.sqm | 61 ++++++++ .../sqldelight/tachiyomi/view/updatesView.sq | 25 +++- .../domain/updates/interactor/GetUpdates.kt | 17 ++- .../updates/repository/UpdatesRepository.kt | 9 +- .../updates/service/UpdatesPreferences.kt | 42 ++++++ .../moko-resources/base/strings.xml | 1 + 17 files changed, 500 insertions(+), 43 deletions(-) rename app/src/main/java/eu/kanade/presentation/updates/{UpdatesDialog.kt => UpdatesDeleteConfirmationDialog.kt} (100%) create mode 100644 app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsScreenModel.kt create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/44.sqm create mode 100644 domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f9e863ba..992de7e76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co ### Added - Automatically remove downloads on Suwayomi after reading, configurable via extension settings ([@cpiber](https://github.com/cpiber)) ([#2673](https://github.com/mihonapp/mihon/pull/2673)) - Display author & artist name in MAL search results ([@MajorTanya](https://github.com/MajorTanya)) ([#2833](https://github.com/mihonapp/mihon/pull/2833)) +- Add filter options to Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#2851](https://github.com/mihonapp/mihon/pull/2851)) ### Improved - Minimize memory usage by reducing in-memory cover cache size ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2266](https://github.com/mihonapp/mihon/pull/2266)) diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index e9fb97c74..c075ce7a0 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -10,8 +10,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter @@ -24,8 +23,10 @@ import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.theme.TachiyomiPreviewTheme import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.tachiyomi.ui.history.HistoryScreenModel +import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel import kotlinx.collections.immutable.persistentListOf import tachiyomi.domain.history.model.HistoryWithRelations +import tachiyomi.domain.updates.service.UpdatesPreferences import tachiyomi.i18n.MR import tachiyomi.i18n.kmk.KMR import tachiyomi.presentation.core.components.FastScrollLazyColumn @@ -34,6 +35,7 @@ import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen +import tachiyomi.presentation.core.util.collectAsState import java.time.LocalDate @Composable @@ -45,10 +47,14 @@ fun HistoryScreen( onClickResume: (mangaId: Long, chapterId: Long) -> Unit, onClickFavorite: (mangaId: Long) -> Unit, onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, + // KMK --> + settingsScreenModel: UpdatesSettingsScreenModel, + // KMK <-- ) { // KMK --> - val usePanoramaCover = remember { mutableStateOf(false) } + val usePanoramaCover by settingsScreenModel.updatesPreferences.usePanoramaCover().collectAsState() // KMK <-- + Scaffold( topBar = { scrollBehavior -> SearchToolbar( @@ -65,10 +71,8 @@ fun HistoryScreen( AppBar.Action( title = stringResource(KMR.strings.action_panorama_cover), icon = Icons.Outlined.Panorama, - iconTint = MaterialTheme.colorScheme.primary.takeIf { usePanoramaCover.value }, - onClick = { - usePanoramaCover.value = !usePanoramaCover.value - }, + iconTint = MaterialTheme.colorScheme.primary.takeIf { usePanoramaCover }, + onClick = { settingsScreenModel.toggleSwitch(UpdatesPreferences::usePanoramaCover) }, ), ) } @@ -115,7 +119,7 @@ fun HistoryScreen( onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, onClickFavorite = { history -> onClickFavorite(history.mangaId) }, // KMK --> - usePanoramaCover = usePanoramaCover.value, + usePanoramaCover = usePanoramaCover, // KMK <-- ) } @@ -194,6 +198,7 @@ internal fun HistoryScreenPreviews( @PreviewParameter(HistoryScreenModelStateProvider::class) historyState: HistoryScreenModel.State, ) { + val settingsScreenModel = UpdatesSettingsScreenModel() TachiyomiPreviewTheme { HistoryScreen( state = historyState, @@ -203,6 +208,7 @@ internal fun HistoryScreenPreviews( onClickResume = { _, _ -> run {} }, onDialogChange = {}, onClickFavorite = {}, + settingsScreenModel = settingsScreenModel, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesDeleteConfirmationDialog.kt similarity index 100% rename from app/src/main/java/eu/kanade/presentation/updates/UpdatesDialog.kt rename to app/src/main/java/eu/kanade/presentation/updates/UpdatesDeleteConfirmationDialog.kt diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt new file mode 100644 index 000000000..f4c603940 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt @@ -0,0 +1,139 @@ +package eu.kanade.presentation.updates + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import eu.kanade.presentation.components.TabbedDialog +import eu.kanade.presentation.components.TabbedDialogPaddings +import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel +import kotlinx.collections.immutable.persistentListOf +import tachiyomi.domain.updates.service.UpdatesPreferences +import tachiyomi.i18n.MR +import tachiyomi.i18n.kmk.KMR +import tachiyomi.presentation.core.components.SettingsItemsPaddings +import tachiyomi.presentation.core.components.TriStateItem +import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.collectAsState + +@Composable +fun UpdatesFilterDialog( + onDismissRequest: () -> Unit, + screenModel: UpdatesSettingsScreenModel, +) { + TabbedDialog( + onDismissRequest = onDismissRequest, + tabTitles = persistentListOf( + stringResource(MR.strings.action_filter), + ), + ) { + Column( + modifier = Modifier + .padding(vertical = TabbedDialogPaddings.Vertical) + .verticalScroll(rememberScrollState()), + ) { + FilterSheet(screenModel = screenModel) + } + } +} + +@Composable +private fun ColumnScope.FilterSheet( + screenModel: UpdatesSettingsScreenModel, +) { + val filterDownloaded by screenModel.updatesPreferences.filterDownloaded().collectAsState() + TriStateItem( + label = stringResource(MR.strings.label_downloaded), + state = filterDownloaded, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterDownloaded) }, + ) + + val filterUnread by screenModel.updatesPreferences.filterUnread().collectAsState() + TriStateItem( + label = stringResource(MR.strings.action_filter_unread), + state = filterUnread, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterUnread) }, + ) + + val filterStarted by screenModel.updatesPreferences.filterStarted().collectAsState() + TriStateItem( + label = stringResource(MR.strings.label_started), + state = filterStarted, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterStarted) }, + ) + + val filterBookmarked by screenModel.updatesPreferences.filterBookmarked().collectAsState() + TriStateItem( + label = stringResource(MR.strings.action_filter_bookmarked), + state = filterBookmarked, + onClick = { screenModel.toggleFilter(UpdatesPreferences::filterBookmarked) }, + ) + + HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small)) + + val filterExcludedScanlators by screenModel.updatesPreferences.filterExcludedScanlators().collectAsState() + + Row( + modifier = Modifier + // KMK --> + .clickable { screenModel.toggleSwitch(UpdatesPreferences::filterExcludedScanlators) } + // KMK <-- + .fillMaxWidth() + .padding(horizontal = SettingsItemsPaddings.Horizontal), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(MR.strings.action_filter_excluded_scanlators), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyMedium, + ) + + Switch( + checked = filterExcludedScanlators, + // KMK --> + onCheckedChange = { screenModel.toggleSwitch(UpdatesPreferences::filterExcludedScanlators) }, + // KMK <-- + ) + } + + // KMK --> + HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.small)) + + val panoramaCover by screenModel.updatesPreferences.usePanoramaCover().collectAsState() + + Row( + modifier = Modifier + .clickable { screenModel.toggleSwitch(UpdatesPreferences::usePanoramaCover) } + .fillMaxWidth() + .padding(horizontal = SettingsItemsPaddings.Horizontal), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(KMR.strings.action_panorama_cover), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyMedium, + ) + + Switch( + checked = panoramaCover, + onCheckedChange = { screenModel.toggleSwitch(UpdatesPreferences::usePanoramaCover) }, + ) + } + // KMK <-- +} diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index 767b6f646..94c30a50a 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -5,10 +5,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.CalendarMonth +import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FlipToBack -import androidx.compose.material.icons.outlined.Panorama import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.SelectAll +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -34,13 +35,13 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.i18n.MR -import tachiyomi.i18n.kmk.KMR import tachiyomi.presentation.core.components.FastScrollLazyColumn import tachiyomi.presentation.core.components.material.PullRefresh import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.screens.LoadingScreen +import tachiyomi.presentation.core.theme.active import java.time.LocalDate import kotlin.time.Duration.Companion.seconds @@ -68,13 +69,13 @@ fun UpdateScreen( // KMK <-- onUpdateSelected: (UpdatesItem, /* KMK --> */ UpdatesScreenModel.UpdateSelectionOptions /* KMK <-- */) -> Unit, onOpenChapter: (UpdatesItem) -> Unit, + onFilterClicked: () -> Unit, + hasActiveFilters: Boolean, // KMK --> + usePanoramaCover: Boolean, collapseToggle: (key: String) -> Unit, // KMK <-- ) { - // KMK --> - val usePanoramaCover = remember { mutableStateOf(false) } - // KMK <-- BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) }) Scaffold( @@ -82,15 +83,13 @@ fun UpdateScreen( UpdatesAppBar( onCalendarClicked = { onCalendarClicked() }, onUpdateLibrary = { onUpdateLibrary() }, + onFilterClicked = { onFilterClicked() }, + hasFilters = hasActiveFilters, actionModeCounter = state.selected.size, onSelectAll = { onSelectAll(true) }, onInvertSelection = { onInvertSelection() }, onCancelActionMode = { onSelectAll(false) }, scrollBehavior = scrollBehavior, - // KMK --> - usePanoramaCover = usePanoramaCover.value, - usePanoramaCoverClick = { usePanoramaCover.value = !usePanoramaCover.value }, - // KMK ) }, bottomBar = { @@ -139,7 +138,7 @@ fun UpdateScreen( // KMK --> expandedState = state.expandedState, collapseToggle = collapseToggle, - usePanoramaCover = usePanoramaCover.value, + usePanoramaCover = usePanoramaCover, // KMK <-- selectionMode = state.selectionMode, // SY --> @@ -166,16 +165,14 @@ fun UpdateScreen( private fun UpdatesAppBar( onCalendarClicked: () -> Unit, onUpdateLibrary: () -> Unit, + onFilterClicked: () -> Unit, + hasFilters: Boolean, // For action mode actionModeCounter: Int, onSelectAll: () -> Unit, onInvertSelection: () -> Unit, onCancelActionMode: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, - // KMK --> - usePanoramaCover: Boolean, - usePanoramaCoverClick: () -> Unit, - // KMK <-- modifier: Modifier = Modifier, ) { AppBar( @@ -184,14 +181,12 @@ private fun UpdatesAppBar( actions = { AppBarActions( persistentListOf( - // KMK --> AppBar.Action( - title = stringResource(KMR.strings.action_panorama_cover), - icon = Icons.Outlined.Panorama, - iconTint = MaterialTheme.colorScheme.primary.takeIf { usePanoramaCover }, - onClick = usePanoramaCoverClick, + title = stringResource(MR.strings.action_filter), + icon = Icons.Outlined.FilterList, + iconTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current, + onClick = onFilterClicked, ), - // KMK <-- AppBar.Action( title = stringResource(MR.strings.action_view_upcoming), icon = Icons.Outlined.CalendarMonth, diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt index 7446e80f0..6e6608f74 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -19,6 +19,7 @@ import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.storage.service.StoragePreferences +import tachiyomi.domain.updates.service.UpdatesPreferences import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addSingletonFactory @@ -48,6 +49,9 @@ class PreferenceModule(val app: Application) : InjektModule { addSingletonFactory { LibraryPreferences(get()) } + addSingletonFactory { + UpdatesPreferences(get()) + } addSingletonFactory { ReaderPreferences(get()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt index d2634bdd5..545f7c75f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt @@ -33,6 +33,7 @@ import eu.kanade.tachiyomi.ui.category.CategoryScreen import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsScreenModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow @@ -85,6 +86,9 @@ data object HistoryTab : Tab { val context = LocalContext.current val screenModel = rememberScreenModel { HistoryScreenModel() } val state by screenModel.state.collectAsState() + // KMK --> + val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() } + // KMK <-- HistoryScreen( state = state, @@ -94,6 +98,9 @@ data object HistoryTab : Tab { onClickResume = screenModel::getNextChapterForManga, onDialogChange = screenModel::setDialog, onClickFavorite = screenModel::addFavorite, + // KMK --> + settingsScreenModel = settingsScreenModel, + // KMK <-- ) val onDismissRequest = { screenModel.setDialog(null) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index 0000ee372..5ed4f222f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue +import androidx.compose.ui.util.fastFilter import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import eu.kanade.core.preference.asState @@ -30,11 +31,16 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.LogPriority +import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.system.logcat @@ -43,9 +49,11 @@ import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.ChapterUpdate import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.GetManga +import tachiyomi.domain.manga.model.applyFilter import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.updates.interactor.GetUpdates import tachiyomi.domain.updates.model.UpdatesWithRelations +import tachiyomi.domain.updates.service.UpdatesPreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.time.ZonedDateTime @@ -60,6 +68,7 @@ class UpdatesScreenModel( private val getManga: GetManga = Injekt.get(), private val getChapter: GetChapter = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(), + private val updatesPreferences: UpdatesPreferences = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), // SY --> readerPreferences: ReaderPreferences = Injekt.get(), @@ -85,19 +94,39 @@ class UpdatesScreenModel( val limit = ZonedDateTime.now().minusMonths(3).toInstant() combine( - getUpdates.subscribe(limit).distinctUntilChanged(), + // needed for SQL filters (unread, started, bookmarked, etc) + getUpdatesItemPreferenceFlow() + .distinctUntilChanged() + .flatMapLatest { + getUpdates.subscribe( + limit, + unread = it.filterUnread.toBooleanOrNull(), + started = it.filterStarted.toBooleanOrNull(), + bookmarked = it.filterBookmarked.toBooleanOrNull(), + hideExcludedScanlators = it.filterExcludedScanlators, + ).distinctUntilChanged() + }, downloadCache.changes, downloadManager.queueState, - ) { updates, _, _ -> updates } + // needed for Kotlin filters (downloaded) + getUpdatesItemPreferenceFlow().distinctUntilChanged { old, new -> + old.filterDownloaded == new.filterDownloaded + }, + ) { updates, _, _, itemPreferences -> + updates + .toUpdateItems() + .applyFilters(itemPreferences) + .toPersistentList() + } .catch { logcat(LogPriority.ERROR, it) _events.send(Event.InternalError) } - .collectLatest { updates -> + .collectLatest { updateItems -> mutableState.update { state -> state.copy( isLoading = false, - items = updates.toUpdateItems() + items = updateItems // KMK --> .groupBy { it.update.dateFetch.toLocalDate() } .flatMap { (_, mangas) -> @@ -121,9 +150,43 @@ class UpdatesScreenModel( .catch { logcat(LogPriority.ERROR, it) } .collect(this@UpdatesScreenModel::updateDownloadState) } + + getUpdatesItemPreferenceFlow() + .map { prefs -> + listOf( + prefs.filterUnread, + prefs.filterDownloaded, + prefs.filterStarted, + prefs.filterBookmarked, + ) + .any { it != TriState.DISABLED } + } + .distinctUntilChanged() + .onEach { + mutableState.update { state -> + state.copy(hasActiveFilters = it) + } + } + .launchIn(screenModelScope) } - private fun List.toUpdateItems(): PersistentList { + private fun List.applyFilters( + preferences: ItemPreferences, + ): List { + val filterDownloaded = preferences.filterDownloaded + + val filterFnDownloaded: (UpdatesItem) -> Boolean = { + applyFilter(filterDownloaded) { + it.downloadStateProvider() == Download.State.DOWNLOADED + } + } + + return fastFilter { + filterFnDownloaded(it) + } + } + + private fun List.toUpdateItems(): List { return this .map { update -> val activeDownload = downloadManager.getQueuedDownloadOrNull(update.chapterId) @@ -148,7 +211,6 @@ class UpdatesScreenModel( selected = update.chapterId in selectedChapterIds, ) } - .toPersistentList() } fun updateLibrary(): Boolean { @@ -484,9 +546,41 @@ class UpdatesScreenModel( } // KMK <-- + private fun getUpdatesItemPreferenceFlow(): Flow { + return combine( + updatesPreferences.filterDownloaded().changes(), + updatesPreferences.filterUnread().changes(), + updatesPreferences.filterStarted().changes(), + updatesPreferences.filterBookmarked().changes(), + updatesPreferences.filterExcludedScanlators().changes(), + ) { downloaded, unread, started, bookmarked, excludedScanlators -> + ItemPreferences( + filterDownloaded = downloaded, + filterUnread = unread, + filterStarted = started, + filterBookmarked = bookmarked, + filterExcludedScanlators = excludedScanlators, + ) + } + } + + fun showFilterDialog() { + mutableState.update { it.copy(dialog = Dialog.FilterSheet) } + } + + @Immutable + private data class ItemPreferences( + val filterDownloaded: TriState, + val filterUnread: TriState, + val filterStarted: TriState, + val filterBookmarked: TriState, + val filterExcludedScanlators: Boolean, + ) + @Immutable data class State( val isLoading: Boolean = true, + val hasActiveFilters: Boolean = false, val items: PersistentList = persistentListOf(), // KMK --> val expandedState: Set = persistentSetOf(), @@ -531,6 +625,7 @@ class UpdatesScreenModel( sealed interface Dialog { data class DeleteConfirmation(val toDelete: List) : Dialog + data object FilterSheet : Dialog } sealed interface Event { @@ -539,6 +634,14 @@ class UpdatesScreenModel( } } +private fun TriState.toBooleanOrNull(): Boolean? { + return when (this) { + TriState.DISABLED -> null + TriState.ENABLED_IS -> true + TriState.ENABLED_NOT -> false + } +} + @Immutable data class UpdatesItem( val update: UpdatesWithRelations, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsScreenModel.kt new file mode 100644 index 000000000..598e4086e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsScreenModel.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.ui.updates + +import cafe.adriel.voyager.core.model.ScreenModel +import tachiyomi.core.common.preference.Preference +import tachiyomi.core.common.preference.TriState +import tachiyomi.core.common.preference.getAndSet +import tachiyomi.domain.updates.service.UpdatesPreferences +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class UpdatesSettingsScreenModel( + val updatesPreferences: UpdatesPreferences = Injekt.get(), +) : ScreenModel { + + fun toggleFilter(preference: (UpdatesPreferences) -> Preference) { + preference(updatesPreferences).getAndSet { + it.next() + } + } + + // KMK --> + fun toggleSwitch(preference: (UpdatesPreferences) -> Preference) { + preference(updatesPreferences).getAndSet { !it } + } + // KMK <-- +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt index 4e71b6db2..7d2923bc5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesTab.kt @@ -21,6 +21,7 @@ import eu.kanade.core.preference.asState import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.updates.UpdateScreen import eu.kanade.presentation.updates.UpdatesDeleteConfirmationDialog +import eu.kanade.presentation.updates.UpdatesFilterDialog import eu.kanade.presentation.util.Tab import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.connections.discord.DiscordRPCService @@ -37,10 +38,12 @@ import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.launchIO import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get data object UpdatesTab : Tab { + @Suppress("unused") private fun readResolve(): Any = UpdatesTab override val options: TabOptions @@ -74,8 +77,13 @@ data object UpdatesTab : Tab { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow val screenModel = rememberScreenModel { UpdatesScreenModel() } + val settingsScreenModel = rememberScreenModel { UpdatesSettingsScreenModel() } val state by screenModel.state.collectAsState() + // KMK --> + val usePanoramaCover by settingsScreenModel.updatesPreferences.usePanoramaCover().collectAsState() + // KMK <-- + UpdateScreen( state = state, snackbarHostState = screenModel.snackbarHostState, @@ -102,7 +110,10 @@ data object UpdatesTab : Tab { context.startActivity(intent) }, onCalendarClicked = { navigator.push(UpcomingScreen()) }, + onFilterClicked = screenModel::showFilterDialog, + hasActiveFilters = state.hasActiveFilters, // KMK --> + usePanoramaCover = usePanoramaCover, collapseToggle = screenModel::toggleExpandedState, // KMK <-- ) @@ -115,6 +126,12 @@ data object UpdatesTab : Tab { onConfirm = { screenModel.deleteChapters(dialog.toDelete) }, ) } + is UpdatesScreenModel.Dialog.FilterSheet -> { + UpdatesFilterDialog( + onDismissRequest = onDismissDialog, + screenModel = settingsScreenModel, + ) + } null -> {} } diff --git a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt index 9683a8e8d..511010bc6 100644 --- a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt @@ -1,6 +1,7 @@ package tachiyomi.data.updates import kotlinx.coroutines.flow.Flow +import tachiyomi.core.common.util.lang.toLong import tachiyomi.data.DatabaseHandler import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.updates.model.UpdatesWithRelations @@ -25,9 +26,25 @@ class UpdatesRepositoryImpl( } } - override fun subscribeAll(after: Long, limit: Long): Flow> { + override fun subscribeAll( + after: Long, + limit: Long, + unread: Boolean?, + started: Boolean?, + bookmarked: Boolean?, + hideExcludedScanlators: Boolean, + ): Flow> { return databaseHandler.subscribeToList { - updatesViewQueries.getRecentUpdates(after, limit, ::mapUpdatesWithRelations) + updatesViewQueries.getRecentUpdatesWithFilters( + after = after, + limit = limit, + // invert because unread in Kotlin -> read column in SQL + read = unread?.let { !it }, + started = started?.toLong(), + bookmarked = bookmarked, + hideExcludedScanlators = hideExcludedScanlators.toLong(), + mapper = ::mapUpdatesWithRelations, + ) } } @@ -62,6 +79,7 @@ class UpdatesRepositoryImpl( coverLastModified: Long, @Suppress("UNUSED_PARAMETER") dateUpload: Long, dateFetch: Long, + excludedScanlator: String?, ): UpdatesWithRelations = UpdatesWithRelations( mangaId = mangaId, // SY --> diff --git a/data/src/main/sqldelight/tachiyomi/migrations/44.sqm b/data/src/main/sqldelight/tachiyomi/migrations/44.sqm new file mode 100644 index 000000000..f16ee195d --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/44.sqm @@ -0,0 +1,61 @@ +-- Add excluded_scanlators to updatesView +DROP VIEW IF EXISTS updatesView; + +CREATE VIEW updatesView AS +SELECT + mangas._id AS mangaId, + mangas.title AS mangaTitle, + chapters._id AS chapterId, + chapters.name AS chapterName, + chapters.scanlator, + chapters.url AS chapterUrl, + chapters.read, + chapters.bookmark, + chapters.last_page_read, + mangas.source, + mangas.favorite, + mangas.thumbnail_url AS thumbnailUrl, + mangas.cover_last_modified AS coverLastModified, + chapters.date_upload AS dateUpload, + chapters.date_fetch AS datefetch, + excluded_scanlators.scanlator AS excludedScanlator +FROM mangas JOIN chapters +ON mangas._id = chapters.manga_id +LEFT JOIN excluded_scanlators +ON mangas._id = excluded_scanlators.manga_id +AND chapters.scanlator = excluded_scanlators.scanlator +WHERE favorite = 1 AND source <> 6969 +AND date_fetch > date_added +UNION +SELECT + mangas._id AS mangaId, + mangas.title AS mangaTitle, + chapters._id AS chapterId, + chapters.name AS chapterName, + chapters.scanlator, + chapters.url AS chapterUrl, + chapters.read, + chapters.bookmark, + chapters.last_page_read, + mangas.source, + mangas.favorite, + mangas.thumbnail_url AS thumbnailUrl, + mangas.cover_last_modified AS coverLastModified, + chapters.date_upload AS dateUpload, + chapters.date_fetch AS datefetch, + excluded_scanlators.scanlator AS excludedScanlator +FROM mangas +LEFT JOIN ( + SELECT merged.manga_id,merged.merge_id + FROM merged + GROUP BY merged.merge_id +) AS ME +ON ME.merge_id = mangas._id +JOIN chapters +ON ME.manga_id = chapters.manga_id +LEFT JOIN excluded_scanlators +ON mangas._id = excluded_scanlators.manga_id +AND chapters.scanlator = excluded_scanlators.scanlator +WHERE favorite = 1 AND source = 6969 +AND date_fetch > date_added +ORDER BY datefetch DESC; diff --git a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq index 2f90951ff..1a9804d3d 100644 --- a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq @@ -15,14 +15,14 @@ SELECT mangas.thumbnail_url AS thumbnailUrl, mangas.cover_last_modified AS coverLastModified, chapters.date_upload AS dateUpload, - chapters.date_fetch AS datefetch + chapters.date_fetch AS datefetch, + excluded_scanlators.scanlator AS excludedScanlator FROM mangas JOIN chapters ON mangas._id = chapters.manga_id LEFT JOIN excluded_scanlators ON mangas._id = excluded_scanlators.manga_id AND chapters.scanlator = excluded_scanlators.scanlator WHERE favorite = 1 AND source <> 6969 -AND excluded_scanlators.scanlator IS NULL AND date_fetch > date_added UNION SELECT @@ -40,7 +40,8 @@ SELECT mangas.thumbnail_url AS thumbnailUrl, mangas.cover_last_modified AS coverLastModified, chapters.date_upload AS dateUpload, - chapters.date_fetch AS datefetch + chapters.date_fetch AS datefetch, + excluded_scanlators.scanlator AS excludedScanlator FROM mangas LEFT JOIN ( SELECT merged.manga_id,merged.merge_id @@ -54,7 +55,6 @@ LEFT JOIN excluded_scanlators ON mangas._id = excluded_scanlators.manga_id AND chapters.scanlator = excluded_scanlators.scanlator WHERE favorite = 1 AND source = 6969 -AND excluded_scanlators.scanlator IS NULL AND date_fetch > date_added ORDER BY datefetch DESC; @@ -64,6 +64,23 @@ FROM updatesView WHERE dateUpload > :after LIMIT :limit; +getRecentUpdatesWithFilters: +SELECT * +FROM updatesView +WHERE dateUpload > :after +AND (:read IS NULL OR read = :read) +-- Started means some progress but not finished, Read means finished chapter, thus: +AND ( + :started IS NULL + OR (:started = 1 AND last_page_read > 0 AND read = 0) + OR (:started = 0 AND last_page_read = 0 AND read = 0) +) +AND (:bookmarked IS NULL OR bookmark = :bookmarked) +AND ( + (excludedScanlator IS NULL OR :hideExcludedScanlators = 0) +) +LIMIT :limit; + getUpdatesByReadStatus: SELECT * FROM updatesView diff --git a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt index e3ec2bc5a..1d8d98519 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt @@ -19,8 +19,21 @@ class GetUpdates( return repository.awaitWithRead(read, after, limit = 500) } - fun subscribe(instant: Instant): Flow> { - return repository.subscribeAll(instant.toEpochMilli(), limit = 500) + fun subscribe( + instant: Instant, + unread: Boolean?, + started: Boolean?, + bookmarked: Boolean?, + hideExcludedScanlators: Boolean, + ): Flow> { + return repository.subscribeAll( + instant.toEpochMilli(), + limit = 500, + unread = unread, + started = started, + bookmarked = bookmarked, + hideExcludedScanlators = hideExcludedScanlators, + ) // SY --> .catchNPE() // SY <-- diff --git a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt index 3b583ea90..ca738f92d 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt @@ -7,7 +7,14 @@ interface UpdatesRepository { suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List - fun subscribeAll(after: Long, limit: Long): Flow> + fun subscribeAll( + after: Long, + limit: Long, + unread: Boolean?, + started: Boolean?, + bookmarked: Boolean?, + hideExcludedScanlators: Boolean, + ): Flow> fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow> } diff --git a/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt b/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt new file mode 100644 index 000000000..7b932c421 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt @@ -0,0 +1,42 @@ +package tachiyomi.domain.updates.service + +import tachiyomi.core.common.preference.PreferenceStore +import tachiyomi.core.common.preference.TriState +import tachiyomi.core.common.preference.getEnum + +class UpdatesPreferences( + private val preferenceStore: PreferenceStore, +) { + + fun filterDownloaded() = preferenceStore.getEnum( + "pref_filter_updates_downloaded", + TriState.DISABLED, + ) + + fun filterUnread() = preferenceStore.getEnum( + "pref_filter_updates_unread", + TriState.DISABLED, + ) + + fun filterStarted() = preferenceStore.getEnum( + "pref_filter_updates_started", + TriState.DISABLED, + ) + + fun filterBookmarked() = preferenceStore.getEnum( + "pref_filter_updates_bookmarked", + TriState.DISABLED, + ) + + fun filterExcludedScanlators() = preferenceStore.getBoolean( + "pref_filter_updates_hide_excluded_scanlators", + false, + ) + + // KMK --> + fun usePanoramaCover() = preferenceStore.getBoolean( + "pref_updates_screen_use_panorama_cover", + false, + ) + // KMK <-- +} diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 2bd750b6c..608a39356 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -857,6 +857,7 @@ Just now Never View Upcoming Updates + Filter excluded scanlators Upcoming Guide