From 9bb4c341dff0fe5cd5df30d27118b3fa142da46b Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 4 Apr 2026 01:36:49 +0530 Subject: [PATCH] feat: refactor bottom sheet into reusable component --- .../com/nuvio/app/core/ui/NuvioBottomSheet.kt | 125 +++++++++++++ .../app/core/ui/NuvioPosterActionSheet.kt | 75 +------- .../details/components/CommentDetailSheet.kt | 5 +- .../components/EpisodeWatchedActionSheet.kt | 87 ++------- .../details/components/TrailerPlayerPopup.kt | 16 +- .../features/search/SearchDiscoverContent.kt | 169 +++++++++++++----- 6 files changed, 284 insertions(+), 193 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioBottomSheet.kt diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioBottomSheet.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioBottomSheet.kt new file mode 100644 index 000000000..af5f9c265 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioBottomSheet.kt @@ -0,0 +1,125 @@ +package com.nuvio.app.core.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NuvioModalBottomSheet( + onDismissRequest: () -> Unit, + sheetState: SheetState, + modifier: Modifier = Modifier, + containerColor: Color = MaterialTheme.colorScheme.surface, + contentColor: Color = MaterialTheme.colorScheme.onSurface, + shape: Shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), + showDragHandle: Boolean = true, + content: @Composable ColumnScope.() -> Unit, +) { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + modifier = modifier, + containerColor = containerColor, + contentColor = contentColor, + shape = shape, + dragHandle = if (showDragHandle) { + { NuvioBottomSheetDragHandle() } + } else { + null + }, + content = content, + ) +} + +@Composable +fun NuvioBottomSheetDivider( + modifier: Modifier = Modifier, +) { + HorizontalDivider( + modifier = modifier, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f), + ) +} + +@Composable +fun NuvioBottomSheetActionRow( + title: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + icon: ImageVector? = null, + trailingContent: (@Composable RowScope.() -> Unit)? = null, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(22.dp), + ) + } + Text( + text = title, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + trailingContent?.invoke(this) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +suspend fun dismissNuvioBottomSheet( + sheetState: SheetState, + onDismiss: () -> Unit, +) { + if (sheetState.isVisible) { + sheetState.hide() + } + onDismiss() +} + +@Composable +private fun NuvioBottomSheetDragHandle() { + Box( + modifier = Modifier + .padding(top = 10.dp, bottom = 6.dp) + .size(width = 54.dp, height = 5.dp) + .clip(RoundedCornerShape(999.dp)) + .background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.65f)), + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPosterActionSheet.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPosterActionSheet.kt index 37fcb63be..a3fc1897d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPosterActionSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioPosterActionSheet.kt @@ -4,14 +4,11 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -23,11 +20,8 @@ import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircleOutline import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -58,26 +52,16 @@ fun NuvioPosterActionSheet( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val coroutineScope = rememberCoroutineScope() - ModalBottomSheet( + NuvioModalBottomSheet( onDismissRequest = { coroutineScope.launch { - dismissPosterActionSheet( + dismissNuvioBottomSheet( sheetState = sheetState, onDismiss = onDismiss, ) } }, sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, - dragHandle = { - Box( - modifier = Modifier - .padding(top = 10.dp, bottom = 6.dp) - .size(width = 54.dp, height = 5.dp) - .clip(RoundedCornerShape(999.dp)) - .background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.65f)), - ) - }, ) { Column( modifier = Modifier @@ -85,28 +69,28 @@ fun NuvioPosterActionSheet( .padding(bottom = 16.dp + nuvioPlatformExtraBottomPadding), ) { PosterSheetHeader(item = item) - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) - PosterSheetActionRow( + NuvioBottomSheetDivider() + NuvioBottomSheetActionRow( icon = if (isSaved) Icons.Default.Bookmark else Icons.Default.BookmarkBorder, title = if (isSaved) "Remove from Library" else "Add to Library", onClick = { onToggleLibrary() coroutineScope.launch { - dismissPosterActionSheet( + dismissNuvioBottomSheet( sheetState = sheetState, onDismiss = onDismiss, ) } }, ) - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) - PosterSheetActionRow( + NuvioBottomSheetDivider() + NuvioBottomSheetActionRow( icon = if (isWatched) Icons.Default.CheckCircle else Icons.Default.CheckCircleOutline, title = if (isWatched) "Mark as Unwatched" else "Mark as Watched", onClick = { onToggleWatched() coroutineScope.launch { - dismissPosterActionSheet( + dismissNuvioBottomSheet( sheetState = sheetState, onDismiss = onDismiss, ) @@ -117,17 +101,6 @@ fun NuvioPosterActionSheet( } } -@OptIn(ExperimentalMaterial3Api::class) -private suspend fun dismissPosterActionSheet( - sheetState: SheetState, - onDismiss: () -> Unit, -) { - if (sheetState.isVisible) { - sheetState.hide() - } - onDismiss() -} - @Composable fun NuvioWatchedBadge( modifier: Modifier = Modifier, @@ -226,35 +199,3 @@ private fun PosterSheetHeader( } } -@Composable -private fun PosterSheetActionRow( - icon: androidx.compose.ui.graphics.vector.ImageVector, - title: String, - onClick: () -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(14.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(22.dp), - ) - Column { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - Spacer(modifier = Modifier.weight(1f)) - } -} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/CommentDetailSheet.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/CommentDetailSheet.kt index b4c7eb569..c157b529e 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/CommentDetailSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/CommentDetailSheet.kt @@ -23,7 +23,6 @@ import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -35,6 +34,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.nuvio.app.core.ui.NuvioModalBottomSheet import com.nuvio.app.features.trakt.TraktCommentReview @OptIn(ExperimentalMaterial3Api::class) @@ -56,10 +56,9 @@ fun CommentDetailSheet( scrollState.scrollTo(0) } - ModalBottomSheet( + NuvioModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.onSurface, shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), ) { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/EpisodeWatchedActionSheet.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/EpisodeWatchedActionSheet.kt index 66c300ba3..74dd96419 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/EpisodeWatchedActionSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/EpisodeWatchedActionSheet.kt @@ -1,12 +1,10 @@ package com.nuvio.app.features.details.components import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.size @@ -16,21 +14,19 @@ import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.DoneAll import androidx.compose.material.icons.filled.PlaylistAddCheckCircle import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.nuvio.app.core.ui.NuvioBottomSheetActionRow +import com.nuvio.app.core.ui.NuvioBottomSheetDivider +import com.nuvio.app.core.ui.NuvioModalBottomSheet +import com.nuvio.app.core.ui.dismissNuvioBottomSheet import com.nuvio.app.features.details.MetaVideo import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding import kotlinx.coroutines.launch @@ -52,25 +48,13 @@ fun EpisodeWatchedActionSheet( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val coroutineScope = rememberCoroutineScope() - ModalBottomSheet( + NuvioModalBottomSheet( onDismissRequest = { coroutineScope.launch { - dismissEpisodeActionSheet(sheetState = sheetState, onDismiss = onDismiss) + dismissNuvioBottomSheet(sheetState = sheetState, onDismiss = onDismiss) } }, sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, - dragHandle = { - Box( - modifier = Modifier - .padding(top = 10.dp, bottom = 6.dp) - .size(width = 54.dp, height = 5.dp) - .background( - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.65f), - shape = RoundedCornerShape(999.dp), - ), - ) - }, ) { Column( modifier = Modifier @@ -81,20 +65,20 @@ fun EpisodeWatchedActionSheet( episode = episode, seasonLabel = seasonLabel, ) - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) - EpisodeActionSheetRow( + NuvioBottomSheetDivider() + NuvioBottomSheetActionRow( icon = Icons.Default.CheckCircle, title = if (isEpisodeWatched) "Mark as unwatched" else "Mark as watched", onClick = { onToggleWatched() coroutineScope.launch { - dismissEpisodeActionSheet(sheetState = sheetState, onDismiss = onDismiss) + dismissNuvioBottomSheet(sheetState = sheetState, onDismiss = onDismiss) } }, ) if (canMarkPreviousEpisodes) { - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) - EpisodeActionSheetRow( + NuvioBottomSheetDivider() + NuvioBottomSheetActionRow( icon = Icons.Default.DoneAll, title = if (arePreviousEpisodesWatched) { "Mark previous as unwatched" @@ -104,13 +88,13 @@ fun EpisodeWatchedActionSheet( onClick = { onTogglePreviousWatched() coroutineScope.launch { - dismissEpisodeActionSheet(sheetState = sheetState, onDismiss = onDismiss) + dismissNuvioBottomSheet(sheetState = sheetState, onDismiss = onDismiss) } }, ) } - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) - EpisodeActionSheetRow( + NuvioBottomSheetDivider() + NuvioBottomSheetActionRow( icon = Icons.Default.PlaylistAddCheckCircle, title = if (isSeasonWatched) { "Mark $seasonLabel as unwatched" @@ -120,7 +104,7 @@ fun EpisodeWatchedActionSheet( onClick = { onToggleSeasonWatched() coroutineScope.launch { - dismissEpisodeActionSheet(sheetState = sheetState, onDismiss = onDismiss) + dismissNuvioBottomSheet(sheetState = sheetState, onDismiss = onDismiss) } }, ) @@ -128,17 +112,6 @@ fun EpisodeWatchedActionSheet( } } -@OptIn(ExperimentalMaterial3Api::class) -private suspend fun dismissEpisodeActionSheet( - sheetState: SheetState, - onDismiss: () -> Unit, -) { - if (sheetState.isVisible) { - sheetState.hide() - } - onDismiss() -} - @Composable private fun EpisodeActionSheetHeader( episode: MetaVideo, @@ -174,33 +147,3 @@ private fun EpisodeActionSheetHeader( } } -@Composable -private fun EpisodeActionSheetRow( - icon: ImageVector, - title: String, - onClick: () -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(14.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(22.dp), - ) - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Spacer(modifier = Modifier.weight(1f)) - } -} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/TrailerPlayerPopup.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/TrailerPlayerPopup.kt index 4641ddd54..2695831a1 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/TrailerPlayerPopup.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/TrailerPlayerPopup.kt @@ -13,11 +13,9 @@ import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.Icons import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState @@ -33,6 +31,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import com.nuvio.app.core.ui.NuvioBottomSheetDivider +import com.nuvio.app.core.ui.NuvioModalBottomSheet +import com.nuvio.app.core.ui.dismissNuvioBottomSheet import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding import com.nuvio.app.features.player.PlatformPlayerSurface import com.nuvio.app.features.player.PlayerResizeMode @@ -74,17 +75,13 @@ fun TrailerPlayerPopup( val dismissSheet: () -> Unit = { coroutineScope.launch { - if (sheetState.isVisible) { - sheetState.hide() - } - onDismiss() + dismissNuvioBottomSheet(sheetState = sheetState, onDismiss = onDismiss) } } - ModalBottomSheet( + NuvioModalBottomSheet( onDismissRequest = dismissSheet, sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, ) { Column( modifier = Modifier @@ -112,7 +109,6 @@ fun TrailerPlayerPopup( if (headerSubtitle.isNotBlank()) { Text( text = headerSubtitle, - style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -129,7 +125,7 @@ fun TrailerPlayerPopup( } } - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f)) + NuvioBottomSheetDivider() Box( modifier = Modifier diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt index 1b627efe4..936c56a6a 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt @@ -13,24 +13,30 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.KeyboardArrowDown import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SheetState import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -43,11 +49,17 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.nuvio.app.core.format.formatReleaseDateForDisplay import com.nuvio.app.core.ui.NuvioAnimatedWatchedBadge +import com.nuvio.app.core.ui.NuvioBottomSheetActionRow +import com.nuvio.app.core.ui.NuvioBottomSheetDivider +import com.nuvio.app.core.ui.NuvioModalBottomSheet +import com.nuvio.app.core.ui.dismissNuvioBottomSheet +import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding import com.nuvio.app.core.ui.posterCardClickable import com.nuvio.app.features.home.MetaPreview import com.nuvio.app.features.home.PosterShape import com.nuvio.app.features.home.components.HomeEmptyStateCard import com.nuvio.app.features.watching.application.WatchingState +import kotlinx.coroutines.launch internal fun LazyListScope.discoverContent( state: DiscoverUiState, @@ -150,13 +162,17 @@ private fun DiscoverFilterRow( horizontalArrangement = Arrangement.spacedBy(10.dp), ) { DiscoverDropdownChip( + title = "Select Type", label = state.selectedType?.displayTypeLabel() ?: "Type", + selectedKey = state.selectedType, options = state.typeOptions.map { DiscoverOptionItem(key = it, label = it.displayTypeLabel()) }, enabled = state.typeOptions.isNotEmpty(), onSelected = { onTypeSelected(it.key) }, ) DiscoverDropdownChip( + title = "Select Catalog", label = state.selectedCatalog?.catalogName ?: "Catalog", + selectedKey = state.selectedCatalogKey, options = state.catalogOptions.map { option -> DiscoverOptionItem(key = option.key, label = option.catalogName) }, enabled = state.catalogOptions.isNotEmpty(), onSelected = { onCatalogSelected(it.key) }, @@ -170,7 +186,9 @@ private fun DiscoverFilterRow( addAll(state.genreOptions.map { genre -> DiscoverOptionItem(key = genre, label = genre) }) } DiscoverDropdownChip( + title = "Select Genre", label = state.selectedGenre ?: "All Genres", + selectedKey = state.selectedGenre ?: "", options = genreOptions, enabled = genreOptions.size > 1 || selectedCatalog?.genreRequired == true, onSelected = { option -> @@ -180,57 +198,126 @@ private fun DiscoverFilterRow( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun DiscoverDropdownChip( + title: String, label: String, + selectedKey: String?, options: List, enabled: Boolean, onSelected: (DiscoverOptionItem) -> Unit, ) { - var expanded by remember { mutableStateOf(false) } + var isSheetVisible by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val coroutineScope = rememberCoroutineScope() - Box { - Row( + Row( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .background(MaterialTheme.colorScheme.surface) + .then( + if (enabled) { + Modifier.clickable { isSheetVisible = true } + } else { + Modifier + }, + ) + .padding(horizontal = 18.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null, + tint = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.outline, + ) + } + + if (isSheetVisible) { + DiscoverOptionsSheet( + title = title, + options = options, + selectedKey = selectedKey, + sheetState = sheetState, + onDismiss = { + coroutineScope.launch { + dismissNuvioBottomSheet( + sheetState = sheetState, + onDismiss = { isSheetVisible = false }, + ) + } + }, + onSelected = { option -> + onSelected(option) + coroutineScope.launch { + dismissNuvioBottomSheet( + sheetState = sheetState, + onDismiss = { isSheetVisible = false }, + ) + } + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DiscoverOptionsSheet( + title: String, + options: List, + selectedKey: String?, + sheetState: SheetState, + onDismiss: () -> Unit, + onSelected: (DiscoverOptionItem) -> Unit, +) { + NuvioModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( modifier = Modifier - .clip(RoundedCornerShape(20.dp)) - .background(MaterialTheme.colorScheme.surface) - .then( - if (enabled) { - Modifier.clickable { expanded = true } - } else { - Modifier - }, - ) - .padding(horizontal = 18.dp, vertical = 14.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, + .fillMaxWidth() + .padding(bottom = 16.dp + nuvioPlatformExtraBottomPadding), ) { Text( - text = label, - style = MaterialTheme.typography.titleMedium, - color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + text = title, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, ) - Icon( - imageVector = Icons.Rounded.KeyboardArrowDown, - contentDescription = null, - tint = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.outline, - ) - } - - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - options.forEach { option -> - DropdownMenuItem( - text = { Text(option.label) }, - onClick = { - expanded = false - onSelected(option) - }, - ) + NuvioBottomSheetDivider() + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 420.dp), + ) { + itemsIndexed(options) { index, option -> + NuvioBottomSheetActionRow( + title = option.label, + onClick = { onSelected(option) }, + trailingContent = { + if (option.key == selectedKey) { + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp), + ) + } + }, + ) + if (index < options.lastIndex) { + NuvioBottomSheetDivider() + } + } } } }