diff --git a/CHANGELOG.md b/CHANGELOG.md index e850428a0..212c28a5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - `Other` - for technical stuff. ## [Unreleased] +### Added +- Add Category filtering for the Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#3589](https://github.com/mihonapp/mihon/pull/3589)) ## [v0.20.2] - 2026-08-01 ### Added diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt index 8cf3f7328..d555a42ab 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesFilterDialog.kt @@ -14,12 +14,19 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import eu.kanade.presentation.category.visualName import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.tachiyomi.ui.updates.UpdatesSettingsViewModel +import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.getAndSet import tachiyomi.domain.updates.service.UpdatesPreferences import tachiyomi.i18n.MR @@ -27,6 +34,7 @@ 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.screens.LoadingScreen import tachiyomi.presentation.core.util.collectAsState @Composable @@ -38,14 +46,18 @@ fun UpdatesFilterDialog( onDismissRequest = onDismissRequest, tabTitles = listOf( stringResource(MR.strings.action_filter), + stringResource(MR.strings.categories), ), - ) { + ) { page -> Column( modifier = Modifier .padding(vertical = TabbedDialogPaddings.Vertical) .verticalScroll(rememberScrollState()), ) { - FilterSheet(viewModel = viewModel) + when (page) { + 0 -> FilterSheet(viewModel = viewModel) + 1 -> CategoryFilterSheet(viewModel = viewModel) + } } } } @@ -108,3 +120,55 @@ private fun ColumnScope.FilterSheet( ) } } + +@Composable +private fun ColumnScope.CategoryFilterSheet( + viewModel: UpdatesSettingsViewModel, +) { + Text( + stringResource(MR.strings.pref_filter_update_categories_details), + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = SettingsItemsPaddings.Horizontal, + vertical = SettingsItemsPaddings.Vertical, + ), + ) + + HorizontalDivider(modifier = Modifier.padding(MaterialTheme.padding.extraSmall)) + + val allCategories by viewModel.getCategories.subscribe().collectAsState(initial = emptyList()) + + if (allCategories.isEmpty()) { + // since it includes the system category, this should only happen when loading is required + LoadingScreen(modifier = Modifier.padding(16.dp)) + return + } + + val excluded by viewModel.updatesPreferences.filterExcludedCategories.collectAsState() + val included by viewModel.updatesPreferences.filterIncludedCategories.collectAsState() + + val selected = remember { + allCategories.map { category -> + when (category.id) { + in included -> TriState.ENABLED_IS + in excluded -> TriState.ENABLED_NOT + else -> TriState.DISABLED + } + }.toMutableStateList() + } + + Column { + allCategories.fastForEachIndexed { idx, category -> + val state = selected[idx] + TriStateItem( + label = category.visualName, + state = state, + onClick = { + selected[idx] = state.next() + viewModel.cycleCategory(category) + }, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsViewModel.kt index f4f202994..4ca15163d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesSettingsViewModel.kt @@ -4,14 +4,32 @@ import androidx.lifecycle.ViewModel import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.getAndSet +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.category.model.Category import tachiyomi.domain.updates.service.UpdatesPreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class UpdatesSettingsViewModel( val updatesPreferences: UpdatesPreferences = Injekt.get(), + val getCategories: GetCategories = Injekt.get(), ) : ViewModel() { + val includedCategories = updatesPreferences.filterIncludedCategories + val excludedCategories = updatesPreferences.filterExcludedCategories + + fun cycleCategory(category: Category) { + when (category.id) { + in includedCategories.get() -> { + includedCategories.getAndSet { it - category.id } + excludedCategories.getAndSet { it + category.id } + } + + in excludedCategories.get() -> excludedCategories.getAndSet { it - category.id } + else -> includedCategories.getAndSet { it + category.id } + } + } + fun toggleFilter(preference: (UpdatesPreferences) -> Preference) { preference(updatesPreferences).getAndSet { it.next() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesViewModel.kt index e19ecacdb..ca8d8661e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesViewModel.kt @@ -93,6 +93,8 @@ class UpdatesViewModel( started = it.filterStarted.toBooleanOrNull(), bookmarked = it.filterBookmarked.toBooleanOrNull(), hideExcludedScanlators = it.filterExcludedScanlators, + includedCategories = it.filterIncludedCategories, + excludedCategories = it.filterExcludedCategories, ).distinctUntilChanged() }, downloadCache.changes, @@ -423,13 +425,18 @@ class UpdatesViewModel( updatesPreferences.filterStarted.changes(), updatesPreferences.filterBookmarked.changes(), updatesPreferences.filterExcludedScanlators.changes(), - ) { downloaded, unread, started, bookmarked, excludedScanlators -> + updatesPreferences.filterIncludedCategories.changes(), + updatesPreferences.filterExcludedCategories.changes(), + ) { + @Suppress("UNCHECKED_CAST") ItemPreferences( - filterDownloaded = downloaded, - filterUnread = unread, - filterStarted = started, - filterBookmarked = bookmarked, - filterExcludedScanlators = excludedScanlators, + filterDownloaded = it[0] as TriState, + filterUnread = it[1] as TriState, + filterStarted = it[2] as TriState, + filterBookmarked = it[3] as TriState, + filterExcludedScanlators = it[4] as Boolean, + filterIncludedCategories = it[5] as List, + filterExcludedCategories = it[6] as List, ) } } @@ -445,6 +452,8 @@ class UpdatesViewModel( val filterStarted: TriState, val filterBookmarked: TriState, val filterExcludedScanlators: Boolean, + val filterIncludedCategories: List, + val filterExcludedCategories: List, ) @Immutable diff --git a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt index 8dc663381..7c1102233 100644 --- a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt @@ -35,6 +35,8 @@ class UpdatesRepositoryImpl( started: Boolean?, bookmarked: Boolean?, hideExcludedScanlators: Boolean, + includedCategories: List, + excludedCategories: List, ): Flow> { return database.updatesViewQueries .getRecentUpdatesWithFilters( @@ -44,6 +46,10 @@ class UpdatesRepositoryImpl( started = started?.toLong(), bookmarked = bookmarked, hideExcludedScanlators = hideExcludedScanlators.toLong(), + includedEmpty = includedCategories.isEmpty(), + excludedEmpty = excludedCategories.isEmpty(), + includedCategories = includedCategories, + excludedCategories = excludedCategories, mapper = ::mapUpdatesWithRelations, ) .subscribeToList() diff --git a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq index cfe904cf1..5995b1bed 100644 --- a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq @@ -46,6 +46,33 @@ AND (:bookmarked IS NULL OR bookmark = :bookmarked) AND ( (excludedScanlator IS NULL OR :hideExcludedScanlators = 0) ) +AND ( + -- includedEmpty being true expresses "don't care" state and bypasses the membership filter + :includedEmpty + OR EXISTS ( + SELECT 1 FROM mangas_categories + WHERE mangas_categories.manga_id = mangaId + AND COALESCE(mangas_categories.category_id, 0) IN :includedCategories + ) + OR (0 IN :includedCategories AND NOT EXISTS ( + SELECT 1 FROM mangas_categories mc WHERE mc.manga_id = mangaId) + ) +) +AND ( + -- excludedEmpty being true expresses "don't care" state and bypasses the membership filter + :excludedEmpty + OR ( + NOT EXISTS ( + SELECT 1 FROM mangas_categories + WHERE mangas_categories.manga_id = mangaId + AND COALESCE(mangas_categories.category_id, 0) IN :excludedCategories + ) + AND ( + 0 NOT IN :excludedCategories + OR EXISTS (SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangaId) + ) + ) +) LIMIT :limit; getUpdatesByReadStatus: 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 8c4bea360..836358645 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/interactor/GetUpdates.kt @@ -19,6 +19,8 @@ class GetUpdates( started: Boolean?, bookmarked: Boolean?, hideExcludedScanlators: Boolean, + includedCategories: List, + excludedCategories: List, ): Flow> { return repository.subscribeAll( instant.toEpochMilliseconds(), @@ -27,6 +29,8 @@ class GetUpdates( started = started, bookmarked = bookmarked, hideExcludedScanlators = hideExcludedScanlators, + includedCategories = includedCategories, + excludedCategories = excludedCategories, ) } 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 ca738f92d..a991f6822 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/repository/UpdatesRepository.kt @@ -14,6 +14,8 @@ interface UpdatesRepository { started: Boolean?, bookmarked: Boolean?, hideExcludedScanlators: Boolean, + includedCategories: List, + excludedCategories: List, ): 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 index 3d2989126..47e650859 100644 --- a/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/updates/service/UpdatesPreferences.kt @@ -4,6 +4,7 @@ import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.getEnum +import tachiyomi.core.common.preference.getLongArray class UpdatesPreferences( preferenceStore: PreferenceStore, @@ -33,4 +34,14 @@ class UpdatesPreferences( "pref_filter_updates_hide_excluded_scanlators", false, ) + + val filterIncludedCategories: Preference> = preferenceStore.getLongArray( + "pref_filter_updates_included_categories", + emptyList(), + ) + + val filterExcludedCategories: Preference> = preferenceStore.getLongArray( + "pref_filter_updates_excluded_categories", + emptyList(), + ) } diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index a713612ab..05f54924a 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -861,6 +861,7 @@ Never View Upcoming Updates Filter excluded scanlators + Updates in excluded categories will not be shown even if they are also in any included categories. Upcoming Guide