feat: Add HomeSkeletonHero component for loading state representation in HomeScreen

This commit is contained in:
tapframe 2026-03-28 21:38:46 +05:30
parent 3929ad32a7
commit 71d5ccbd8b
2 changed files with 148 additions and 11 deletions

View file

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

View file

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