Library filtering and sorting modes

This commit is contained in:
Syer10
2022-04-06 20:41:48 -04:00
parent 0413ab5b42
commit e11eed95d7
24 changed files with 957 additions and 96 deletions

View File

@@ -9,12 +9,25 @@ package ca.gosyer.jui.data.library
import ca.gosyer.jui.core.prefs.Preference import ca.gosyer.jui.core.prefs.Preference
import ca.gosyer.jui.core.prefs.PreferenceStore import ca.gosyer.jui.core.prefs.PreferenceStore
import ca.gosyer.jui.data.library.model.DisplayMode import ca.gosyer.jui.data.library.model.DisplayMode
import ca.gosyer.jui.data.library.model.FilterState
import ca.gosyer.jui.data.library.model.Sort import ca.gosyer.jui.data.library.model.Sort
class LibraryPreferences(private val preferenceStore: PreferenceStore) { class LibraryPreferences(private val preferenceStore: PreferenceStore) {
fun displayMode(): Preference<DisplayMode> { fun showAllCategory(): Preference<Boolean> {
return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer()) return preferenceStore.getBoolean("show_all_category", false)
}
fun filterDownloaded(): Preference<FilterState> {
return preferenceStore.getJsonObject("filter_downloaded", FilterState.IGNORED, FilterState.serializer())
}
fun filterUnread(): Preference<FilterState> {
return preferenceStore.getJsonObject("filter_unread", FilterState.IGNORED, FilterState.serializer())
}
fun filterCompleted(): Preference<FilterState> {
return preferenceStore.getJsonObject("filter_completed", FilterState.IGNORED, FilterState.serializer())
} }
fun sortMode(): Preference<Sort> { fun sortMode(): Preference<Sort> {
@@ -25,6 +38,10 @@ class LibraryPreferences(private val preferenceStore: PreferenceStore) {
return preferenceStore.getBoolean("sort_ascending", true) return preferenceStore.getBoolean("sort_ascending", true)
} }
fun displayMode(): Preference<DisplayMode> {
return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer())
}
fun gridColumns(): Preference<Int> { fun gridColumns(): Preference<Int> {
return preferenceStore.getInt("grid_columns", 0) return preferenceStore.getInt("grid_columns", 0)
} }
@@ -33,7 +50,19 @@ class LibraryPreferences(private val preferenceStore: PreferenceStore) {
return preferenceStore.getInt("grid_size", 160) return preferenceStore.getInt("grid_size", 160)
} }
fun showAllCategory(): Preference<Boolean> { fun unreadBadge(): Preference<Boolean> {
return preferenceStore.getBoolean("show_all_category", false) return preferenceStore.getBoolean("unread_badge", true)
}
fun downloadBadge(): Preference<Boolean> {
return preferenceStore.getBoolean("download_badge", false)
}
fun languageBadge(): Preference<Boolean> {
return preferenceStore.getBoolean("language_badge", false)
}
fun localBadge(): Preference<Boolean> {
return preferenceStore.getBoolean("local_badge", false)
} }
} }

View File

@@ -19,6 +19,6 @@ enum class DisplayMode(@Transient val res: StringResource) {
List(MR.strings.display_list); List(MR.strings.display_list);
companion object { companion object {
val values = values() val values = values().asList()
} }
} }

View File

@@ -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
}

View File

@@ -6,18 +6,21 @@
package ca.gosyer.jui.data.library.model 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.Serializable
import kotlinx.serialization.Transient
@Serializable @Serializable
enum class Sort { enum class Sort(@Transient val res: StringResource) {
ALPHABETICAL, ALPHABETICAL(MR.strings.sort_alphabetical),
// LAST_READ, // LAST_READ,
// LAST_CHECKED, // LAST_CHECKED,
UNREAD, UNREAD(MR.strings.sort_unread),
// TOTAL_CHAPTERS, // TOTAL_CHAPTERS,
// LATEST_CHAPTER, // LATEST_CHAPTER,
// DATE_FETCHED, // DATE_FETCHED,
DATE_ADDED; DATE_ADDED(MR.strings.sort_date_added);
} }

View File

