diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteFragment.kt index 0ca487ea7..992702a55 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/favorite/FavoriteFragment.kt @@ -1,12 +1,15 @@ package com.programmersbox.uiviews.favorite import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow @@ -29,7 +32,6 @@ import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Circle import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.FavoriteBorder -import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.SortByAlpha import androidx.compose.material.icons.filled.Star import androidx.compose.material3.Button @@ -40,7 +42,12 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -48,8 +55,6 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -60,7 +65,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate -import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource @@ -77,7 +82,7 @@ import com.programmersbox.uiviews.R import com.programmersbox.uiviews.utils.BackButton import com.programmersbox.uiviews.utils.BannerScope import com.programmersbox.uiviews.utils.ComponentState -import com.programmersbox.uiviews.utils.InsetSmallTopAppBar +import com.programmersbox.uiviews.utils.InsetCenterAlignedTopAppBar import com.programmersbox.uiviews.utils.LightAndDarkPreviews import com.programmersbox.uiviews.utils.LocalItemDao import com.programmersbox.uiviews.utils.LocalNavController @@ -89,12 +94,12 @@ import com.programmersbox.uiviews.utils.PreviewTheme import com.programmersbox.uiviews.utils.Screen import com.programmersbox.uiviews.utils.SourceNotInstalledModal import com.programmersbox.uiviews.utils.adaptiveGridCell +import com.programmersbox.uiviews.utils.bounds import com.programmersbox.uiviews.utils.components.DynamicSearchBar -import com.programmersbox.uiviews.utils.components.GroupButton -import com.programmersbox.uiviews.utils.components.GroupButtonModel import com.programmersbox.uiviews.utils.components.ListBottomScreen import com.programmersbox.uiviews.utils.components.ListBottomSheetItemModel import com.programmersbox.uiviews.utils.navigateToDetails +import dev.chrisbanes.haze.haze import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.koin.compose.koinInject @@ -115,8 +120,6 @@ fun FavoriteUi( val focusManager = LocalFocusManager.current - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) - var showBanner by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() @@ -124,6 +127,8 @@ fun FavoriteUi( var showDbModel by remember { mutableStateOf(null) } + var showSort by remember { mutableStateOf(false) } + val logo = koinInject().logoId SourceNotInstalledModal( @@ -146,166 +151,216 @@ fun FavoriteUi( ) } + if (showSort) { + ModalBottomSheet( + onDismissRequest = { showSort = false } + ) { + InsetCenterAlignedTopAppBar( + title = { Text("Sort By") }, + insetPadding = WindowInsets(0.dp) + ) + + val rotateIcon: @Composable (SortFavoritesBy<*>) -> Float = { + animateFloatAsState(if (it == viewModel.sortedBy && viewModel.reverse) 180f else 0f, label = "").value + } + + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth() + ) { + @Composable + fun SegmentedButtonItem( + sortFavoritesBy: SortFavoritesBy<*>, + label: String, + index: Int, + icon: ImageVector, + ) { + val isSelected = viewModel.sortedBy == sortFavoritesBy + SegmentedButton( + selected = isSelected, + onClick = { + if (viewModel.sortedBy != sortFavoritesBy) + viewModel.sortedBy = sortFavoritesBy + else + viewModel.reverse = !viewModel.reverse + }, + label = { Text(label) }, + icon = { + SegmentedButtonDefaults.Icon( + active = isSelected, + activeContent = { + Icon( + icon, + null, + modifier = Modifier.rotate(rotateIcon(sortFavoritesBy)) + ) + } + ) + }, + shape = SegmentedButtonDefaults.itemShape(index, 3) + ) + } + + SegmentedButtonItem( + sortFavoritesBy = SortFavoritesBy.TITLE, + label = "Title", + index = 0, + icon = Icons.Default.SortByAlpha + ) + + SegmentedButtonItem( + sortFavoritesBy = SortFavoritesBy.COUNT, + label = "Count", + index = 1, + icon = Icons.AutoMirrored.Filled.Sort + ) + + SegmentedButtonItem( + sortFavoritesBy = SortFavoritesBy.CHAPTERS, + label = "Chapters", + index = 2, + icon = Icons.AutoMirrored.Filled.ReadMore + ) + } + } + } + OtakuBannerBox( showBanner = showBanner, placeholder = logo, modifier = Modifier.padding(WindowInsets.statusBars.asPaddingValues()) ) { OtakuScaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { - Surface { - Column( - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - InsetSmallTopAppBar( - scrollBehavior = scrollBehavior, - navigationIcon = { BackButton() }, - title = { Text(stringResource(R.string.viewFavoritesMenu)) }, - actions = { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + var active by rememberSaveable { mutableStateOf(false) } - val rotateIcon: @Composable (SortFavoritesBy<*>) -> Float = { - animateFloatAsState(if (it == viewModel.sortedBy && viewModel.reverse) 180f else 0f, label = "").value + fun closeSearchBar() { + focusManager.clearFocus() + active = false + } + DynamicSearchBar( + isDocked = isHorizontal, + query = viewModel.searchText, + onQueryChange = { viewModel.searchText = it }, + onSearch = { closeSearchBar() }, + active = active, + onActiveChange = { + active = it + if (!active) focusManager.clearFocus() + }, + placeholder = { + Text( + context.resources.getQuantityString( + R.plurals.numFavorites, + viewModel.listSources.size, + viewModel.listSources.size + ) + ) + }, + leadingIcon = { BackButton() }, + trailingIcon = { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + AnimatedVisibility(viewModel.searchText.isNotEmpty()) { + IconButton(onClick = { viewModel.searchText = "" }) { + Icon(Icons.Default.Cancel, null) + } } - GroupButton( - selected = viewModel.sortedBy, - options = listOf( - GroupButtonModel(SortFavoritesBy.TITLE) { - Icon( - Icons.Default.SortByAlpha, - null, - modifier = Modifier.rotate(rotateIcon(SortFavoritesBy.TITLE)) - ) - }, - GroupButtonModel(SortFavoritesBy.COUNT) { - Icon( - Icons.AutoMirrored.Filled.Sort, - null, - modifier = Modifier.rotate(rotateIcon(SortFavoritesBy.COUNT)) - ) - }, - GroupButtonModel(SortFavoritesBy.CHAPTERS) { - Icon( - Icons.AutoMirrored.Filled.ReadMore, - null, - modifier = Modifier.rotate(rotateIcon(SortFavoritesBy.CHAPTERS)) - ) - } - ) - ) { if (viewModel.sortedBy != it) viewModel.sortedBy = it else viewModel.reverse = !viewModel.reverse } - } - ) - - var active by rememberSaveable { mutableStateOf(false) } - - fun closeSearchBar() { - focusManager.clearFocus() - active = false - } - DynamicSearchBar( - windowInsets = WindowInsets(0.dp), - isDocked = isHorizontal, - query = viewModel.searchText, - onQueryChange = { viewModel.searchText = it }, - onSearch = { closeSearchBar() }, - active = active, - onActiveChange = { - active = it - if (!active) focusManager.clearFocus() - }, - placeholder = { - Text( - context.resources.getQuantityString( - R.plurals.numFavorites, - viewModel.listSources.size, - viewModel.listSources.size - ) - ) - }, - leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, - trailingIcon = { - IconButton(onClick = { viewModel.searchText = "" }) { - Icon(Icons.Default.Cancel, null) + AnimatedVisibility(!active) { + IconButton(onClick = { showSort = true }) { + Icon(Icons.AutoMirrored.Filled.Sort, null) + } } - }, - modifier = Modifier.fillMaxWidth(), + } + }, + colors = SearchBarDefaults.colors( + containerColor = animateColorAsState( + MaterialTheme.colorScheme.surface.copy( + alpha = if (active) 1f else 0f + ), + label = "" + ).value, + ), + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - viewModel.listSources.take(4).forEachIndexed { index, dbModel -> - ListItem( - headlineContent = { Text(dbModel.title) }, - supportingContent = { Text(dbModel.source) }, - leadingContent = { Icon(Icons.Filled.Star, contentDescription = null) }, - modifier = Modifier.clickable { - viewModel.searchText = dbModel.title - closeSearchBar() - } - ) - if (index != 3) { - HorizontalDivider() + viewModel.listSources.take(4).forEachIndexed { index, dbModel -> + ListItem( + headlineContent = { Text(dbModel.title) }, + supportingContent = { Text(dbModel.source) }, + leadingContent = { Icon(Icons.Filled.Star, contentDescription = null) }, + modifier = Modifier.clickable { + viewModel.searchText = dbModel.title + closeSearchBar() } + ) + if (index != 3) { + HorizontalDivider() } } } + } - var showFilterBySourceModal by remember { mutableStateOf(false) } + var showFilterBySourceModal by remember { mutableStateOf(false) } - if (showFilterBySourceModal) { - BackHandler { showFilterBySourceModal = false } + if (showFilterBySourceModal) { + BackHandler { showFilterBySourceModal = false } - ModalBottomSheet(onDismissRequest = { showFilterBySourceModal = false }) { - CenterAlignedTopAppBar(title = { Text("Filter by Source") }) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp) - ) { + ModalBottomSheet(onDismissRequest = { showFilterBySourceModal = false }) { + CenterAlignedTopAppBar(title = { Text("Filter by Source") }) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp) + ) { + FilterChip( + selected = true, + modifier = Modifier.combinedClickable( + onClick = { viewModel.resetSources() }, + onLongClick = { viewModel.selectedSources.clear() } + ), + label = { Text("ALL") }, + onClick = { viewModel.allClick() } + ) + + viewModel.allSources.forEach { FilterChip( - selected = true, + selected = it.first in viewModel.selectedSources, + onClick = { viewModel.newSource(it.first) }, + label = { Text(it.first) }, + leadingIcon = { Text("${it.second.size - 1}") }, modifier = Modifier.combinedClickable( - onClick = { viewModel.resetSources() }, - onLongClick = { viewModel.selectedSources.clear() } - ), - label = { Text("ALL") }, - onClick = { viewModel.allClick() } - ) - - viewModel.allSources.forEach { - FilterChip( - selected = it.first in viewModel.selectedSources, onClick = { viewModel.newSource(it.first) }, - label = { Text(it.first) }, - leadingIcon = { Text("${it.second.size - 1}") }, - modifier = Modifier.combinedClickable( - onClick = { viewModel.newSource(it.first) }, - onLongClick = { viewModel.singleSource(it.first) } - ) + onLongClick = { viewModel.singleSource(it.first) } ) - } + ) } } } + } - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp) - ) { - SuggestionChip( - onClick = { showFilterBySourceModal = true }, - label = { Text("Filter By Source") } - ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp) + ) { + SuggestionChip( + onClick = { showFilterBySourceModal = true }, + label = { Text("Filter By Source") } + ) - SuggestionChip( - label = { Text("ALL") }, - onClick = { viewModel.resetSources() } - ) - } + SuggestionChip( + label = { Text("ALL") }, + onClick = { viewModel.resetSources() } + ) } } } @@ -346,29 +401,35 @@ fun FavoriteUi( } } } else { - FavoritesGrid( - paddingValues = p, - groupedSources = viewModel.groupedSources, - sourceRepository = sourceRepository, - navController = navController, - moreInfoClick = { - scope.launch { - snackbarHostState.currentSnackbarData?.dismiss() - val result = snackbarHostState.showSnackbar( - "Something went wrong. Source might not be installed", - duration = SnackbarDuration.Long, - actionLabel = "More Options", - withDismissAction = true - ) - showDbModel = when (result) { - SnackbarResult.Dismissed -> null - SnackbarResult.ActionPerformed -> it + BoxWithConstraints { + FavoritesGrid( + paddingValues = p, + groupedSources = viewModel.groupedSources, + sourceRepository = sourceRepository, + navController = navController, + moreInfoClick = { + scope.launch { + snackbarHostState.currentSnackbarData?.dismiss() + val result = snackbarHostState.showSnackbar( + "Something went wrong. Source might not be installed", + duration = SnackbarDuration.Long, + actionLabel = "More Options", + withDismissAction = true + ) + showDbModel = when (result) { + SnackbarResult.Dismissed -> null + SnackbarResult.ActionPerformed -> it + } } - } - }, - onShowBanner = { showBanner = it }, - logo = logo - ) + }, + onShowBanner = { showBanner = it }, + logo = logo, + modifier = Modifier.haze( + *bounds(p), + backgroundColor = MaterialTheme.colorScheme.surface + ) + ) + } } } }