Add skeleton loading rows to home screens on both platforms

While the home catalog is loading and no rows are available yet, both the mobile
and TV home screens now show 4 animated skeleton shelf placeholders instead of a
blank screen. The pulse animation matches the explore screen skeleton.
This commit is contained in:
KhooLy 2026-07-03 12:41:07 +03:00
parent 462de853ed
commit 88d7e22ec2
4 changed files with 103 additions and 0 deletions

View file

@ -6,6 +6,11 @@ import com.fluxa.app.common.AppStrings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
@ -23,6 +28,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
@ -386,6 +392,43 @@ private fun ShelfExpandedDescription(expandedMeta: Meta?, expandedOffsetX: Dp, r
}
}
@Composable
internal fun TvHomeShelfSkeleton(titleStartPadding: Dp) {
val transition = rememberInfiniteTransition(label = "shelf-skeleton")
val alpha by transition.animateFloat(
initialValue = 0.05f,
targetValue = 0.13f,
animationSpec = infiniteRepeatable(
animation = tween(FluxaDimensions.AnimDuration.ambientColor, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "shelf-skeleton-alpha"
)
Column(modifier = Modifier.padding(bottom = 28.dp)) {
Box(
modifier = Modifier
.padding(start = titleStartPadding, bottom = 12.dp)
.width(140.dp)
.height(18.dp)
.clip(androidx.compose.foundation.shape.RoundedCornerShape(5.dp))
.background(Color.White.copy(alpha = alpha))
)
LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(horizontal = titleStartPadding)
) {
items(5) {
Box(
modifier = Modifier
.size(200.dp, 120.dp)
.clip(androidx.compose.foundation.shape.RoundedCornerShape(10.dp))
.background(Color.White.copy(alpha = alpha))
)
}
}
}
}
internal fun Meta.remainingWatchLabel(lang: String? = null): String? {
val total = duration ?: return null
val watched = timeOffset ?: return null

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
@ -95,6 +96,11 @@ fun HomeScreen(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 120.dp)
) {
if (catalogState.isLoading && homeRowSpecs.isEmpty()) {
items(4, key = { "skeleton-$it" }, contentType = { "skeleton" }) {
MobileHomeShelfSkeleton(horizontalPadding)
}
}
if (catalogState.showHeroSection) {
item(key = "home-hero", contentType = "hero") {
MobileHomeHeroItem(

View file

@ -4,9 +4,17 @@
package com.fluxa.app.ui.catalog
import com.fluxa.app.common.AppStrings
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@ -14,9 +22,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.getValue
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
@ -469,3 +480,40 @@ private fun HomeRowHeader(
}
}
}
@Composable
internal fun MobileHomeShelfSkeleton(horizontalPadding: androidx.compose.ui.unit.Dp) {
val transition = rememberInfiniteTransition(label = "shelf-skeleton")
val alpha by transition.animateFloat(
initialValue = 0.05f,
targetValue = 0.13f,
animationSpec = infiniteRepeatable(
animation = tween(FluxaDimensions.AnimDuration.ambientColor, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "shelf-skeleton-alpha"
)
Column(modifier = Modifier.padding(bottom = 24.dp)) {
Box(
modifier = Modifier
.padding(start = horizontalPadding, bottom = 10.dp)
.width(120.dp)
.height(16.dp)
.clip(RoundedCornerShape(5.dp))
.background(Color.White.copy(alpha = alpha))
)
LazyRow(
horizontalArrangement = Arrangement.spacedBy(10.dp),
contentPadding = PaddingValues(horizontal = horizontalPadding)
) {
items(5) {
Box(
modifier = Modifier
.size(130.dp, 195.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color.White.copy(alpha = alpha))
)
}
}
}
}

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.LazyListPrefetchStrategy
import androidx.compose.foundation.lazy.rememberLazyListState
@ -228,6 +229,11 @@ fun HomeScreen(
}
}
if (catalogState.isLoading && rowSpecs.isEmpty()) {
items(4, key = { "skeleton-$it" }, contentType = { "skeleton" }) {
TvHomeShelfSkeleton(titleStartPadding = railGutter)
}
}
itemsIndexed(
rowSpecs,
key = { _, row -> row.categoryId },