@@ -45,6 +45,7 @@
<string name="action_more_actions">More actions</string> <string name="action_more_actions">More actions</string>
<string name="action_ok">Ok</string> <string name="action_ok">Ok</string>
<string name="action_browser">Browser</string> <string name="action_browser">Browser</string>
<string name="action_filter">Filter</string>
<!-- Locations --> <!-- Locations -->
<string name="location_library">Library</string> <string name="location_library">Library</string>
@@ -81,6 +82,23 @@
<string name="default_category">Default</string> <string name="default_category">Default</string>
<string name="library_empty">Library is empty</string> <string name="library_empty">Library is empty</string>
<string name="library_sort">Sort</string>
<string name="library_display">Display</string>
<string name="filter_downloaded">Downloaded</string>
<string name="filter_unread">Unread</string>
<string name="filter_completed">Completed</string>
<string name="sort_alphabetical">Alphabetically</string>
<string name="sort_unread">Unread</string>
<string name="sort_date_added">Date added</string>
<string name="display_badges">Badges</string>
<string name="display_badge_downloaded">Downloaded chapters</string>
<string name="display_badge_unread">Unread chapters</string>
<string name="display_badge_local">Local manga</string>
<string name="display_badge_language">Language</string>
<!-- Manga Menu --> <!-- Manga Menu -->
<string name="page_progress">Page %1$d</string> <string name="page_progress">Page %1$d</string>
<string name="no_chapters_found">No chapters found</string> <string name="no_chapters_found">No chapters found</string>
@@ -100,7 +118,6 @@
<string name="move_to_browse">Browse</string> <string name="move_to_browse">Browse</string>
<string name="move_to_latest">Latest</string> <string name="move_to_latest">Latest</string>
<string name="reset_filters">Reset</string> <string name="reset_filters">Reset</string>
<string name="filter_source">Filter</string>
<string name="in_library">In library</string> <string name="in_library">In library</string>
<string name="no_results_found">No results found</string> <string name="no_results_found">No results found</string>

View File

