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