This commit is contained in:
AdityasahuX07 2026-05-14 11:34:15 +00:00 committed by GitHub
commit 1bcb37e4cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 95 additions and 245 deletions

View file

@ -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,14 +1472,27 @@ private fun MainAppContent(
return@composable return@composable
} }
fun openSelectedStream( StreamsScreen(
stream: com.nuvio.app.features.streams.StreamItem, type = launch.type,
resolvedResumePositionMs: Long?, videoId = effectiveVideoId,
resolvedResumeProgressFraction: Float?, parentMetaId = launch.parentMetaId ?: effectiveVideoId,
forceExternal: Boolean, parentMetaType = launch.parentMetaType ?: launch.type,
forceInternal: Boolean, title = launch.title,
) { logo = launch.logo,
val sourceUrl = stream.directPlaybackUrl ?: return 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 ->
val sourceUrl = stream.directPlaybackUrl
if (sourceUrl != null) {
// Persist for Reuse Last Link
if (playerSettings.streamReuseLastLinkEnabled) { if (playerSettings.streamReuseLastLinkEnabled) {
val cacheKey = StreamLinkCacheRepository.contentKey( val cacheKey = StreamLinkCacheRepository.contentKey(
type = launch.type, type = launch.type,
@ -1565,7 +1514,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),
@ -1590,54 +1540,12 @@ private fun MainAppContent(
initialPositionMs = resolvedResumePositionMs ?: 0L, initialPositionMs = resolvedResumePositionMs ?: 0L,
initialProgressFraction = resolvedResumeProgressFraction, initialProgressFraction = resolvedResumeProgressFraction,
) )
)
if (!forceInternal && (forceExternal || playerSettings.externalPlayerEnabled)) {
openExternalPlayback(playerLaunch)
StreamsRepository.cancelLoading()
return
}
val launchId = PlayerLaunchStore.put(playerLaunch)
StreamsRepository.cancelLoading() StreamsRepository.cancelLoading()
navController.navigate( navController.navigate(
PlayerRoute(launchId = launchId) PlayerRoute(launchId = launchId)
) )
} }
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 = { 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,
) )
} }

View file

@ -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,
@ -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