mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
Merge ffb0b1961f into 37203d1fc1
This commit is contained in:
commit
1bcb37e4cb
2 changed files with 95 additions and 245 deletions
|
|
@ -128,9 +128,6 @@ import com.nuvio.app.features.player.PlayerLaunch
|
||||||
import com.nuvio.app.features.player.PlayerLaunchStore
|
import com.nuvio.app.features.player.PlayerLaunchStore
|
||||||
import com.nuvio.app.features.player.PlayerRoute
|
import com.nuvio.app.features.player.PlayerRoute
|
||||||
import com.nuvio.app.features.player.PlayerScreen
|
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.sanitizePlaybackHeaders
|
||||||
import com.nuvio.app.features.player.sanitizePlaybackResponseHeaders
|
import com.nuvio.app.features.player.sanitizePlaybackResponseHeaders
|
||||||
import com.nuvio.app.features.profiles.AvatarRepository
|
import com.nuvio.app.features.profiles.AvatarRepository
|
||||||
|
|
@ -291,14 +288,6 @@ private fun NativeNavigationTab.toAppScreenTab(): AppScreenTab = when (this) {
|
||||||
NativeNavigationTab.Settings -> AppScreenTab.Settings
|
NativeNavigationTab.Settings -> AppScreenTab.Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun PlayerLaunch.toExternalPlayerPlaybackRequest(): ExternalPlayerPlaybackRequest =
|
|
||||||
ExternalPlayerPlaybackRequest(
|
|
||||||
sourceUrl = sourceUrl,
|
|
||||||
title = title,
|
|
||||||
streamTitle = streamTitle,
|
|
||||||
sourceHeaders = sourceHeaders,
|
|
||||||
)
|
|
||||||
|
|
||||||
private enum class AppGateScreen {
|
private enum class AppGateScreen {
|
||||||
Loading,
|
Loading,
|
||||||
Auth,
|
Auth,
|
||||||
|
|
@ -531,7 +520,6 @@ private fun MainAppContent(
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) }
|
var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) }
|
||||||
var searchFocusRequestCount by remember { mutableStateOf(0) }
|
|
||||||
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val nativeRequestedTab by remember { NativeTabBridge.requestedTab }.collectAsStateWithLifecycle()
|
val nativeRequestedTab by remember { NativeTabBridge.requestedTab }.collectAsStateWithLifecycle()
|
||||||
val liquidGlassNativeTabBarEnabled by remember {
|
val liquidGlassNativeTabBarEnabled by remember {
|
||||||
|
|
@ -574,9 +562,6 @@ private fun MainAppContent(
|
||||||
NetworkStatusRepository.uiState
|
NetworkStatusRepository.uiState
|
||||||
}.collectAsStateWithLifecycle()
|
}.collectAsStateWithLifecycle()
|
||||||
val downloadedProviderLabel = stringResource(Res.string.provider_downloaded)
|
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
|
val isTraktLibrarySource = libraryUiState.sourceMode == LibrarySourceMode.TRAKT
|
||||||
var initialHomeReady by rememberSaveable { mutableStateOf(false) }
|
var initialHomeReady by rememberSaveable { mutableStateOf(false) }
|
||||||
var offlineLaunchRouteHandled by rememberSaveable { mutableStateOf(false) }
|
var offlineLaunchRouteHandled by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
@ -598,9 +583,6 @@ private fun MainAppContent(
|
||||||
|
|
||||||
LaunchedEffect(selectedTab) {
|
LaunchedEffect(selectedTab) {
|
||||||
NativeTabBridge.publishSelectedTab(selectedTab.toNativeNavigationTab())
|
NativeTabBridge.publishSelectedTab(selectedTab.toNativeNavigationTab())
|
||||||
if (selectedTab != AppScreenTab.Search) {
|
|
||||||
searchFocusRequestCount = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(
|
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(
|
fun launchPlaybackWithDownloadPreference(
|
||||||
type: String,
|
type: String,
|
||||||
videoId: String,
|
videoId: String,
|
||||||
|
|
@ -824,7 +783,8 @@ private fun MainAppContent(
|
||||||
)
|
)
|
||||||
val localSourceUrl = downloadedItem?.let(DownloadsRepository::playableLocalFileUri)
|
val localSourceUrl = downloadedItem?.let(DownloadsRepository::playableLocalFileUri)
|
||||||
if (!localSourceUrl.isNullOrBlank()) {
|
if (!localSourceUrl.isNullOrBlank()) {
|
||||||
val playerLaunch = PlayerLaunch(
|
val launchId = PlayerLaunchStore.put(
|
||||||
|
PlayerLaunch(
|
||||||
title = title,
|
title = title,
|
||||||
sourceUrl = localSourceUrl,
|
sourceUrl = localSourceUrl,
|
||||||
sourceHeaders = emptyMap(),
|
sourceHeaders = emptyMap(),
|
||||||
|
|
@ -847,12 +807,8 @@ private fun MainAppContent(
|
||||||
parentMetaType = parentMetaType,
|
parentMetaType = parentMetaType,
|
||||||
initialPositionMs = targetResumePositionMs,
|
initialPositionMs = targetResumePositionMs,
|
||||||
initialProgressFraction = targetResumeProgressFraction,
|
initialProgressFraction = targetResumeProgressFraction,
|
||||||
)
|
),
|
||||||
if (playerSettingsUiState.externalPlayerEnabled) {
|
)
|
||||||
openExternalPlayback(playerLaunch)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val launchId = PlayerLaunchStore.put(playerLaunch)
|
|
||||||
navController.navigate(PlayerRoute(launchId = launchId))
|
navController.navigate(PlayerRoute(launchId = launchId))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1053,13 +1009,7 @@ private fun MainAppContent(
|
||||||
)
|
)
|
||||||
NavItem(
|
NavItem(
|
||||||
selected = selectedTab == AppScreenTab.Search,
|
selected = selectedTab == AppScreenTab.Search,
|
||||||
onClick = {
|
onClick = { selectedTab = AppScreenTab.Search },
|
||||||
if (selectedTab == AppScreenTab.Search) {
|
|
||||||
searchFocusRequestCount++
|
|
||||||
} else {
|
|
||||||
selectedTab = AppScreenTab.Search
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon = Res.drawable.sidebar_search,
|
icon = Res.drawable.sidebar_search,
|
||||||
contentDescription = stringResource(Res.string.compose_nav_search),
|
contentDescription = stringResource(Res.string.compose_nav_search),
|
||||||
)
|
)
|
||||||
|
|
@ -1093,7 +1043,6 @@ private fun MainAppContent(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(innerPadding),
|
.padding(innerPadding),
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
searchFocusRequestCount = searchFocusRequestCount,
|
|
||||||
animateHomeCollectionGifs = tabsRouteActive,
|
animateHomeCollectionGifs = tabsRouteActive,
|
||||||
onCatalogClick = onCatalogClick,
|
onCatalogClick = onCatalogClick,
|
||||||
onPosterClick = { meta ->
|
onPosterClick = { meta ->
|
||||||
|
|
@ -1148,13 +1097,7 @@ private fun MainAppContent(
|
||||||
if (isTabletLayout && !useNativeBottomTabs) {
|
if (isTabletLayout && !useNativeBottomTabs) {
|
||||||
TabletFloatingTopBar(
|
TabletFloatingTopBar(
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { tab ->
|
onTabSelected = { selectedTab = it },
|
||||||
if (tab == AppScreenTab.Search && selectedTab == AppScreenTab.Search) {
|
|
||||||
searchFocusRequestCount++
|
|
||||||
} else {
|
|
||||||
selectedTab = tab
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onProfileSelected = onProfileSelected,
|
onProfileSelected = onProfileSelected,
|
||||||
onAddProfileRequested = onSwitchProfile,
|
onAddProfileRequested = onSwitchProfile,
|
||||||
)
|
)
|
||||||
|
|
@ -1405,8 +1348,10 @@ private fun MainAppContent(
|
||||||
val maxAgeMs = playerSettings.streamReuseLastLinkCacheHours * 60L * 60L * 1000L
|
val maxAgeMs = playerSettings.streamReuseLastLinkCacheHours * 60L * 60L * 1000L
|
||||||
val cached = StreamLinkCacheRepository.getValid(cacheKey, maxAgeMs)
|
val cached = StreamLinkCacheRepository.getValid(cacheKey, maxAgeMs)
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
|
reuseNavigated = true
|
||||||
StreamsRepository.clear()
|
StreamsRepository.clear()
|
||||||
val playerLaunch = PlayerLaunch(
|
val launchId = PlayerLaunchStore.put(
|
||||||
|
PlayerLaunch(
|
||||||
title = launch.title,
|
title = launch.title,
|
||||||
sourceUrl = cached.url,
|
sourceUrl = cached.url,
|
||||||
sourceHeaders = sanitizePlaybackHeaders(cached.requestHeaders),
|
sourceHeaders = sanitizePlaybackHeaders(cached.requestHeaders),
|
||||||
|
|
@ -1431,13 +1376,7 @@ private fun MainAppContent(
|
||||||
initialPositionMs = launch.resumePositionMs ?: 0L,
|
initialPositionMs = launch.resumePositionMs ?: 0L,
|
||||||
initialProgressFraction = launch.resumeProgressFraction,
|
initialProgressFraction = launch.resumeProgressFraction,
|
||||||
)
|
)
|
||||||
if (playerSettings.externalPlayerEnabled) {
|
)
|
||||||
openExternalPlayback(playerLaunch)
|
|
||||||
reuseNavigated = true
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
|
||||||
reuseNavigated = true
|
|
||||||
val launchId = PlayerLaunchStore.put(playerLaunch)
|
|
||||||
navController.navigate(PlayerRoute(launchId = launchId)) {
|
navController.navigate(PlayerRoute(launchId = launchId)) {
|
||||||
popUpTo<StreamRoute> { inclusive = true }
|
popUpTo<StreamRoute> { inclusive = true }
|
||||||
}
|
}
|
||||||
|
|
@ -1489,7 +1428,8 @@ private fun MainAppContent(
|
||||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val playerLaunch = PlayerLaunch(
|
val launchId = PlayerLaunchStore.put(
|
||||||
|
PlayerLaunch(
|
||||||
title = launch.title,
|
title = launch.title,
|
||||||
sourceUrl = sourceUrl,
|
sourceUrl = sourceUrl,
|
||||||
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||||
|
|
@ -1514,13 +1454,9 @@ private fun MainAppContent(
|
||||||
initialPositionMs = launch.resumePositionMs ?: 0L,
|
initialPositionMs = launch.resumePositionMs ?: 0L,
|
||||||
initialProgressFraction = launch.resumeProgressFraction,
|
initialProgressFraction = launch.resumeProgressFraction,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
StreamsRepository.consumeAutoPlay()
|
StreamsRepository.consumeAutoPlay()
|
||||||
StreamsRepository.cancelLoading()
|
StreamsRepository.cancelLoading()
|
||||||
if (playerSettings.externalPlayerEnabled) {
|
|
||||||
openExternalPlayback(playerLaunch)
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
|
||||||
val launchId = PlayerLaunchStore.put(playerLaunch)
|
|
||||||
navController.navigate(PlayerRoute(launchId = launchId)) {
|
navController.navigate(PlayerRoute(launchId = launchId)) {
|
||||||
popUpTo<StreamRoute> { inclusive = true }
|
popUpTo<StreamRoute> { inclusive = true }
|
||||||
}
|
}
|
||||||
|
|
@ -1536,74 +1472,6 @@ private fun MainAppContent(
|
||||||
return@composable
|
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(
|
StreamsScreen(
|
||||||
type = launch.type,
|
type = launch.type,
|
||||||
videoId = effectiveVideoId,
|
videoId = effectiveVideoId,
|
||||||
|
|
@ -1622,22 +1490,62 @@ private fun MainAppContent(
|
||||||
manualSelection = launch.manualSelection,
|
manualSelection = launch.manualSelection,
|
||||||
startFromBeginning = launch.startFromBeginning,
|
startFromBeginning = launch.startFromBeginning,
|
||||||
onStreamSelected = { stream, resolvedResumePositionMs, resolvedResumeProgressFraction ->
|
onStreamSelected = { stream, resolvedResumePositionMs, resolvedResumeProgressFraction ->
|
||||||
openSelectedStream(
|
val sourceUrl = stream.directPlaybackUrl
|
||||||
stream = stream,
|
if (sourceUrl != null) {
|
||||||
resolvedResumePositionMs = resolvedResumePositionMs,
|
// Persist for Reuse Last Link
|
||||||
resolvedResumeProgressFraction = resolvedResumeProgressFraction,
|
if (playerSettings.streamReuseLastLinkEnabled) {
|
||||||
forceExternal = false,
|
val cacheKey = StreamLinkCacheRepository.contentKey(
|
||||||
forceInternal = false,
|
type = launch.type,
|
||||||
)
|
videoId = effectiveVideoId,
|
||||||
},
|
parentMetaId = launch.parentMetaId,
|
||||||
onStreamActionOpen = { stream, openExternally, resolvedResumePositionMs, resolvedResumeProgressFraction ->
|
season = launch.seasonNumber,
|
||||||
openSelectedStream(
|
episode = launch.episodeNumber,
|
||||||
stream = stream,
|
)
|
||||||
resolvedResumePositionMs = resolvedResumePositionMs,
|
StreamLinkCacheRepository.save(
|
||||||
resolvedResumeProgressFraction = resolvedResumeProgressFraction,
|
contentKey = cacheKey,
|
||||||
forceExternal = openExternally,
|
url = sourceUrl,
|
||||||
forceInternal = !openExternally,
|
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 = {
|
onBack = {
|
||||||
StreamsRepository.clear()
|
StreamsRepository.clear()
|
||||||
|
|
@ -1722,6 +1630,9 @@ private fun MainAppContent(
|
||||||
onPosterClick = { meta ->
|
onPosterClick = { meta ->
|
||||||
navController.navigate(DetailRoute(type = meta.type, id = meta.id))
|
navController.navigate(DetailRoute(type = meta.type, id = meta.id))
|
||||||
},
|
},
|
||||||
|
onPosterLongClick = { meta ->
|
||||||
|
selectedPosterForActions = meta
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1766,7 +1677,8 @@ private fun MainAppContent(
|
||||||
?.let(WatchProgressRepository::progressForVideo)
|
?.let(WatchProgressRepository::progressForVideo)
|
||||||
?.takeIf { it.isResumable }
|
?.takeIf { it.isResumable }
|
||||||
|
|
||||||
val playerLaunch = PlayerLaunch(
|
val launchId = PlayerLaunchStore.put(
|
||||||
|
PlayerLaunch(
|
||||||
title = item.title,
|
title = item.title,
|
||||||
sourceUrl = sourceUrl,
|
sourceUrl = sourceUrl,
|
||||||
sourceHeaders = emptyMap(),
|
sourceHeaders = emptyMap(),
|
||||||
|
|
@ -1788,12 +1700,8 @@ private fun MainAppContent(
|
||||||
parentMetaType = item.parentMetaType,
|
parentMetaType = item.parentMetaType,
|
||||||
initialPositionMs = resumeEntry?.lastPositionMs?.takeIf { it > 0L } ?: 0L,
|
initialPositionMs = resumeEntry?.lastPositionMs?.takeIf { it > 0L } ?: 0L,
|
||||||
initialProgressFraction = resumeEntry?.progressFraction?.takeIf { it > 0f },
|
initialProgressFraction = resumeEntry?.progressFraction?.takeIf { it > 0f },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if (playerSettingsUiState.externalPlayerEnabled) {
|
|
||||||
openExternalPlayback(playerLaunch)
|
|
||||||
return@DownloadsScreen
|
|
||||||
}
|
|
||||||
val launchId = PlayerLaunchStore.put(playerLaunch)
|
|
||||||
navController.navigate(PlayerRoute(launchId = launchId))
|
navController.navigate(PlayerRoute(launchId = launchId))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -2102,7 +2010,6 @@ private fun rememberGuardedPopBackStack(
|
||||||
private fun AppTabHost(
|
private fun AppTabHost(
|
||||||
selectedTab: AppScreenTab,
|
selectedTab: AppScreenTab,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
searchFocusRequestCount: Int = 0,
|
|
||||||
animateHomeCollectionGifs: Boolean = true,
|
animateHomeCollectionGifs: Boolean = true,
|
||||||
onCatalogClick: ((HomeCatalogSection) -> Unit)? = null,
|
onCatalogClick: ((HomeCatalogSection) -> Unit)? = null,
|
||||||
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
||||||
|
|
@ -2150,7 +2057,6 @@ private fun AppTabHost(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
onPosterClick = onPosterClick,
|
onPosterClick = onPosterClick,
|
||||||
onPosterLongClick = onPosterLongClick,
|
onPosterLongClick = onPosterLongClick,
|
||||||
searchFocusRequestCount = searchFocusRequestCount,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
@ -44,17 +43,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.nuvio.app.core.network.NetworkCondition
|
import com.nuvio.app.core.network.NetworkCondition
|
||||||
import com.nuvio.app.core.network.NetworkStatusRepository
|
import com.nuvio.app.core.network.NetworkStatusRepository
|
||||||
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
|
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import com.nuvio.app.core.format.formatReleaseDateForDisplay
|
|
||||||
import com.nuvio.app.core.ui.NuvioBackButton
|
import com.nuvio.app.core.ui.NuvioBackButton
|
||||||
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
|
||||||
import com.nuvio.app.core.ui.posterCardClickable
|
|
||||||
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
|
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
|
||||||
import com.nuvio.app.core.ui.withDuplicateSafeLazyKeys
|
import com.nuvio.app.core.ui.withDuplicateSafeLazyKeys
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
|
||||||
import com.nuvio.app.features.home.PosterShape
|
|
||||||
import com.nuvio.app.features.home.stableKey
|
import com.nuvio.app.features.home.stableKey
|
||||||
|
import com.nuvio.app.features.home.components.HomePosterCard
|
||||||
|
import com.nuvio.app.features.watched.WatchedRepository
|
||||||
|
import com.nuvio.app.features.watching.application.WatchingState
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -72,17 +68,20 @@ fun CatalogScreen(
|
||||||
genre: String? = null,
|
genre: String? = null,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
||||||
|
onPosterLongClick: ((MetaPreview) -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val uiState by CatalogRepository.uiState.collectAsStateWithLifecycle()
|
val uiState by CatalogRepository.uiState.collectAsStateWithLifecycle()
|
||||||
val homeCatalogSettingsUiState by HomeCatalogSettingsRepository.uiState.collectAsStateWithLifecycle()
|
|
||||||
val posterCardStyle = rememberPosterCardStyleUiState()
|
|
||||||
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
|
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
|
||||||
|
val watchedUiState by remember {
|
||||||
|
WatchedRepository.ensureLoaded()
|
||||||
|
WatchedRepository.uiState
|
||||||
|
}.collectAsStateWithLifecycle()
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
var headerHeightPx by remember { mutableIntStateOf(0) }
|
var headerHeightPx by remember { mutableIntStateOf(0) }
|
||||||
var observedOfflineState by remember { mutableStateOf(false) }
|
var observedOfflineState by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination, homeCatalogSettingsUiState.hideUnreleasedContent) {
|
LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination) {
|
||||||
CatalogRepository.load(
|
CatalogRepository.load(
|
||||||
manifestUrl = manifestUrl,
|
manifestUrl = manifestUrl,
|
||||||
type = type,
|
type = type,
|
||||||
|
|
@ -110,7 +109,7 @@ fun CatalogScreen(
|
||||||
when (networkStatusUiState.condition) {
|
when (networkStatusUiState.condition) {
|
||||||
NetworkCondition.NoInternet,
|
NetworkCondition.NoInternet,
|
||||||
NetworkCondition.ServersUnreachable,
|
NetworkCondition.ServersUnreachable,
|
||||||
-> {
|
-> {
|
||||||
observedOfflineState = true
|
observedOfflineState = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,7 +128,7 @@ fun CatalogScreen(
|
||||||
|
|
||||||
NetworkCondition.Unknown,
|
NetworkCondition.Unknown,
|
||||||
NetworkCondition.Checking,
|
NetworkCondition.Checking,
|
||||||
-> Unit
|
-> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,7 +155,7 @@ fun CatalogScreen(
|
||||||
) {
|
) {
|
||||||
if (uiState.items.isEmpty() && uiState.isLoading) {
|
if (uiState.items.isEmpty() && uiState.isLoading) {
|
||||||
items(columns * 3) {
|
items(columns * 3) {
|
||||||
CatalogSkeletonTile(cornerRadiusDp = posterCardStyle.cornerRadiusDp)
|
CatalogSkeletonTile()
|
||||||
}
|
}
|
||||||
} else if (uiState.items.isEmpty()) {
|
} else if (uiState.items.isEmpty()) {
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
|
@ -182,11 +181,14 @@ fun CatalogScreen(
|
||||||
key = { item -> item.lazyKey },
|
key = { item -> item.lazyKey },
|
||||||
) { keyedItem ->
|
) { keyedItem ->
|
||||||
val item = keyedItem.value
|
val item = keyedItem.value
|
||||||
CatalogPosterTile(
|
HomePosterCard(
|
||||||
item = item,
|
item = item,
|
||||||
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
|
isWatched = WatchingState.isPosterWatched(
|
||||||
hideLabels = posterCardStyle.hideLabelsEnabled,
|
watchedKeys = watchedUiState.watchedKeys,
|
||||||
|
item = item,
|
||||||
|
),
|
||||||
onClick = onPosterClick?.let { { it(item) } },
|
onClick = onPosterClick?.let { { it(item) } },
|
||||||
|
onLongClick = onPosterLongClick?.let { { it(item) } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (uiState.isLoading) {
|
if (uiState.isLoading) {
|
||||||
|
|
@ -252,63 +254,12 @@ private fun CatalogHeader(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CatalogPosterTile(
|
private fun CatalogSkeletonTile() {
|
||||||
item: MetaPreview,
|
|
||||||
cornerRadiusDp: Int,
|
|
||||||
hideLabels: Boolean,
|
|
||||||
onClick: (() -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(item.posterShape.catalogAspectRatio())
|
|
||||||
.clip(RoundedCornerShape(cornerRadiusDp.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
|
||||||
.posterCardClickable(onClick = onClick, onLongClick = null),
|
|
||||||
) {
|
|
||||||
if (item.poster != null) {
|
|
||||||
AsyncImage(
|
|
||||||
model = item.poster,
|
|
||||||
contentDescription = item.name,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hideLabels) {
|
|
||||||
Text(
|
|
||||||
text = item.name,
|
|
||||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
val detail = item.releaseInfo?.let { formatReleaseDateForDisplay(it) }
|
|
||||||
if (detail != null) {
|
|
||||||
Text(
|
|
||||||
text = detail,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CatalogSkeletonTile(cornerRadiusDp: Int) {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(0.68f)
|
.aspectRatio(0.68f)
|
||||||
.clip(RoundedCornerShape(cornerRadiusDp.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(MaterialTheme.colorScheme.surface),
|
.background(MaterialTheme.colorScheme.surface),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -363,13 +314,6 @@ private fun CatalogLoadingFooter() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun PosterShape.catalogAspectRatio(): Float =
|
|
||||||
when (this) {
|
|
||||||
PosterShape.Poster -> 0.68f
|
|
||||||
PosterShape.Square -> 1f
|
|
||||||
PosterShape.Landscape -> 1.2f
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun catalogGridColumnsForWidth(screenWidth: Dp): Int =
|
private fun catalogGridColumnsForWidth(screenWidth: Dp): Int =
|
||||||
when {
|
when {
|
||||||
screenWidth >= 1400.dp -> 7
|
screenWidth >= 1400.dp -> 7
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue