From e11eed95d7a17c593bf3b3103189e37eec000ebb Mon Sep 17 00:00:00 2001 From: Syer10 Date: Wed, 6 Apr 2022 20:41:48 -0400 Subject: [PATCH] Library filtering and sorting modes --- .../jui/data/library/LibraryPreferences.kt | 37 +++- .../jui/data/library/model/DisplayMode.kt | 2 +- .../jui/data/library/model/FilterState.kt | 16 ++ .../ca/gosyer/jui/data/library/model/Sort.kt | 11 +- .../resources/MR/values/base/strings.xml | 19 +- .../gosyer/jui/ui/base/vm/ViewModelFactory.kt | 3 + .../ca/gosyer/jui/ui/library/LibraryScreen.kt | 16 +- .../jui/ui/library/LibraryScreenViewModel.kt | 38 ++++ .../library/components/LibraryMangaBadges.kt | 76 +++++-- .../ui/library/components/LibraryMangaList.kt | 30 ++- .../jui/ui/library/components/LibraryPager.kt | 30 ++- .../components/LibraryScreenContent.kt | 186 +++++++++++++++--- .../components/MangaComfortableGrid.kt | 27 ++- .../ui/library/components/MangaCompactGrid.kt | 27 ++- .../library/components/MangaCoverOnlyGrid.kt | 27 ++- .../jui/ui/library/settings/LibraryDisplay.kt | 127 ++++++++++++ .../jui/ui/library/settings/LibraryFilters.kt | 86 ++++++++ .../settings/LibrarySettingsViewModel.kt | 30 +++ .../jui/ui/library/settings/LibrarySheet.kt | 98 +++++++++ .../ui/library/settings/LibrarySideMenu.kt | 69 +++++++ .../jui/ui/library/settings/LibrarySort.kt | 78 ++++++++ .../browse/components/SourceScreenContent.kt | 6 +- .../browse/filter/SourceFiltersMenu.kt | 6 +- .../gosyer/jui/ui/base/vm/ViewModelFactory.kt | 8 +- 24 files changed, 957 insertions(+), 96 deletions(-) create mode 100644 data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/FilterState.kt create mode 100644 presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryDisplay.kt create mode 100644 presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryFilters.kt create mode 100644 presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt create mode 100644 presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySheet.kt create mode 100644 presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySideMenu.kt create mode 100644 presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySort.kt diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/LibraryPreferences.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/LibraryPreferences.kt index efc8972e..d1631944 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/LibraryPreferences.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/LibraryPreferences.kt @@ -9,12 +9,25 @@ package ca.gosyer.jui.data.library import ca.gosyer.jui.core.prefs.Preference import ca.gosyer.jui.core.prefs.PreferenceStore import ca.gosyer.jui.data.library.model.DisplayMode +import ca.gosyer.jui.data.library.model.FilterState import ca.gosyer.jui.data.library.model.Sort class LibraryPreferences(private val preferenceStore: PreferenceStore) { - fun displayMode(): Preference { - return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer()) + fun showAllCategory(): Preference { + return preferenceStore.getBoolean("show_all_category", false) + } + + fun filterDownloaded(): Preference { + return preferenceStore.getJsonObject("filter_downloaded", FilterState.IGNORED, FilterState.serializer()) + } + + fun filterUnread(): Preference { + return preferenceStore.getJsonObject("filter_unread", FilterState.IGNORED, FilterState.serializer()) + } + + fun filterCompleted(): Preference { + return preferenceStore.getJsonObject("filter_completed", FilterState.IGNORED, FilterState.serializer()) } fun sortMode(): Preference { @@ -25,6 +38,10 @@ class LibraryPreferences(private val preferenceStore: PreferenceStore) { return preferenceStore.getBoolean("sort_ascending", true) } + fun displayMode(): Preference { + return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer()) + } + fun gridColumns(): Preference { return preferenceStore.getInt("grid_columns", 0) } @@ -33,7 +50,19 @@ class LibraryPreferences(private val preferenceStore: PreferenceStore) { return preferenceStore.getInt("grid_size", 160) } - fun showAllCategory(): Preference { - return preferenceStore.getBoolean("show_all_category", false) + fun unreadBadge(): Preference { + return preferenceStore.getBoolean("unread_badge", true) + } + + fun downloadBadge(): Preference { + return preferenceStore.getBoolean("download_badge", false) + } + + fun languageBadge(): Preference { + return preferenceStore.getBoolean("language_badge", false) + } + + fun localBadge(): Preference { + return preferenceStore.getBoolean("local_badge", false) } } diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/DisplayMode.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/DisplayMode.kt index 64cd7aa3..a1c2a851 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/DisplayMode.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/DisplayMode.kt @@ -19,6 +19,6 @@ enum class DisplayMode(@Transient val res: StringResource) { List(MR.strings.display_list); companion object { - val values = values() + val values = values().asList() } } diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/FilterState.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/FilterState.kt new file mode 100644 index 00000000..dafd651c --- /dev/null +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/FilterState.kt @@ -0,0 +1,16 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.data.library.model + +import kotlinx.serialization.Serializable + +@Serializable +enum class FilterState { + IGNORED, + INCLUDED, + EXCLUDED +} \ No newline at end of file diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/Sort.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/Sort.kt index fcf6e73b..885053b2 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/Sort.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/model/Sort.kt @@ -6,18 +6,21 @@ package ca.gosyer.jui.data.library.model +import ca.gosyer.jui.i18n.MR +import dev.icerock.moko.resources.StringResource import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient @Serializable -enum class Sort { - ALPHABETICAL, +enum class Sort(@Transient val res: StringResource) { + ALPHABETICAL(MR.strings.sort_alphabetical), // LAST_READ, // LAST_CHECKED, - UNREAD, + UNREAD(MR.strings.sort_unread), // TOTAL_CHAPTERS, // LATEST_CHAPTER, // DATE_FETCHED, - DATE_ADDED; + DATE_ADDED(MR.strings.sort_date_added); } diff --git a/i18n/src/commonMain/resources/MR/values/base/strings.xml b/i18n/src/commonMain/resources/MR/values/base/strings.xml index c0df1bf4..a01359ff 100644 --- a/i18n/src/commonMain/resources/MR/values/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/values/base/strings.xml @@ -45,6 +45,7 @@ More actions Ok Browser + Filter Library @@ -81,6 +82,23 @@ Default Library is empty + Sort + Display + + Downloaded + Unread + Completed + + Alphabetically + Unread + Date added + + Badges + Downloaded chapters + Unread chapters + Local manga + Language + Page %1$d No chapters found @@ -100,7 +118,6 @@ Browse Latest Reset - Filter In library No results found diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/vm/ViewModelFactory.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/vm/ViewModelFactory.kt index fa85db1d..02534422 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/vm/ViewModelFactory.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/vm/ViewModelFactory.kt @@ -11,6 +11,7 @@ import ca.gosyer.jui.ui.categories.CategoriesScreenViewModel import ca.gosyer.jui.ui.downloads.DownloadsScreenViewModel import ca.gosyer.jui.ui.extensions.ExtensionsScreenViewModel import ca.gosyer.jui.ui.library.LibraryScreenViewModel +import ca.gosyer.jui.ui.library.settings.LibrarySettingsViewModel import ca.gosyer.jui.ui.main.MainViewModel import ca.gosyer.jui.ui.main.about.AboutViewModel import ca.gosyer.jui.ui.main.components.DebugOverlayViewModel @@ -43,6 +44,7 @@ actual class ViewModelFactoryImpl( private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel, private val extensionsFactory: () -> ExtensionsScreenViewModel, private val libraryFactory: () -> LibraryScreenViewModel, + private val librarySettingsFactory: () -> LibrarySettingsViewModel, private val debugOverlayFactory: () -> DebugOverlayViewModel, private val mainFactory: () -> MainViewModel, private val mangaFactory: (params: MangaScreenViewModel.Params) -> MangaScreenViewModel, @@ -72,6 +74,7 @@ actual class ViewModelFactoryImpl( DownloadsScreenViewModel::class -> downloadsFactory(arg1 as Boolean) ExtensionsScreenViewModel::class -> extensionsFactory() LibraryScreenViewModel::class -> libraryFactory() + LibrarySettingsViewModel::class -> librarySettingsFactory() DebugOverlayViewModel::class -> debugOverlayFactory() MainViewModel::class -> mainFactory() MangaScreenViewModel::class -> mangaFactory(arg1 as MangaScreenViewModel.Params) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreen.kt index 930e064f..1af30b63 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreen.kt @@ -9,6 +9,10 @@ package ca.gosyer.jui.ui.library import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import ca.gosyer.jui.ui.library.components.LibraryScreenContent +import ca.gosyer.jui.ui.library.settings.LibrarySettingsViewModel +import ca.gosyer.jui.ui.library.settings.getLibraryDisplay +import ca.gosyer.jui.ui.library.settings.getLibraryFilters +import ca.gosyer.jui.ui.library.settings.getLibrarySort import ca.gosyer.jui.ui.manga.MangaScreen import ca.gosyer.jui.uicore.vm.viewModel import cafe.adriel.voyager.core.screen.Screen @@ -24,6 +28,7 @@ class LibraryScreen : Screen { @Composable override fun Content() { val vm = viewModel() + val settingsVM = viewModel() val navigator = LocalNavigator.currentOrThrow LibraryScreenContent( categories = vm.categories.collectAsState().value, @@ -38,7 +43,16 @@ class LibraryScreen : Screen { getLibraryForPage = { vm.getLibraryForCategoryId(it).collectAsState() }, onPageChanged = vm::setSelectedPage, onClickManga = { navigator push MangaScreen(it) }, - onRemoveMangaClicked = vm::removeManga + onRemoveMangaClicked = vm::removeManga, + showingMenu = vm.showingMenu.collectAsState().value, + setShowingMenu = vm::setShowingMenu, + libraryFilters = getLibraryFilters(settingsVM), + librarySort = getLibrarySort(settingsVM), + libraryDisplay = getLibraryDisplay(settingsVM), + showUnread = vm.unreadBadges.collectAsState().value, + showDownloaded = vm.downloadBadges.collectAsState().value, + showLanguage = vm.languageBadges.collectAsState().value, + showLocal = vm.localBadges.collectAsState().value ) } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt index fab014aa..b79c6392 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt @@ -9,10 +9,13 @@ package ca.gosyer.jui.ui.library import ca.gosyer.jui.core.lang.getDefault import ca.gosyer.jui.core.lang.lowercase import ca.gosyer.jui.core.lang.withDefaultContext +import ca.gosyer.jui.core.prefs.getAsFlow import ca.gosyer.jui.data.library.LibraryPreferences +import ca.gosyer.jui.data.library.model.FilterState import ca.gosyer.jui.data.library.model.Sort import ca.gosyer.jui.data.models.Category import ca.gosyer.jui.data.models.Manga +import ca.gosyer.jui.data.models.MangaStatus import ca.gosyer.jui.data.server.interactions.CategoryInteractionHandler import ca.gosyer.jui.data.server.interactions.LibraryInteractionHandler import ca.gosyer.jui.data.server.interactions.UpdatesInteractionHandler @@ -67,13 +70,42 @@ class LibraryScreenViewModel @Inject constructor( private val _selectedCategoryIndex = MutableStateFlow(0) val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() + private val _showingMenu = MutableStateFlow(false) + val showingMenu = _showingMenu.asStateFlow() + val displayMode = libraryPreferences.displayMode().stateIn(scope) val gridColumns = libraryPreferences.gridColumns().stateIn(scope) val gridSize = libraryPreferences.gridSize().stateIn(scope) + val unreadBadges = libraryPreferences.unreadBadge().stateIn(scope) + val downloadBadges = libraryPreferences.downloadBadge().stateIn(scope) + val languageBadges = libraryPreferences.languageBadge().stateIn(scope) + val localBadges = libraryPreferences.localBadge().stateIn(scope) private val sortMode = libraryPreferences.sortMode().stateIn(scope) private val sortAscending = libraryPreferences.sortAscending().stateIn(scope) + private val filter = combine( + libraryPreferences.filterDownloaded().getAsFlow(), + libraryPreferences.filterUnread().getAsFlow(), + libraryPreferences.filterCompleted().getAsFlow() + ) { downloaded, unread, completed -> + { manga: Manga -> + when (downloaded) { + FilterState.EXCLUDED -> manga.downloadCount == null || manga.downloadCount == 0 + FilterState.INCLUDED -> manga.downloadCount != null && (manga.downloadCount ?: 0) > 0 + FilterState.IGNORED -> true + } && when (unread) { + FilterState.EXCLUDED -> manga.unreadCount == null || manga.unreadCount == 0 + FilterState.INCLUDED -> manga.unreadCount != null && (manga.unreadCount ?: 0) > 0 + FilterState.IGNORED -> true + } && when (completed) { + FilterState.EXCLUDED -> manga.status != MangaStatus.COMPLETED + FilterState.INCLUDED -> manga.status == MangaStatus.COMPLETED + FilterState.IGNORED -> true + } + } + } + private val _isLoading = MutableStateFlow(true) val isLoading = _isLoading.asStateFlow() @@ -114,6 +146,10 @@ class LibraryScreenViewModel @Inject constructor( _selectedCategoryIndex.value = page } + fun setShowingMenu(showingMenu: Boolean) { + _showingMenu.value = showingMenu + } + private fun getComparator(sortMode: Sort, ascending: Boolean): Comparator { val sortFn = when (sortMode) { Sort.ALPHABETICAL -> { @@ -164,6 +200,8 @@ class LibraryScreenViewModel @Inject constructor( private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow>): StateFlow> { return combine(unfilteredItemsFlow, query) { unfilteredItems, query -> filterManga(query, unfilteredItems) + }.combine(filter) { filteredManga, filterer -> + filteredManga.filter(filterer) }.combine(comparator) { filteredManga, comparator -> filteredManga.sortedWith(comparator) }.stateIn(scope, SharingStarted.Eagerly, emptyList()) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaBadges.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaBadges.kt index 2a35587e..95271f65 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaBadges.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaBadges.kt @@ -7,7 +7,10 @@ package ca.gosyer.jui.ui.library.components import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -15,33 +18,66 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import ca.gosyer.jui.data.models.Manga +import ca.gosyer.jui.data.models.Source @Composable fun LibraryMangaBadges( - unread: Int?, - downloaded: Int?, modifier: Modifier = Modifier, + manga: Manga, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { - if (unread == null && downloaded == null) return + val unread = manga.unreadCount + val downloaded = manga.downloadCount + val isLocal = manga.sourceId == Source.LOCAL_SOURCE_ID - Row(modifier = modifier.clip(MaterialTheme.shapes.medium)) { - if (unread != null && unread > 0) { - Text( - text = unread.toString(), - modifier = Modifier.background(MaterialTheme.colors.primary).then(BadgesInnerPadding), - style = MaterialTheme.typography.caption, - color = MaterialTheme.colors.onPrimary - ) + Row(modifier then Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + if ((unread != null && unread > 0) || (downloaded != null && downloaded > 0) || isLocal) { + Row(modifier = Modifier.clip(MaterialTheme.shapes.medium)) { + if (showLocal && isLocal) { + Text( + text = unread.toString(), + modifier = Modifier.background(MaterialTheme.colors.secondary).then(BadgesInnerPadding), + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onSecondary + ) + } + if (showUnread && unread != null && unread > 0) { + Text( + text = unread.toString(), + modifier = Modifier.background(MaterialTheme.colors.primary).then(BadgesInnerPadding), + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onPrimary + ) + } + if (showDownloaded && downloaded != null && downloaded > 0) { + Text( + text = downloaded.toString(), + modifier = Modifier.background(MaterialTheme.colors.secondary).then( + BadgesInnerPadding + ), + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onSecondary + ) + } + } + } else { + Spacer(Modifier) } - if (downloaded != null && downloaded > 0) { - Text( - text = downloaded.toString(), - modifier = Modifier.background(MaterialTheme.colors.secondary).then( - BadgesInnerPadding - ), - style = MaterialTheme.typography.caption, - color = MaterialTheme.colors.onSecondary - ) + + val lang = manga.source?.lang + if (showLanguage && lang != null) { + Row(modifier = Modifier.clip(MaterialTheme.shapes.medium)) { + Text( + text = lang.uppercase(), + modifier = Modifier.background(MaterialTheme.colors.secondary).then(BadgesInnerPadding), + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.onSecondary + ) + } } } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaList.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaList.kt index 8cac48dc..e3490d94 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaList.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryMangaList.kt @@ -7,11 +7,13 @@ package ca.gosyer.jui.ui.library.components import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -34,7 +36,11 @@ import io.kamel.image.lazyPainterResource fun LibraryMangaList( library: List, onClickManga: (Long) -> Unit, - onRemoveMangaClicked: (Long) -> Unit + onRemoveMangaClicked: (Long) -> Unit, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { Box { val state = rememberLazyListState() @@ -49,8 +55,10 @@ fun LibraryMangaList( { onRemoveMangaClicked(manga.id) } ), manga = manga, - unread = manga.unreadCount, - downloaded = manga.downloadCount, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) } } @@ -67,8 +75,10 @@ fun LibraryMangaList( private fun LibraryMangaListItem( modifier: Modifier, manga: Manga, - unread: Int?, - downloaded: Int?, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) MangaListItem( @@ -89,6 +99,14 @@ private fun LibraryMangaListItem( .padding(horizontal = 16.dp), text = manga.title, ) - LibraryMangaBadges(unread, downloaded) + Box(Modifier.width(IntrinsicSize.Min)) { + LibraryMangaBadges( + manga = manga, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal + ) + } } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryPager.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryPager.kt index b70f1743..a3c9fa57 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryPager.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryPager.kt @@ -25,7 +25,11 @@ fun LibraryPager( gridSize: Int, getLibraryForPage: @Composable (Long) -> State>, onClickManga: (Long) -> Unit, - onRemoveMangaClicked: (Long) -> Unit + onRemoveMangaClicked: (Long) -> Unit, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { if (categories.isEmpty()) return @@ -37,26 +41,42 @@ fun LibraryPager( gridColumns = gridColumns, gridSize = gridSize, onClickManga = onClickManga, - onRemoveMangaClicked = onRemoveMangaClicked + onRemoveMangaClicked = onRemoveMangaClicked, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) DisplayMode.ComfortableGrid -> LibraryMangaComfortableGrid( library = library, gridColumns = gridColumns, gridSize = gridSize, onClickManga = onClickManga, - onRemoveMangaClicked = onRemoveMangaClicked + onRemoveMangaClicked = onRemoveMangaClicked, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) DisplayMode.CoverOnlyGrid -> LibraryMangaCoverOnlyGrid( library = library, gridColumns = gridColumns, gridSize = gridSize, onClickManga = onClickManga, - onRemoveMangaClicked = onRemoveMangaClicked + onRemoveMangaClicked = onRemoveMangaClicked, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) DisplayMode.List -> LibraryMangaList( library = library, onClickManga = onClickManga, - onRemoveMangaClicked = onRemoveMangaClicked + onRemoveMangaClicked = onRemoveMangaClicked, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) else -> Box {} } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryScreenContent.kt index 95ffc0c7..4c47ceab 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/LibraryScreenContent.kt @@ -6,23 +6,40 @@ package ca.gosyer.jui.ui.library.components +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.BottomSheetScaffold +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Scaffold -import androidx.compose.material.rememberBottomSheetScaffoldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.FilterList +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.State +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import ca.gosyer.jui.data.library.model.DisplayMode import ca.gosyer.jui.data.models.Category import ca.gosyer.jui.data.models.Manga import ca.gosyer.jui.i18n.MR +import ca.gosyer.jui.ui.base.navigation.ActionItem import ca.gosyer.jui.ui.base.navigation.Toolbar +import ca.gosyer.jui.ui.library.settings.LibrarySheet +import ca.gosyer.jui.ui.library.settings.LibrarySideMenu import ca.gosyer.jui.uicore.components.LoadingScreen import ca.gosyer.jui.uicore.resources.stringResource import com.google.accompanist.pager.PagerState @@ -42,7 +59,16 @@ fun LibraryScreenContent( getLibraryForPage: @Composable (Long) -> State>, onPageChanged: (Int) -> Unit, onClickManga: (Long) -> Unit, - onRemoveMangaClicked: (Long) -> Unit + onRemoveMangaClicked: (Long) -> Unit, + showingMenu: Boolean, + setShowingMenu: (Boolean) -> Unit, + libraryFilters: @Composable () -> Unit, + librarySort: @Composable () -> Unit, + libraryDisplay: @Composable () -> Unit, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { BoxWithConstraints { val pagerState = rememberPagerState(selectedCategoryIndex) @@ -71,7 +97,16 @@ fun LibraryScreenContent( getLibraryForPage = getLibraryForPage, onPageChanged = onPageChanged, onClickManga = onClickManga, - onRemoveMangaClicked = onRemoveMangaClicked + onRemoveMangaClicked = onRemoveMangaClicked, + showingMenu = showingMenu, + setShowingMenu = setShowingMenu, + libraryFilters = libraryFilters, + librarySort = librarySort, + libraryDisplay = libraryDisplay, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) } else { ThinLibraryScreenContent( @@ -88,7 +123,16 @@ fun LibraryScreenContent( getLibraryForPage = getLibraryForPage, onPageChanged = onPageChanged, onClickManga = onClickManga, - onRemoveMangaClicked = onRemoveMangaClicked + onRemoveMangaClicked = onRemoveMangaClicked, + showingSheet = showingMenu, + setShowingSheet = setShowingMenu, + libraryFilters = libraryFilters, + librarySort = librarySort, + libraryDisplay = libraryDisplay, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) } } @@ -109,7 +153,16 @@ fun WideLibraryScreenContent( getLibraryForPage: @Composable (Long) -> State>, onPageChanged: (Int) -> Unit, onClickManga: (Long) -> Unit, - onRemoveMangaClicked: (Long) -> Unit + onRemoveMangaClicked: (Long) -> Unit, + showingMenu: Boolean, + setShowingMenu: (Boolean) -> Unit, + libraryFilters: @Composable () -> Unit, + librarySort: @Composable () -> Unit, + libraryDisplay: @Composable () -> Unit, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { Scaffold( topBar = { @@ -117,7 +170,12 @@ fun WideLibraryScreenContent( Toolbar( stringResource(MR.strings.location_library), searchText = query, - search = updateQuery + search = updateQuery, + actions = { + getActionItems( + onToggleFiltersClick = { setShowingMenu(true) } + ) + } ) LibraryTabs( visible = true, // vm.showCategoryTabs, @@ -128,8 +186,8 @@ fun WideLibraryScreenContent( ) } } - ) { - Box(Modifier.padding(it)) { + ) { padding -> + Box(Modifier.padding(padding)) { if (categories.isEmpty()) { LoadingScreen(isLoading, errorMessage = error) } else { @@ -141,8 +199,36 @@ fun WideLibraryScreenContent( gridSize = gridSize, getLibraryForPage = getLibraryForPage, onClickManga = onClickManga, - onRemoveMangaClicked = onRemoveMangaClicked + onRemoveMangaClicked = onRemoveMangaClicked, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) + + if (showingMenu) { + Box( + Modifier.fillMaxSize().pointerInput(isLoading) { + forEachGesture { + detectTapGestures { + setShowingMenu(false) + } + } + } + ) + } + AnimatedVisibility( + showingMenu, + enter = fadeIn() + slideInHorizontally(initialOffsetX = { it * 2 }), + exit = fadeOut() + slideOutHorizontally(targetOffsetX = { it * 2 }), + modifier = Modifier.align(Alignment.TopEnd) + ) { + LibrarySideMenu( + libraryFilters = libraryFilters, + librarySort = librarySort, + libraryDisplay = libraryDisplay + ) + } } } } @@ -163,17 +249,47 @@ fun ThinLibraryScreenContent( getLibraryForPage: @Composable (Long) -> State>, onPageChanged: (Int) -> Unit, onClickManga: (Long) -> Unit, - onRemoveMangaClicked: (Long) -> Unit + onRemoveMangaClicked: (Long) -> Unit, + showingSheet: Boolean, + setShowingSheet: (Boolean) -> Unit, + libraryFilters: @Composable () -> Unit, + librarySort: @Composable () -> Unit, + libraryDisplay: @Composable () -> Unit, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { - val sheetState = rememberBottomSheetScaffoldState() - BottomSheetScaffold( - scaffoldState = sheetState, + val bottomSheetState = rememberModalBottomSheetState( + ModalBottomSheetValue.Hidden, + confirmStateChange = { + when (it) { + ModalBottomSheetValue.Hidden -> setShowingSheet(false) + ModalBottomSheetValue.Expanded, + ModalBottomSheetValue.HalfExpanded -> setShowingSheet(true) + } + true + } + ) + LaunchedEffect(showingSheet) { + if (showingSheet) { + bottomSheetState.show() + } else { + bottomSheetState.hide() + } + } + Scaffold( topBar = { Column { Toolbar( stringResource(MR.strings.location_library), searchText = query, - search = updateQuery + search = updateQuery, + actions = { + getActionItems( + onToggleFiltersClick = { setShowingSheet(true) } + ) + } ) LibraryTabs( visible = true, // vm.showCategoryTabs, @@ -183,13 +299,19 @@ fun ThinLibraryScreenContent( onPageChanged = onPageChanged ) } - }, - sheetContent = { - // LibrarySheetContent() - }, - sheetPeekHeight = 0.dp - ) { - Box(Modifier.padding(it)) { + } + ) { padding -> + ModalBottomSheetLayout( + sheetState = bottomSheetState, + modifier = Modifier.padding(padding), + sheetContent = { + LibrarySheet( + libraryFilters = libraryFilters, + librarySort = librarySort, + libraryDisplay = libraryDisplay + ) + } + ) { if (categories.isEmpty()) { LoadingScreen(isLoading, errorMessage = error) } else { @@ -201,9 +323,27 @@ fun ThinLibraryScreenContent( gridSize = gridSize, getLibraryForPage = getLibraryForPage, onClickManga = onClickManga, - onRemoveMangaClicked = onRemoveMangaClicked + onRemoveMangaClicked = onRemoveMangaClicked, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) } } } } + +@Composable +@Stable +private fun getActionItems( + onToggleFiltersClick: () -> Unit, +): List { + return listOfNotNull( + ActionItem( + name = stringResource(MR.strings.action_filter), + icon = Icons.Rounded.FilterList, + doAction = onToggleFiltersClick + ) + ) +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaComfortableGrid.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaComfortableGrid.kt index 9bc9d2b9..43f7bafe 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaComfortableGrid.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaComfortableGrid.kt @@ -43,7 +43,11 @@ fun LibraryMangaComfortableGrid( gridColumns: Int, gridSize: Int, onClickManga: (Long) -> Unit, - onRemoveMangaClicked: (Long) -> Unit + onRemoveMangaClicked: (Long) -> Unit, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { Box { val state = rememberLazyListState() @@ -64,8 +68,10 @@ fun LibraryMangaComfortableGrid( { onRemoveMangaClicked(manga.id) } ), manga = manga, - unread = manga.unreadCount, - downloaded = manga.downloadCount + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) } } @@ -82,8 +88,10 @@ fun LibraryMangaComfortableGrid( private fun LibraryMangaComfortableGridItem( modifier: Modifier, manga: Manga, - unread: Int?, - downloaded: Int? + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val fontStyle = LocalTextStyle.current.merge( @@ -114,9 +122,12 @@ private fun LibraryMangaComfortableGridItem( ) } LibraryMangaBadges( - unread = unread, - downloaded = downloaded, - modifier = Modifier.padding(4.dp) + modifier = Modifier.padding(4.dp), + manga = manga, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCompactGrid.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCompactGrid.kt index 97e24f85..d918ff2e 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCompactGrid.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCompactGrid.kt @@ -51,7 +51,11 @@ fun LibraryMangaCompactGrid( gridColumns: Int, gridSize: Int, onClickManga: (Long) -> Unit, - onRemoveMangaClicked: (Long) -> Unit + onRemoveMangaClicked: (Long) -> Unit, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { Box { val state = rememberLazyListState() @@ -72,8 +76,10 @@ fun LibraryMangaCompactGrid( { onRemoveMangaClicked(manga.id) } ), manga = manga, - unread = manga.unreadCount, - downloaded = manga.downloadCount + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) } } @@ -90,8 +96,10 @@ fun LibraryMangaCompactGrid( private fun LibraryMangaCompactGridItem( modifier: Modifier, manga: Manga, - unread: Int?, - downloaded: Int?, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val fontStyle = LocalTextStyle.current.merge( @@ -119,9 +127,12 @@ private fun LibraryMangaCompactGridItem( modifier = Modifier.align(Alignment.BottomStart).padding(8.dp) ) LibraryMangaBadges( - unread = unread, - downloaded = downloaded, - modifier = Modifier.padding(4.dp) + modifier = Modifier.padding(4.dp), + manga = manga, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCoverOnlyGrid.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCoverOnlyGrid.kt index 72f59b66..71c7a1e7 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCoverOnlyGrid.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/components/MangaCoverOnlyGrid.kt @@ -37,7 +37,11 @@ fun LibraryMangaCoverOnlyGrid( gridColumns: Int, gridSize: Int, onClickManga: (Long) -> Unit, - onRemoveMangaClicked: (Long) -> Unit + onRemoveMangaClicked: (Long) -> Unit, + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { Box { val state = rememberLazyListState() @@ -58,8 +62,10 @@ fun LibraryMangaCoverOnlyGrid( { onRemoveMangaClicked(manga.id) } ), manga = manga, - unread = manga.unreadCount, - downloaded = manga.downloadCount + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) } } @@ -76,8 +82,10 @@ fun LibraryMangaCoverOnlyGrid( private fun LibraryMangaCoverOnlyGridItem( modifier: Modifier, manga: Manga, - unread: Int?, - downloaded: Int? + showUnread: Boolean, + showDownloaded: Boolean, + showLanguage: Boolean, + showLocal: Boolean ) { val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) @@ -94,9 +102,12 @@ private fun LibraryMangaCoverOnlyGridItem( contentScale = ContentScale.Crop ) LibraryMangaBadges( - unread = unread, - downloaded = downloaded, - modifier = Modifier.padding(4.dp) + modifier = Modifier.padding(4.dp), + manga = manga, + showUnread = showUnread, + showDownloaded = showDownloaded, + showLanguage = showLanguage, + showLocal = showLocal ) } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryDisplay.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryDisplay.kt new file mode 100644 index 00000000..e448b597 --- /dev/null +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryDisplay.kt @@ -0,0 +1,127 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.ui.library.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Checkbox +import androidx.compose.material.RadioButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import ca.gosyer.jui.data.library.model.DisplayMode +import ca.gosyer.jui.i18n.MR +import ca.gosyer.jui.ui.sources.browse.filter.SourceFilterAction +import ca.gosyer.jui.uicore.resources.stringResource + +@Composable +fun getLibraryDisplay(vm: LibrarySettingsViewModel): @Composable () -> Unit = remember(vm) { + @Composable { + LibraryDisplay( + displayMode = vm.displayMode.collectAsState().value, + unreadBadges = vm.unreadBadges.collectAsState().value, + downloadBadges = vm.downloadBadges.collectAsState().value, + languageBadges = vm.languageBadges.collectAsState().value, + localBadges = vm.localBadges.collectAsState().value, + setDisplayMode = { vm.displayMode.value = it }, + setUnreadBadges = { vm.unreadBadges.value = it }, + setDownloadBadges = { vm.downloadBadges.value = it }, + setLanguageBadges = { vm.languageBadges.value = it }, + setLocalBadges = { vm.localBadges.value = it } + ) + } +} + +@Composable +fun LibraryDisplay( + displayMode: DisplayMode, + unreadBadges: Boolean, + downloadBadges: Boolean, + languageBadges: Boolean, + localBadges: Boolean, + setDisplayMode: (DisplayMode) -> Unit, + setUnreadBadges: (Boolean) -> Unit, + setDownloadBadges: (Boolean) -> Unit, + setLanguageBadges: (Boolean) -> Unit, + setLocalBadges: (Boolean) -> Unit +) { + Column(Modifier.fillMaxWidth()) { + TitleText(stringResource(MR.strings.display_mode)) + DisplayMode.values.fastForEach { + RadioSelectionItem( + text = stringResource(it.res), + selected = it == displayMode, + onClick = { setDisplayMode(it) } + ) + } + TitleText(stringResource(MR.strings.display_badges)) + CheckboxItem( + text = stringResource(MR.strings.display_badge_downloaded), + checked = downloadBadges, + onClick = { setDownloadBadges(!downloadBadges) } + ) + CheckboxItem( + text = stringResource(MR.strings.display_badge_unread), + checked = unreadBadges, + onClick = { setUnreadBadges(!unreadBadges) } + ) + CheckboxItem( + text = stringResource(MR.strings.display_badge_local), + checked = localBadges, + onClick = { setLocalBadges(!localBadges) } + ) + // TODO: 2022-04-06 Enable when library contains manga source in manga object + /*CheckboxItem( + text = stringResource(MR.strings.display_badge_language), + checked = languageBadges, + onClick = { setLanguageBadges(!languageBadges) } + )*/ + } +} + +@Composable +private fun TitleText(text: String) { + Text( + text = text, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) +} + +@Composable +private fun RadioSelectionItem(text: String, selected: Boolean, onClick: () -> Unit) { + SourceFilterAction( + name = text, + onClick = onClick, + action = { + RadioButton( + selected = selected, + onClick = null + ) + } + ) +} + +@Composable +private fun CheckboxItem(text: String, checked: Boolean, onClick: () -> Unit) { + SourceFilterAction( + name = text, + onClick = onClick, + action = { + Checkbox( + checked = checked, + onCheckedChange = null + ) + } + ) +} \ No newline at end of file diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryFilters.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryFilters.kt new file mode 100644 index 00000000..f34d0170 --- /dev/null +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryFilters.kt @@ -0,0 +1,86 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.ui.library.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.TriStateCheckbox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.state.ToggleableState +import ca.gosyer.jui.data.library.model.FilterState +import ca.gosyer.jui.i18n.MR +import ca.gosyer.jui.ui.sources.browse.filter.SourceFilterAction +import ca.gosyer.jui.uicore.resources.stringResource + +@Composable +fun getLibraryFilters(vm: LibrarySettingsViewModel): @Composable () -> Unit = remember(vm) { + @Composable { + LibraryFilters( + downloaded = vm.filterDownloaded.collectAsState().value, + unread = vm.filterUnread.collectAsState().value, + completed = vm.filterCompleted.collectAsState().value, + setDownloadedFilter = { vm.filterDownloaded.value = it }, + setUnreadFilter = { vm.filterUnread.value = it }, + setCompletedFilter = { vm.filterCompleted.value = it }, + ) + } +} + +@Composable +fun LibraryFilters( + downloaded: FilterState, + unread: FilterState, + completed: FilterState, + setDownloadedFilter: (FilterState) -> Unit, + setUnreadFilter: (FilterState) -> Unit, + setCompletedFilter: (FilterState) -> Unit, +) { + Column(Modifier.fillMaxWidth()) { + Filter( + stringResource(MR.strings.filter_downloaded), + downloaded, + onClick = { setDownloadedFilter(toggleState(downloaded)) } + ) + Filter( + stringResource(MR.strings.filter_unread), + unread, + onClick = { setUnreadFilter(toggleState(unread)) } + ) + Filter( + stringResource(MR.strings.filter_completed), + completed, + onClick = { setCompletedFilter(toggleState(completed)) } + ) + } +} + +fun toggleState(filterState: FilterState) = when (filterState) { + FilterState.IGNORED -> FilterState.INCLUDED + FilterState.INCLUDED -> FilterState.EXCLUDED + FilterState.EXCLUDED -> FilterState.IGNORED +} + +@Composable +private fun Filter(text: String, state: FilterState, onClick: () -> Unit) { + SourceFilterAction( + text, + onClick = onClick, + action = { + TriStateCheckbox( + state = when (state) { + FilterState.INCLUDED -> ToggleableState.On + FilterState.EXCLUDED -> ToggleableState.Indeterminate + FilterState.IGNORED -> ToggleableState.Off + }, + onClick = null + ) + } + ) +} \ No newline at end of file diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt new file mode 100644 index 00000000..e4ba45e6 --- /dev/null +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt @@ -0,0 +1,30 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.ui.library.settings + +import ca.gosyer.jui.data.library.LibraryPreferences +import ca.gosyer.jui.uicore.vm.ContextWrapper +import ca.gosyer.jui.uicore.vm.ViewModel +import me.tatarka.inject.annotations.Inject + +class LibrarySettingsViewModel @Inject constructor( + libraryPreferences: LibraryPreferences, + contextWrapper: ContextWrapper +) : ViewModel(contextWrapper) { + val filterDownloaded = libraryPreferences.filterDownloaded().asStateFlow() + val filterUnread = libraryPreferences.filterUnread().asStateFlow() + val filterCompleted = libraryPreferences.filterCompleted().asStateFlow() + + val sortMode = libraryPreferences.sortMode().asStateFlow() + val sortAscending = libraryPreferences.sortAscending().asStateFlow() + + val displayMode = libraryPreferences.displayMode().asStateFlow() + val unreadBadges = libraryPreferences.unreadBadge().asStateFlow() + val downloadBadges = libraryPreferences.downloadBadge().asStateFlow() + val languageBadges = libraryPreferences.languageBadge().asStateFlow() + val localBadges = libraryPreferences.localBadge().asStateFlow() +} \ No newline at end of file diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySheet.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySheet.kt new file mode 100644 index 00000000..7d389816 --- /dev/null +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySheet.kt @@ -0,0 +1,98 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.ui.library.settings + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +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.material.Tab +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import ca.gosyer.jui.i18n.MR +import ca.gosyer.jui.uicore.components.VerticalScrollbar +import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter +import ca.gosyer.jui.uicore.resources.stringResource +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.pagerTabIndicatorOffset +import com.google.accompanist.pager.rememberPagerState +import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.launch + +enum class LibrarySheetTabs(val res: StringResource) { + FILTERS(MR.strings.action_filter), + SORT(MR.strings.library_sort), + DISPLAY(MR.strings.library_display) +} + +@Composable +fun LibrarySheet( + libraryFilters: @Composable () -> Unit, + librarySort: @Composable () -> Unit, + libraryDisplay: @Composable () -> Unit +) { + val pagerState = rememberPagerState() + val selectedPage = pagerState.currentPage + val scope = rememberCoroutineScope() + Column(Modifier.fillMaxSize()) { + TabRow( + selectedTabIndex = pagerState.currentPage, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + Modifier.pagerTabIndicatorOffset(pagerState, tabPositions) + ) + } + ) { + LibrarySheetTabs.values().asList().fastForEachIndexed { index, tab -> + Tab( + selected = selectedPage == index, + onClick = { + scope.launch { pagerState.animateScrollToPage(index) } + }, + text = { Text(stringResource(tab.res)) } + ) + } + } + HorizontalPager( + count = LibrarySheetTabs.values().size, + state = pagerState, + verticalAlignment = Alignment.Top + ) { + val scrollState = rememberScrollState() + Box { + Column( + Modifier.fillMaxWidth() + .verticalScroll(scrollState) + ) { + when (it) { + LibrarySheetTabs.FILTERS.ordinal -> libraryFilters() + LibrarySheetTabs.SORT.ordinal -> librarySort() + LibrarySheetTabs.DISPLAY.ordinal -> libraryDisplay() + } + } + + VerticalScrollbar( + rememberScrollbarAdapter(scrollState), + Modifier.align(Alignment.CenterEnd) + .fillMaxHeight() + .padding(horizontal = 4.dp, vertical = 8.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySideMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySideMenu.kt new file mode 100644 index 00000000..c75b94e1 --- /dev/null +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySideMenu.kt @@ -0,0 +1,69 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.ui.library.settings + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Divider +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ca.gosyer.jui.i18n.MR +import ca.gosyer.jui.uicore.components.VerticalScrollbar +import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter +import ca.gosyer.jui.uicore.resources.stringResource + +@Composable +fun LibrarySideMenu( + libraryFilters: @Composable () -> Unit, + librarySort: @Composable () -> Unit, + libraryDisplay: @Composable () -> Unit, +) { + Surface(Modifier.fillMaxHeight().width(260.dp), elevation = 1.dp) { + Box { + val scrollState = rememberScrollState() + Column( + Modifier.fillMaxWidth() + .verticalScroll(scrollState) + ) { + TitleText(stringResource(MR.strings.action_filter)) + libraryFilters() + Divider() + TitleText(stringResource(MR.strings.library_sort)) + librarySort() + Divider() + TitleText(stringResource(MR.strings.library_display)) + libraryDisplay() + } + + VerticalScrollbar( + rememberScrollbarAdapter(scrollState), + Modifier.align(Alignment.CenterEnd) + .fillMaxHeight() + .padding(horizontal = 4.dp, vertical = 8.dp) + ) + } + } +} + +@Composable +private fun TitleText(text: String) { + Box(Modifier.fillMaxWidth().padding(vertical = 8.dp), contentAlignment = Alignment.Center) { + Text(text, fontWeight = FontWeight.Bold, fontSize = 18.sp) + } +} \ No newline at end of file diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySort.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySort.kt new file mode 100644 index 00000000..10c02ecf --- /dev/null +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySort.kt @@ -0,0 +1,78 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.ui.library.settings + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowDownward +import androidx.compose.material.icons.rounded.ArrowUpward +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import ca.gosyer.jui.data.library.model.Sort +import ca.gosyer.jui.ui.sources.browse.filter.SourceFilterAction +import ca.gosyer.jui.uicore.resources.stringResource + +@Composable +fun getLibrarySort(vm: LibrarySettingsViewModel): @Composable () -> Unit = remember(vm) { + @Composable { + LibrarySort( + mode = vm.sortMode.collectAsState().value, + ascending = vm.sortAscending.collectAsState().value, + setMode = { + vm.sortMode.value = it + vm.sortAscending.value = true + }, + setAscending = { vm.sortAscending.value = it } + ) + } +} + +@Composable +fun LibrarySort( + mode: Sort, + ascending: Boolean, + setMode: (Sort) -> Unit, + setAscending: (Boolean) -> Unit +) { + Column(Modifier.fillMaxWidth()) { + Sort.values().asList().fastForEach { sort -> + SourceFilterAction( + name = stringResource(sort.res), + onClick = { + if (mode == sort) { + setAscending(!ascending) + } else { + setMode(sort) + } + }, + action = { + if (mode == sort) { + Icon( + imageVector = when (ascending) { + true -> Icons.Rounded.ArrowUpward + false -> Icons.Rounded.ArrowDownward + }, + contentDescription = stringResource(sort.res), + modifier = Modifier.fillMaxHeight() + ) + } else { + Box(Modifier.size(24.dp)) + } + } + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceScreenContent.kt index b31fd979..501fa2e7 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/components/SourceScreenContent.kt @@ -353,7 +353,7 @@ private fun SourceThinScreenContent( if (showFilterButton && !isLatest) { ExtendedFloatingActionButton( text = { - Text(stringResource(MR.strings.filter_source)) + Text(stringResource(MR.strings.action_filter)) }, onClick = { setShowingFilters(true) @@ -361,7 +361,7 @@ private fun SourceThinScreenContent( icon = { Icon( Icons.Rounded.FilterList, - stringResource(MR.strings.filter_source) + stringResource(MR.strings.action_filter) ) }, modifier = Modifier.align(Alignment.BottomEnd) @@ -506,7 +506,7 @@ private fun getActionItems( return listOfNotNull( if (showFilterButton) { ActionItem( - name = stringResource(MR.strings.filter_source), + name = stringResource(MR.strings.action_filter), icon = Icons.Rounded.FilterList, doAction = onToggleFiltersClick, enabled = !isLatest diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersMenu.kt index a2e7a2f8..91a35f45 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersMenu.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight @@ -86,7 +87,7 @@ fun SourceFiltersMenu( Text(stringResource(MR.strings.reset_filters)) } Button(onSearchClicked) { - Text(stringResource(MR.strings.filter_source)) + Text(stringResource(MR.strings.action_filter)) } } } @@ -141,7 +142,8 @@ fun SourceFilterAction( Row( Modifier.fillMaxWidth().defaultMinSize(minHeight = 56.dp) .clickable(onClick = onClick) - .padding(horizontal = 16.dp), + .padding(horizontal = 16.dp) + .height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically ) { action() diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/vm/ViewModelFactory.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/vm/ViewModelFactory.kt index 75aeea96..71bee235 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/vm/ViewModelFactory.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/vm/ViewModelFactory.kt @@ -11,10 +11,12 @@ import ca.gosyer.jui.ui.categories.CategoriesScreenViewModel import ca.gosyer.jui.ui.downloads.DownloadsScreenViewModel import ca.gosyer.jui.ui.extensions.ExtensionsScreenViewModel import ca.gosyer.jui.ui.library.LibraryScreenViewModel +import ca.gosyer.jui.ui.library.settings.LibrarySettingsViewModel import ca.gosyer.jui.ui.main.MainViewModel import ca.gosyer.jui.ui.main.about.AboutViewModel import ca.gosyer.jui.ui.main.components.DebugOverlayViewModel import ca.gosyer.jui.ui.main.components.TrayViewModel +import ca.gosyer.jui.ui.manga.MangaScreenViewModel import ca.gosyer.jui.ui.reader.ReaderMenuViewModel import ca.gosyer.jui.ui.settings.SettingsAdvancedViewModel import ca.gosyer.jui.ui.settings.SettingsBackupViewModel @@ -43,10 +45,11 @@ actual class ViewModelFactoryImpl( private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel, private val extensionsFactory: () -> ExtensionsScreenViewModel, private val libraryFactory: () -> LibraryScreenViewModel, + private val librarySettingsFactory: () -> LibrarySettingsViewModel, private val debugOverlayFactory: () -> DebugOverlayViewModel, private val trayFactory: () -> TrayViewModel, private val mainFactory: () -> MainViewModel, - private val mangaFactory: (params: ca.gosyer.jui.ui.manga.MangaScreenViewModel.Params) -> ca.gosyer.jui.ui.manga.MangaScreenViewModel, + private val mangaFactory: (params: MangaScreenViewModel.Params) -> MangaScreenViewModel, private val readerFactory: (params: ReaderMenuViewModel.Params) -> ReaderMenuViewModel, private val settingsAdvancedFactory: () -> SettingsAdvancedViewModel, private val themesFactory: () -> ThemesViewModel, @@ -73,10 +76,11 @@ actual class ViewModelFactoryImpl( DownloadsScreenViewModel::class -> downloadsFactory(arg1 as Boolean) ExtensionsScreenViewModel::class -> extensionsFactory() LibraryScreenViewModel::class -> libraryFactory() + LibrarySettingsViewModel::class -> librarySettingsFactory() DebugOverlayViewModel::class -> debugOverlayFactory() TrayViewModel::class -> trayFactory() MainViewModel::class -> mainFactory() - ca.gosyer.jui.ui.manga.MangaScreenViewModel::class -> mangaFactory(arg1 as ca.gosyer.jui.ui.manga.MangaScreenViewModel.Params) + MangaScreenViewModel::class -> mangaFactory(arg1 as MangaScreenViewModel.Params) ReaderMenuViewModel::class -> readerFactory(arg1 as ReaderMenuViewModel.Params) SettingsAdvancedViewModel::class -> settingsAdvancedFactory() ThemesViewModel::class -> themesFactory()