@@ -11,6 +11,7 @@ import ca.gosyer.jui.ui.categories.CategoriesScreenViewModel
import ca.gosyer.jui.ui.downloads.DownloadsScreenViewModel import ca.gosyer.jui.ui.downloads.DownloadsScreenViewModel
import ca.gosyer.jui.ui.extensions.ExtensionsScreenViewModel import ca.gosyer.jui.ui.extensions.ExtensionsScreenViewModel
import ca.gosyer.jui.ui.library.LibraryScreenViewModel 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.MainViewModel
import ca.gosyer.jui.ui.main.about.AboutViewModel import ca.gosyer.jui.ui.main.about.AboutViewModel
import ca.gosyer.jui.ui.main.components.DebugOverlayViewModel import ca.gosyer.jui.ui.main.components.DebugOverlayViewModel
@@ -43,6 +44,7 @@ actual class ViewModelFactoryImpl(
private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel, private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel,
private val extensionsFactory: () -> ExtensionsScreenViewModel, private val extensionsFactory: () -> ExtensionsScreenViewModel,
private val libraryFactory: () -> LibraryScreenViewModel, private val libraryFactory: () -> LibraryScreenViewModel,
private val librarySettingsFactory: () -> LibrarySettingsViewModel,
private val debugOverlayFactory: () -> DebugOverlayViewModel, private val debugOverlayFactory: () -> DebugOverlayViewModel,
private val mainFactory: () -> MainViewModel, private val mainFactory: () -> MainViewModel,
private val mangaFactory: (params: MangaScreenViewModel.Params) -> MangaScreenViewModel, private val mangaFactory: (params: MangaScreenViewModel.Params) -> MangaScreenViewModel,
@@ -72,6 +74,7 @@ actual class ViewModelFactoryImpl(
DownloadsScreenViewModel::class -> downloadsFactory(arg1 as Boolean) DownloadsScreenViewModel::class -> downloadsFactory(arg1 as Boolean)
ExtensionsScreenViewModel::class -> extensionsFactory() ExtensionsScreenViewModel::class -> extensionsFactory()
LibraryScreenViewModel::class -> libraryFactory() LibraryScreenViewModel::class -> libraryFactory()
LibrarySettingsViewModel::class -> librarySettingsFactory()
DebugOverlayViewModel::class -> debugOverlayFactory() DebugOverlayViewModel::class -> debugOverlayFactory()
MainViewModel::class -> mainFactory() MainViewModel::class -> mainFactory()
MangaScreenViewModel::class -> mangaFactory(arg1 as MangaScreenViewModel.Params) MangaScreenViewModel::class -> mangaFactory(arg1 as MangaScreenViewModel.Params)

View File

@@ -9,6 +9,10 @@ package ca.gosyer.jui.ui.library
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import ca.gosyer.jui.ui.library.components.LibraryScreenContent 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.ui.manga.MangaScreen
import ca.gosyer.jui.uicore.vm.viewModel import ca.gosyer.jui.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
@@ -24,6 +28,7 @@ class LibraryScreen : Screen {
@Composable @Composable
override fun Content() { override fun Content() {
val vm = viewModel<LibraryScreenViewModel>() val vm = viewModel<LibraryScreenViewModel>()
val settingsVM = viewModel<LibrarySettingsViewModel>()
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
LibraryScreenContent( LibraryScreenContent(
categories = vm.categories.collectAsState().value, categories = vm.categories.collectAsState().value,
@@ -38,7 +43,16 @@ class LibraryScreen : Screen {
getLibraryForPage = { vm.getLibraryForCategoryId(it).collectAsState() }, getLibraryForPage = { vm.getLibraryForCategoryId(it).collectAsState() },
onPageChanged = vm::setSelectedPage, onPageChanged = vm::setSelectedPage,
onClickManga = { navigator push MangaScreen(it) }, 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
) )
} }
} }

View File

@@ -9,10 +9,13 @@ package ca.gosyer.jui.ui.library
import ca.gosyer.jui.core.lang.getDefault import ca.gosyer.jui.core.lang.getDefault
import ca.gosyer.jui.core.lang.lowercase import ca.gosyer.jui.core.lang.lowercase
import ca.gosyer.jui.core.lang.withDefaultContext 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.LibraryPreferences
import ca.gosyer.jui.data.library.model.FilterState
import ca.gosyer.jui.data.library.model.Sort import ca.gosyer.jui.data.library.model.Sort
import ca.gosyer.jui.data.models.Category import ca.gosyer.jui.data.models.Category
import ca.gosyer.jui.data.models.Manga 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.CategoryInteractionHandler
import ca.gosyer.jui.data.server.interactions.LibraryInteractionHandler import ca.gosyer.jui.data.server.interactions.LibraryInteractionHandler
import ca.gosyer.jui.data.server.interactions.UpdatesInteractionHandler import ca.gosyer.jui.data.server.interactions.UpdatesInteractionHandler
@@ -67,13 +70,42 @@ class LibraryScreenViewModel @Inject constructor(
private val _selectedCategoryIndex = MutableStateFlow(0) private val _selectedCategoryIndex = MutableStateFlow(0)
val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow()
private val _showingMenu = MutableStateFlow(false)
val showingMenu = _showingMenu.asStateFlow()
val displayMode = libraryPreferences.displayMode().stateIn(scope) val displayMode = libraryPreferences.displayMode().stateIn(scope)
val gridColumns = libraryPreferences.gridColumns().stateIn(scope) val gridColumns = libraryPreferences.gridColumns().stateIn(scope)
val gridSize = libraryPreferences.gridSize().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 sortMode = libraryPreferences.sortMode().stateIn(scope)
private val sortAscending = libraryPreferences.sortAscending().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) private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()
@@ -114,6 +146,10 @@ class LibraryScreenViewModel @Inject constructor(
_selectedCategoryIndex.value = page _selectedCategoryIndex.value = page
} }
fun setShowingMenu(showingMenu: Boolean) {
_showingMenu.value = showingMenu
}
private fun getComparator(sortMode: Sort, ascending: Boolean): Comparator<Manga> { private fun getComparator(sortMode: Sort, ascending: Boolean): Comparator<Manga> {
val sortFn = when (sortMode) { val sortFn = when (sortMode) {
Sort.ALPHABETICAL -> { Sort.ALPHABETICAL -> {
@@ -164,6 +200,8 @@ class LibraryScreenViewModel @Inject constructor(
private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow<List<Manga>>): StateFlow<List<Manga>> { private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow<List<Manga>>): StateFlow<List<Manga>> {
return combine(unfilteredItemsFlow, query) { unfilteredItems, query -> return combine(unfilteredItemsFlow, query) { unfilteredItems, query ->
filterManga(query, unfilteredItems) filterManga(query, unfilteredItems)
}.combine(filter) { filteredManga, filterer ->
filteredManga.filter(filterer)
}.combine(comparator) { filteredManga, comparator -> }.combine(comparator) { filteredManga, comparator ->
filteredManga.sortedWith(comparator) filteredManga.sortedWith(comparator)
}.stateIn(scope, SharingStarted.Eagerly, emptyList()) }.stateIn(scope, SharingStarted.Eagerly, emptyList())

View File

@@ -7,7 +7,10 @@
package ca.gosyer.jui.ui.library.components package ca.gosyer.jui.ui.library.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
@@ -15,33 +18,66 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.jui.data.models.Manga
import ca.gosyer.jui.data.models.Source
@Composable @Composable
fun LibraryMangaBadges( fun LibraryMangaBadges(
unread: Int?,
downloaded: Int?,
modifier: Modifier = Modifier, 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)) { Row(modifier then Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
if (unread != null && unread > 0) { if ((unread != null && unread > 0) || (downloaded != null && downloaded > 0) || isLocal) {
Text( Row(modifier = Modifier.clip(MaterialTheme.shapes.medium)) {
text = unread.toString(), if (showLocal && isLocal) {
modifier = Modifier.background(MaterialTheme.colors.primary).then(BadgesInnerPadding), Text(
style = MaterialTheme.typography.caption, text = unread.toString(),
color = MaterialTheme.colors.onPrimary 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( val lang = manga.source?.lang
text = downloaded.toString(), if (showLanguage && lang != null) {
modifier = Modifier.background(MaterialTheme.colors.secondary).then( Row(modifier = Modifier.clip(MaterialTheme.shapes.medium)) {
BadgesInnerPadding Text(
), text = lang.uppercase(),
style = MaterialTheme.typography.caption, modifier = Modifier.background(MaterialTheme.colors.secondary).then(BadgesInnerPadding),
color = MaterialTheme.colors.onSecondary style = MaterialTheme.typography.caption,
) color = MaterialTheme.colors.onSecondary
)
}
} }
} }
} }

View File

@@ -7,11 +7,13 @@
package ca.gosyer.jui.ui.library.components package ca.gosyer.jui.ui.library.components
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@@ -34,7 +36,11 @@ import io.kamel.image.lazyPainterResource
fun LibraryMangaList( fun LibraryMangaList(
library: List<Manga>, library: List<Manga>,
onClickManga: (Long) -> Unit, onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit onRemoveMangaClicked: (Long) -> Unit,
showUnread: Boolean,
showDownloaded: Boolean,
showLanguage: Boolean,
showLocal: Boolean
) { ) {
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
@@ -49,8 +55,10 @@ fun LibraryMangaList(
{ onRemoveMangaClicked(manga.id) } { onRemoveMangaClicked(manga.id) }
), ),
manga = manga, manga = manga,
unread = manga.unreadCount, showUnread = showUnread,
downloaded = manga.downloadCount, showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
} }
} }
@@ -67,8 +75,10 @@ fun LibraryMangaList(
private fun LibraryMangaListItem( private fun LibraryMangaListItem(
modifier: Modifier, modifier: Modifier,
manga: Manga, manga: Manga,
unread: Int?, showUnread: Boolean,
downloaded: Int?, showDownloaded: Boolean,
showLanguage: Boolean,
showLocal: Boolean
) { ) {
val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium)
MangaListItem( MangaListItem(
@@ -89,6 +99,14 @@ private fun LibraryMangaListItem(
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
text = manga.title, text = manga.title,
) )
LibraryMangaBadges(unread, downloaded) Box(Modifier.width(IntrinsicSize.Min)) {
LibraryMangaBadges(
manga = manga,
showUnread = showUnread,
showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
)
}
} }
} }

View File

@@ -25,7 +25,11 @@ fun LibraryPager(
gridSize: Int, gridSize: Int,
getLibraryForPage: @Composable (Long) -> State<List<Manga>>, getLibraryForPage: @Composable (Long) -> State<List<Manga>>,
onClickManga: (Long) -> Unit, onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit onRemoveMangaClicked: (Long) -> Unit,
showUnread: Boolean,
showDownloaded: Boolean,
showLanguage: Boolean,
showLocal: Boolean
) { ) {
if (categories.isEmpty()) return if (categories.isEmpty()) return
@@ -37,26 +41,42 @@ fun LibraryPager(
gridColumns = gridColumns, gridColumns = gridColumns,
gridSize = gridSize, gridSize = gridSize,
onClickManga = onClickManga, onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked onRemoveMangaClicked = onRemoveMangaClicked,
showUnread = showUnread,
showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
DisplayMode.ComfortableGrid -> LibraryMangaComfortableGrid( DisplayMode.ComfortableGrid -> LibraryMangaComfortableGrid(
library = library, library = library,
gridColumns = gridColumns, gridColumns = gridColumns,
gridSize = gridSize, gridSize = gridSize,
onClickManga = onClickManga, onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked onRemoveMangaClicked = onRemoveMangaClicked,
showUnread = showUnread,
showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
DisplayMode.CoverOnlyGrid -> LibraryMangaCoverOnlyGrid( DisplayMode.CoverOnlyGrid -> LibraryMangaCoverOnlyGrid(
library = library, library = library,
gridColumns = gridColumns, gridColumns = gridColumns,
gridSize = gridSize, gridSize = gridSize,
onClickManga = onClickManga, onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked onRemoveMangaClicked = onRemoveMangaClicked,
showUnread = showUnread,
showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
DisplayMode.List -> LibraryMangaList( DisplayMode.List -> LibraryMangaList(
library = library, library = library,
onClickManga = onClickManga, onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked onRemoveMangaClicked = onRemoveMangaClicked,
showUnread = showUnread,
showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
else -> Box {} else -> Box {}
} }

View File

@@ -6,23 +6,40 @@
package ca.gosyer.jui.ui.library.components 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.Box
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding 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.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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.jui.data.library.model.DisplayMode import ca.gosyer.jui.data.library.model.DisplayMode
import ca.gosyer.jui.data.models.Category import ca.gosyer.jui.data.models.Category
import ca.gosyer.jui.data.models.Manga import ca.gosyer.jui.data.models.Manga
import ca.gosyer.jui.i18n.MR 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.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.components.LoadingScreen
import ca.gosyer.jui.uicore.resources.stringResource import ca.gosyer.jui.uicore.resources.stringResource
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
@@ -42,7 +59,16 @@ fun LibraryScreenContent(
getLibraryForPage: @Composable (Long) -> State<List<Manga>>, getLibraryForPage: @Composable (Long) -> State<List<Manga>>,
onPageChanged: (Int) -> Unit, onPageChanged: (Int) -> Unit,
onClickManga: (Long) -> 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 { BoxWithConstraints {
val pagerState = rememberPagerState(selectedCategoryIndex) val pagerState = rememberPagerState(selectedCategoryIndex)
@@ -71,7 +97,16 @@ fun LibraryScreenContent(
getLibraryForPage = getLibraryForPage, getLibraryForPage = getLibraryForPage,
onPageChanged = onPageChanged, onPageChanged = onPageChanged,
onClickManga = onClickManga, 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 { } else {
ThinLibraryScreenContent( ThinLibraryScreenContent(
@@ -88,7 +123,16 @@ fun LibraryScreenContent(
getLibraryForPage = getLibraryForPage, getLibraryForPage = getLibraryForPage,
onPageChanged = onPageChanged, onPageChanged = onPageChanged,
onClickManga = onClickManga, 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<List<Manga>>, getLibraryForPage: @Composable (Long) -> State<List<Manga>>,
onPageChanged: (Int) -> Unit, onPageChanged: (Int) -> Unit,
onClickManga: (Long) -> 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( Scaffold(
topBar = { topBar = {
@@ -117,7 +170,12 @@ fun WideLibraryScreenContent(
Toolbar( Toolbar(
stringResource(MR.strings.location_library), stringResource(MR.strings.location_library),
searchText = query, searchText = query,
search = updateQuery search = updateQuery,
actions = {
getActionItems(
onToggleFiltersClick = { setShowingMenu(true) }
)
}
) )
LibraryTabs( LibraryTabs(
visible = true, // vm.showCategoryTabs, visible = true, // vm.showCategoryTabs,
@@ -128,8 +186,8 @@ fun WideLibraryScreenContent(
) )
} }
} }
) { ) { padding ->
Box(Modifier.padding(it)) { Box(Modifier.padding(padding)) {
if (categories.isEmpty()) { if (categories.isEmpty()) {
LoadingScreen(isLoading, errorMessage = error) LoadingScreen(isLoading, errorMessage = error)
} else { } else {
@@ -141,8 +199,36 @@ fun WideLibraryScreenContent(
gridSize = gridSize, gridSize = gridSize,
getLibraryForPage = getLibraryForPage, getLibraryForPage = getLibraryForPage,
onClickManga = onClickManga, 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<List<Manga>>, getLibraryForPage: @Composable (Long) -> State<List<Manga>>,
onPageChanged: (Int) -> Unit, onPageChanged: (Int) -> Unit,
onClickManga: (Long) -> 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() val bottomSheetState = rememberModalBottomSheetState(
BottomSheetScaffold( ModalBottomSheetValue.Hidden,
scaffoldState = sheetState, 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 = { topBar = {
Column { Column {
Toolbar( Toolbar(
stringResource(MR.strings.location_library), stringResource(MR.strings.location_library),
searchText = query, searchText = query,
search = updateQuery search = updateQuery,
actions = {
getActionItems(
onToggleFiltersClick = { setShowingSheet(true) }
)
}
) )
LibraryTabs( LibraryTabs(
visible = true, // vm.showCategoryTabs, visible = true, // vm.showCategoryTabs,
@@ -183,13 +299,19 @@ fun ThinLibraryScreenContent(
onPageChanged = onPageChanged onPageChanged = onPageChanged
) )
} }
}, }
sheetContent = { ) { padding ->
// LibrarySheetContent() ModalBottomSheetLayout(
}, sheetState = bottomSheetState,
sheetPeekHeight = 0.dp modifier = Modifier.padding(padding),
) { sheetContent = {
Box(Modifier.padding(it)) { LibrarySheet(
libraryFilters = libraryFilters,
librarySort = librarySort,
libraryDisplay = libraryDisplay
)
}
) {
if (categories.isEmpty()) { if (categories.isEmpty()) {
LoadingScreen(isLoading, errorMessage = error) LoadingScreen(isLoading, errorMessage = error)
} else { } else {
@@ -201,9 +323,27 @@ fun ThinLibraryScreenContent(
gridSize = gridSize, gridSize = gridSize,
getLibraryForPage = getLibraryForPage, getLibraryForPage = getLibraryForPage,
onClickManga = onClickManga, onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked onRemoveMangaClicked = onRemoveMangaClicked,
showUnread = showUnread,
showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
} }
} }
} }
} }
@Composable
@Stable
private fun getActionItems(
onToggleFiltersClick: () -> Unit,
): List<ActionItem> {
return listOfNotNull(
ActionItem(
name = stringResource(MR.strings.action_filter),
icon = Icons.Rounded.FilterList,
doAction = onToggleFiltersClick
)
)
}

View File

@@ -43,7 +43,11 @@ fun LibraryMangaComfortableGrid(
gridColumns: Int, gridColumns: Int,
gridSize: Int, gridSize: Int,
onClickManga: (Long) -> Unit, onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit onRemoveMangaClicked: (Long) -> Unit,
showUnread: Boolean,
showDownloaded: Boolean,
showLanguage: Boolean,
showLocal: Boolean
) { ) {
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
@@ -64,8 +68,10 @@ fun LibraryMangaComfortableGrid(
{ onRemoveMangaClicked(manga.id) } { onRemoveMangaClicked(manga.id) }
), ),
manga = manga, manga = manga,
unread = manga.unreadCount, showUnread = showUnread,
downloaded = manga.downloadCount showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
} }
} }
@@ -82,8 +88,10 @@ fun LibraryMangaComfortableGrid(
private fun LibraryMangaComfortableGridItem( private fun LibraryMangaComfortableGridItem(
modifier: Modifier, modifier: Modifier,
manga: Manga, manga: Manga,
unread: Int?, showUnread: Boolean,
downloaded: Int? showDownloaded: Boolean,
showLanguage: Boolean,
showLocal: Boolean
) { ) {
val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium)
val fontStyle = LocalTextStyle.current.merge( val fontStyle = LocalTextStyle.current.merge(
@@ -114,9 +122,12 @@ private fun LibraryMangaComfortableGridItem(
) )
} }
LibraryMangaBadges( LibraryMangaBadges(
unread = unread, modifier = Modifier.padding(4.dp),
downloaded = downloaded, manga = manga,
modifier = Modifier.padding(4.dp) showUnread = showUnread,
showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
} }
} }

View File

@@ -51,7 +51,11 @@ fun LibraryMangaCompactGrid(
gridColumns: Int, gridColumns: Int,
gridSize: Int, gridSize: Int,
onClickManga: (Long) -> Unit, onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit onRemoveMangaClicked: (Long) -> Unit,
showUnread: Boolean,
showDownloaded: Boolean,
showLanguage: Boolean,
showLocal: Boolean
) { ) {
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
@@ -72,8 +76,10 @@ fun LibraryMangaCompactGrid(
{ onRemoveMangaClicked(manga.id) } { onRemoveMangaClicked(manga.id) }
), ),
manga = manga, manga = manga,
unread = manga.unreadCount, showUnread = showUnread,
downloaded = manga.downloadCount showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
} }
} }
@@ -90,8 +96,10 @@ fun LibraryMangaCompactGrid(
private fun LibraryMangaCompactGridItem( private fun LibraryMangaCompactGridItem(
modifier: Modifier, modifier: Modifier,
manga: Manga, manga: Manga,
unread: Int?, showUnread: Boolean,
downloaded: Int?, showDownloaded: Boolean,
showLanguage: Boolean,
showLocal: Boolean
) { ) {
val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium)
val fontStyle = LocalTextStyle.current.merge( val fontStyle = LocalTextStyle.current.merge(
@@ -119,9 +127,12 @@ private fun LibraryMangaCompactGridItem(
modifier = Modifier.align(Alignment.BottomStart).padding(8.dp) modifier = Modifier.align(Alignment.BottomStart).padding(8.dp)
) )
LibraryMangaBadges( LibraryMangaBadges(
unread = unread, modifier = Modifier.padding(4.dp),
downloaded = downloaded, manga = manga,
modifier = Modifier.padding(4.dp) showUnread = showUnread,
showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
} }
} }

View File

@@ -37,7 +37,11 @@ fun LibraryMangaCoverOnlyGrid(
gridColumns: Int, gridColumns: Int,
gridSize: Int, gridSize: Int,
onClickManga: (Long) -> Unit, onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit onRemoveMangaClicked: (Long) -> Unit,
showUnread: Boolean,
showDownloaded: Boolean,
showLanguage: Boolean,
showLocal: Boolean
) { ) {
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
@@ -58,8 +62,10 @@ fun LibraryMangaCoverOnlyGrid(
{ onRemoveMangaClicked(manga.id) } { onRemoveMangaClicked(manga.id) }
), ),
manga = manga, manga = manga,
unread = manga.unreadCount, showUnread = showUnread,
downloaded = manga.downloadCount showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
} }
} }
@@ -76,8 +82,10 @@ fun LibraryMangaCoverOnlyGrid(
private fun LibraryMangaCoverOnlyGridItem( private fun LibraryMangaCoverOnlyGridItem(
modifier: Modifier, modifier: Modifier,
manga: Manga, manga: Manga,
unread: Int?, showUnread: Boolean,
downloaded: Int? showDownloaded: Boolean,
showLanguage: Boolean,
showLocal: Boolean
) { ) {
val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium) val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium)
@@ -94,9 +102,12 @@ private fun LibraryMangaCoverOnlyGridItem(
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
LibraryMangaBadges( LibraryMangaBadges(
unread = unread, modifier = Modifier.padding(4.dp),
downloaded = downloaded, manga = manga,
modifier = Modifier.padding(4.dp) showUnread = showUnread,
showDownloaded = showDownloaded,
showLanguage = showLanguage,
showLocal = showLocal
) )
} }
} }

View File

@@ -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
)
}
)
}

View File

@@ -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
)
}
)
}

View File

@@ -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()
}

View File

@@ -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)
)
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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))
}
}
)
}
}
}

