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:
parent
70fcc3dff0
commit
7f820b590c
12 changed files with 252 additions and 11 deletions
|
|
@ -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()) }
|
||||||
|
|
|
||||||
|
|
@ -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 <--
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 -> {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,13 @@ 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 <--
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <--
|
||||||
|
|
|
||||||
|
|
@ -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 <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue