diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt index ac66e4182..9f97430b0 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt @@ -18,6 +18,7 @@ import com.nuvio.app.features.home.components.HomeCatalogRowSection import com.nuvio.app.features.home.components.HomeContinueWatchingSection import com.nuvio.app.features.home.components.HomeEmptyStateCard import com.nuvio.app.features.home.components.HomeHeroSection +import com.nuvio.app.features.home.components.HomeSkeletonHero import com.nuvio.app.features.home.components.HomeSkeletonRow import com.nuvio.app.features.watched.WatchedRepository import com.nuvio.app.features.watchprogress.CurrentDateProvider @@ -45,6 +46,7 @@ fun HomeScreen( val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle() val homeUiState by HomeRepository.uiState.collectAsStateWithLifecycle() + val homeSettingsUiState by HomeCatalogSettingsRepository.uiState.collectAsStateWithLifecycle() val continueWatchingPreferences by ContinueWatchingPreferencesRepository.uiState.collectAsStateWithLifecycle() val watchedUiState by WatchedRepository.uiState.collectAsStateWithLifecycle() val watchProgressUiState by WatchProgressRepository.uiState.collectAsStateWithLifecycle() @@ -125,11 +127,35 @@ fun HomeScreen( nextUpItemsBySeries = resolvedItems } + val hasActiveAddons = addonsUiState.addons.any { it.manifest != null } + val homeDataResolved = !homeUiState.isLoading && + (homeUiState.sections.isNotEmpty() || homeUiState.errorMessage != null) + val showHeroSkeleton = homeSettingsUiState.heroEnabled && + hasActiveAddons && + homeUiState.heroItems.isEmpty() && + !homeDataResolved + NuvioScreen( modifier = modifier, horizontalPadding = 0.dp, - topPadding = if (homeUiState.heroItems.isNotEmpty()) 0.dp else null, + topPadding = if (homeUiState.heroItems.isNotEmpty() || showHeroSkeleton) 0.dp else null, ) { + if (showHeroSkeleton) { + item { + HomeSkeletonHero( + modifier = Modifier.padding(bottom = 12.dp), + ) + } + } else if (homeUiState.heroItems.isNotEmpty()) { + item { + HomeHeroSection( + items = homeUiState.heroItems, + modifier = Modifier.padding(bottom = 0.dp), + onItemClick = onPosterClick, + ) + } + } + when { addonsUiState.addons.none { it.manifest != null } -> { if (continueWatchingPreferences.isVisible && continueWatchingItems.isNotEmpty()) { @@ -182,15 +208,6 @@ fun HomeScreen( } else -> { - if (homeUiState.heroItems.isNotEmpty()) { - item { - HomeHeroSection( - items = homeUiState.heroItems, - modifier = Modifier.padding(bottom = 0.dp), - onItemClick = onPosterClick, - ) - } - } if (continueWatchingPreferences.isVisible && continueWatchingItems.isNotEmpty()) { item { HomeContinueWatchingSection( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeSkeletonLoading.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeSkeletonLoading.kt index 47e777edc..49403c028 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeSkeletonLoading.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeSkeletonLoading.kt @@ -9,12 +9,17 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -23,10 +28,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @Composable -fun HomeSkeletonRow(modifier: Modifier = Modifier) { +private fun rememberHomeSkeletonBrush(): Brush { val shimmerColors = listOf( MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), @@ -46,6 +52,94 @@ fun HomeSkeletonRow(modifier: Modifier = Modifier) { start = Offset(translateAnim - 200f, 0f), end = Offset(translateAnim, 0f), ) + return brush +} + +@Composable +fun HomeSkeletonHero(modifier: Modifier = Modifier) { + val brush = rememberHomeSkeletonBrush() + + BoxWithConstraints( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(bottomStart = 28.dp, bottomEnd = 28.dp)), + ) { + val heroHeight = (maxWidth.value * 1.22f).dp.coerceIn(440.dp, 800.dp) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(heroHeight) + .background(brush), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.background.copy(alpha = 0.04f), + MaterialTheme.colorScheme.background.copy(alpha = 0.18f), + MaterialTheme.colorScheme.background.copy(alpha = 0.42f), + MaterialTheme.colorScheme.background.copy(alpha = 0.92f), + ), + ), + ), + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 24.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + SkeletonBlock( + brush = brush, + width = 220.dp, + height = 64.dp, + cornerRadius = 20.dp, + ) + Spacer(modifier = Modifier.height(14.dp)) + Row { + SkeletonBlock(brush = brush, width = 72.dp, height = 14.dp, cornerRadius = 999.dp) + Spacer(modifier = Modifier.width(8.dp)) + SkeletonDot(brush = brush) + Spacer(modifier = Modifier.width(8.dp)) + SkeletonBlock(brush = brush, width = 88.dp, height = 14.dp, cornerRadius = 999.dp) + Spacer(modifier = Modifier.width(8.dp)) + SkeletonDot(brush = brush) + Spacer(modifier = Modifier.width(8.dp)) + SkeletonBlock(brush = brush, width = 54.dp, height = 14.dp, cornerRadius = 999.dp) + } + Spacer(modifier = Modifier.height(18.dp)) + SkeletonBlock( + brush = brush, + width = 148.dp, + height = 48.dp, + cornerRadius = 40.dp, + ) + Spacer(modifier = Modifier.height(16.dp)) + Row { + repeat(4) { index -> + if (index > 0) { + Spacer(modifier = Modifier.width(8.dp)) + } + SkeletonBlock( + brush = brush, + width = if (index == 0) 32.dp else 8.dp, + height = 8.dp, + cornerRadius = 999.dp, + ) + } + } + } + } + } +} + +@Composable +fun HomeSkeletonRow(modifier: Modifier = Modifier) { + val brush = rememberHomeSkeletonBrush() Column( modifier = modifier.fillMaxWidth(), @@ -84,3 +178,29 @@ fun HomeSkeletonRow(modifier: Modifier = Modifier) { } } } + +@Composable +private fun SkeletonBlock( + brush: Brush, + width: Dp, + height: Dp, + cornerRadius: Dp, +) { + Box( + modifier = Modifier + .width(width) + .height(height) + .clip(RoundedCornerShape(cornerRadius)) + .background(brush), + ) +} + +@Composable +private fun SkeletonDot(brush: Brush) { + Box( + modifier = Modifier + .size(4.dp) + .clip(CircleShape) + .background(brush), + ) +}