View File

@@ -353,7 +353,7 @@ private fun SourceThinScreenContent(
if (showFilterButton && !isLatest) { if (showFilterButton && !isLatest) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
text = { text = {
Text(stringResource(MR.strings.filter_source)) Text(stringResource(MR.strings.action_filter))
}, },
onClick = { onClick = {
setShowingFilters(true) setShowingFilters(true)
@@ -361,7 +361,7 @@ private fun SourceThinScreenContent(
icon = { icon = {
Icon( Icon(
Icons.Rounded.FilterList, Icons.Rounded.FilterList,
stringResource(MR.strings.filter_source) stringResource(MR.strings.action_filter)
) )
}, },
modifier = Modifier.align(Alignment.BottomEnd) modifier = Modifier.align(Alignment.BottomEnd)
@@ -506,7 +506,7 @@ private fun getActionItems(
return listOfNotNull( return listOfNotNull(
if (showFilterButton) { if (showFilterButton) {
ActionItem( ActionItem(
name = stringResource(MR.strings.filter_source), name = stringResource(MR.strings.action_filter),
icon = Icons.Rounded.FilterList, icon = Icons.Rounded.FilterList,
doAction = onToggleFiltersClick, doAction = onToggleFiltersClick,
enabled = !isLatest enabled = !isLatest

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -86,7 +87,7 @@ fun SourceFiltersMenu(
Text(stringResource(MR.strings.reset_filters)) Text(stringResource(MR.strings.reset_filters))
} }
Button(onSearchClicked) { Button(onSearchClicked) {
Text(stringResource(MR.strings.filter_source)) Text(stringResource(MR.strings.action_filter))
} }
} }
} }
@@ -141,7 +142,8 @@ fun SourceFilterAction(
Row( Row(
Modifier.fillMaxWidth().defaultMinSize(minHeight = 56.dp) Modifier.fillMaxWidth().defaultMinSize(minHeight = 56.dp)
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp)
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
action() action()

View File

@@ -11,10 +11,12 @@ import ca.gosyer.jui.ui.categories.CategoriesScreenViewModel
import ca.gosyer.jui.ui.downloads.DownloadsScreenViewModel import ca.gosyer.jui.ui.downloads.DownloadsScreenViewModel
import ca.gosyer.jui.ui.extensions.ExtensionsScreenViewModel import ca.gosyer.jui.ui.extensions.ExtensionsScreenViewModel
import ca.gosyer.jui.ui.library.LibraryScreenViewModel 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.MainViewModel
import ca.gosyer.jui.ui.main.about.AboutViewModel import ca.gosyer.jui.ui.main.about.AboutViewModel
import ca.gosyer.jui.ui.main.components.DebugOverlayViewModel import ca.gosyer.jui.ui.main.components.DebugOverlayViewModel
import ca.gosyer.jui.ui.main.components.TrayViewModel 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.reader.ReaderMenuViewModel
import ca.gosyer.jui.ui.settings.SettingsAdvancedViewModel import ca.gosyer.jui.ui.settings.SettingsAdvancedViewModel
import ca.gosyer.jui.ui.settings.SettingsBackupViewModel import ca.gosyer.jui.ui.settings.SettingsBackupViewModel
@@ -43,10 +45,11 @@ actual class ViewModelFactoryImpl(
private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel, private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel,
private val extensionsFactory: () -> ExtensionsScreenViewModel, private val extensionsFactory: () -> ExtensionsScreenViewModel,
private val libraryFactory: () -> LibraryScreenViewModel, private val libraryFactory: () -> LibraryScreenViewModel,
private val librarySettingsFactory: () -> LibrarySettingsViewModel,
private val debugOverlayFactory: () -> DebugOverlayViewModel, private val debugOverlayFactory: () -> DebugOverlayViewModel,
private val trayFactory: () -> TrayViewModel, private val trayFactory: () -> TrayViewModel,
private val mainFactory: () -> MainViewModel, 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 readerFactory: (params: ReaderMenuViewModel.Params) -> ReaderMenuViewModel,
private val settingsAdvancedFactory: () -> SettingsAdvancedViewModel, private val settingsAdvancedFactory: () -> SettingsAdvancedViewModel,
private val themesFactory: () -> ThemesViewModel, private val themesFactory: () -> ThemesViewModel,
@@ -73,10 +76,11 @@ actual class ViewModelFactoryImpl(
DownloadsScreenViewModel::class -> downloadsFactory(arg1 as Boolean) DownloadsScreenViewModel::class -> downloadsFactory(arg1 as Boolean)
ExtensionsScreenViewModel::class -> extensionsFactory() ExtensionsScreenViewModel::class -> extensionsFactory()
LibraryScreenViewModel::class -> libraryFactory() LibraryScreenViewModel::class -> libraryFactory()
LibrarySettingsViewModel::class -> librarySettingsFactory()
DebugOverlayViewModel::class -> debugOverlayFactory() DebugOverlayViewModel::class -> debugOverlayFactory()
TrayViewModel::class -> trayFactory() TrayViewModel::class -> trayFactory()
MainViewModel::class -> mainFactory() 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) ReaderMenuViewModel::class -> readerFactory(arg1 as ReaderMenuViewModel.Params)
SettingsAdvancedViewModel::class -> settingsAdvancedFactory() SettingsAdvancedViewModel::class -> settingsAdvancedFactory()
ThemesViewModel::class -> themesFactory() ThemesViewModel::class -> themesFactory()