From 59246ba91ae89adbf327dc7aa30cc79d5c8da78a Mon Sep 17 00:00:00 2001 From: urFate Date: Mon, 1 Jul 2024 14:58:37 +0300 Subject: [PATCH] feat(Favourites): sorting bottom sheet --- .../java/org/shirabox/app/ValuesHelper.kt | 8 + .../app/ui/component/general/Emoticons.kt | 12 +- .../ui/screen/favourites/FavouritesScreen.kt | 252 ++++++++++++++++-- .../screen/favourites/FavouritesViewModel.kt | 34 ++- app/src/main/res/values/strings.xml | 11 + 5 files changed, 288 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/shirabox/app/ValuesHelper.kt b/app/src/main/java/org/shirabox/app/ValuesHelper.kt index 73fce8f..6b6067a 100644 --- a/app/src/main/java/org/shirabox/app/ValuesHelper.kt +++ b/app/src/main/java/org/shirabox/app/ValuesHelper.kt @@ -1,6 +1,7 @@ package org.shirabox.app import android.content.Context +import org.shirabox.app.ui.screen.favourites.SortType import org.shirabox.core.model.ContentKind import org.shirabox.core.model.ReleaseStatus @@ -24,4 +25,11 @@ object ValuesHelper { ReleaseStatus.UNKNOWN -> context.getString(R.string.release_status_unknown) } + fun decodeSortingType(sortType: SortType, context: Context) = when (sortType) { + SortType.DEFAULT -> context.getString(R.string.default_order) + SortType.ALPHABETICAL -> context.getString(R.string.alphabetical_order) + SortType.RATING -> context.getString(R.string.rating_order) + SortType.RECENT -> context.getString(R.string.recent_order) + SortType.STATUS -> context.getString(R.string.status_order) + } } \ No newline at end of file diff --git a/app/src/main/java/org/shirabox/app/ui/component/general/Emoticons.kt b/app/src/main/java/org/shirabox/app/ui/component/general/Emoticons.kt index 99ec843..fe8f0a8 100644 --- a/app/src/main/java/org/shirabox/app/ui/component/general/Emoticons.kt +++ b/app/src/main/java/org/shirabox/app/ui/component/general/Emoticons.kt @@ -21,7 +21,7 @@ fun DespondencyEmoticon( ) { Column( modifier = Modifier - .padding(64.dp) + .padding(48.dp) .then(modifier), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -30,13 +30,14 @@ fun DespondencyEmoticon( text = emoticon, fontSize = 48.sp, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.outline ) Text( text = text, fontSize = 14.sp, fontWeight = FontWeight.Normal, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.outline ) } } @@ -58,13 +59,14 @@ fun ScaredEmoticon( text = emoticon, fontSize = 48.sp, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.outline ) Text( text = text, fontSize = 14.sp, fontWeight = FontWeight.Normal, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.outline ) } } \ No newline at end of file diff --git a/app/src/main/java/org/shirabox/app/ui/screen/favourites/FavouritesScreen.kt b/app/src/main/java/org/shirabox/app/ui/screen/favourites/FavouritesScreen.kt index 02ffa80..5fb713e 100644 --- a/app/src/main/java/org/shirabox/app/ui/screen/favourites/FavouritesScreen.kt +++ b/app/src/main/java/org/shirabox/app/ui/screen/favourites/FavouritesScreen.kt @@ -3,7 +3,11 @@ package org.shirabox.app.ui.screen.favourites import android.content.Intent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -12,29 +16,51 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Done +import androidx.compose.material.icons.rounded.FilterList +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import kotlinx.coroutines.launch import org.shirabox.app.R +import org.shirabox.app.ValuesHelper import org.shirabox.app.ui.activity.resource.ResourceActivity import org.shirabox.app.ui.component.general.ContentCard import org.shirabox.app.ui.component.general.DespondencyEmoticon import org.shirabox.app.ui.component.top.TopBar -import org.shirabox.core.model.ContentType +import org.shirabox.core.model.Content +import org.shirabox.core.model.ContentKind import org.shirabox.core.util.Util @Composable @@ -43,15 +69,7 @@ fun FavouritesScreen( model: FavouritesViewModel = hiltViewModel() ) { val favourites by model.fetchFavouriteContents().collectAsState(initial = emptyList()) - val currentType by remember { - mutableStateOf(ContentType.ANIME) - } - - val filteredFavourites by remember(currentType) { - derivedStateOf { - favourites.filter { it.type == currentType }.map { Util.mapEntityToContent(it) } - } - } + val bottomSheetVisibilityState = remember { mutableStateOf(false) } Column( modifier = Modifier @@ -65,11 +83,35 @@ fun FavouritesScreen( modifier = Modifier.padding(16.dp, 0.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Text( - text = stringResource(R.string.favourites), - fontSize = 22.sp, - fontWeight = FontWeight(500) - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.favourites), + fontSize = 22.sp, + fontWeight = FontWeight(500) + ) + + TextButton(onClick = { bottomSheetVisibilityState.value = true }) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Rounded.FilterList, + contentDescription = "Filter", + tint = MaterialTheme.colorScheme.secondary + ) + Text( + text = stringResource(id = R.string.sorting), + fontSize = 16.sp, + color = MaterialTheme.colorScheme.secondary + ) + } + } + } Column( modifier = Modifier @@ -78,18 +120,20 @@ fun FavouritesScreen( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - if (filteredFavourites.isEmpty()) { - DespondencyEmoticon(text = stringResource(id = R.string.empty_library)) + if (favourites.isEmpty()) { + DespondencyEmoticon(text = stringResource(id = R.string.empty_library_filter)) } else { - FavouritesGrid(contents = filteredFavourites) + FavouritesGrid(contents = favourites) } } } } + + if (bottomSheetVisibilityState.value) SortingSheetScreen(visibilityState = bottomSheetVisibilityState) } @Composable -fun FavouritesGrid(contents: List) { +fun FavouritesGrid(contents: List) { val context = LocalContext.current val configuration = LocalConfiguration.current @@ -128,4 +172,174 @@ fun FavouritesGrid(contents: List) { } } } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun SortingSheetScreen( + visibilityState: MutableState, + model: FavouritesViewModel = hiltViewModel() +) { + val skipPartiallyExpanded by remember { mutableStateOf(false) } + val state = rememberModalBottomSheetState( + skipPartiallyExpanded = skipPartiallyExpanded + ) + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + + val selectedSortType by remember { derivedStateOf { model.selectedSortType.value } } + val selectedKind by remember { derivedStateOf { model.selectedKind.value } } + + ModalBottomSheet( + sheetState = state, + onDismissRequest = { + coroutineScope.launch { + state.hide() + visibilityState.value = false + } + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp, 0.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.sorting), + fontSize = 22.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.W400, + color = MaterialTheme.colorScheme.onSurface + ) + + Text( + text = stringResource(id = R.string.sort_by), + color = MaterialTheme.colorScheme.onSurface + ) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + SortType.entries.forEach { sortType -> + val selected = remember(selectedSortType) { selectedSortType == sortType } + + MyFilterChip( + selected = selected, + label = { Text(text = ValuesHelper.decodeSortingType(sortType, context)) } + ) { + model.selectedSortType.value = sortType + } + } + } + + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) + + Text( + text = stringResource(id = R.string.kind), + color = MaterialTheme.colorScheme.onSurface + ) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + ContentKind.entries.forEach { kind -> + val selected = remember(selectedKind) { selectedKind == kind } + + MyFilterChip( + selected = selected, + label = { Text(text = ValuesHelper.decodeKind(kind, context)) } + ) { + model.selectedKind.value = kind + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(0.dp, 32.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(32.dp) + ) { + OutlinedButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.dp), + onClick = { + coroutineScope.launch { + state.hide() + visibilityState.value = false + } + + model.selectedSortType.value = SortType.DEFAULT + model.selectedKind.value = null + }, + ) { + Text(text = stringResource(id = R.string.reset)) + } + + Button( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .height(48.dp), + onClick = { + coroutineScope.launch { + state.hide() + visibilityState.value = false + } + } + ) { + Text(text = stringResource(id = R.string.apply)) + } + } + } + } +} + +@Composable +private fun MyFilterChip( + selected: Boolean, + label: @Composable () -> Unit, + onClick: () -> Unit +) { + val chipColors = FilterChipDefaults.filterChipColors().copy( + labelColor = MaterialTheme.colorScheme.primary, + leadingIconColor = MaterialTheme.colorScheme.primary + ) + val borderBrush = Brush.linearGradient( + if (!selected) listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.primary + ) else listOf(Color.Transparent, Color.Transparent) + ) + val border = FilterChipDefaults.filterChipBorder(true, selected).copy( + brush = borderBrush + ) + + FilterChip( + onClick = onClick, + selected = selected, + label = label, + leadingIcon = { + if (selected) Icon( + modifier = Modifier.size(16.dp), + imageVector = Icons.Rounded.Done, + contentDescription = "Selected", + tint = MaterialTheme.colorScheme.primary + ) + }, + colors = chipColors, + border = border + ) } \ No newline at end of file diff --git a/app/src/main/java/org/shirabox/app/ui/screen/favourites/FavouritesViewModel.kt b/app/src/main/java/org/shirabox/app/ui/screen/favourites/FavouritesViewModel.kt index e9b7c72..414540e 100644 --- a/app/src/main/java/org/shirabox/app/ui/screen/favourites/FavouritesViewModel.kt +++ b/app/src/main/java/org/shirabox/app/ui/screen/favourites/FavouritesViewModel.kt @@ -1,19 +1,43 @@ package org.shirabox.app.ui.screen.favourites import android.content.Context +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map import org.shirabox.core.db.AppDatabase -import org.shirabox.core.entity.ContentEntity +import org.shirabox.core.model.Content +import org.shirabox.core.model.ContentKind +import org.shirabox.core.util.Util import javax.inject.Inject @HiltViewModel class FavouritesViewModel @Inject constructor(@ApplicationContext context: Context) : ViewModel() { - private val db = AppDatabase.getAppDataBase(context) + private val db = AppDatabase.getAppDataBase(context)!! + val selectedSortType = mutableStateOf(SortType.DEFAULT) + val selectedKind = mutableStateOf(null) - fun fetchFavouriteContents(): Flow> = - db?.contentDao()?.getFavourites() ?: emptyFlow() + fun fetchFavouriteContents(): Flow> = + db.contentDao().getFavourites() + .map { entityList -> + when (selectedSortType.value) { + SortType.ALPHABETICAL -> entityList.sortedByDescending { it.name } + SortType.RATING -> entityList.sortedByDescending { it.rating.average } + SortType.RECENT -> entityList.sortedByDescending { it.lastViewTimestamp } + SortType.STATUS -> entityList.sortedByDescending { it.status.ordinal } + SortType.DEFAULT -> entityList.reversed() + } + } + .map { entityList -> + entityList + .takeIf { selectedKind.value != null } + ?.filter { it.kind == selectedKind.value } ?: entityList + } + .map { entityList -> entityList.map { Util.mapEntityToContent(it) } } +} + +enum class SortType { + DEFAULT, ALPHABETICAL, RATING, RECENT, STATUS } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bb6e850..7e99a8e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,6 +37,17 @@ Favourites Screen --> Библиотека пуста + Библиотека пуста.\nВозможно следует сбросить фильтры сортировки + Сортировать + Расположить по + Умолчанию + Алфавиту + Рейтингу + Времени просмотра + Статусу выпуска + Формат выпуска + Сбросить + Применить