diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 2069679d..ee1f4124 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -128,9 +128,6 @@ import com.nuvio.app.features.player.PlayerLaunch import com.nuvio.app.features.player.PlayerLaunchStore import com.nuvio.app.features.player.PlayerRoute import com.nuvio.app.features.player.PlayerScreen -import com.nuvio.app.features.player.ExternalPlayerOpenResult -import com.nuvio.app.features.player.ExternalPlayerPlatform -import com.nuvio.app.features.player.ExternalPlayerPlaybackRequest import com.nuvio.app.features.player.sanitizePlaybackHeaders import com.nuvio.app.features.player.sanitizePlaybackResponseHeaders import com.nuvio.app.features.profiles.AvatarRepository @@ -291,14 +288,6 @@ private fun NativeNavigationTab.toAppScreenTab(): AppScreenTab = when (this) { NativeNavigationTab.Settings -> AppScreenTab.Settings } -private fun PlayerLaunch.toExternalPlayerPlaybackRequest(): ExternalPlayerPlaybackRequest = - ExternalPlayerPlaybackRequest( - sourceUrl = sourceUrl, - title = title, - streamTitle = streamTitle, - sourceHeaders = sourceHeaders, - ) - private enum class AppGateScreen { Loading, Auth, @@ -531,7 +520,6 @@ private fun MainAppContent( val hapticFeedback = LocalHapticFeedback.current val coroutineScope = rememberCoroutineScope() var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) } - var searchFocusRequestCount by remember { mutableStateOf(0) } val currentBackStackEntry by navController.currentBackStackEntryAsState() val nativeRequestedTab by remember { NativeTabBridge.requestedTab }.collectAsStateWithLifecycle() val liquidGlassNativeTabBarEnabled by remember { @@ -574,9 +562,6 @@ private fun MainAppContent( NetworkStatusRepository.uiState }.collectAsStateWithLifecycle() val downloadedProviderLabel = stringResource(Res.string.provider_downloaded) - val externalPlayerNotConfiguredText = stringResource(Res.string.external_player_not_configured) - val externalPlayerUnavailableText = stringResource(Res.string.external_player_unavailable) - val externalPlayerFailedText = stringResource(Res.string.external_player_failed) val isTraktLibrarySource = libraryUiState.sourceMode == LibrarySourceMode.TRAKT var initialHomeReady by rememberSaveable { mutableStateOf(false) } var offlineLaunchRouteHandled by rememberSaveable { mutableStateOf(false) } @@ -598,9 +583,6 @@ private fun MainAppContent( LaunchedEffect(selectedTab) { NativeTabBridge.publishSelectedTab(selectedTab.toNativeNavigationTab()) - if (selectedTab != AppScreenTab.Search) { - searchFocusRequestCount = 0 - } } DisposableEffect( @@ -770,29 +752,6 @@ private fun MainAppContent( } } - fun openExternalPlayback(launch: PlayerLaunch): Boolean { - return when ( - ExternalPlayerPlatform.open( - request = launch.toExternalPlayerPlaybackRequest(), - playerId = playerSettingsUiState.externalPlayerId, - ) - ) { - ExternalPlayerOpenResult.Opened -> true - ExternalPlayerOpenResult.NotConfigured -> { - NuvioToastController.show(externalPlayerNotConfiguredText) - false - } - ExternalPlayerOpenResult.NoPlayerAvailable -> { - NuvioToastController.show(externalPlayerUnavailableText) - false - } - ExternalPlayerOpenResult.Failed -> { - NuvioToastController.show(externalPlayerFailedText) - false - } - } - } - fun launchPlaybackWithDownloadPreference( type: String, videoId: String, @@ -824,7 +783,8 @@ private fun MainAppContent( ) val localSourceUrl = downloadedItem?.let(DownloadsRepository::playableLocalFileUri) if (!localSourceUrl.isNullOrBlank()) { - val playerLaunch = PlayerLaunch( + val launchId = PlayerLaunchStore.put( + PlayerLaunch( title = title, sourceUrl = localSourceUrl, sourceHeaders = emptyMap(), @@ -847,12 +807,8 @@ private fun MainAppContent( parentMetaType = parentMetaType, initialPositionMs = targetResumePositionMs, initialProgressFraction = targetResumeProgressFraction, - ) - if (playerSettingsUiState.externalPlayerEnabled) { - openExternalPlayback(playerLaunch) - return - } - val launchId = PlayerLaunchStore.put(playerLaunch) + ), + ) navController.navigate(PlayerRoute(launchId = launchId)) return } @@ -1053,13 +1009,7 @@ private fun MainAppContent( ) NavItem( selected = selectedTab == AppScreenTab.Search, - onClick = { - if (selectedTab == AppScreenTab.Search) { - searchFocusRequestCount++ - } else { - selectedTab = AppScreenTab.Search - } - }, + onClick = { selectedTab = AppScreenTab.Search }, icon = Res.drawable.sidebar_search, contentDescription = stringResource(Res.string.compose_nav_search), ) @@ -1093,7 +1043,6 @@ private fun MainAppContent( .fillMaxSize() .padding(innerPadding), selectedTab = selectedTab, - searchFocusRequestCount = searchFocusRequestCount, animateHomeCollectionGifs = tabsRouteActive, onCatalogClick = onCatalogClick, onPosterClick = { meta -> @@ -1148,13 +1097,7 @@ private fun MainAppContent( if (isTabletLayout && !useNativeBottomTabs) { TabletFloatingTopBar( selectedTab = selectedTab, - onTabSelected = { tab -> - if (tab == AppScreenTab.Search && selectedTab == AppScreenTab.Search) { - searchFocusRequestCount++ - } else { - selectedTab = tab - } - }, + onTabSelected = { selectedTab = it }, onProfileSelected = onProfileSelected, onAddProfileRequested = onSwitchProfile, ) @@ -1405,8 +1348,10 @@ private fun MainAppContent( val maxAgeMs = playerSettings.streamReuseLastLinkCacheHours * 60L * 60L * 1000L val cached = StreamLinkCacheRepository.getValid(cacheKey, maxAgeMs) if (cached != null) { + reuseNavigated = true StreamsRepository.clear() - val playerLaunch = PlayerLaunch( + val launchId = PlayerLaunchStore.put( + PlayerLaunch( title = launch.title, sourceUrl = cached.url, sourceHeaders = sanitizePlaybackHeaders(cached.requestHeaders), @@ -1431,13 +1376,7 @@ private fun MainAppContent( initialPositionMs = launch.resumePositionMs ?: 0L, initialProgressFraction = launch.resumeProgressFraction, ) - if (playerSettings.externalPlayerEnabled) { - openExternalPlayback(playerLaunch) - reuseNavigated = true - return@LaunchedEffect - } - reuseNavigated = true - val launchId = PlayerLaunchStore.put(playerLaunch) + ) navController.navigate(PlayerRoute(launchId = launchId)) { popUpTo { inclusive = true } } @@ -1489,7 +1428,8 @@ private fun MainAppContent( bingeGroup = stream.behaviorHints.bingeGroup, ) } - val playerLaunch = PlayerLaunch( + val launchId = PlayerLaunchStore.put( + PlayerLaunch( title = launch.title, sourceUrl = sourceUrl, sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request), @@ -1514,13 +1454,9 @@ private fun MainAppContent( initialPositionMs = launch.resumePositionMs ?: 0L, initialProgressFraction = launch.resumeProgressFraction, ) + ) StreamsRepository.consumeAutoPlay() StreamsRepository.cancelLoading() - if (playerSettings.externalPlayerEnabled) { - openExternalPlayback(playerLaunch) - return@LaunchedEffect - } - val launchId = PlayerLaunchStore.put(playerLaunch) navController.navigate(PlayerRoute(launchId = launchId)) { popUpTo { inclusive = true } } @@ -1536,74 +1472,6 @@ private fun MainAppContent( return@composable } - fun openSelectedStream( - stream: com.nuvio.app.features.streams.StreamItem, - resolvedResumePositionMs: Long?, - resolvedResumeProgressFraction: Float?, - forceExternal: Boolean, - forceInternal: Boolean, - ) { - val sourceUrl = stream.directPlaybackUrl ?: return - if (playerSettings.streamReuseLastLinkEnabled) { - val cacheKey = StreamLinkCacheRepository.contentKey( - type = launch.type, - videoId = effectiveVideoId, - parentMetaId = launch.parentMetaId, - season = launch.seasonNumber, - episode = launch.episodeNumber, - ) - StreamLinkCacheRepository.save( - contentKey = cacheKey, - url = sourceUrl, - streamName = stream.streamLabel, - addonName = stream.addonName, - addonId = stream.addonId, - requestHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request), - responseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response), - filename = stream.behaviorHints.filename, - videoSize = stream.behaviorHints.videoSize, - bingeGroup = stream.behaviorHints.bingeGroup, - ) - } - val playerLaunch = PlayerLaunch( - title = launch.title, - sourceUrl = sourceUrl, - sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request), - sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response), - logo = launch.logo, - poster = launch.poster, - background = launch.background, - seasonNumber = launch.seasonNumber, - episodeNumber = launch.episodeNumber, - episodeTitle = launch.episodeTitle, - episodeThumbnail = launch.episodeThumbnail, - streamTitle = stream.streamLabel, - streamSubtitle = stream.streamSubtitle, - bingeGroup = stream.behaviorHints.bingeGroup, - pauseDescription = pauseDescription, - providerName = stream.addonName, - providerAddonId = stream.addonId, - contentType = launch.type, - videoId = effectiveVideoId, - parentMetaId = launch.parentMetaId ?: effectiveVideoId, - parentMetaType = launch.parentMetaType ?: launch.type, - initialPositionMs = resolvedResumePositionMs ?: 0L, - initialProgressFraction = resolvedResumeProgressFraction, - ) - - if (!forceInternal && (forceExternal || playerSettings.externalPlayerEnabled)) { - openExternalPlayback(playerLaunch) - StreamsRepository.cancelLoading() - return - } - - val launchId = PlayerLaunchStore.put(playerLaunch) - StreamsRepository.cancelLoading() - navController.navigate( - PlayerRoute(launchId = launchId) - ) - } - StreamsScreen( type = launch.type, videoId = effectiveVideoId, @@ -1622,22 +1490,62 @@ private fun MainAppContent( 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, - ) + val sourceUrl = stream.directPlaybackUrl + if (sourceUrl != null) { + // Persist for Reuse Last Link + if (playerSettings.streamReuseLastLinkEnabled) { + val cacheKey = StreamLinkCacheRepository.contentKey( + type = launch.type, + videoId = effectiveVideoId, + parentMetaId = launch.parentMetaId, + season = launch.seasonNumber, + episode = launch.episodeNumber, + ) + StreamLinkCacheRepository.save( + contentKey = cacheKey, + url = sourceUrl, + streamName = stream.streamLabel, + addonName = stream.addonName, + addonId = stream.addonId, + requestHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request), + responseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response), + filename = stream.behaviorHints.filename, + videoSize = stream.behaviorHints.videoSize, + bingeGroup = stream.behaviorHints.bingeGroup, + ) + } + val launchId = PlayerLaunchStore.put( + PlayerLaunch( + title = launch.title, + sourceUrl = sourceUrl, + sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request), + sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response), + logo = launch.logo, + poster = launch.poster, + background = launch.background, + seasonNumber = launch.seasonNumber, + episodeNumber = launch.episodeNumber, + episodeTitle = launch.episodeTitle, + episodeThumbnail = launch.episodeThumbnail, + streamTitle = stream.streamLabel, + streamSubtitle = stream.streamSubtitle, + bingeGroup = stream.behaviorHints.bingeGroup, + pauseDescription = pauseDescription, + providerName = stream.addonName, + providerAddonId = stream.addonId, + contentType = launch.type, + videoId = effectiveVideoId, + parentMetaId = launch.parentMetaId ?: effectiveVideoId, + parentMetaType = launch.parentMetaType ?: launch.type, + initialPositionMs = resolvedResumePositionMs ?: 0L, + initialProgressFraction = resolvedResumeProgressFraction, + ) + ) + StreamsRepository.cancelLoading() + navController.navigate( + PlayerRoute(launchId = launchId) + ) + } }, onBack = { StreamsRepository.clear() @@ -1722,6 +1630,9 @@ private fun MainAppContent( onPosterClick = { meta -> navController.navigate(DetailRoute(type = meta.type, id = meta.id)) }, + onPosterLongClick = { meta -> + selectedPosterForActions = meta + }, modifier = Modifier.fillMaxSize(), ) } @@ -1766,7 +1677,8 @@ private fun MainAppContent( ?.let(WatchProgressRepository::progressForVideo) ?.takeIf { it.isResumable } - val playerLaunch = PlayerLaunch( + val launchId = PlayerLaunchStore.put( + PlayerLaunch( title = item.title, sourceUrl = sourceUrl, sourceHeaders = emptyMap(), @@ -1788,12 +1700,8 @@ private fun MainAppContent( parentMetaType = item.parentMetaType, initialPositionMs = resumeEntry?.lastPositionMs?.takeIf { it > 0L } ?: 0L, initialProgressFraction = resumeEntry?.progressFraction?.takeIf { it > 0f }, + ), ) - if (playerSettingsUiState.externalPlayerEnabled) { - openExternalPlayback(playerLaunch) - return@DownloadsScreen - } - val launchId = PlayerLaunchStore.put(playerLaunch) navController.navigate(PlayerRoute(launchId = launchId)) }, ) @@ -2102,7 +2010,6 @@ private fun rememberGuardedPopBackStack( private fun AppTabHost( selectedTab: AppScreenTab, modifier: Modifier = Modifier, - searchFocusRequestCount: Int = 0, animateHomeCollectionGifs: Boolean = true, onCatalogClick: ((HomeCatalogSection) -> Unit)? = null, onPosterClick: ((MetaPreview) -> Unit)? = null, @@ -2150,7 +2057,6 @@ private fun AppTabHost( modifier = Modifier.fillMaxSize(), onPosterClick = onPosterClick, onPosterLongClick = onPosterLongClick, - searchFocusRequestCount = searchFocusRequestCount, ) }