Add category filters for Updates tab (#3589)
* Add category filters for Updates tab Works just like Library Updates. Provides a tab in the Updates tab filter dialog to allow for including & excluding of updates belonging to a title from a given category. Excludes overpower Includes, as with Library Updates. A little helpful text informs users about this. * Changelog * [skip ci] Remove unused string Leftover from before I realized the Default category will always be available for in-/exclusion. * Make :includeEmpty/:excludeEmpty true Boolean params * "include" -> "included" Consistency with #3607. * Remove DISTINCT from getRecentUpdatesWithFilters Apparently not needed anymore (though it was required at some point). * [skip ci] Fix Changelog
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TriState>) {
|
||||
preference(updatesPreferences).getAndSet {
|
||||
it.next()
|
||||
|
||||
@@ -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<Long>,
|
||||
filterExcludedCategories = it[6] as List<Long>,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -445,6 +452,8 @@ class UpdatesViewModel(
|
||||
val filterStarted: TriState,
|
||||
val filterBookmarked: TriState,
|
||||
val filterExcludedScanlators: Boolean,
|
||||
val filterIncludedCategories: List<Long>,
|
||||
val filterExcludedCategories: List<Long>,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
||||
@@ -35,6 +35,8 @@ class UpdatesRepositoryImpl(
|
||||
started: Boolean?,
|
||||
bookmarked: Boolean?,
|
||||
hideExcludedScanlators: Boolean,
|
||||
includedCategories: List<Long>,
|
||||
excludedCategories: List<Long>,
|
||||
): Flow<List<UpdatesWithRelations>> {
|
||||
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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -19,6 +19,8 @@ class GetUpdates(
|
||||
started: Boolean?,
|
||||
bookmarked: Boolean?,
|
||||
hideExcludedScanlators: Boolean,
|
||||
includedCategories: List<Long>,
|
||||
excludedCategories: List<Long>,
|
||||
): Flow<List<UpdatesWithRelations>> {
|
||||
return repository.subscribeAll(
|
||||
instant.toEpochMilliseconds(),
|
||||
@@ -27,6 +29,8 @@ class GetUpdates(
|
||||
started = started,
|
||||
bookmarked = bookmarked,
|
||||
hideExcludedScanlators = hideExcludedScanlators,
|
||||
includedCategories = includedCategories,
|
||||
excludedCategories = excludedCategories,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ interface UpdatesRepository {
|
||||
started: Boolean?,
|
||||
bookmarked: Boolean?,
|
||||
hideExcludedScanlators: Boolean,
|
||||
includedCategories: List<Long>,
|
||||
excludedCategories: List<Long>,
|
||||
): Flow<List<UpdatesWithRelations>>
|
||||
|
||||
fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow<List<UpdatesWithRelations>>
|
||||
|
||||
@@ -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<List<Long>> = preferenceStore.getLongArray(
|
||||
"pref_filter_updates_included_categories",
|
||||
emptyList(),
|
||||
)
|
||||
|
||||
val filterExcludedCategories: Preference<List<Long>> = preferenceStore.getLongArray(
|
||||
"pref_filter_updates_excluded_categories",
|
||||
emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -861,6 +861,7 @@
|
||||
<string name="relative_time_span_never">Never</string>
|
||||
<string name="action_view_upcoming">View Upcoming Updates</string>
|
||||
<string name="action_filter_excluded_scanlators">Filter excluded scanlators</string>
|
||||
<string name="pref_filter_update_categories_details">Updates in excluded categories will not be shown even if they are also in any included categories.</string>
|
||||
|
||||
<!-- Upcoming -->
|
||||
<string name="upcoming_guide">Upcoming Guide</string>
|
||||
|
||||
Reference in New Issue
Block a user