From 1dbbbdecbcbcc7e81c7fd69a1cb3ddd96c321279 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:33:27 +0530 Subject: [PATCH] Refine iOS sheets and ratings --- .../app/core/ui/NativeBottomSheet.android.kt | 23 +++ .../com/nuvio/app/core/ui/BottomSheet.kt | 45 +++-- .../nuvio/app/core/ui/NativeBottomSheet.kt | 23 +++ .../details/components/CommentDetailSheet.kt | 38 ++-- .../components/DetailCommentsSection.kt | 41 ++-- .../PosterCustomizationSettingsPage.kt | 1 + .../app/core/ui/NativeBottomSheet.ios.kt | 185 ++++++++++++++++++ 7 files changed, 310 insertions(+), 46 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.android.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.kt create mode 100644 composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.ios.kt diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.android.kt new file mode 100644 index 00000000..90ef15db --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.android.kt @@ -0,0 +1,23 @@ +package com.nuvio.app.core.ui + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +internal actual val usesNativeNuvioBottomSheet: Boolean = false + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal actual fun NuvioNativeModalBottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier, + containerColor: Color, + contentColor: Color, + showDragHandle: Boolean, + fullHeight: Boolean, + content: @Composable ColumnScope.() -> Unit, +) = Unit + +internal actual fun dismissNativeNuvioBottomSheet() = Unit diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/BottomSheet.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/BottomSheet.kt index a4731e37..87d6d7fc 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/BottomSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/BottomSheet.kt @@ -37,22 +37,35 @@ fun NuvioModalBottomSheet( contentColor: Color = MaterialTheme.nuvio.colors.textPrimary, shape: Shape = RoundedCornerShape(topStart = NuvioTokens.Space.s28, topEnd = NuvioTokens.Space.s28), showDragHandle: Boolean = true, + fullHeight: Boolean = false, 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, - ) + if (usesNativeNuvioBottomSheet) { + NuvioNativeModalBottomSheet( + onDismissRequest = onDismissRequest, + modifier = modifier, + containerColor = containerColor, + contentColor = contentColor, + showDragHandle = showDragHandle, + fullHeight = fullHeight, + content = content, + ) + } else { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + modifier = modifier, + containerColor = containerColor, + contentColor = contentColor, + shape = shape, + dragHandle = if (showDragHandle) { + { NuvioBottomSheetDragHandle() } + } else { + null + }, + content = content, + ) + } } @Composable @@ -107,7 +120,9 @@ suspend fun dismissNuvioBottomSheet( sheetState: SheetState, onDismiss: () -> Unit, ) { - if (sheetState.isVisible) { + if (usesNativeNuvioBottomSheet) { + dismissNativeNuvioBottomSheet() + } else if (sheetState.isVisible) { sheetState.hide() } onDismiss() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.kt new file mode 100644 index 00000000..52604a17 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.kt @@ -0,0 +1,23 @@ +package com.nuvio.app.core.ui + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal expect fun NuvioNativeModalBottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier, + containerColor: Color, + contentColor: Color, + showDragHandle: Boolean, + fullHeight: Boolean, + content: @Composable ColumnScope.() -> Unit, +) + +internal expect val usesNativeNuvioBottomSheet: Boolean + +internal expect fun dismissNativeNuvioBottomSheet() 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 c13ae124..e2cadb54 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 @@ -151,17 +151,16 @@ fun CommentDetailSheet( } } - Spacer(modifier = Modifier.height(12.dp)) + if (comment.review || comment.hasSpoilerContent) { + Spacer(modifier = Modifier.height(12.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - if (comment.review) { - CommentDetailChip(text = stringResource(Res.string.detail_comments_badge_review)) - } - if (comment.hasSpoilerContent) { - CommentDetailChip(text = stringResource(Res.string.detail_comments_badge_spoiler)) - } - comment.rating?.let { rating -> - CommentDetailChip(text = stringResource(Res.string.detail_comments_badge_rating, rating)) + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + if (comment.review) { + CommentDetailChip(text = stringResource(Res.string.detail_comments_badge_review)) + } + if (comment.hasSpoilerContent) { + CommentDetailChip(text = stringResource(Res.string.detail_comments_badge_spoiler)) + } } } @@ -190,11 +189,20 @@ fun CommentDetailSheet( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { - Text( - text = stringResource(Res.string.detail_comments_likes, comment.likes), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + comment.rating?.let { rating -> + Text( + text = stringResource(Res.string.detail_comments_badge_rating, rating), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Text( + text = stringResource(Res.string.detail_comments_likes, comment.likes), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } comment.createdAt?.take(10)?.let { date -> Text( text = date, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCommentsSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCommentsSection.kt index ea5331b4..277a7975 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCommentsSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCommentsSection.kt @@ -214,15 +214,14 @@ private fun CommentCard( fontWeight = FontWeight.SemiBold, ) - Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - if (review.review) { - CommentChip(text = stringResource(Res.string.detail_comments_badge_review)) - } - if (review.hasSpoilerContent) { - CommentChip(text = stringResource(Res.string.detail_comments_badge_spoiler)) - } - review.rating?.let { rating -> - CommentChip(text = stringResource(Res.string.detail_comments_badge_rating, rating)) + if (review.review || review.hasSpoilerContent) { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + if (review.review) { + CommentChip(text = stringResource(Res.string.detail_comments_badge_review)) + } + if (review.hasSpoilerContent) { + CommentChip(text = stringResource(Res.string.detail_comments_badge_spoiler)) + } } } @@ -235,13 +234,23 @@ private fun CommentCard( modifier = Modifier.weight(1f, fill = false), ) - Text( - text = stringResource(Res.string.detail_comments_likes, review.likes), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + review.rating?.let { rating -> + Text( + text = stringResource(Res.string.detail_comments_badge_rating, rating), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + maxLines = 1, + ) + } + Text( + text = stringResource(Res.string.detail_comments_likes, review.likes), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt index ccd3fdcf..4c679899 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt @@ -315,6 +315,7 @@ private fun CardDepthFineTuneSheet( onDismiss() }, sheetState = sheetState, + fullHeight = true, ) { Column( modifier = Modifier diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.ios.kt new file mode 100644 index 00000000..be66c5cf --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeBottomSheet.ios.kt @@ -0,0 +1,185 @@ +package com.nuvio.app.core.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalRippleConfiguration +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.uikit.LocalUIViewController +import androidx.compose.ui.unit.Density +import androidx.compose.ui.window.ComposeUIViewController +import platform.UIKit.UIAdaptivePresentationControllerDelegateProtocol +import platform.UIKit.UIColor +import platform.UIKit.UIModalPresentationPageSheet +import platform.UIKit.UIPresentationController +import platform.UIKit.UISheetPresentationController +import platform.UIKit.UISheetPresentationControllerDetent +import platform.UIKit.UISheetPresentationControllerDetentIdentifierMedium +import platform.UIKit.UIViewController +import platform.UIKit.presentationController +import platform.darwin.NSObject + +internal actual val usesNativeNuvioBottomSheet: Boolean = true + +private var activeNativeBottomSheet: UIViewController? = null +private var activeNativeBottomSheetDelegate: NuvioNativeBottomSheetDelegate? = null + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal actual fun NuvioNativeModalBottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier, + containerColor: Color, + contentColor: Color, + showDragHandle: Boolean, + fullHeight: Boolean, + content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit, +) { + val parentViewController = LocalUIViewController.current + val latestContent = rememberUpdatedState(content) + val latestOnDismissRequest = rememberUpdatedState(onDismissRequest) + val latestContainerColor = rememberUpdatedState(containerColor) + val latestContentColor = rememberUpdatedState(contentColor) + val latestModifier = rememberUpdatedState(modifier) + val colorScheme = MaterialTheme.colorScheme + val typography = MaterialTheme.typography + val shapes = MaterialTheme.shapes + val themeTokens = LocalNuvioThemeTokens.current + val typeScale = LocalNuvioTypeScale.current + val appTheme = LocalAppTheme.current + val density = LocalDensity.current + val rippleConfiguration = LocalRippleConfiguration.current + + val latestColorScheme = rememberUpdatedState(colorScheme) + val latestTypography = rememberUpdatedState(typography) + val latestShapes = rememberUpdatedState(shapes) + val latestThemeTokens = rememberUpdatedState(themeTokens) + val latestTypeScale = rememberUpdatedState(typeScale) + val latestAppTheme = rememberUpdatedState(appTheme) + val latestDensity = rememberUpdatedState(density) + val latestRippleConfiguration = rememberUpdatedState(rippleConfiguration) + + DisposableEffect(parentViewController) { + val contentController = ComposeUIViewController { + CompositionLocalProvider( + LocalDensity provides Density( + density = latestDensity.value.density, + fontScale = latestDensity.value.fontScale, + ), + LocalNuvioThemeTokens provides latestThemeTokens.value, + LocalNuvioTypeScale provides latestTypeScale.value, + LocalAppTheme provides latestAppTheme.value, + LocalRippleConfiguration provides latestRippleConfiguration.value, + ) { + MaterialTheme( + colorScheme = latestColorScheme.value, + typography = latestTypography.value, + shapes = latestShapes.value, + ) { + CompositionLocalProvider( + LocalContentColor provides latestContentColor.value, + ) { + Column( + modifier = latestModifier.value + .fillMaxSize() + .background(latestContainerColor.value) + .padding(top = NuvioTokens.Space.s20), + ) { + latestContent.value(this) + } + } + } + } + } + val sheetController = contentController + sheetController.modalPresentationStyle = UIModalPresentationPageSheet + sheetController.view.backgroundColor = latestContainerColor.value.toUIColor() + (sheetController.presentationController() as? UISheetPresentationController)?.apply { + if (fullHeight) { + detents = listOf(UISheetPresentationControllerDetent.largeDetent()) + } else { + detents = listOf( + UISheetPresentationControllerDetent.mediumDetent(), + UISheetPresentationControllerDetent.largeDetent(), + ) + selectedDetentIdentifier = UISheetPresentationControllerDetentIdentifierMedium + } + prefersGrabberVisible = showDragHandle + prefersScrollingExpandsWhenScrolledToEdge = false + } + + val dismissalDelegate = NuvioNativeBottomSheetDelegate { + if (activeNativeBottomSheet === sheetController) { + activeNativeBottomSheet = null + activeNativeBottomSheetDelegate = null + } + latestOnDismissRequest.value() + } + sheetController.presentationController()?.delegate = dismissalDelegate + + activeNativeBottomSheet?.dismissViewControllerAnimated( + flag = false, + completion = null, + ) + activeNativeBottomSheet = sheetController + activeNativeBottomSheetDelegate = dismissalDelegate + parentViewController.presentViewController( + viewControllerToPresent = sheetController, + animated = true, + completion = null, + ) + + onDispose { + if (activeNativeBottomSheet === sheetController) { + activeNativeBottomSheet = null + activeNativeBottomSheetDelegate = null + } + if (sheetController.presentingViewController != null) { + sheetController.dismissViewControllerAnimated( + flag = true, + completion = null, + ) + } + } + } +} + +internal actual fun dismissNativeNuvioBottomSheet() { + activeNativeBottomSheet?.dismissViewControllerAnimated( + flag = true, + completion = null, + ) +} + +private class NuvioNativeBottomSheetDelegate( + private val onDismissRequest: () -> Unit, +) : NSObject(), UIAdaptivePresentationControllerDelegateProtocol { + private var didNotifyDismissal = false + + override fun presentationControllerDidDismiss(presentationController: UIPresentationController) { + notifyDismissed() + } + + private fun notifyDismissed() { + if (didNotifyDismissal) return + didNotifyDismissal = true + onDismissRequest() + } +} + +private fun Color.toUIColor(): UIColor = UIColor( + red = red.toDouble(), + green = green.toDouble(), + blue = blue.toDouble(), + alpha = alpha.toDouble(), +)