Add category filters to Upcoming calendar (#3607)
This commit is contained in:
@@ -13,6 +13,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- Add Category filtering for the Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#3589](https://github.com/mihonapp/mihon/pull/3589))
|
- Add Category filtering for the Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#3589](https://github.com/mihonapp/mihon/pull/3589))
|
||||||
|
- Add Category filtering for the Upcoming calendar ([@MajorTanya](https://github.com/MajorTanya)) ([#3607](https://github.com/mihonapp/mihon/pull/3607))
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Update default user agent to Chrome 149 ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
- Update default user agent to Chrome 149 ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
|||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.storage.service.StoragePreferences
|
import tachiyomi.domain.storage.service.StoragePreferences
|
||||||
|
import tachiyomi.domain.upcoming.service.UpcomingPreferences
|
||||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
@@ -47,6 +48,9 @@ class PreferenceModule(val app: Application) : InjektModule {
|
|||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
LibraryPreferences(get())
|
LibraryPreferences(get())
|
||||||
}
|
}
|
||||||
|
addSingletonFactory {
|
||||||
|
UpcomingPreferences(get())
|
||||||
|
}
|
||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
UpdatesPreferences(get())
|
UpdatesPreferences(get())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,38 @@
|
|||||||
package mihon.feature.upcoming
|
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.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.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 androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
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.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
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() {
|
class UpcomingScreen : Screen() {
|
||||||
|
|
||||||
@@ -18,10 +43,93 @@ class UpcomingScreen : Screen() {
|
|||||||
val viewModel = viewModel<UpcomingViewModel>()
|
val viewModel = viewModel<UpcomingViewModel>()
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
when (state.dialog) {
|
||||||
|
is UpcomingViewModel.Dialog.FilterSheet -> {
|
||||||
|
UpcomingFilterDialog(
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
|
||||||
UpcomingScreenContent(
|
UpcomingScreenContent(
|
||||||
state = state,
|
state = state,
|
||||||
setSelectedYearMonth = viewModel::setSelectedYearMonth,
|
setSelectedYearMonth = viewModel::setSelectedYearMonth,
|
||||||
onClickUpcoming = { navigator.push(MangaScreen(it.id)) },
|
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.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||||
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
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
|
||||||
@@ -23,6 +23,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.relativeDateText
|
import eu.kanade.presentation.components.relativeDateText
|
||||||
import eu.kanade.presentation.util.isTabletUi
|
import eu.kanade.presentation.util.isTabletUi
|
||||||
import kotlinx.coroutines.launch
|
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.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.theme.active
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpcomingScreenContent(
|
fun UpcomingScreenContent(
|
||||||
state: UpcomingViewModel.State,
|
state: UpcomingViewModel.State,
|
||||||
setSelectedYearMonth: (YearMonth) -> Unit,
|
setSelectedYearMonth: (YearMonth) -> Unit,
|
||||||
onClickUpcoming: (manga: Manga) -> Unit,
|
onClickUpcoming: (manga: Manga) -> Unit,
|
||||||
|
onClickFilter: () -> Unit,
|
||||||
|
hasActiveFilters: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -56,7 +60,12 @@ fun UpcomingScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { UpcomingToolbar() },
|
topBar = {
|
||||||
|
UpcomingToolbar(
|
||||||
|
hasFilters = hasActiveFilters,
|
||||||
|
onClickFilter = onClickFilter,
|
||||||
|
)
|
||||||
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
if (isTabletUi()) {
|
if (isTabletUi()) {
|
||||||
@@ -86,7 +95,10 @@ fun UpcomingScreenContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UpcomingToolbar() {
|
private fun UpcomingToolbar(
|
||||||
|
hasFilters: Boolean,
|
||||||
|
onClickFilter: () -> Unit,
|
||||||
|
) {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
@@ -94,12 +106,21 @@ private fun UpcomingToolbar() {
|
|||||||
title = stringResource(MR.strings.label_upcoming),
|
title = stringResource(MR.strings.label_upcoming),
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = { uriHandler.openUri(Constants.URL_HELP_UPCOMING) }) {
|
AppBarActions(
|
||||||
Icon(
|
listOf(
|
||||||
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
|
AppBar.Action(
|
||||||
contentDescription = stringResource(MR.strings.upcoming_guide),
|
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) },
|
onClick = { onClickUpcoming(item.manga) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is UpcomingUIModel.Header -> {
|
is UpcomingUIModel.Header -> {
|
||||||
DateHeading(
|
DateHeading(
|
||||||
date = item.date,
|
date = item.date,
|
||||||
@@ -222,6 +244,7 @@ private fun UpcomingScreenLargeImpl(
|
|||||||
onClick = { onClickUpcoming(item.manga) },
|
onClick = { onClickUpcoming(item.manga) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is UpcomingUIModel.Header -> {
|
is UpcomingUIModel.Header -> {
|
||||||
DateHeading(
|
DateHeading(
|
||||||
date = item.date,
|
date = item.date,
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package mihon.feature.upcoming
|
package mihon.feature.upcoming
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.util.fastMap
|
import androidx.compose.ui.util.fastMap
|
||||||
import androidx.compose.ui.util.fastMapIndexedNotNull
|
import androidx.compose.ui.util.fastMapIndexedNotNull
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import eu.kanade.core.util.insertSeparatorsReversed
|
import eu.kanade.core.util.insertSeparatorsReversed
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
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.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.YearMonth
|
import kotlinx.datetime.YearMonth
|
||||||
@@ -14,27 +18,46 @@ import kotlinx.datetime.toLocalDateTime
|
|||||||
import kotlinx.datetime.yearMonth
|
import kotlinx.datetime.yearMonth
|
||||||
import mihon.core.viewmodel.StateViewModel
|
import mihon.core.viewmodel.StateViewModel
|
||||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
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.manga.model.Manga
|
||||||
|
import tachiyomi.domain.upcoming.service.UpcomingPreferences
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
class UpcomingViewModel(
|
class UpcomingViewModel(
|
||||||
private val getUpcomingManga: GetUpcomingManga = Injekt.get(),
|
private val getUpcomingManga: GetUpcomingManga = Injekt.get(),
|
||||||
|
val getCategories: GetCategories = Injekt.get(),
|
||||||
|
val upcomingPreferences: UpcomingPreferences = Injekt.get(),
|
||||||
) : StateViewModel<UpcomingViewModel.State>(State()) {
|
) : StateViewModel<UpcomingViewModel.State>(State()) {
|
||||||
|
|
||||||
|
val excludedCategories = upcomingPreferences.filterExcludedCategories
|
||||||
|
val includedCategories = upcomingPreferences.filterIncludedCategories
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launchIO {
|
||||||
getUpcomingManga.subscribe().collectLatest {
|
getUpcomingItemPreferenceFlow()
|
||||||
mutableState.update { state ->
|
.distinctUntilChanged()
|
||||||
val upcomingItems = it.toUpcomingUIModels()
|
.flatMapLatest {
|
||||||
state.copy(
|
getUpcomingManga.subscribe(
|
||||||
items = upcomingItems,
|
excludedCategories = it.filterExcludedCategories,
|
||||||
events = upcomingItems.toEvents(),
|
includedCategories = it.filterIncludedCategories,
|
||||||
headerIndexes = upcomingItems.getHeaderIndexes(),
|
|
||||||
)
|
)
|
||||||
|
.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) }
|
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(
|
data class State(
|
||||||
val selectedYearMonth: YearMonth = Clock.System.now()
|
val selectedYearMonth: YearMonth = Clock.System.now()
|
||||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
@@ -89,5 +150,11 @@ class UpcomingViewModel(
|
|||||||
val items: List<UpcomingUIModel> = listOf(),
|
val items: List<UpcomingUIModel> = listOf(),
|
||||||
val events: Map<LocalDate, Int> = mapOf(),
|
val events: Map<LocalDate, Int> = mapOf(),
|
||||||
val headerIndexes: 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,12 +87,24 @@ class MangaRepositoryImpl(
|
|||||||
.awaitAsList()
|
.awaitAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>> {
|
override suspend fun getUpcomingManga(
|
||||||
|
statuses: Set<Long>,
|
||||||
|
excludedCategories: List<Long>,
|
||||||
|
includedCategories: List<Long>,
|
||||||
|
): Flow<List<Manga>> {
|
||||||
val timeZone = TimeZone.currentSystemDefault()
|
val timeZone = TimeZone.currentSystemDefault()
|
||||||
val epochMillis =
|
val epochMillis =
|
||||||
Clock.System.now().toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone).toEpochMilliseconds()
|
Clock.System.now().toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone).toEpochMilliseconds()
|
||||||
return database.mangasQueries
|
return database.mangasQueries
|
||||||
.getUpcomingManga(epochMillis, statuses, MangaMapper::mapManga)
|
.getUpcomingManga(
|
||||||
|
startOfDay = epochMillis,
|
||||||
|
statuses = statuses,
|
||||||
|
includedEmpty = includedCategories.isEmpty(),
|
||||||
|
includedCategories = includedCategories,
|
||||||
|
excludedEmpty = excludedCategories.isEmpty(),
|
||||||
|
excludedCategories = excludedCategories,
|
||||||
|
mapper = MangaMapper::mapManga,
|
||||||
|
)
|
||||||
.subscribeToList()
|
.subscribeToList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,33 @@ FROM mangas
|
|||||||
WHERE next_update >= :startOfDay
|
WHERE next_update >= :startOfDay
|
||||||
AND favorite = 1
|
AND favorite = 1
|
||||||
AND status IN :statuses
|
AND status IN :statuses
|
||||||
|
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 = mangas._id
|
||||||
|
AND COALESCE(mangas_categories.category_id, 0) IN :includedCategories
|
||||||
|
)
|
||||||
|
OR (0 IN :includedCategories AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangas._id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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 = mangas._id
|
||||||
|
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 = mangas._id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
ORDER BY next_update ASC;
|
ORDER BY next_update ASC;
|
||||||
|
|
||||||
resetViewerFlags:
|
resetViewerFlags:
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ class GetUpcomingManga(
|
|||||||
SManga.PUBLISHING_FINISHED.toLong(),
|
SManga.PUBLISHING_FINISHED.toLong(),
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun subscribe(): Flow<List<Manga>> {
|
suspend fun subscribe(
|
||||||
return mangaRepository.getUpcomingManga(includedStatuses)
|
excludedCategories: List<Long>,
|
||||||
|
includedCategories: List<Long>,
|
||||||
|
): Flow<List<Manga>> {
|
||||||
|
return mangaRepository.getUpcomingManga(
|
||||||
|
includedStatuses,
|
||||||
|
excludedCategories = excludedCategories,
|
||||||
|
includedCategories = includedCategories,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ interface MangaRepository {
|
|||||||
|
|
||||||
suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount>
|
suspend fun getDuplicateLibraryManga(id: Long, title: String): List<MangaWithChapterCount>
|
||||||
|
|
||||||
suspend fun getUpcomingManga(statuses: Set<Long>): Flow<List<Manga>>
|
suspend fun getUpcomingManga(
|
||||||
|
statuses: Set<Long>,
|
||||||
|
excludedCategories: List<Long>,
|
||||||
|
includedCategories: List<Long>,
|
||||||
|
): Flow<List<Manga>>
|
||||||
|
|
||||||
suspend fun resetViewerFlags(): Boolean
|
suspend fun resetViewerFlags(): Boolean
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package tachiyomi.domain.upcoming.service
|
||||||
|
|
||||||
|
import tachiyomi.core.common.preference.Preference
|
||||||
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
|
import tachiyomi.core.common.preference.getLongArray
|
||||||
|
|
||||||
|
class UpcomingPreferences(
|
||||||
|
preferenceStore: PreferenceStore,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val filterIncludedCategories: Preference<List<Long>> = preferenceStore.getLongArray(
|
||||||
|
"pref_filter_upcoming_included_categories",
|
||||||
|
emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val filterExcludedCategories: Preference<List<Long>> = preferenceStore.getLongArray(
|
||||||
|
"pref_filter_upcoming_excluded_categories",
|
||||||
|
emptyList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -867,6 +867,7 @@
|
|||||||
<string name="upcoming_guide">Upcoming Guide</string>
|
<string name="upcoming_guide">Upcoming Guide</string>
|
||||||
<string name="upcoming_calendar_next">Next Month</string>
|
<string name="upcoming_calendar_next">Next Month</string>
|
||||||
<string name="upcoming_calendar_prev">Previous Month</string>
|
<string name="upcoming_calendar_prev">Previous Month</string>
|
||||||
|
<string name="pref_filter_upcoming_categories_details">Titles in excluded categories are not shown, even if they are also in any of the included categories.</string>
|
||||||
|
|
||||||
<!-- History -->
|
<!-- History -->
|
||||||
<string name="recent_manga_time">Ch. %1$s - %2$s</string>
|
<string name="recent_manga_time">Ch. %1$s - %2$s</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user