Add category filters to Upcoming calendar (#3607)
This commit is contained in:
@@ -17,6 +17,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.upcoming.service.UpcomingPreferences
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
@@ -47,6 +48,9 @@ class PreferenceModule(val app: Application) : InjektModule {
|
||||
addSingletonFactory {
|
||||
LibraryPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
UpcomingPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
UpdatesPreferences(get())
|
||||
}
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
package mihon.feature.upcoming
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
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.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.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.presentation.components.TabbedDialog
|
||||
import eu.kanade.presentation.components.TabbedDialogPaddings
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.i18n.MR
|
||||
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
|
||||
|
||||
class UpcomingScreen : Screen() {
|
||||
|
||||
@@ -18,10 +43,93 @@ class UpcomingScreen : Screen() {
|
||||
val viewModel = viewModel<UpcomingViewModel>()
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
when (state.dialog) {
|
||||
is UpcomingViewModel.Dialog.FilterSheet -> {
|
||||
UpcomingFilterDialog(
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
|
||||
UpcomingScreenContent(
|
||||
state = state,
|
||||
setSelectedYearMonth = viewModel::setSelectedYearMonth,
|
||||
onClickUpcoming = { navigator.push(MangaScreen(it.id)) },
|
||||
hasActiveFilters = state.hasActiveFilters,
|
||||
onClickFilter = viewModel::showFilterDialog,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.CategoryFilterSheet(
|
||||
viewModel: UpcomingViewModel,
|
||||
) {
|
||||
Text(
|
||||
stringResource(MR.strings.pref_filter_upcoming_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()) {
|
||||
LoadingScreen(modifier = Modifier.padding(16.dp))
|
||||
return
|
||||
}
|
||||
|
||||
val excluded by viewModel.upcomingPreferences.filterExcludedCategories.collectAsState()
|
||||
val included by viewModel.upcomingPreferences.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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpcomingFilterDialog(
|
||||
viewModel: UpcomingViewModel,
|
||||
) {
|
||||
TabbedDialog(
|
||||
onDismissRequest = viewModel::resetDialog,
|
||||
tabTitles = listOf(
|
||||
stringResource(MR.strings.categories),
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
CategoryFilterSheet(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.relativeDateText
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -38,12 +39,15 @@ import tachiyomi.presentation.core.components.TwoPanelBox
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.theme.active
|
||||
|
||||
@Composable
|
||||
fun UpcomingScreenContent(
|
||||
state: UpcomingViewModel.State,
|
||||
setSelectedYearMonth: (YearMonth) -> Unit,
|
||||
onClickUpcoming: (manga: Manga) -> Unit,
|
||||
onClickFilter: () -> Unit,
|
||||
hasActiveFilters: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -56,7 +60,12 @@ fun UpcomingScreenContent(
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
topBar = { UpcomingToolbar() },
|
||||
topBar = {
|
||||
UpcomingToolbar(
|
||||
hasFilters = hasActiveFilters,
|
||||
onClickFilter = onClickFilter,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { paddingValues ->
|
||||
if (isTabletUi()) {
|
||||
@@ -86,7 +95,10 @@ fun UpcomingScreenContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpcomingToolbar() {
|
||||
private fun UpcomingToolbar(
|
||||
hasFilters: Boolean,
|
||||
onClickFilter: () -> Unit,
|
||||
) {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
@@ -94,12 +106,21 @@ private fun UpcomingToolbar() {
|
||||
title = stringResource(MR.strings.label_upcoming),
|
||||
navigateUp = navigator::pop,
|
||||
actions = {
|
||||
IconButton(onClick = { uriHandler.openUri(Constants.URL_HELP_UPCOMING) }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
contentDescription = stringResource(MR.strings.upcoming_guide),
|
||||
)
|
||||
}
|
||||
AppBarActions(
|
||||
listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Outlined.FilterList,
|
||||
iconTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current,
|
||||
onClick = onClickFilter,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.upcoming_guide),
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
onClick = { uriHandler.openUri(Constants.URL_HELP_UPCOMING) },
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -171,6 +192,7 @@ private fun UpcomingScreenSmallImpl(
|
||||
onClick = { onClickUpcoming(item.manga) },
|
||||
)
|
||||
}
|
||||
|
||||
is UpcomingUIModel.Header -> {
|
||||
DateHeading(
|
||||
date = item.date,
|
||||
@@ -222,6 +244,7 @@ private fun UpcomingScreenLargeImpl(
|
||||
onClick = { onClickUpcoming(item.manga) },
|
||||
)
|
||||
}
|
||||
|
||||
is UpcomingUIModel.Header -> {
|
||||
DateHeading(
|
||||
date = item.date,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package mihon.feature.upcoming
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import androidx.compose.ui.util.fastMapIndexedNotNull
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import eu.kanade.core.util.insertSeparatorsReversed
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.YearMonth
|
||||
@@ -14,27 +18,46 @@ import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.datetime.yearMonth
|
||||
import mihon.core.viewmodel.StateViewModel
|
||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.upcoming.service.UpcomingPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.time.Clock
|
||||
|
||||
class UpcomingViewModel(
|
||||
private val getUpcomingManga: GetUpcomingManga = Injekt.get(),
|
||||
val getCategories: GetCategories = Injekt.get(),
|
||||
val upcomingPreferences: UpcomingPreferences = Injekt.get(),
|
||||
) : StateViewModel<UpcomingViewModel.State>(State()) {
|
||||
|
||||
val excludedCategories = upcomingPreferences.filterExcludedCategories
|
||||
val includedCategories = upcomingPreferences.filterIncludedCategories
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
getUpcomingManga.subscribe().collectLatest {
|
||||
mutableState.update { state ->
|
||||
val upcomingItems = it.toUpcomingUIModels()
|
||||
state.copy(
|
||||
items = upcomingItems,
|
||||
events = upcomingItems.toEvents(),
|
||||
headerIndexes = upcomingItems.getHeaderIndexes(),
|
||||
viewModelScope.launchIO {
|
||||
getUpcomingItemPreferenceFlow()
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest {
|
||||
getUpcomingManga.subscribe(
|
||||
excludedCategories = it.filterExcludedCategories,
|
||||
includedCategories = it.filterIncludedCategories,
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
.collectLatest {
|
||||
mutableState.update { state ->
|
||||
val upcomingItems = it.toUpcomingUIModels()
|
||||
state.copy(
|
||||
items = upcomingItems,
|
||||
events = upcomingItems.toEvents(),
|
||||
headerIndexes = upcomingItems.getHeaderIndexes(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +104,44 @@ class UpcomingViewModel(
|
||||
mutableState.update { it.copy(selectedYearMonth = yearMonth) }
|
||||
}
|
||||
|
||||
private fun getUpcomingItemPreferenceFlow(): Flow<ItemPreferences> {
|
||||
return combine(
|
||||
upcomingPreferences.filterExcludedCategories.changes(),
|
||||
upcomingPreferences.filterIncludedCategories.changes(),
|
||||
) { excluded, included ->
|
||||
ItemPreferences(
|
||||
filterExcludedCategories = excluded,
|
||||
filterIncludedCategories = included,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetDialog() {
|
||||
mutableState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
fun showFilterDialog() {
|
||||
mutableState.update { it.copy(dialog = Dialog.FilterSheet) }
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private data class ItemPreferences(
|
||||
val filterExcludedCategories: List<Long>,
|
||||
val filterIncludedCategories: List<Long>,
|
||||
)
|
||||
|
||||
data class State(
|
||||
val selectedYearMonth: YearMonth = Clock.System.now()
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
@@ -89,5 +150,11 @@ class UpcomingViewModel(
|
||||
val items: List<UpcomingUIModel> = listOf(),
|
||||
val events: Map<LocalDate, Int> = mapOf(),
|
||||
val headerIndexes: Map<LocalDate, Int> = mapOf(),
|
||||
val hasActiveFilters: Boolean = false,
|
||||
val dialog: Dialog? = null,
|
||||
)
|
||||
|
||||
sealed interface Dialog {
|
||||
data object FilterSheet : Dialog
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user