From 727399611ddff3530d66ad7a758f357f95053b92 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 9 Dec 2022 17:34:24 -0500 Subject: [PATCH] Migrate library settings sheet to Compose --- .../library/service/LibraryPreferences.kt | 14 +- .../presentation/components/SettingsItems.kt | 49 +- .../presentation/components/TabbedDialog.kt | 22 +- .../library/LibrarySettingsDialog.kt | 249 +++++++++ .../java/eu/kanade/tachiyomi/Migrations.kt | 6 +- .../ui/library/LibraryScreenModel.kt | 35 +- .../ui/library/LibrarySettingsScreenModel.kt | 84 ++++ .../ui/library/LibrarySettingsSheet.kt | 474 ------------------ .../kanade/tachiyomi/ui/library/LibraryTab.kt | 23 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 28 -- .../widget/ExtendedNavigationView.kt | 270 ---------- .../eu/kanade/tachiyomi/widget/TriState.kt | 19 + 12 files changed, 443 insertions(+), 830 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt diff --git a/app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt b/app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt index a29893dd69..2394696012 100644 --- a/app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt @@ -4,7 +4,7 @@ import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ -import eu.kanade.tachiyomi.widget.ExtendedNavigationView +import eu.kanade.tachiyomi.widget.TriState import tachiyomi.core.preference.PreferenceStore import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibrarySort @@ -36,17 +36,17 @@ class LibraryPreferences( // region Filter - fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value) + fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", TriState.DISABLED.value) - fun filterUnread() = preferenceStore.getInt("pref_filter_library_unread", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value) + fun filterUnread() = preferenceStore.getInt("pref_filter_library_unread", TriState.DISABLED.value) - fun filterStarted() = preferenceStore.getInt("pref_filter_library_started", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value) + fun filterStarted() = preferenceStore.getInt("pref_filter_library_started", TriState.DISABLED.value) - fun filterBookmarked() = preferenceStore.getInt("pref_filter_library_bookmarked", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value) + fun filterBookmarked() = preferenceStore.getInt("pref_filter_library_bookmarked", TriState.DISABLED.value) - fun filterCompleted() = preferenceStore.getInt("pref_filter_library_completed", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value) + fun filterCompleted() = preferenceStore.getInt("pref_filter_library_completed", TriState.DISABLED.value) - fun filterTracking(name: Int) = preferenceStore.getInt("pref_filter_library_tracked_$name", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value) + fun filterTracking(name: Int) = preferenceStore.getInt("pref_filter_library_tracked_$name", TriState.DISABLED.value) // endregion diff --git a/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt b/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt index e4fa2172a8..f670acf54b 100644 --- a/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt +++ b/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt @@ -1,5 +1,6 @@ package eu.kanade.presentation.components +import androidx.annotation.StringRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -14,6 +15,7 @@ import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.rounded.CheckBox import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank import androidx.compose.material.icons.rounded.DisabledByDefault +import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton @@ -21,19 +23,35 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import tachiyomi.domain.manga.model.TriStateFilter +import tachiyomi.presentation.core.theme.header + +@Composable +fun HeadingItem( + @StringRes labelRes: Int, +) { + Text( + text = stringResource(labelRes), + style = MaterialTheme.typography.header, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp), + ) +} @Composable fun TriStateItem( label: String, state: TriStateFilter, + enabled: Boolean = true, onClick: ((TriStateFilter) -> Unit)?, ) { Row( modifier = Modifier .clickable( - enabled = onClick != null, + enabled = enabled && onClick != null, onClick = { when (state) { TriStateFilter.DISABLED -> onClick?.invoke(TriStateFilter.ENABLED_IS) @@ -47,7 +65,7 @@ fun TriStateItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp), ) { - val stateAlpha = if (onClick != null) 1f else ContentAlpha.disabled + val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled Icon( imageVector = when (state) { @@ -56,7 +74,7 @@ fun TriStateItem( TriStateFilter.ENABLED_NOT -> Icons.Rounded.DisabledByDefault }, contentDescription = null, - tint = if (state == TriStateFilter.DISABLED) { + tint = if (!enabled || state == TriStateFilter.DISABLED) { MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha) } else { when (onClick) { @@ -109,6 +127,31 @@ fun SortItem( } } +@Composable +fun CheckboxItem( + label: String, + checked: Boolean, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .clickable(onClick = onClick) + .fillMaxWidth() + .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(24.dp), + ) { + Checkbox( + checked = checked, + onCheckedChange = null, + ) + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + ) + } +} + @Composable fun RadioItem( label: String, diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt index f7438e175b..15af354496 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt @@ -1,11 +1,11 @@ package eu.kanade.presentation.components +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert @@ -20,12 +20,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -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.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed @@ -85,26 +82,13 @@ fun TabbedDialog( } Divider() - val density = LocalDensity.current - var largestHeight by rememberSaveable { mutableStateOf(0f) } HorizontalPager( - modifier = Modifier.heightIn(min = largestHeight.dp), + modifier = Modifier.animateContentSize(), count = tabTitles.size, state = pagerState, verticalAlignment = Alignment.Top, ) { page -> - Box( - modifier = Modifier.onSizeChanged { - with(density) { - val heightDp = it.height.toDp() - if (heightDp.value > largestHeight) { - largestHeight = heightDp.value - } - } - }, - ) { - content(contentPadding, page) - } + content(contentPadding, page) } } } diff --git a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt new file mode 100644 index 0000000000..afa6ccad66 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt @@ -0,0 +1,249 @@ +package eu.kanade.presentation.library + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import eu.kanade.domain.library.service.LibraryPreferences +import eu.kanade.presentation.components.CheckboxItem +import eu.kanade.presentation.components.HeadingItem +import eu.kanade.presentation.components.RadioItem +import eu.kanade.presentation.components.SortItem +import eu.kanade.presentation.components.TabbedDialog +import eu.kanade.presentation.components.TabbedDialogPaddings +import eu.kanade.presentation.components.TriStateItem +import eu.kanade.presentation.util.collectAsState +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel +import eu.kanade.tachiyomi.widget.toTriStateFilter +import tachiyomi.domain.category.model.Category +import tachiyomi.domain.library.model.LibraryDisplayMode +import tachiyomi.domain.library.model.LibrarySort +import tachiyomi.domain.library.model.display +import tachiyomi.domain.library.model.sort +import tachiyomi.domain.manga.model.TriStateFilter + +@Composable +fun LibrarySettingsDialog( + onDismissRequest: () -> Unit, + screenModel: LibrarySettingsScreenModel, + activeCategoryIndex: Int, +) { + val state by screenModel.state.collectAsState() + val category by remember(activeCategoryIndex) { + derivedStateOf { state.categories[activeCategoryIndex] } + } + + TabbedDialog( + onDismissRequest = onDismissRequest, + tabTitles = listOf( + stringResource(R.string.action_filter), + stringResource(R.string.action_sort), + stringResource(R.string.action_display), + ), + ) { contentPadding, page -> + Column( + modifier = Modifier + .padding(contentPadding) + .padding(vertical = TabbedDialogPaddings.Vertical) + .verticalScroll(rememberScrollState()), + ) { + when (page) { + 0 -> FilterPage( + screenModel = screenModel, + ) + 1 -> SortPage( + category = category, + screenModel = screenModel, + ) + 2 -> DisplayPage( + category = category, + screenModel = screenModel, + ) + } + } + } +} + +@Composable +private fun ColumnScope.FilterPage( + screenModel: LibrarySettingsScreenModel, +) { + val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState() + val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState() + TriStateItem( + label = stringResource(R.string.label_downloaded), + state = if (downloadedOnly) { + TriStateFilter.ENABLED_IS + } else { + filterDownloaded.toTriStateFilter() + }, + enabled = !downloadedOnly, + onClick = { screenModel.toggleFilter(LibraryPreferences::filterDownloaded) }, + ) + val filterUnread by screenModel.libraryPreferences.filterUnread().collectAsState() + TriStateItem( + label = stringResource(R.string.action_filter_unread), + state = filterUnread.toTriStateFilter(), + onClick = { screenModel.toggleFilter(LibraryPreferences::filterUnread) }, + ) + val filterStarted by screenModel.libraryPreferences.filterStarted().collectAsState() + TriStateItem( + label = stringResource(R.string.label_started), + state = filterStarted.toTriStateFilter(), + onClick = { screenModel.toggleFilter(LibraryPreferences::filterStarted) }, + ) + val filterBookmarked by screenModel.libraryPreferences.filterBookmarked().collectAsState() + TriStateItem( + label = stringResource(R.string.action_filter_bookmarked), + state = filterBookmarked.toTriStateFilter(), + onClick = { screenModel.toggleFilter(LibraryPreferences::filterBookmarked) }, + ) + val filterCompleted by screenModel.libraryPreferences.filterCompleted().collectAsState() + TriStateItem( + label = stringResource(R.string.completed), + state = filterCompleted.toTriStateFilter(), + onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) }, + ) + + when (screenModel.trackServices.size) { + 0 -> { + // No trackers + } + 1 -> { + val service = screenModel.trackServices[0] + val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState() + TriStateItem( + label = stringResource(R.string.action_filter_tracked), + state = filterTracker.toTriStateFilter(), + onClick = { screenModel.toggleTracker(service.id.toInt()) }, + ) + } + else -> { + HeadingItem(R.string.action_filter_tracked) + screenModel.trackServices.map { service -> + val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState() + TriStateItem( + label = stringResource(service.nameRes()), + state = filterTracker.toTriStateFilter(), + onClick = { screenModel.toggleTracker(service.id.toInt()) }, + ) + } + } + } +} + +@Composable +private fun ColumnScope.SortPage( + category: Category, + screenModel: LibrarySettingsScreenModel, +) { + val sortingMode = category.sort.type + val sortDescending = !category.sort.isAscending + + listOf( + R.string.action_sort_alpha to LibrarySort.Type.Alphabetical, + R.string.action_sort_total to LibrarySort.Type.TotalChapters, + R.string.action_sort_last_read to LibrarySort.Type.LastRead, + R.string.action_sort_last_manga_update to LibrarySort.Type.LastUpdate, + R.string.action_sort_unread_count to LibrarySort.Type.UnreadCount, + R.string.action_sort_latest_chapter to LibrarySort.Type.LatestChapter, + R.string.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate, + R.string.action_sort_date_added to LibrarySort.Type.DateAdded, + ).map { (titleRes, mode) -> + SortItem( + label = stringResource(titleRes), + sortDescending = sortDescending.takeIf { sortingMode == mode }, + onClick = { + val isTogglingDirection = sortingMode == mode + val direction = when { + isTogglingDirection -> if (sortDescending) LibrarySort.Direction.Ascending else LibrarySort.Direction.Descending + else -> if (sortDescending) LibrarySort.Direction.Descending else LibrarySort.Direction.Ascending + } + screenModel.setSort(category, mode, direction) + }, + ) + } +} + +@Composable +private fun ColumnScope.DisplayPage( + category: Category, + screenModel: LibrarySettingsScreenModel, +) { + HeadingItem(R.string.action_display_mode) + listOf( + R.string.action_display_grid to LibraryDisplayMode.CompactGrid, + R.string.action_display_comfortable_grid to LibraryDisplayMode.ComfortableGrid, + R.string.action_display_cover_only_grid to LibraryDisplayMode.CoverOnlyGrid, + R.string.action_display_list to LibraryDisplayMode.List, + ).map { (titleRes, mode) -> + RadioItem( + label = stringResource(titleRes), + selected = category.display == mode, + onClick = { screenModel.setDisplayMode(category, mode) }, + ) + } + + HeadingItem(R.string.badges_header) + val downloadBadge by screenModel.libraryPreferences.downloadBadge().collectAsState() + CheckboxItem( + label = stringResource(R.string.action_display_download_badge), + checked = downloadBadge, + onClick = { + screenModel.togglePreference(LibraryPreferences::downloadBadge) + }, + ) + val localBadge by screenModel.libraryPreferences.localBadge().collectAsState() + CheckboxItem( + label = stringResource(R.string.action_display_local_badge), + checked = localBadge, + onClick = { + screenModel.togglePreference(LibraryPreferences::localBadge) + }, + ) + val languageBadge by screenModel.libraryPreferences.languageBadge().collectAsState() + CheckboxItem( + label = stringResource(R.string.action_display_language_badge), + checked = languageBadge, + onClick = { + screenModel.togglePreference(LibraryPreferences::languageBadge) + }, + ) + + HeadingItem(R.string.tabs_header) + val categoryTabs by screenModel.libraryPreferences.categoryTabs().collectAsState() + CheckboxItem( + label = stringResource(R.string.action_display_show_tabs), + checked = categoryTabs, + onClick = { + screenModel.togglePreference(LibraryPreferences::categoryTabs) + }, + ) + val categoryNumberOfItems by screenModel.libraryPreferences.categoryNumberOfItems().collectAsState() + CheckboxItem( + label = stringResource(R.string.action_display_show_number_of_items), + checked = categoryNumberOfItems, + onClick = { + screenModel.togglePreference(LibraryPreferences::categoryNumberOfItems) + }, + ) + + HeadingItem(R.string.other_header) + val showContinueReadingButton by screenModel.libraryPreferences.showContinueReadingButton().collectAsState() + CheckboxItem( + label = stringResource(R.string.action_display_show_continue_reading_button), + checked = showContinueReadingButton, + onClick = { + screenModel.togglePreference(LibraryPreferences::showContinueReadingButton) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 821a46c296..dd2e6ee5e2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.util.preference.minusAssign import eu.kanade.tachiyomi.util.preference.plusAssign import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.widget.ExtendedNavigationView +import eu.kanade.tachiyomi.widget.TriState import tachiyomi.core.preference.PreferenceStore import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -114,9 +114,9 @@ object Migrations { fun convertBooleanPrefToTriState(key: String): Int { val oldPrefValue = prefs.getBoolean(key, false) return if (oldPrefValue) { - ExtendedNavigationView.Item.TriStateGroup.State.ENABLED_IS.value + TriState.ENABLED_IS.value } else { - ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value + TriState.DISABLED.value } } prefs.edit { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index 8c13b4a851..e6724463b0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -33,7 +33,7 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.chapter.getNextUnread import eu.kanade.tachiyomi.util.removeCovers -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup +import eu.kanade.tachiyomi.widget.TriState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -149,8 +149,8 @@ class LibraryScreenModel( prefs.filterStarted or prefs.filterBookmarked or prefs.filterCompleted - ) != TriStateGroup.State.DISABLED.value - val b = trackFilter.values.any { it != TriStateGroup.State.DISABLED.value } + ) != TriState.DISABLED.value + val b = trackFilter.values.any { it != TriState.DISABLED.value } a || b } .distinctUntilChanged() @@ -179,17 +179,17 @@ class LibraryScreenModel( val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty() - val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.ENABLED_NOT.value) it.key else null } - val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.ENABLED_IS.value) it.key else null } + val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_NOT.value) it.key else null } + val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_IS.value) it.key else null } val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty() val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ - if (!downloadedOnly && filterDownloaded == TriStateGroup.State.DISABLED.value) return@downloaded true + if (!downloadedOnly && filterDownloaded == TriState.DISABLED.value) return@downloaded true val isDownloaded = it.libraryManga.manga.isLocal() || it.downloadCount > 0 || downloadManager.getDownloadCount(it.libraryManga.manga) > 0 - return@downloaded if (downloadedOnly || filterDownloaded == TriStateGroup.State.ENABLED_IS.value) { + return@downloaded if (downloadedOnly || filterDownloaded == TriState.ENABLED_IS.value) { isDownloaded } else { !isDownloaded @@ -197,10 +197,10 @@ class LibraryScreenModel( } val filterFnUnread: (LibraryItem) -> Boolean = unread@{ - if (filterUnread == TriStateGroup.State.DISABLED.value) return@unread true + if (filterUnread == TriState.DISABLED.value) return@unread true val isUnread = it.libraryManga.unreadCount > 0 - return@unread if (filterUnread == TriStateGroup.State.ENABLED_IS.value) { + return@unread if (filterUnread == TriState.ENABLED_IS.value) { isUnread } else { !isUnread @@ -208,10 +208,10 @@ class LibraryScreenModel( } val filterFnStarted: (LibraryItem) -> Boolean = started@{ - if (filterStarted == TriStateGroup.State.DISABLED.value) return@started true + if (filterStarted == TriState.DISABLED.value) return@started true val hasStarted = it.libraryManga.hasStarted - return@started if (filterStarted == TriStateGroup.State.ENABLED_IS.value) { + return@started if (filterStarted == TriState.ENABLED_IS.value) { hasStarted } else { !hasStarted @@ -219,10 +219,10 @@ class LibraryScreenModel( } val filterFnBookmarked: (LibraryItem) -> Boolean = bookmarked@{ - if (filterBookmarked == TriStateGroup.State.DISABLED.value) return@bookmarked true + if (filterBookmarked == TriState.DISABLED.value) return@bookmarked true val hasBookmarks = it.libraryManga.hasBookmarks - return@bookmarked if (filterBookmarked == TriStateGroup.State.ENABLED_IS.value) { + return@bookmarked if (filterBookmarked == TriState.ENABLED_IS.value) { hasBookmarks } else { !hasBookmarks @@ -230,10 +230,10 @@ class LibraryScreenModel( } val filterFnCompleted: (LibraryItem) -> Boolean = completed@{ - if (filterCompleted == TriStateGroup.State.DISABLED.value) return@completed true + if (filterCompleted == TriState.DISABLED.value) return@completed true val isCompleted = it.libraryManga.manga.status.toInt() == SManga.COMPLETED - return@completed if (filterCompleted == TriStateGroup.State.ENABLED_IS.value) { + return@completed if (filterCompleted == TriState.ENABLED_IS.value) { isCompleted } else { !isCompleted @@ -572,6 +572,10 @@ class LibraryScreenModel( } } + fun showSettingsDialog() { + mutableState.update { it.copy(dialog = Dialog.SettingsSheet) } + } + fun clearSelection() { mutableState.update { it.copy(selection = emptyList()) } } @@ -690,6 +694,7 @@ class LibraryScreenModel( } sealed class Dialog { + object SettingsSheet : Dialog() data class ChangeCategory(val manga: List, val initialSelection: List>) : Dialog() data class DeleteManga(val manga: List) : Dialog() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt new file mode 100644 index 0000000000..6cb681f89c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt @@ -0,0 +1,84 @@ +package eu.kanade.tachiyomi.ui.library + +import androidx.compose.runtime.Immutable +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import eu.kanade.domain.base.BasePreferences +import eu.kanade.domain.category.interactor.SetDisplayModeForCategory +import eu.kanade.domain.category.interactor.SetSortModeForCategory +import eu.kanade.domain.library.service.LibraryPreferences +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.util.preference.toggle +import eu.kanade.tachiyomi.widget.TriState +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update +import tachiyomi.core.preference.Preference +import tachiyomi.core.preference.getAndSet +import tachiyomi.core.util.lang.launchIO +import tachiyomi.domain.category.interactor.GetCategories +import tachiyomi.domain.category.model.Category +import tachiyomi.domain.library.model.LibraryDisplayMode +import tachiyomi.domain.library.model.LibrarySort +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class LibrarySettingsScreenModel( + val preferences: BasePreferences = Injekt.get(), + val libraryPreferences: LibraryPreferences = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val setDisplayModeForCategory: SetDisplayModeForCategory = Injekt.get(), + private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(), + trackManager: TrackManager = Injekt.get(), +) : StateScreenModel(State()) { + + val trackServices = trackManager.services.filter { service -> service.isLogged } + + init { + coroutineScope.launchIO { + getCategories.subscribe() + .collectLatest { + mutableState.update { state -> + state.copy( + categories = it, + ) + } + } + } + } + + fun togglePreference(preference: (LibraryPreferences) -> Preference) { + preference(libraryPreferences).toggle() + } + + fun toggleFilter(preference: (LibraryPreferences) -> Preference) { + preference(libraryPreferences).getAndSet { + when (it) { + TriState.DISABLED.value -> TriState.ENABLED_IS.value + TriState.ENABLED_IS.value -> TriState.ENABLED_NOT.value + TriState.ENABLED_NOT.value -> TriState.DISABLED.value + else -> throw IllegalStateException("Unknown TriStateGroup state: $this") + } + } + } + + fun toggleTracker(id: Int) { + toggleFilter { libraryPreferences.filterTracking(id) } + } + + fun setDisplayMode(category: Category, mode: LibraryDisplayMode) { + coroutineScope.launchIO { + setDisplayModeForCategory.await(category, mode) + } + } + + fun setSort(category: Category, mode: LibrarySort.Type, direction: LibrarySort.Direction) { + coroutineScope.launchIO { + setSortModeForCategory.await(category, mode, direction) + } + } + + @Immutable + data class State( + val categories: List = emptyList(), + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt deleted file mode 100644 index 8db1fc3475..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt +++ /dev/null @@ -1,474 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.app.Activity -import android.content.Context -import android.util.AttributeSet -import android.view.View -import eu.kanade.domain.base.BasePreferences -import eu.kanade.domain.category.interactor.SetDisplayModeForCategory -import eu.kanade.domain.category.interactor.SetSortModeForCategory -import eu.kanade.domain.library.service.LibraryPreferences -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.data.track.TrackService -import eu.kanade.tachiyomi.widget.ExtendedNavigationView -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State -import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import tachiyomi.core.util.lang.launchIO -import tachiyomi.domain.category.model.Category -import tachiyomi.domain.library.model.LibraryDisplayMode -import tachiyomi.domain.library.model.LibrarySort -import tachiyomi.domain.library.model.display -import tachiyomi.domain.library.model.sort -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy - -class LibrarySettingsSheet( - activity: Activity, - private val trackManager: TrackManager = Injekt.get(), - private val setDisplayModeForCategory: SetDisplayModeForCategory = Injekt.get(), - private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(), -) : TabbedBottomSheetDialog(activity) { - - val filters: Filter - private val sort: Sort - private val display: Display - - val sheetScope = CoroutineScope(Job() + Dispatchers.IO) - - init { - filters = Filter(activity) - sort = Sort(activity) - display = Display(activity) - } - - /** - * adjusts selected button to match real state. - * @param currentCategory ID of currently shown category - */ - fun show(currentCategory: Category) { - filters.adjustFilterSelection() - - sort.currentCategory = currentCategory - sort.adjustDisplaySelection() - - display.currentCategory = currentCategory - display.adjustDisplaySelection() - - super.show() - } - - override fun getTabViews(): List = listOf( - filters, - sort, - display, - ) - - override fun getTabTitles(): List = listOf( - R.string.action_filter, - R.string.action_sort, - R.string.action_display, - ) - - /** - * Filters group (unread, downloaded, ...). - */ - inner class Filter @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - Settings(context, attrs) { - - private val filterGroup = FilterGroup() - - init { - setGroups(listOf(filterGroup)) - } - - // Refreshes Filter Setting selections - fun adjustFilterSelection() { - filterGroup.initModels() - filterGroup.items.forEach { adapter.notifyItemChanged(it) } - } - - /** - * Returns true if there's at least one filter from [FilterGroup] active. - */ - fun hasActiveFilters(): Boolean { - return filterGroup.items.filterIsInstance().any { it.state != State.DISABLED.value } - } - - inner class FilterGroup : Group { - - private val downloaded = Item.TriStateGroup(R.string.label_downloaded, this) - private val unread = Item.TriStateGroup(R.string.action_filter_unread, this) - private val started = Item.TriStateGroup(R.string.label_started, this) - private val bookmarked = Item.TriStateGroup(R.string.action_filter_bookmarked, this) - private val completed = Item.TriStateGroup(R.string.completed, this) - private val trackFilters: Map - - override val header = null - override val items: List - override val footer = null - - init { - trackManager.services.filter { service -> service.isLogged } - .also { services -> - val size = services.size - trackFilters = services.associate { service -> - Pair(service.id, Item.TriStateGroup(getServiceResId(service, size), this)) - } - val list: MutableList = mutableListOf(downloaded, unread, started, bookmarked, completed) - if (size > 1) list.add(Item.Header(R.string.action_filter_tracked)) - list.addAll(trackFilters.values) - items = list - } - } - - private fun getServiceResId(service: TrackService, size: Int): Int { - return if (size > 1) service.nameRes() else R.string.action_filter_tracked - } - - override fun initModels() { - if (preferences.downloadedOnly().get()) { - downloaded.state = State.ENABLED_IS.value - downloaded.enabled = false - } else { - downloaded.state = libraryPreferences.filterDownloaded().get() - downloaded.enabled = true - } - unread.state = libraryPreferences.filterUnread().get() - started.state = libraryPreferences.filterStarted().get() - bookmarked.state = libraryPreferences.filterBookmarked().get() - completed.state = libraryPreferences.filterCompleted().get() - - trackFilters.forEach { trackFilter -> - trackFilter.value.state = libraryPreferences.filterTracking(trackFilter.key.toInt()).get() - } - } - - override fun onItemClicked(item: Item) { - item as Item.TriStateGroup - val newState = when (item.state) { - State.DISABLED.value -> State.ENABLED_IS.value - State.ENABLED_IS.value -> State.ENABLED_NOT.value - State.ENABLED_NOT.value -> State.DISABLED.value - else -> throw Exception("Unknown State") - } - item.state = newState - when (item) { - downloaded -> libraryPreferences.filterDownloaded().set(newState) - unread -> libraryPreferences.filterUnread().set(newState) - started -> libraryPreferences.filterStarted().set(newState) - bookmarked -> libraryPreferences.filterBookmarked().set(newState) - completed -> libraryPreferences.filterCompleted().set(newState) - else -> { - trackFilters.forEach { trackFilter -> - if (trackFilter.value == item) { - libraryPreferences.filterTracking(trackFilter.key.toInt()).set(newState) - } - } - } - } - - adapter.notifyItemChanged(item) - } - } - } - - /** - * Sorting group (alphabetically, by last read, ...) and ascending or descending. - */ - inner class Sort @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - Settings(context, attrs) { - - private val sort = SortGroup() - - init { - setGroups(listOf(sort)) - } - - // Refreshes Display Setting selections - fun adjustDisplaySelection() { - sort.initModels() - sort.items.forEach { adapter.notifyItemChanged(it) } - } - - inner class SortGroup : Group { - - private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this) - private val total = Item.MultiSort(R.string.action_sort_total, this) - private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this) - private val lastChecked = Item.MultiSort(R.string.action_sort_last_manga_update, this) - private val unread = Item.MultiSort(R.string.action_sort_unread_count, this) - private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, this) - private val chapterFetchDate = Item.MultiSort(R.string.action_sort_chapter_fetch_date, this) - private val dateAdded = Item.MultiSort(R.string.action_sort_date_added, this) - - override val header = null - override val items = - listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, chapterFetchDate, dateAdded) - override val footer = null - - override fun initModels() { - val sort = currentCategory.sort - val order = if (sort.isAscending) Item.MultiSort.SORT_ASC else Item.MultiSort.SORT_DESC - - alphabetically.state = - if (sort.type == LibrarySort.Type.Alphabetical) order else Item.MultiSort.SORT_NONE - lastRead.state = - if (sort.type == LibrarySort.Type.LastRead) order else Item.MultiSort.SORT_NONE - lastChecked.state = - if (sort.type == LibrarySort.Type.LastUpdate) order else Item.MultiSort.SORT_NONE - unread.state = - if (sort.type == LibrarySort.Type.UnreadCount) order else Item.MultiSort.SORT_NONE - total.state = - if (sort.type == LibrarySort.Type.TotalChapters) order else Item.MultiSort.SORT_NONE - latestChapter.state = - if (sort.type == LibrarySort.Type.LatestChapter) order else Item.MultiSort.SORT_NONE - chapterFetchDate.state = - if (sort.type == LibrarySort.Type.ChapterFetchDate) order else Item.MultiSort.SORT_NONE - dateAdded.state = - if (sort.type == LibrarySort.Type.DateAdded) order else Item.MultiSort.SORT_NONE - } - - override fun onItemClicked(item: Item) { - item as Item.MultiStateGroup - val prevState = item.state - - item.group.items.forEach { - (it as Item.MultiStateGroup).state = - Item.MultiSort.SORT_NONE - } - item.state = when (prevState) { - Item.MultiSort.SORT_NONE -> Item.MultiSort.SORT_ASC - Item.MultiSort.SORT_ASC -> Item.MultiSort.SORT_DESC - Item.MultiSort.SORT_DESC -> Item.MultiSort.SORT_ASC - else -> throw Exception("Unknown state") - } - - setSortPreference(item) - - item.group.items.forEach { adapter.notifyItemChanged(it) } - } - - private fun setSortPreference(item: Item.MultiStateGroup) { - val mode = when (item) { - alphabetically -> LibrarySort.Type.Alphabetical - lastRead -> LibrarySort.Type.LastRead - lastChecked -> LibrarySort.Type.LastUpdate - unread -> LibrarySort.Type.UnreadCount - total -> LibrarySort.Type.TotalChapters - latestChapter -> LibrarySort.Type.LatestChapter - chapterFetchDate -> LibrarySort.Type.ChapterFetchDate - dateAdded -> LibrarySort.Type.DateAdded - else -> throw NotImplementedError("Unknown display mode") - } - val direction = if (item.state == Item.MultiSort.SORT_ASC) { - LibrarySort.Direction.Ascending - } else { - LibrarySort.Direction.Descending - } - - sheetScope.launchIO { - setSortModeForCategory.await(currentCategory!!, mode, direction) - } - } - } - } - - /** - * Display group, to show the library as a list or a grid. - */ - inner class Display @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - Settings(context, attrs) { - - private val displayGroup: DisplayGroup - private val badgeGroup: BadgeGroup - private val tabsGroup: TabsGroup - private val otherGroup: OtherGroup - - init { - displayGroup = DisplayGroup() - badgeGroup = BadgeGroup() - tabsGroup = TabsGroup() - otherGroup = OtherGroup() - setGroups(listOf(displayGroup, badgeGroup, tabsGroup, otherGroup)) - } - - // Refreshes Display Setting selections - fun adjustDisplaySelection() { - val mode = getDisplayModePreference() - displayGroup.setGroupSelections(mode) - displayGroup.items.forEach { adapter.notifyItemChanged(it) } - } - - // Gets user preference of currently selected display mode at current category - private fun getDisplayModePreference(): LibraryDisplayMode { - return currentCategory.display - } - - inner class DisplayGroup : Group { - - private val compactGrid = Item.Radio(R.string.action_display_grid, this) - private val comfortableGrid = Item.Radio(R.string.action_display_comfortable_grid, this) - private val coverOnlyGrid = Item.Radio(R.string.action_display_cover_only_grid, this) - private val list = Item.Radio(R.string.action_display_list, this) - - override val header = Item.Header(R.string.action_display_mode) - override val items = listOf(compactGrid, comfortableGrid, coverOnlyGrid, list) - override val footer = null - - override fun initModels() { - val mode = getDisplayModePreference() - setGroupSelections(mode) - } - - override fun onItemClicked(item: Item) { - item as Item.Radio - if (item.checked) return - - item.group.items.forEach { (it as Item.Radio).checked = false } - item.checked = true - - setDisplayModePreference(item) - - item.group.items.forEach { adapter.notifyItemChanged(it) } - } - - // Sets display group selections based on given mode - fun setGroupSelections(mode: LibraryDisplayMode) { - compactGrid.checked = mode == LibraryDisplayMode.CompactGrid - comfortableGrid.checked = mode == LibraryDisplayMode.ComfortableGrid - coverOnlyGrid.checked = mode == LibraryDisplayMode.CoverOnlyGrid - list.checked = mode == LibraryDisplayMode.List - } - - private fun setDisplayModePreference(item: Item) { - val flag = when (item) { - compactGrid -> LibraryDisplayMode.CompactGrid - comfortableGrid -> LibraryDisplayMode.ComfortableGrid - coverOnlyGrid -> LibraryDisplayMode.CoverOnlyGrid - list -> LibraryDisplayMode.List - else -> throw NotImplementedError("Unknown display mode") - } - - sheetScope.launchIO { - setDisplayModeForCategory.await(currentCategory!!, flag) - } - } - } - - inner class BadgeGroup : Group { - private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this) - private val localBadge = Item.CheckboxGroup(R.string.action_display_local_badge, this) - private val languageBadge = Item.CheckboxGroup(R.string.action_display_language_badge, this) - - override val header = Item.Header(R.string.badges_header) - override val items = listOf(downloadBadge, localBadge, languageBadge) - override val footer = null - - override fun initModels() { - downloadBadge.checked = libraryPreferences.downloadBadge().get() - localBadge.checked = libraryPreferences.localBadge().get() - languageBadge.checked = libraryPreferences.languageBadge().get() - } - - override fun onItemClicked(item: Item) { - item as Item.CheckboxGroup - item.checked = !item.checked - when (item) { - downloadBadge -> libraryPreferences.downloadBadge().set((item.checked)) - localBadge -> libraryPreferences.localBadge().set((item.checked)) - languageBadge -> libraryPreferences.languageBadge().set((item.checked)) - else -> {} - } - adapter.notifyItemChanged(item) - } - } - - inner class TabsGroup : Group { - private val showTabs = Item.CheckboxGroup(R.string.action_display_show_tabs, this) - private val showNumberOfItems = Item.CheckboxGroup(R.string.action_display_show_number_of_items, this) - - override val header = Item.Header(R.string.tabs_header) - override val items = listOf(showTabs, showNumberOfItems) - override val footer = null - - override fun initModels() { - showTabs.checked = libraryPreferences.categoryTabs().get() - showNumberOfItems.checked = libraryPreferences.categoryNumberOfItems().get() - } - - override fun onItemClicked(item: Item) { - item as Item.CheckboxGroup - item.checked = !item.checked - when (item) { - showTabs -> libraryPreferences.categoryTabs().set(item.checked) - showNumberOfItems -> libraryPreferences.categoryNumberOfItems().set(item.checked) - else -> {} - } - adapter.notifyItemChanged(item) - } - } - - inner class OtherGroup : Group { - private val showContinueReadingButton = Item.CheckboxGroup(R.string.action_display_show_continue_reading_button, this) - - override val header = Item.Header(R.string.other_header) - override val items = listOf(showContinueReadingButton) - override val footer = null - - override fun initModels() { - showContinueReadingButton.checked = libraryPreferences.showContinueReadingButton().get() - } - - override fun onItemClicked(item: Item) { - item as Item.CheckboxGroup - item.checked = !item.checked - when (item) { - showContinueReadingButton -> libraryPreferences.showContinueReadingButton().set(item.checked) - else -> {} - } - adapter.notifyItemChanged(item) - } - } - } - - open inner class Settings(context: Context, attrs: AttributeSet?) : - ExtendedNavigationView(context, attrs) { - - val preferences: BasePreferences by injectLazy() - val libraryPreferences: LibraryPreferences by injectLazy() - lateinit var adapter: Adapter - - /** - * Click listener to notify the parent fragment when an item from a group is clicked. - */ - var onGroupClicked: (Group) -> Unit = {} - - var currentCategory: Category? = null - - fun setGroups(groups: List) { - adapter = Adapter(groups.map { it.createItems() }.flatten()) - recycler.adapter = adapter - - groups.forEach { it.initModels() } - addView(recycler) - } - - /** - * Adapter of the recycler view. - */ - inner class Adapter(items: List) : ExtendedNavigationView.Adapter(items) { - - override fun onItemClicked(item: Item) { - if (item is GroupedItem) { - item.group.onItemClicked(item) - onGroupClicked(item.group) - } - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index 0e44d2d7b8..7f4749fa4a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -33,6 +33,7 @@ import eu.kanade.presentation.category.ChangeCategoryDialog import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreenAction import eu.kanade.presentation.library.DeleteLibraryMangaDialog +import eu.kanade.presentation.library.LibrarySettingsDialog import eu.kanade.presentation.library.components.LibraryContent import eu.kanade.presentation.library.components.LibraryToolbar import eu.kanade.presentation.manga.components.LibraryBottomActionMenu @@ -83,6 +84,7 @@ object LibraryTab : Tab { val haptic = LocalHapticFeedback.current val screenModel = rememberScreenModel { LibraryScreenModel() } + val settingsScreenModel = rememberScreenModel { LibrarySettingsScreenModel() } val state by screenModel.state.collectAsState() val snackbarHostState = remember { SnackbarHostState() } @@ -95,9 +97,6 @@ object LibraryTab : Tab { } started } - val onClickFilter: () -> Unit = { - scope.launch { sendSettingsSheetIntent(state.categories[screenModel.activeCategoryIndex]) } - } Scaffold( topBar = { scrollBehavior -> @@ -114,7 +113,7 @@ object LibraryTab : Tab { onClickUnselectAll = screenModel::clearSelection, onClickSelectAll = { screenModel.selectAll(screenModel.activeCategoryIndex) }, onClickInvertSelection = { screenModel.invertSelection(screenModel.activeCategoryIndex) }, - onClickFilter = onClickFilter, + onClickFilter = { screenModel.showSettingsDialog() }, onClickRefresh = { onClickRefresh(null) }, onClickOpenRandomManga = { scope.launch { @@ -201,6 +200,11 @@ object LibraryTab : Tab { val onDismissRequest = screenModel::closeDialog when (val dialog = state.dialog) { + is LibraryScreenModel.Dialog.SettingsSheet -> LibrarySettingsDialog( + onDismissRequest = onDismissRequest, + screenModel = settingsScreenModel, + activeCategoryIndex = screenModel.activeCategoryIndex, + ) is LibraryScreenModel.Dialog.ChangeCategory -> { ChangeCategoryDialog( initialSelection = dialog.initialSelection, @@ -235,8 +239,8 @@ object LibraryTab : Tab { } } - LaunchedEffect(state.selectionMode) { - HomeScreen.showBottomNav(!state.selectionMode) + LaunchedEffect(state.selectionMode, state.dialog) { + HomeScreen.showBottomNav(!state.selectionMode && state.dialog !is LibraryScreenModel.Dialog.SettingsSheet) } LaunchedEffect(state.isLoading) { @@ -247,7 +251,7 @@ object LibraryTab : Tab { LaunchedEffect(Unit) { launch { queryEvent.receiveAsFlow().collect(screenModel::search) } - launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { onClickFilter() } } + launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { screenModel.showSettingsDialog() } } } } @@ -257,8 +261,5 @@ object LibraryTab : Tab { // For opening settings sheet in LibraryController private val requestSettingsSheetEvent = Channel() - private val openSettingsSheetEvent_ = Channel() - val openSettingsSheetEvent = openSettingsSheetEvent_.receiveAsFlow() - private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.send(category) - suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.send(Unit) + private suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.send(Unit) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index e5536a764a..dc3e767208 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -78,8 +78,6 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.home.HomeScreen -import eu.kanade.tachiyomi.ui.library.LibrarySettingsSheet -import eu.kanade.tachiyomi.ui.library.LibraryTab import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.more.NewUpdateScreen import eu.kanade.tachiyomi.util.system.dpToPx @@ -87,7 +85,6 @@ import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.setComposeContent -import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.callbackFlow @@ -100,7 +97,6 @@ import kotlinx.coroutines.launch import logcat.LogPriority import tachiyomi.core.Constants import tachiyomi.core.util.system.logcat -import tachiyomi.domain.category.model.Category import tachiyomi.presentation.core.components.material.Scaffold import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -121,11 +117,6 @@ class MainActivity : BaseActivity() { // To be checked by splash screen. If true then splash screen will be removed. var ready = false - /** - * Sheet containing filter/sort/display items. - */ - private var settingsSheet: LibrarySettingsSheet? = null - private var navigator: Navigator? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -160,11 +151,6 @@ class MainActivity : BaseActivity() { // Draw edge-to-edge WindowCompat.setDecorFitsSystemWindows(window, false) - settingsSheet = LibrarySettingsSheet(this) - LibraryTab.openSettingsSheetEvent - .onEach(::showSettingsSheet) - .launchIn(lifecycleScope) - setComposeContent { val incognito by preferences.incognitoMode().collectAsState() val downloadOnly by preferences.downloadedOnly().collectAsState() @@ -303,14 +289,6 @@ class MainActivity : BaseActivity() { } } - private fun showSettingsSheet(category: Category? = null) { - if (category != null) { - settingsSheet?.show(category) - } else { - lifecycleScope.launch { LibraryTab.requestOpenSettingsSheet() } - } - } - @Composable private fun ConfirmExit() { val scope = rememberCoroutineScope() @@ -470,12 +448,6 @@ class MainActivity : BaseActivity() { return true } - override fun onDestroy() { - settingsSheet?.sheetScope?.cancel() - settingsSheet = null - super.onDestroy() - } - override fun onBackPressed() { if (navigator?.size == 1 && !onBackPressedDispatcher.hasEnabledCallbacks() && diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt deleted file mode 100644 index d8cb3bf874..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt +++ /dev/null @@ -1,270 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.View.OnClickListener -import android.view.ViewGroup -import androidx.annotation.AttrRes -import androidx.annotation.CallSuper -import androidx.appcompat.content.res.AppCompatResources -import androidx.core.view.isVisible -import androidx.core.view.updatePadding -import androidx.recyclerview.widget.RecyclerView -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.system.getResourceColor - -/** - * An alternative implementation of [com.google.android.material.navigation.NavigationView], without menu - * inflation and allowing customizable items (multiple selections, custom views, etc). - */ -open class ExtendedNavigationView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, -) : SimpleNavigationView(context, attrs, defStyleAttr) { - - /** - * Every item of the nav view. Generic items must belong to this list, custom items could be - * implemented by an abstract class. If more customization is needed in the future, this can be - * changed to an interface instead of sealed class. - */ - sealed class Item { - /** - * A view separator. - */ - class Separator(val paddingTop: Int = 0, val paddingBottom: Int = 0) : Item() - - /** - * A header with a title. - */ - class Header(val resTitle: Int) : Item() - - /** - * A checkbox. - */ - open class Checkbox(val resTitle: Int, var checked: Boolean = false, var enabled: Boolean = true) : Item() - - /** - * A checkbox belonging to a group. The group must handle selections and restrictions. - */ - class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false, enabled: Boolean = true) : - Checkbox(resTitle, checked, enabled), GroupedItem - - /** - * A radio belonging to a group (a sole radio makes no sense). The group must handle - * selections and restrictions. - */ - class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false, var enabled: Boolean = true) : - Item(), GroupedItem - - /** - * An item with which needs more than two states (selected/deselected). - */ - abstract class MultiState(val resTitle: Int, var state: Int = 0, var enabled: Boolean = true, var isVisible: Boolean = true) : Item() { - - /** - * Returns the drawable associated to every possible each state. - */ - abstract fun getStateDrawable(context: Context): Drawable? - - /** - * Creates a vector tinted with the accent color. - * - * @param context any context. - * @param resId the vector resource to load and tint - */ - fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorPrimary): Drawable { - return AppCompatResources.getDrawable(context, resId)!!.apply { - setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal)) - } - } - } - - /** - * An item with which needs more than two states (selected/deselected) belonging to a group. - * The group must handle selections and restrictions. - */ - abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0, enabled: Boolean = true) : - MultiState(resTitle, state, enabled), GroupedItem - - /** - * A multistate item for sorting lists (unselected, ascending, descending). - */ - class MultiSort(resId: Int, group: Group) : MultiStateGroup(resId, group) { - - companion object { - const val SORT_NONE = 0 - const val SORT_ASC = 1 - const val SORT_DESC = 2 - } - - override fun getStateDrawable(context: Context): Drawable? { - return when (state) { - SORT_ASC -> tintVector(context, R.drawable.ic_arrow_up_white_32dp) - SORT_DESC -> tintVector(context, R.drawable.ic_arrow_down_white_32dp) - SORT_NONE -> AppCompatResources.getDrawable(context, R.drawable.empty_drawable_32dp) - else -> null - } - } - } - - /** - * A checkbox with 3 states (unselected, checked, explicitly unchecked). - */ - class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) { - - enum class State(val value: Int) { - DISABLED(0), - ENABLED_IS(1), - ENABLED_NOT(2), - } - - override fun getStateDrawable(context: Context): Drawable? { - return when (state) { - State.DISABLED.value -> tintVector(context, R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal) - State.ENABLED_IS.value -> tintVector(context, R.drawable.ic_check_box_24dp) - State.ENABLED_NOT.value -> tintVector(context, R.drawable.ic_check_box_x_24dp) - else -> throw Exception("Unknown state") - } - } - } - } - - /** - * Interface for an item belonging to a group. - */ - interface GroupedItem { - val group: Group - } - - /** - * A group containing a list of items. - */ - interface Group { - - /** - * An optional header for the group, typically a [Item.Header]. - */ - val header: Item? - - /** - * An optional footer for the group, typically a [Item.Separator]. - */ - val footer: Item? - - /** - * The items of the group, excluding header and footer. - */ - val items: List - - /** - * Creates all the elements of this group. Implementations can override this method for more - * customization. - */ - fun createItems() = (mutableListOf() + header + items + footer).filterNotNull() - - /** - * Called after creating the list of items. Implementations should load the current values - * into the models. - */ - fun initModels() - - /** - * Called when an item of this group is clicked. The group is responsible for all the - * selections of its items. - */ - fun onItemClicked(item: Item) - } - - /** - * Base adapter for the navigation view. It knows how to create and render every subclass of - * [Item]. - */ - abstract inner class Adapter(private val items: List) : RecyclerView.Adapter() { - - private val onClick = OnClickListener { - val pos = recycler.getChildAdapterPosition(it) - val item = items[pos] - onItemClicked(item) - } - - fun notifyItemChanged(item: Item) { - val pos = items.indexOf(item) - if (pos != -1) notifyItemChanged(pos) - } - - override fun getItemCount(): Int { - return items.size - } - - @CallSuper - override fun getItemViewType(position: Int): Int { - return when (items[position]) { - is Item.Header -> VIEW_TYPE_HEADER - is Item.Separator -> VIEW_TYPE_SEPARATOR - is Item.Radio -> VIEW_TYPE_RADIO - is Item.Checkbox -> VIEW_TYPE_CHECKBOX - is Item.MultiState -> VIEW_TYPE_MULTISTATE - } - } - - @CallSuper - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - return when (viewType) { - VIEW_TYPE_HEADER -> HeaderHolder(parent) - VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent) - VIEW_TYPE_RADIO -> RadioHolder(parent, onClick) - VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, onClick) - VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, onClick) - else -> throw Exception("Unknown view type") - } - } - - @CallSuper - override fun onBindViewHolder(holder: Holder, position: Int) { - when (holder) { - is HeaderHolder -> { - val item = items[position] as Item.Header - holder.title.setText(item.resTitle) - } - is SeparatorHolder -> { - val view = holder.itemView - val item = items[position] as Item.Separator - view.updatePadding(top = item.paddingTop, bottom = item.paddingBottom) - } - is RadioHolder -> { - val item = items[position] as Item.Radio - holder.radio.setText(item.resTitle) - holder.radio.isChecked = item.checked - - holder.itemView.isClickable = item.enabled - holder.radio.isEnabled = item.enabled - } - is CheckboxHolder -> { - val item = items[position] as Item.CheckboxGroup - holder.check.setText(item.resTitle) - holder.check.isChecked = item.checked - - holder.itemView.isClickable = item.enabled - holder.check.isEnabled = item.enabled - } - is MultiStateHolder -> { - val item = items[position] as Item.MultiStateGroup - val drawable = item.getStateDrawable(context) - holder.text.setText(item.resTitle) - holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) - - holder.itemView.isClickable = item.enabled - holder.text.isEnabled = item.enabled - - // Mimics checkbox/radio button - holder.text.alpha = if (item.enabled) 1f else 0.4f - holder.itemView.isVisible = item.isVisible - } - } - } - - abstract fun onItemClicked(item: Item) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt new file mode 100644 index 0000000000..0b25a1dbdf --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt @@ -0,0 +1,19 @@ +package eu.kanade.tachiyomi.widget + +import tachiyomi.domain.manga.model.TriStateFilter + +// TODO: replace this with TriStateFilter entirely +enum class TriState(val value: Int) { + DISABLED(0), + ENABLED_IS(1), + ENABLED_NOT(2), +} + +fun Int.toTriStateFilter(): TriStateFilter { + return when (this) { + TriState.DISABLED.value -> TriStateFilter.DISABLED + TriState.ENABLED_IS.value -> TriStateFilter.ENABLED_IS + TriState.ENABLED_NOT.value -> TriStateFilter.ENABLED_NOT + else -> throw IllegalStateException("Unknown TriStateGroup state: $this") + } +}