From fc272ef3a67bfb2ef319b0e07cc8e922f3a21c54 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:10:59 +0530 Subject: [PATCH 1/2] refactor: replace rememberModalBottomSheetState with rememberNuvioBottomSheetState across multiple components --- .../com/nuvio/app/core/ui/NuvioBottomSheet.kt | 34 ++++++++++++++----- .../ui/NuvioContinueWatchingActionSheet.kt | 3 +- .../nuvio/app/core/ui/NuvioDropdownChip.kt | 6 ++-- .../app/core/ui/NuvioPosterActionSheet.kt | 3 +- .../collection/CollectionEditorScreen.kt | 4 +-- .../details/components/CommentDetailSheet.kt | 4 +-- .../components/EpisodeWatchedActionSheet.kt | 6 ++-- .../details/components/TrailerPlayerPopup.kt | 4 +-- .../settings/AppearanceSettingsPage.kt | 4 +-- .../app/features/streams/StreamsScreen.kt | 4 +-- 10 files changed, 43 insertions(+), 29 deletions(-) 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 index af5f9c265..22ce83b9d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioBottomSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioBottomSheet.kt @@ -7,6 +7,7 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -15,8 +16,6 @@ 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 @@ -27,12 +26,25 @@ 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 +import com.nuvio.app.isIos +import com.mohamedrejeb.calf.ui.sheet.AdaptiveBottomSheet +import com.mohamedrejeb.calf.ui.sheet.AdaptiveSheetState +import com.mohamedrejeb.calf.ui.sheet.rememberAdaptiveSheetState + +typealias NuvioBottomSheetState = AdaptiveSheetState + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun rememberNuvioBottomSheetState( + skipPartiallyExpanded: Boolean = !isIos, +): NuvioBottomSheetState = + rememberAdaptiveSheetState(skipPartiallyExpanded = skipPartiallyExpanded) @OptIn(ExperimentalMaterial3Api::class) @Composable fun NuvioModalBottomSheet( onDismissRequest: () -> Unit, - sheetState: SheetState, + sheetState: NuvioBottomSheetState, modifier: Modifier = Modifier, containerColor: Color = MaterialTheme.colorScheme.surface, contentColor: Color = MaterialTheme.colorScheme.onSurface, @@ -40,10 +52,16 @@ fun NuvioModalBottomSheet( showDragHandle: Boolean = true, content: @Composable ColumnScope.() -> Unit, ) { - ModalBottomSheet( + val sheetModifier = if (isIos) { + modifier.fillMaxSize() + } else { + modifier + } + + AdaptiveBottomSheet( onDismissRequest = onDismissRequest, - sheetState = sheetState, - modifier = modifier, + adaptiveSheetState = sheetState, + modifier = sheetModifier, containerColor = containerColor, contentColor = contentColor, shape = shape, @@ -104,7 +122,7 @@ fun NuvioBottomSheetActionRow( @OptIn(ExperimentalMaterial3Api::class) suspend fun dismissNuvioBottomSheet( - sheetState: SheetState, + sheetState: NuvioBottomSheetState, onDismiss: () -> Unit, ) { if (sheetState.isVisible) { @@ -122,4 +140,4 @@ private fun NuvioBottomSheetDragHandle() { .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/NuvioContinueWatchingActionSheet.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioContinueWatchingActionSheet.kt index 693017d33..7d6fafeae 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioContinueWatchingActionSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioContinueWatchingActionSheet.kt @@ -17,7 +17,6 @@ import androidx.compose.material.icons.filled.Replay import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme 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 @@ -52,7 +51,7 @@ fun NuvioContinueWatchingActionSheet( onRemove: () -> Unit, ) { if (item == null) return - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberNuvioBottomSheetState() val coroutineScope = rememberCoroutineScope() fun dismissAfter(action: () -> Unit) { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioDropdownChip.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioDropdownChip.kt index 85ea9052a..f771f9865 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioDropdownChip.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioDropdownChip.kt @@ -18,9 +18,7 @@ import androidx.compose.material.icons.rounded.KeyboardArrowDown 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 @@ -51,7 +49,7 @@ fun NuvioDropdownChip( modifier: Modifier = Modifier, ) { var isSheetVisible by remember { mutableStateOf(false) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberNuvioBottomSheetState() val coroutineScope = rememberCoroutineScope() Row( @@ -117,7 +115,7 @@ private fun NuvioDropdownOptionsSheet( title: String, options: List, selectedKey: String?, - sheetState: SheetState, + sheetState: NuvioBottomSheetState, onDismiss: () -> Unit, onSelected: (NuvioDropdownOption) -> Unit, ) { 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 86520e0aa..b458befe2 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 @@ -24,7 +24,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme 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 @@ -58,7 +57,7 @@ fun NuvioPosterActionSheet( onToggleWatched: () -> Unit, ) { if (item == null) return - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberNuvioBottomSheetState() val coroutineScope = rememberCoroutineScope() NuvioModalBottomSheet( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt index 01c9ec619..243a2525c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt @@ -44,7 +44,7 @@ import androidx.compose.material3.Switch import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState +import com.nuvio.app.core.ui.rememberNuvioBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -1918,7 +1918,7 @@ private fun GenrePickerSheet( onSelect: (String?) -> Unit, onDismiss: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberNuvioBottomSheetState() NuvioModalBottomSheet( onDismissRequest = onDismiss, 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 c13ae1249..3b3dcf118 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 @@ -24,7 +24,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState +import com.nuvio.app.core.ui.rememberNuvioBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -51,7 +51,7 @@ fun CommentDetailSheet( onNext: () -> Unit, onDismiss: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberNuvioBottomSheetState() val scrollState = rememberScrollState() LaunchedEffect(comment.id) { 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 00dd40f26..49f938861 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 @@ -12,7 +12,7 @@ import androidx.compose.material.icons.filled.PlaylistAddCheckCircle import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState +import com.nuvio.app.core.ui.rememberNuvioBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -46,7 +46,7 @@ fun EpisodeWatchedActionSheet( showPlayManually: Boolean = false, onPlayManually: (() -> Unit)? = null, ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberNuvioBottomSheetState() val coroutineScope = rememberCoroutineScope() NuvioModalBottomSheet( @@ -140,7 +140,7 @@ fun SeasonWatchedActionSheet( onToggleSeasonWatched: () -> Unit, onMarkPreviousSeasonsWatched: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberNuvioBottomSheetState() val coroutineScope = rememberCoroutineScope() NuvioModalBottomSheet( 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 c12ead6b9..58776ef8a 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 @@ -18,7 +18,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState +import com.nuvio.app.core.ui.rememberNuvioBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -67,7 +67,7 @@ fun TrailerPlayerPopup( } }.joinToString(separator = " • ") - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberNuvioBottomSheetState() val coroutineScope = rememberCoroutineScope() var playerError by remember(playbackSource?.videoUrl, playbackSource?.audioUrl) { mutableStateOf(null) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt index abbf58e7f..6ca91578a 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt @@ -65,7 +65,7 @@ import nuvio.composeapp.generated.resources.settings_appearance_section_theme import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import com.nuvio.app.core.ui.rememberNuvioBottomSheetState @OptIn(ExperimentalLayoutApi::class) internal fun LazyListScope.appearanceSettingsContent( @@ -196,7 +196,7 @@ private fun AppearanceLanguageBottomSheet( onLanguageSelected: (AppLanguage) -> Unit, onDismiss: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberNuvioBottomSheetState() val coroutineScope = rememberCoroutineScope() val options = remember { AppLanguage.entries.map { language -> diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt index 5cdb03ec6..97a1c28d5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt @@ -86,7 +86,7 @@ import com.nuvio.app.core.ui.dismissNuvioBottomSheet import com.nuvio.app.features.downloads.DownloadsRepository import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.rememberModalBottomSheetState +import com.nuvio.app.core.ui.rememberNuvioBottomSheetState import coil3.compose.AsyncImage import com.nuvio.app.core.ui.nuvioSafeBottomPadding import com.nuvio.app.features.debrid.DebridProviders @@ -1126,7 +1126,7 @@ private fun StreamActionsSheet( ) { if (stream == null) return - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberNuvioBottomSheetState() val coroutineScope = rememberCoroutineScope() NuvioModalBottomSheet( From 739dd56f050aa73dbe9ac03c41766fd840d6e1d3 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:07:51 +0530 Subject: [PATCH 2/2] feat(ios): use bottomsheet for streamscrean --- composeApp/build.gradle.kts | 2 + .../commonMain/kotlin/com/nuvio/app/App.kt | 279 +++++++++++------- .../com/nuvio/app/core/ui/NuvioBottomSheet.kt | 17 +- .../nuvio/app/features/player/PlayerModels.kt | 1 + .../app/features/streams/StreamsScreen.kt | 17 +- gradle/libs.versions.toml | 2 + 6 files changed, 211 insertions(+), 107 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index b1b9fdbe5..64eb7e7e1 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -238,6 +238,7 @@ kotlin { baseName = "ComposeApp" isStatic = true freeCompilerArgs += listOf("-Xbinary=bundleId=$iosFrameworkBundleId") + export("com.mohamedrejeb.calf:calf-ui:${libs.versions.calf.get()}") } } @@ -274,6 +275,7 @@ kotlin { implementation(libs.coil.compose) implementation(libs.coil.network.ktor3) implementation(libs.coil.svg) + api(libs.calf.ui) implementation("dev.chrisbanes.haze:haze:1.7.2") implementation(libs.compose.runtime) implementation(libs.compose.foundation) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index f3fa03306..444573b93 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -56,6 +56,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.zIndex import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -67,6 +68,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.dialog import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import coil3.ImageLoader @@ -99,8 +101,11 @@ import com.nuvio.app.core.ui.NuvioTheme import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding import com.nuvio.app.core.ui.NativeNavigationTab import com.nuvio.app.core.ui.NativeTabBridge +import com.nuvio.app.core.ui.NuvioModalBottomSheet import com.nuvio.app.core.ui.isLiquidGlassNativeTabBarSupported +import com.nuvio.app.core.ui.dismissNuvioBottomSheet import com.nuvio.app.core.ui.localizedContinueWatchingSubtitle +import com.nuvio.app.core.ui.rememberNuvioBottomSheetState import com.nuvio.app.features.auth.AuthScreen import com.nuvio.app.features.addons.AddonRepository import com.nuvio.app.features.catalog.CatalogRepository @@ -799,6 +804,7 @@ private fun MainAppContent( var profileSwitchLoading by remember { mutableStateOf(false) } var resumePromptItem by remember { mutableStateOf(null) } var lastExternalPlayerLaunch by remember { mutableStateOf(null) } + val streamLaunchIdsPreservedForPlayerReturn = remember { mutableSetOf() } val launchExternalPlayer = rememberExternalPlayerLauncher { result -> if (result != null && result.positionMs > 0L) { coroutineScope.launch { @@ -1577,7 +1583,8 @@ private fun MainAppContent( modifier = Modifier.fillMaxSize(), ) } - composable { backStackEntry -> + val streamRouteDestination: @Composable (NavBackStackEntry) -> Unit = + streamRouteDestination@{ backStackEntry -> val route = backStackEntry.toRoute() val launch = remember(route.launchId) { StreamLaunchStore.get(route.launchId) @@ -1587,7 +1594,7 @@ private fun MainAppContent( StreamsRepository.clear() navController.popBackStack() } - return@composable + return@streamRouteDestination } val pauseDescription = launch.pauseDescription val streamRouteScope = rememberCoroutineScope() @@ -1597,7 +1604,9 @@ private fun MainAppContent( DisposableEffect(lifecycleOwner, route.launchId) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_DESTROY) { - StreamLaunchStore.remove(route.launchId) + if (route.launchId !in streamLaunchIdsPreservedForPlayerReturn) { + StreamLaunchStore.remove(route.launchId) + } } } lifecycleOwner.lifecycle.addObserver(observer) @@ -1666,6 +1675,21 @@ private fun MainAppContent( fun p2pSentinelUrl(infoHash: String, fileIdx: Int?): String = "torrent://$infoHash${fileIdx?.let { "?index=$it" }.orEmpty()}" + fun navigateToPlayer( + playerLaunch: PlayerLaunch, + replaceStreamRoute: Boolean = false, + ) { + playerLaunch.returnStreamLaunchId?.let { streamLaunchId -> + streamLaunchIdsPreservedForPlayerReturn.add(streamLaunchId) + } + val playerLaunchId = PlayerLaunchStore.put(playerLaunch) + navController.navigate(PlayerRoute(launchId = playerLaunchId)) { + if (replaceStreamRoute) { + popUpTo { inclusive = true } + } + } + } + fun openP2pStream( stream: StreamItem, resolvedResumePositionMs: Long?, @@ -1726,15 +1750,14 @@ private fun MainAppContent( torrentTrackers = stream.p2pTrackers, initialPositionMs = resolvedResumePositionMs ?: 0L, initialProgressFraction = resolvedResumeProgressFraction, + returnStreamLaunchId = if (isIos && !replaceStreamRoute) route.launchId else null, ) - val launchId = PlayerLaunchStore.put(playerLaunch) StreamsRepository.cancelLoading() - navController.navigate(PlayerRoute(launchId = launchId)) { - if (replaceStreamRoute) { - popUpTo { inclusive = true } - } - } + navigateToPlayer( + playerLaunch = playerLaunch, + replaceStreamRoute = replaceStreamRoute, + ) } fun requestOrOpenP2pStream( @@ -1850,10 +1873,10 @@ private fun MainAppContent( } StreamsRepository.clear() reuseNavigated = true - val launchId = PlayerLaunchStore.put(playerLaunch) - navController.navigate(PlayerRoute(launchId = launchId)) { - popUpTo { inclusive = true } - } + navigateToPlayer( + playerLaunch = playerLaunch, + replaceStreamRoute = true, + ) } } @@ -1982,10 +2005,10 @@ private fun MainAppContent( } StreamsRepository.consumeAutoPlay() StreamsRepository.cancelLoading() - val launchId = PlayerLaunchStore.put(playerLaunch) - navController.navigate(PlayerRoute(launchId = launchId)) { - popUpTo { inclusive = true } - } + navigateToPlayer( + playerLaunch = playerLaunch, + replaceStreamRoute = true, + ) } if (!hasResolvedVideoId) { @@ -1995,7 +2018,7 @@ private fun MainAppContent( ) { CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) } - return@composable + return@streamRouteDestination } fun openSelectedStream( @@ -2097,6 +2120,7 @@ private fun MainAppContent( parentMetaType = launch.parentMetaType ?: launch.type, initialPositionMs = resolvedResumePositionMs ?: 0L, initialProgressFraction = resolvedResumeProgressFraction, + returnStreamLaunchId = if (isIos) route.launchId else null, ) if (!forceInternal && (forceExternal || playerSettings.externalPlayerEnabled)) { @@ -2105,11 +2129,8 @@ private fun MainAppContent( return } - val launchId = PlayerLaunchStore.put(playerLaunch) StreamsRepository.cancelLoading() - navController.navigate( - PlayerRoute(launchId = launchId) - ) + navigateToPlayer(playerLaunch) } // Hide overlay when reuse navigated to external player (prevents reload from showing it again) @@ -2119,90 +2140,142 @@ private fun MainAppContent( } } - Box(modifier = Modifier.fillMaxSize()) { - StreamsScreen( - type = launch.type, - videoId = effectiveVideoId, - parentMetaId = launch.parentMetaId ?: effectiveVideoId, - parentMetaType = launch.parentMetaType ?: launch.type, - title = launch.title, - logo = launch.logo, - poster = launch.poster, - background = launch.background, - seasonNumber = launch.seasonNumber, - episodeNumber = launch.episodeNumber, - episodeTitle = launch.episodeTitle, - episodeThumbnail = launch.episodeThumbnail, - resumePositionMs = launch.resumePositionMs, - resumeProgressFraction = launch.resumeProgressFraction, - manualSelection = launch.manualSelection, - startFromBeginning = launch.startFromBeginning, - onStreamSelected = { stream, resolvedResumePositionMs, resolvedResumeProgressFraction -> - openSelectedStream( - stream = stream, - resolvedResumePositionMs = resolvedResumePositionMs, - resolvedResumeProgressFraction = resolvedResumeProgressFraction, - forceExternal = false, - forceInternal = false, + val streamSheetState = rememberNuvioBottomSheetState(skipPartiallyExpanded = true) + + fun closeStreamRoute() { + streamLaunchIdsPreservedForPlayerReturn.remove(route.launchId) + StreamsRepository.clear() + navController.popBackStack() + } + + fun dismissStreamRoute() { + if (isIos) { + streamRouteScope.launch { + dismissNuvioBottomSheet( + sheetState = streamSheetState, + onDismiss = ::closeStreamRoute, ) - }, - onStreamActionOpen = { stream, openExternally, resolvedResumePositionMs, resolvedResumeProgressFraction -> - openSelectedStream( - stream = stream, - resolvedResumePositionMs = resolvedResumePositionMs, - resolvedResumeProgressFraction = resolvedResumeProgressFraction, - forceExternal = openExternally, - forceInternal = !openExternally, - ) - }, - onBack = { - StreamsRepository.clear() - navController.popBackStack() - }, - modifier = Modifier.fillMaxSize(), - ) - pendingP2pStreamOpen?.let { pending -> - P2pConsentDialog( - onEnableP2p = { - P2pSettingsRepository.setP2pEnabled(true) - pendingP2pStreamOpen = null - openP2pStream( - stream = pending.stream, - resolvedResumePositionMs = pending.resumePositionMs, - resolvedResumeProgressFraction = pending.resumeProgressFraction, - replaceStreamRoute = pending.isAutoPlay, - ) - }, - onDismiss = { - if (pending.isAutoPlay) { - StreamsRepository.skipAutoPlayStream(pending.stream) - StreamsRepository.consumeAutoPlay() - } - pendingP2pStreamOpen = null - }, - ) + } + } else { + closeStreamRoute() } - if (resolvingDebridStream) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.82f)), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - CircularProgressIndicator(color = Color.White) - Text( - text = stringResource(Res.string.streams_finding_source), - color = Color.White.copy(alpha = 0.82f), - style = MaterialTheme.typography.bodyMedium, + } + + val streamRouteContent: @Composable (Modifier) -> Unit = { contentModifier -> + Box(modifier = contentModifier) { + StreamsScreen( + type = launch.type, + videoId = effectiveVideoId, + parentMetaId = launch.parentMetaId ?: effectiveVideoId, + parentMetaType = launch.parentMetaType ?: launch.type, + title = launch.title, + logo = launch.logo, + poster = launch.poster, + background = launch.background, + seasonNumber = launch.seasonNumber, + episodeNumber = launch.episodeNumber, + episodeTitle = launch.episodeTitle, + episodeThumbnail = launch.episodeThumbnail, + resumePositionMs = launch.resumePositionMs, + resumeProgressFraction = launch.resumeProgressFraction, + manualSelection = launch.manualSelection, + startFromBeginning = launch.startFromBeginning, + onStreamSelected = { stream, resolvedResumePositionMs, resolvedResumeProgressFraction -> + openSelectedStream( + stream = stream, + resolvedResumePositionMs = resolvedResumePositionMs, + resolvedResumeProgressFraction = resolvedResumeProgressFraction, + forceExternal = false, + forceInternal = false, ) + }, + onStreamActionOpen = { stream, openExternally, resolvedResumePositionMs, resolvedResumeProgressFraction -> + openSelectedStream( + stream = stream, + resolvedResumePositionMs = resolvedResumePositionMs, + resolvedResumeProgressFraction = resolvedResumeProgressFraction, + forceExternal = openExternally, + forceInternal = !openExternally, + ) + }, + onBack = { dismissStreamRoute() }, + modifier = Modifier.fillMaxSize(), + showBackButton = !isIos, + ) + pendingP2pStreamOpen?.let { pending -> + P2pConsentDialog( + onEnableP2p = { + P2pSettingsRepository.setP2pEnabled(true) + pendingP2pStreamOpen = null + openP2pStream( + stream = pending.stream, + resolvedResumePositionMs = pending.resumePositionMs, + resolvedResumeProgressFraction = pending.resumeProgressFraction, + replaceStreamRoute = pending.isAutoPlay, + ) + }, + onDismiss = { + if (pending.isAutoPlay) { + StreamsRepository.skipAutoPlayStream(pending.stream) + StreamsRepository.consumeAutoPlay() + } + pendingP2pStreamOpen = null + }, + ) + } + if (resolvingDebridStream) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.82f)), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + CircularProgressIndicator(color = Color.White) + Text( + text = stringResource(Res.string.streams_finding_source), + color = Color.White.copy(alpha = 0.82f), + style = MaterialTheme.typography.bodyMedium, + ) + } } } } } + + if (isIos) { + NuvioModalBottomSheet( + onDismissRequest = { closeStreamRoute() }, + sheetState = streamSheetState, + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + iosContentTopPadding = 0.dp, + ) { + streamRouteContent( + Modifier + .fillMaxWidth() + .weight(1f), + ) + } + } else { + streamRouteContent(Modifier.fillMaxSize()) + } + } + if (isIos) { + dialog( + dialogProperties = DialogProperties( + usePlatformDefaultWidth = false, + ), + ) { backStackEntry -> + streamRouteDestination(backStackEntry) + } + } else { + composable { backStackEntry -> + streamRouteDestination(backStackEntry) + } } composable( enterTransition = { @@ -2263,6 +2336,14 @@ private fun MainAppContent( ResumePromptRepository.markPlayerExitedNormally() PlayerLaunchStore.remove(route.launchId) navController.popBackStack() + launch.returnStreamLaunchId?.let { streamLaunchId -> + if (isIos && StreamLaunchStore.get(streamLaunchId) != null) { + navController.navigate(StreamRoute(launchId = streamLaunchId)) { + launchSingleTop = true + } + } + streamLaunchIdsPreservedForPlayerReturn.remove(streamLaunchId) + } }, onOpenInExternalPlayer = { request -> val playerLaunch = PlayerLaunch( 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 index 22ce83b9d..0938c63db 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioBottomSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioBottomSheet.kt @@ -4,6 +4,7 @@ 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.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -25,6 +26,7 @@ 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 import androidx.compose.ui.unit.dp import com.nuvio.app.isIos import com.mohamedrejeb.calf.ui.sheet.AdaptiveBottomSheet @@ -50,6 +52,7 @@ fun NuvioModalBottomSheet( contentColor: Color = MaterialTheme.colorScheme.onSurface, shape: Shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), showDragHandle: Boolean = true, + iosContentTopPadding: Dp = 28.dp, content: @Composable ColumnScope.() -> Unit, ) { val sheetModifier = if (isIos) { @@ -57,6 +60,18 @@ fun NuvioModalBottomSheet( } else { modifier } + val sheetContent: @Composable ColumnScope.() -> Unit = { + if (isIos && iosContentTopPadding > 0.dp) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = iosContentTopPadding), + content = content, + ) + } else { + content() + } + } AdaptiveBottomSheet( onDismissRequest = onDismissRequest, @@ -70,7 +85,7 @@ fun NuvioModalBottomSheet( } else { null }, - content = content, + content = sheetContent, ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt index f9cc53f0a..d4bcc0ecf 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt @@ -48,6 +48,7 @@ data class PlayerLaunch( val torrentTrackers: List = emptyList(), val initialPositionMs: Long = 0L, val initialProgressFraction: Float? = null, + val returnStreamLaunchId: Long? = null, ) object PlayerLaunchStore { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt index 97a1c28d5..b107d575c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt @@ -129,6 +129,7 @@ fun StreamsScreen( ) -> Unit = { _, _, _, _ -> }, onBack: () -> Unit, modifier: Modifier = Modifier, + showBackButton: Boolean = true, ) { val uiState by StreamsRepository.uiState.collectAsStateWithLifecycle() val playerSettings by remember { @@ -262,13 +263,15 @@ fun StreamsScreen( .padding(start = 12.dp, top = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - NuvioBackButton( - onClick = onBack, - modifier = Modifier - .size(40.dp), - containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.45f), - contentColor = MaterialTheme.colorScheme.onBackground, - ) + if (showBackButton) { + NuvioBackButton( + onClick = onBack, + modifier = Modifier + .size(40.dp), + containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.45f), + contentColor = MaterialTheme.colorScheme.onBackground, + ) + } Box( modifier = Modifier diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 39ae4c826..8e64fbdf3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ androidx-work = "2.10.3" androidx-testExt = "1.3.0" composeMultiplatform = "1.11.1" coil = "3.5.0-beta01" +calf = "0.11.0" kermit = "2.0.5" junit = "4.13.2" kotlin = "2.3.0" @@ -52,6 +53,7 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" } coil-network-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" } +calf-ui = { module = "com.mohamedrejeb.calf:calf-ui", version.ref = "calf" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }