feat(library): add category library filter (#895)

* feat(library): add category library filter

* feat(library): pre-calculate filter categories map

* feat(library): ungroup library items if category filter is enabled

* refactor `combine` function and improve flow handling

* fix: Exclude system's `Default` category from the list (as no other category would match with it)

* refactor: categories conversion

* feat: correctly restore categories filter preferences

* feat: improve DB ops performance

* chore: add KMK comment

---------

Co-authored-by: Cuong-Tran <cuongtran.tm@gmail.com>
This commit is contained in:
Fermín Cirella 2025-05-20 05:32:41 -03:00 committed by GitHub
parent 70fcc3dff0
commit 7f820b590c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 252 additions and 11 deletions

View file

@ -48,6 +48,7 @@ import tachiyomi.data.updates.UpdatesRepositoryImpl
import tachiyomi.domain.category.interactor.CreateCategoryWithName import tachiyomi.domain.category.interactor.CreateCategoryWithName
import tachiyomi.domain.category.interactor.DeleteCategory import tachiyomi.domain.category.interactor.DeleteCategory
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.GetCategoriesPerLibraryManga
import tachiyomi.domain.category.interactor.HideCategory import tachiyomi.domain.category.interactor.HideCategory
import tachiyomi.domain.category.interactor.RenameCategory import tachiyomi.domain.category.interactor.RenameCategory
import tachiyomi.domain.category.interactor.ReorderCategory import tachiyomi.domain.category.interactor.ReorderCategory
@ -116,6 +117,7 @@ class DomainModule : InjektModule {
addFactory { DeleteCategory(get(), get(), get()) } addFactory { DeleteCategory(get(), get(), get()) }
// KMK --> // KMK -->
addFactory { HideCategory(get()) } addFactory { HideCategory(get()) }
addFactory { GetCategoriesPerLibraryManga(get()) }
// KMK <-- // KMK <--
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }

View file

@ -1,27 +1,40 @@
package eu.kanade.presentation.library package eu.kanade.presentation.library
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Checkbox
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.components.TabbedDialog import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.isReleaseBuildType
@ -29,6 +42,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import tachiyomi.core.common.preference.TriState import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.toggle
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.model.LibraryGroup import tachiyomi.domain.library.model.LibraryGroup
@ -43,9 +57,11 @@ import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.IconItem import tachiyomi.presentation.core.components.IconItem
import tachiyomi.presentation.core.components.SettingsChipRow import tachiyomi.presentation.core.components.SettingsChipRow
import tachiyomi.presentation.core.components.SettingsItemsPaddings
import tachiyomi.presentation.core.components.SliderItem import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.components.SortItem import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem import tachiyomi.presentation.core.components.TriStateItem
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
@ -57,6 +73,9 @@ fun LibrarySettingsDialog(
// SY --> // SY -->
hasCategories: Boolean, hasCategories: Boolean,
// SY <-- // SY <--
// KMK -->
categories: List<Category>,
// KMK <--
) { ) {
TabbedDialog( TabbedDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -77,6 +96,9 @@ fun LibrarySettingsDialog(
when (page) { when (page) {
0 -> FilterPage( 0 -> FilterPage(
screenModel = screenModel, screenModel = screenModel,
// KMK -->
categories = categories,
// KMK <--
) )
1 -> SortPage( 1 -> SortPage(
category = category, category = category,
@ -100,6 +122,7 @@ fun LibrarySettingsDialog(
@Composable @Composable
private fun ColumnScope.FilterPage( private fun ColumnScope.FilterPage(
screenModel: LibrarySettingsScreenModel, screenModel: LibrarySettingsScreenModel,
categories: List<Category>,
) { ) {
val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState() val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState()
val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState() val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
@ -157,6 +180,13 @@ private fun ColumnScope.FilterPage(
) )
// SY <-- // SY <--
// KMY -->
CategoriesFilter(
libraryPreferences = screenModel.libraryPreferences,
categories = categories,
)
// KMK <--
val trackers by screenModel.trackersFlow.collectAsState() val trackers by screenModel.trackersFlow.collectAsState()
when (trackers.size) { when (trackers.size) {
0 -> { 0 -> {
@ -434,3 +464,58 @@ private fun ColumnScope.GroupPage(
} }
} }
// SY <-- // SY <--
// KMK -->
@Composable
private fun CategoriesFilter(
libraryPreferences: LibraryPreferences,
categories: List<Category>,
) {
val filterCategories by libraryPreferences.filterCategories().collectAsState()
val filterCategoriesInclude = libraryPreferences.filterCategoriesInclude()
val filterCategoriesExclude = libraryPreferences.filterCategoriesExclude()
val included by filterCategoriesInclude.collectAsState()
val excluded by filterCategoriesExclude.collectAsState()
var showCategoriesDialog by rememberSaveable { mutableStateOf(false) }
if (showCategoriesDialog) {
TriStateListDialog(
title = stringResource(MR.strings.categories),
message = stringResource(KMR.strings.pref_library_filter_categories_details),
items = categories,
initialChecked = included.mapNotNull { id -> categories.find { it.id.toString() == id } },
initialInversed = excluded.mapNotNull { id -> categories.find { it.id.toString() == id } },
itemLabel = { it.visualName },
onDismissRequest = { showCategoriesDialog = false },
onValueChanged = { newIncluded, newExcluded ->
filterCategoriesInclude.set(newIncluded.map { it.id.toString() }.toSet())
filterCategoriesExclude.set(newExcluded.map { it.id.toString() }.toSet())
showCategoriesDialog = false
},
)
}
Row(
modifier = Modifier
.clickable(onClick = { libraryPreferences.filterCategories().toggle() })
.fillMaxWidth()
.padding(horizontal = SettingsItemsPaddings.Horizontal),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
Checkbox(
checked = filterCategories,
onCheckedChange = null,
)
Text(
text = stringResource(MR.strings.categories),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(Modifier.weight(1f))
TextButton(onClick = { showCategoriesDialog = true }) {
Text(stringResource(MR.strings.action_edit))
}
}
}
// KMK <--

View file

@ -57,10 +57,12 @@ import exh.util.cancellable
import exh.util.isLewd import exh.util.isLewd
import exh.util.nullIfBlank import exh.util.nullIfBlank
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
@ -87,6 +89,7 @@ import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.UnsortedPreferences import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.GetCategoriesPerLibraryManga
import tachiyomi.domain.category.interactor.SetMangaCategories import tachiyomi.domain.category.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
@ -159,6 +162,7 @@ class LibraryScreenModel(
// SY <-- // SY <--
// KMK --> // KMK -->
private val smartSearchMerge: SmartSearchMerge = Injekt.get(), private val smartSearchMerge: SmartSearchMerge = Injekt.get(),
private val getCategoriesPerLibraryManga: GetCategoriesPerLibraryManga = Injekt.get(),
// KMK <-- // KMK <--
) : StateScreenModel<LibraryScreenModel.State>(State()) { ) : StateScreenModel<LibraryScreenModel.State>(State()) {
@ -171,12 +175,15 @@ class LibraryScreenModel(
init { init {
screenModelScope.launchIO { screenModelScope.launchIO {
combine( combine(
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
getLibraryFlow(),
getTracksPerManga.subscribe(),
combine( combine(
getTrackingFilterFlow(), state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
getLibraryFlow(),
downloadCache.changes, downloadCache.changes,
::Triple,
),
combine(
getTracksPerManga.subscribe(),
getTrackingFilterFlow(),
::Pair, ::Pair,
), ),
// SY --> // SY -->
@ -186,12 +193,25 @@ class LibraryScreenModel(
::Pair, ::Pair,
), ),
// SY <-- // SY <--
) { searchQuery, library, tracks, (trackingFilter, _), (groupType, sort) -> // KMK -->
combine(
getCategoriesPerLibraryManga.subscribe(),
state.map { it.filterCategory }.distinctUntilChanged(),
::Pair,
),
// KMK <--
) { (searchQuery, library, _), (tracks, trackingFilter), (groupType, sort), (categoriesPerManga, filterCategory) ->
library library
// SY --> // SY -->
.applyGrouping(groupType) .applyGrouping(/* KMK --> */ if (filterCategory) LibraryGroup.UNGROUPED else /* KMK <-- */ groupType)
// SY <-- // SY <--
.applyFilters(tracks, trackingFilter) .applyFilters(
tracks,
trackingFilter,
// KMK -->
categoriesPerManga,
// KMK <--
)
.applySort( .applySort(
tracks, tracks,
trackingFilter.keys, trackingFilter.keys,
@ -253,7 +273,10 @@ class LibraryScreenModel(
prefs.filterLewd, prefs.filterLewd,
// SY <-- // SY <--
) + trackFilter.values ) + trackFilter.values
).any { it != TriState.DISABLED } ).any { it != TriState.DISABLED } ||
// KMK -->
prefs.filterCategories
// KMK <--
} }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { .onEach {
@ -294,6 +317,25 @@ class LibraryScreenModel(
} }
.launchIn(screenModelScope) .launchIn(screenModelScope)
// SY <-- // SY <--
// KMK -->
libraryPreferences.filterCategories().changes()
.onEach {
mutableState.update { state ->
state.copy(filterCategory = it)
}
}.launchIn(screenModelScope)
screenModelScope.launchIO {
getCategories
.subscribe()
.collect { categories ->
mutableState.update { state ->
state.copy(libraryCategories = categories.filterNot(Category::isSystemCategory))
}
}
}
// KMK <--
} }
/** /**
@ -302,6 +344,9 @@ class LibraryScreenModel(
private suspend fun LibraryMap.applyFilters( private suspend fun LibraryMap.applyFilters(
trackMap: Map<Long, List<Track>>, trackMap: Map<Long, List<Track>>,
trackingFilter: Map<Long, TriState>, trackingFilter: Map<Long, TriState>,
// KMK -->
categoriesPerManga: Map<Long, Set<Long>>,
// KMK <--
): LibraryMap { ): LibraryMap {
val prefs = getLibraryItemPreferencesFlow().first() val prefs = getLibraryItemPreferencesFlow().first()
val downloadedOnly = prefs.globalFilterDownloaded val downloadedOnly = prefs.globalFilterDownloaded
@ -323,6 +368,12 @@ class LibraryScreenModel(
val filterLewd = prefs.filterLewd val filterLewd = prefs.filterLewd
// SY <-- // SY <--
// KMK -->
val filterCategories = prefs.filterCategories
val includedCategories = prefs.filterCategoriesInclude
val excludedCategories = prefs.filterCategoriesExclude
// KMK <--
val filterFnDownloaded: (LibraryItem) -> Boolean = { val filterFnDownloaded: (LibraryItem) -> Boolean = {
applyFilter(filterDownloaded) { applyFilter(filterDownloaded) {
it.libraryManga.manga.isLocal() || it.libraryManga.manga.isLocal() ||
@ -374,6 +425,19 @@ class LibraryScreenModel(
!isExcluded && isIncluded !isExcluded && isIncluded
} }
// KMK -->
val filterFnCategories: (LibraryItem) -> Boolean = categories@{ item ->
if (!filterCategories) return@categories true
val mangaCategories = categoriesPerManga[item.libraryManga.id].orEmpty()
val isExcluded = excludedCategories.any { it in mangaCategories }
val isIncluded = includedCategories.isEmpty() || includedCategories.all { it in mangaCategories }
!isExcluded && isIncluded
}
// KMK <--
val filterFn: (LibraryItem) -> Boolean = { val filterFn: (LibraryItem) -> Boolean = {
filterFnDownloaded(it) && filterFnDownloaded(it) &&
filterFnUnread(it) && filterFnUnread(it) &&
@ -383,8 +447,11 @@ class LibraryScreenModel(
filterFnIntervalCustom(it) && filterFnIntervalCustom(it) &&
filterFnTracking(it) && filterFnTracking(it) &&
// SY --> // SY -->
filterFnLewd(it) filterFnLewd(it) &&
// SY <-- // SY <--
// KMK -->
filterFnCategories(it)
// KMK <---
} }
return mapValues { (_, value) -> value.fastFilter(filterFn) } return mapValues { (_, value) -> value.fastFilter(filterFn) }
@ -524,6 +591,9 @@ class LibraryScreenModel(
// KMK --> // KMK -->
libraryPreferences.sourceBadge().changes(), libraryPreferences.sourceBadge().changes(),
libraryPreferences.useLangIcon().changes(), libraryPreferences.useLangIcon().changes(),
libraryPreferences.filterCategories().changes(),
libraryPreferences.filterCategoriesInclude().changes(),
libraryPreferences.filterCategoriesExclude().changes(),
// KMK <-- // KMK <--
) { ) {
ItemPreferences( ItemPreferences(
@ -545,6 +615,9 @@ class LibraryScreenModel(
// KMK --> // KMK -->
sourceBadge = it[13] as Boolean, sourceBadge = it[13] as Boolean,
useLangIcon = it[14] as Boolean, useLangIcon = it[14] as Boolean,
filterCategories = it[15] as Boolean,
filterCategoriesInclude = (it[16] as Set<*>).filterIsInstance<String>().mapNotNull(String::toLongOrNull).toImmutableSet(),
filterCategoriesExclude = (it[17] as Set<*>).filterIsInstance<String>().mapNotNull(String::toLongOrNull).toImmutableSet(),
// KMK <-- // KMK <--
) )
} }
@ -1435,6 +1508,11 @@ class LibraryScreenModel(
// SY --> // SY -->
val filterLewd: TriState, val filterLewd: TriState,
// SY <-- // SY <--
// KMK -->
val filterCategories: Boolean,
val filterCategoriesInclude: ImmutableSet<Long>,
val filterCategoriesExclude: ImmutableSet<Long>,
// KMK <--
) )
@Immutable @Immutable
@ -1454,6 +1532,10 @@ class LibraryScreenModel(
val ogCategories: List<Category> = emptyList(), val ogCategories: List<Category> = emptyList(),
val groupType: Int = LibraryGroup.BY_DEFAULT, val groupType: Int = LibraryGroup.BY_DEFAULT,
// SY <-- // SY <--
// KMK -->
val libraryCategories: List<Category> = emptyList(),
val filterCategory: Boolean = false,
// KMK <--
) { ) {
private val libraryCount by lazy { private val libraryCount by lazy {
library.values library.values

View file

@ -328,6 +328,9 @@ data object LibraryTab : Tab {
// SY --> // SY -->
hasCategories = state.categories.fastAny { !it.isSystemCategory }, hasCategories = state.categories.fastAny { !it.isSystemCategory },
// SY <-- // SY <--
// KMK -->
categories = state.libraryCategories,
// KMK <--
) )
} }
is LibraryScreenModel.Dialog.ChangeCategory -> { is LibraryScreenModel.Dialog.ChangeCategory -> {

View file

@ -28,6 +28,10 @@ class CategoryPreferencesCleanupMigration : Migration {
downloadPreferences.removeExcludeCategories(), downloadPreferences.removeExcludeCategories(),
downloadPreferences.downloadNewChapterCategories(), downloadPreferences.downloadNewChapterCategories(),
downloadPreferences.downloadNewChapterCategoriesExclude(), downloadPreferences.downloadNewChapterCategoriesExclude(),
// KMK -->
libraryPreferences.filterCategoriesInclude(),
libraryPreferences.filterCategoriesExclude(),
// KMK <--
) )
categoryPreferences.forEach { preference -> categoryPreferences.forEach { preference ->
val ids = preference.get() val ids = preference.get()

View file

@ -1,6 +1,7 @@
package tachiyomi.data.category package tachiyomi.data.category
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.data.Database import tachiyomi.data.Database
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
@ -35,6 +36,18 @@ class CategoryRepositoryImpl(
} }
} }
// KMK -->
override fun getCategoriesPerLibraryMangaAsFlow(): Flow<Map<Long, Set<Long>>> {
return handler.subscribeToList {
mangas_categoriesQueries.getCategoriesPerLibraryManga { mangaId, categoryId ->
mangaId to categoryId
}
}.map { list ->
list.groupBy({ it.first }, { it.second }).mapValues { it.value.toSet() }
}
}
// KMK <--
// SY --> // SY -->
override suspend fun insert(category: Category): Long { override suspend fun insert(category: Category): Long {
return handler.awaitOneExecutable(true) { return handler.awaitOneExecutable(true) {

View file

@ -22,3 +22,12 @@ VALUES (:mangaId, :categoryId);
deleteMangaCategoryByMangaId: deleteMangaCategoryByMangaId:
DELETE FROM mangas_categories DELETE FROM mangas_categories
WHERE manga_id = :mangaId; WHERE manga_id = :mangaId;
-- KMK -->
getCategoriesPerLibraryManga:
SELECT MC.manga_id, MC.category_id
FROM mangas_categories MC
JOIN mangas M
ON MC.manga_id = M._id
WHERE M.favorite = 1;
-- KMK <--

View file

@ -41,6 +41,10 @@ class DeleteCategory(
downloadPreferences.removeExcludeCategories(), downloadPreferences.removeExcludeCategories(),
downloadPreferences.downloadNewChapterCategories(), downloadPreferences.downloadNewChapterCategories(),
downloadPreferences.downloadNewChapterCategoriesExclude(), downloadPreferences.downloadNewChapterCategoriesExclude(),
// KMK -->
libraryPreferences.filterCategoriesInclude(),
libraryPreferences.filterCategoriesExclude(),
// KMK <--
) )
val categoryIdString = categoryId.toString() val categoryIdString = categoryId.toString()
categoryPreferences.forEach { preference -> categoryPreferences.forEach { preference ->

View file

@ -0,0 +1,13 @@
package tachiyomi.domain.category.interactor
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.category.repository.CategoryRepository
class GetCategoriesPerLibraryManga(
private val categoryRepository: CategoryRepository,
) {
fun subscribe(): Flow<Map<Long, Set<Long>>> {
return categoryRepository.getCategoriesPerLibraryMangaAsFlow()
}
}

View file

@ -16,6 +16,10 @@ interface CategoryRepository {
fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>>
// KMK -->
fun getCategoriesPerLibraryMangaAsFlow(): Flow<Map<Long, Set<Long>>>
// KMK <--
// SY --> // SY -->
suspend fun insert(category: Category): Long suspend fun insert(category: Category): Long
// SY <-- // SY <--

View file

@ -116,6 +116,17 @@ class LibraryPreferences(
) )
// SY <-- // SY <--
// KMK -->
fun filterCategories() = preferenceStore.getBoolean(
"pref_filter_library_categories",
false,
)
fun filterCategoriesInclude() = preferenceStore.getStringSet(FILTER_LIBRARY_CATEGORIES_INCLUDE_PREF_KEY, emptySet())
fun filterCategoriesExclude() = preferenceStore.getStringSet(FILTER_LIBRARY_CATEGORIES_EXCLUDE_PREF_KEY, emptySet())
// KMK <--
fun filterTracking(id: Int) = preferenceStore.getEnum( fun filterTracking(id: Int) = preferenceStore.getEnum(
"pref_filter_library_tracked_${id}_v2", "pref_filter_library_tracked_${id}_v2",
TriState.DISABLED, TriState.DISABLED,
@ -263,10 +274,20 @@ class LibraryPreferences(
const val DEFAULT_CATEGORY_PREF_KEY = "default_category" const val DEFAULT_CATEGORY_PREF_KEY = "default_category"
private const val LIBRARY_UPDATE_CATEGORIES_PREF_KEY = "library_update_categories" private const val LIBRARY_UPDATE_CATEGORIES_PREF_KEY = "library_update_categories"
private const val LIBRARY_UPDATE_CATEGORIES_EXCLUDE_PREF_KEY = "library_update_categories_exclude" private const val LIBRARY_UPDATE_CATEGORIES_EXCLUDE_PREF_KEY = "library_update_categories_exclude"
// KMK -->
private const val FILTER_LIBRARY_CATEGORIES_INCLUDE_PREF_KEY = "pref_filter_library_categories_include"
private const val FILTER_LIBRARY_CATEGORIES_EXCLUDE_PREF_KEY = "pref_filter_library_categories_exclude"
// KMK <--
val categoryPreferenceKeys = setOf( val categoryPreferenceKeys = setOf(
DEFAULT_CATEGORY_PREF_KEY, DEFAULT_CATEGORY_PREF_KEY,
LIBRARY_UPDATE_CATEGORIES_PREF_KEY, LIBRARY_UPDATE_CATEGORIES_PREF_KEY,
LIBRARY_UPDATE_CATEGORIES_EXCLUDE_PREF_KEY, LIBRARY_UPDATE_CATEGORIES_EXCLUDE_PREF_KEY,
// KMK -->
FILTER_LIBRARY_CATEGORIES_INCLUDE_PREF_KEY,
FILTER_LIBRARY_CATEGORIES_EXCLUDE_PREF_KEY,
// KMK <--
) )
} }
} }

View file

@ -75,6 +75,7 @@
<string name="pref_show_updating_progress_banner">Show updating progress banner</string> <string name="pref_show_updating_progress_banner">Show updating progress banner</string>
<string name="action_display_source_badge">Source icon</string> <string name="action_display_source_badge">Source icon</string>
<string name="action_display_language_icon">Use language icon</string> <string name="action_display_language_icon">Use language icon</string>
<string name="pref_library_filter_categories_details">Entries in excluded categories will not be shown even if they are also in included categories.</string>
<!-- Extension section --> <!-- Extension section -->
<string name="extensions_page_need_refresh">Refresh extension page to update list.</string> <string name="extensions_page_need_refresh">Refresh extension page to update list.</string>
<string name="extensions_page_more">More extensions...</string> <string name="extensions_page_more">More extensions...</string>