首页骨架屏:替换占位提示为EmptyMessage,模块加载骨架屏及状态切换动画

This commit is contained in:
HapeLee committed 2026-05-27 19:30:47 +08:00
1 parent 134118011a
commit d3d107a4f8
2 files changed
+601 -171

No files matched your search

@@ -1,9 +1,15 @@
package io.legado.app.ui.main.homepage package io.legado.app.ui.main.homepage
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -63,6 +69,7 @@ import io.legado.app.ui.main.homepage.modules.ButtonGroupModule
import io.legado.app.ui.main.homepage.modules.CardModule import io.legado.app.ui.main.homepage.modules.CardModule
import io.legado.app.ui.main.homepage.modules.GridModule import io.legado.app.ui.main.homepage.modules.GridModule
import io.legado.app.ui.main.homepage.modules.GridRankingModule import io.legado.app.ui.main.homepage.modules.GridRankingModule
import io.legado.app.ui.main.homepage.modules.HomepageModuleSkeleton
import io.legado.app.ui.main.homepage.modules.RankingModule import io.legado.app.ui.main.homepage.modules.RankingModule
import io.legado.app.ui.main.homepage.modules.WaterfallItem import io.legado.app.ui.main.homepage.modules.WaterfallItem
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
@@ -75,8 +82,8 @@ import io.legado.app.ui.widget.components.book.SearchBookPreviewSheet
import io.legado.app.ui.widget.components.button.series.SmallTonalButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
import io.legado.app.ui.widget.components.tabRow.AppTabRow import io.legado.app.ui.widget.components.tabRow.AppTabRow
import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
@@ -198,9 +205,10 @@ fun HomepageScreen(
.padding(paddingValues), .padding(paddingValues),
) { ) {
if (selectedSets.isEmpty()) { if (selectedSets.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { EmptyMessage(
AppText(stringResource(R.string.homepage_no_source_sets_selected)) messageResId = R.string.homepage_no_source_sets_selected,
} modifier = Modifier.fillMaxSize(),
)
} else { } else {
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
@@ -394,100 +402,103 @@ private fun ModuleList(
// 2. 内容正文 // 2. 内容正文
when (val state = moduleUi.state) { when (val state = moduleUi.state) {
is ModuleLoadState.Loading -> { is ModuleLoadState.Loading,
item(
key = "loading_${moduleUi.globalId}",
span = StaggeredGridItemSpan.FullLine
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(120.dp),
contentAlignment = Alignment.Center,
) {
AppCircularProgressIndicator()
}
}
}
is ModuleLoadState.Error -> { is ModuleLoadState.Error -> {
item( item(
key = "error_${moduleUi.globalId}", key = "status_${moduleUi.globalId}",
span = StaggeredGridItemSpan.FullLine span = StaggeredGridItemSpan.FullLine
) { ) {
Column( AnimatedContent(
modifier = Modifier targetState = state,
.fillMaxWidth(), transitionSpec = {
horizontalAlignment = Alignment.CenterHorizontally fadeIn(tween(300)) togetherWith fadeOut(tween(300))
) { },
GlassCard( contentKey = { it::class },
onClick = { onErrorClick(state.message) }, ) { targetState ->
containerColor = LegadoTheme.colorScheme.errorContainer.copy( when (targetState) {
alpha = 0.6f is ModuleLoadState.Loading -> {
), HomepageModuleSkeleton(
) { type = moduleUi.type,
Column( modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 16.dp
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
AppIcon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
tint = LegadoTheme.colorScheme.error
)
AppText(
text = state.message,
color = LegadoTheme.colorScheme.error,
style = LegadoTheme.typography.bodySmall,
modifier = Modifier.weight(1f),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
HorizontalDivider(
color = LegadoTheme.colorScheme.error.copy(alpha = 0.3f)
) )
}
Box( is ModuleLoadState.Error -> {
modifier = Modifier Column(
.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
.clickable { horizontalAlignment = Alignment.CenterHorizontally
viewModel.retryModule(moduleUi.globalId)
}
.padding(vertical = 10.dp),
contentAlignment = Alignment.Center
) { ) {
Row( GlassCard(
horizontalArrangement = Arrangement.spacedBy(8.dp), onClick = { onErrorClick(targetState.message) },
verticalAlignment = Alignment.CenterVertically containerColor = LegadoTheme.colorScheme.errorContainer.copy(
alpha = 0.6f
),
) { ) {
AppIcon( Column(
imageVector = Icons.Default.Refresh, modifier = Modifier.fillMaxWidth()
contentDescription = null, ) {
tint = LegadoTheme.colorScheme.error Row(
) modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 16.dp
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
AppIcon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
tint = LegadoTheme.colorScheme.error
)
AppText( AppText(
text = "重试", text = targetState.message,
color = LegadoTheme.colorScheme.error, color = LegadoTheme.colorScheme.error,
style = LegadoTheme.typography.labelMedium style = LegadoTheme.typography.bodySmall,
) modifier = Modifier.weight(1f),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
HorizontalDivider(
color = LegadoTheme.colorScheme.error.copy(alpha = 0.3f)
)
Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
viewModel.retryModule(moduleUi.globalId)
}
.padding(vertical = 10.dp),
contentAlignment = Alignment.Center
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
AppIcon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
tint = LegadoTheme.colorScheme.error
)
AppText(
text = "重试",
color = LegadoTheme.colorScheme.error,
style = LegadoTheme.typography.labelMedium
)
}
}
}
} }
} }
} }
else -> {}
} }
} }
} }
@@ -520,19 +531,21 @@ private fun ModuleList(
item.book.bookUrl, item.book.bookUrl,
"home:${moduleUi.globalId}:waterfall:$index" "home:${moduleUi.globalId}:waterfall:$index"
) )
WaterfallItem( AnimatedVisibility(visible = true, enter = fadeIn()) {
item = item, WaterfallItem(
onClick = { item = item,
viewModel.onBookClick( onClick = {
item.book, viewModel.onBookClick(
sharedCoverKey item.book,
) sharedCoverKey
}, )
onLongClick = onBookLongClick, },
sharedTransitionScope = sharedTransitionScope, onLongClick = onBookLongClick,
animatedVisibilityScope = animatedVisibilityScope, sharedTransitionScope = sharedTransitionScope,
sharedCoverKey = sharedCoverKey, animatedVisibilityScope = animatedVisibilityScope,
) sharedCoverKey = sharedCoverKey,
)
}
} }
item( item(
@@ -556,20 +569,22 @@ private fun ModuleList(
item.book.bookUrl, item.book.bookUrl,
"home:${moduleUi.globalId}:infinite:$index" "home:${moduleUi.globalId}:infinite:$index"
) )
SearchBookGridItem( AnimatedVisibility(visible = true, enter = fadeIn()) {
book = item.book, SearchBookGridItem(
shelfState = item.shelfState, book = item.book,
onClick = { shelfState = item.shelfState,
viewModel.onBookClick( onClick = {
item.book, viewModel.onBookClick(
sharedCoverKey item.book,
) sharedCoverKey
}, )
onLongClick = onBookLongClick, },
sharedTransitionScope = sharedTransitionScope, onLongClick = onBookLongClick,
animatedVisibilityScope = animatedVisibilityScope, sharedTransitionScope = sharedTransitionScope,
sharedCoverKey = sharedCoverKey animatedVisibilityScope = animatedVisibilityScope,
) sharedCoverKey = sharedCoverKey
)
}
} }
item( item(
@@ -592,19 +607,21 @@ private fun ModuleList(
key = "content_${moduleUi.globalId}", key = "content_${moduleUi.globalId}",
span = StaggeredGridItemSpan.FullLine span = StaggeredGridItemSpan.FullLine
) { ) {
GridModule( AnimatedVisibility(visible = true, enter = fadeIn()) {
books = state.books, GridModule(
onClick = { book, sharedCoverKey -> books = state.books,
viewModel.onBookClick(book, sharedCoverKey) onClick = { book, sharedCoverKey ->
}, viewModel.onBookClick(book, sharedCoverKey)
onLongClick = onBookLongClick, },
modifier = Modifier.fillMaxWidth(), onLongClick = onBookLongClick,
columns = columns, modifier = Modifier.fillMaxWidth(),
maxRows = rows, columns = columns,
sharedTransitionScope = sharedTransitionScope, maxRows = rows,
animatedVisibilityScope = animatedVisibilityScope, sharedTransitionScope = sharedTransitionScope,
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid", animatedVisibilityScope = animatedVisibilityScope,
) sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid",
)
}
} }
} }
@@ -615,17 +632,19 @@ private fun ModuleList(
key = "content_${moduleUi.globalId}", key = "content_${moduleUi.globalId}",
span = StaggeredGridItemSpan.FullLine span = StaggeredGridItemSpan.FullLine
) { ) {
BannerModule( AnimatedVisibility(visible = true, enter = fadeIn()) {
books = state.books, BannerModule(
onClick = { book, sharedCoverKey -> books = state.books,
viewModel.onBookClick(book, sharedCoverKey) onClick = { book, sharedCoverKey ->
}, viewModel.onBookClick(book, sharedCoverKey)
onLongClick = onBookLongClick, },
modifier = Modifier.fillMaxWidth(), onLongClick = onBookLongClick,
sharedTransitionScope = sharedTransitionScope, modifier = Modifier.fillMaxWidth(),
animatedVisibilityScope = animatedVisibilityScope, sharedTransitionScope = sharedTransitionScope,
sharedCoverKeySourceId = "home:${moduleUi.globalId}:banner", animatedVisibilityScope = animatedVisibilityScope,
) sharedCoverKeySourceId = "home:${moduleUi.globalId}:banner",
)
}
} }
} }
@@ -634,17 +653,19 @@ private fun ModuleList(
key = "content_${moduleUi.globalId}", key = "content_${moduleUi.globalId}",
span = StaggeredGridItemSpan.FullLine span = StaggeredGridItemSpan.FullLine
) { ) {
RankingModule( AnimatedVisibility(visible = true, enter = fadeIn()) {
books = state.books, RankingModule(
onClick = { book, sharedCoverKey -> books = state.books,
viewModel.onBookClick(book, sharedCoverKey) onClick = { book, sharedCoverKey ->
}, viewModel.onBookClick(book, sharedCoverKey)
onLongClick = onBookLongClick, },
modifier = Modifier.fillMaxWidth(), onLongClick = onBookLongClick,
sharedTransitionScope = sharedTransitionScope, modifier = Modifier.fillMaxWidth(),
animatedVisibilityScope = animatedVisibilityScope, sharedTransitionScope = sharedTransitionScope,
sharedCoverKeySourceId = "home:${moduleUi.globalId}:ranking", animatedVisibilityScope = animatedVisibilityScope,
) sharedCoverKeySourceId = "home:${moduleUi.globalId}:ranking",
)
}
} }
} }
@@ -653,18 +674,20 @@ private fun ModuleList(
key = "content_${moduleUi.globalId}", key = "content_${moduleUi.globalId}",
span = StaggeredGridItemSpan.FullLine span = StaggeredGridItemSpan.FullLine
) { ) {
GridRankingModule( AnimatedVisibility(visible = true, enter = fadeIn()) {
books = state.books, GridRankingModule(
onClick = { book, sharedCoverKey -> books = state.books,
viewModel.onBookClick(book, sharedCoverKey) onClick = { book, sharedCoverKey ->
}, viewModel.onBookClick(book, sharedCoverKey)
onLongClick = onBookLongClick, },
modifier = Modifier.fillMaxWidth(), onLongClick = onBookLongClick,
rows = config["layout_rows"]?.toIntOrNull() ?: 4, modifier = Modifier.fillMaxWidth(),
sharedTransitionScope = sharedTransitionScope, rows = config["layout_rows"]?.toIntOrNull() ?: 4,
animatedVisibilityScope = animatedVisibilityScope, sharedTransitionScope = sharedTransitionScope,
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid-ranking", animatedVisibilityScope = animatedVisibilityScope,
) sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid-ranking",
)
}
} }
} }
@@ -673,17 +696,19 @@ private fun ModuleList(
key = "content_${moduleUi.globalId}", key = "content_${moduleUi.globalId}",
span = StaggeredGridItemSpan.FullLine span = StaggeredGridItemSpan.FullLine
) { ) {
CardModule( AnimatedVisibility(visible = true, enter = fadeIn()) {
books = state.books, CardModule(
onClick = { book, sharedCoverKey -> books = state.books,
viewModel.onBookClick(book, sharedCoverKey) onClick = { book, sharedCoverKey ->
}, viewModel.onBookClick(book, sharedCoverKey)
onLongClick = onBookLongClick, },
modifier = Modifier.fillMaxWidth(), onLongClick = onBookLongClick,
sharedTransitionScope = sharedTransitionScope, modifier = Modifier.fillMaxWidth(),
animatedVisibilityScope = animatedVisibilityScope, sharedTransitionScope = sharedTransitionScope,
sharedCoverKeySourceId = "home:${moduleUi.globalId}:card", animatedVisibilityScope = animatedVisibilityScope,
) sharedCoverKeySourceId = "home:${moduleUi.globalId}:card",
)
}
} }
} }
@@ -0,0 +1,405 @@
package io.legado.app.ui.main.homepage.modules
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
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.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
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.lazy.LazyRow
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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
import io.legado.app.domain.model.HomepageModuleType
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.card.GlassCard
@Composable
private fun rememberShimmerBrush(): Brush {
val infiniteTransition = rememberInfiniteTransition(label = "shimmer")
val offset by infiniteTransition.animateFloat(
initialValue = -1f,
targetValue = 2f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1200, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "shimmerOffset",
)
val colorScheme = LegadoTheme.colorScheme
val colors = remember(colorScheme) {
listOf(
colorScheme.surfaceContainerHighest,
colorScheme.surfaceContainerHigh,
colorScheme.surfaceContainerHighest,
)
}
return Brush.linearGradient(
colors = colors,
start = Offset(offset * 300f, 0f),
end = Offset(offset * 300f + 300f, 0f),
)
}
@Composable
private fun SkeletonBox(
modifier: Modifier = Modifier,
cornerRadius: Dp = 8.dp,
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(cornerRadius))
.background(rememberShimmerBrush())
)
}
// ── Waterfall skeleton ──
@Composable
fun WaterfallSkeletonItem(modifier: Modifier = Modifier) {
GlassCard(
modifier = modifier,
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
) {
Column {
SkeletonBox(
modifier = Modifier
.fillMaxWidth()
.height(180.dp),
cornerRadius = 16.dp,
)
Column(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
) {
SkeletonBox(
modifier = Modifier
.fillMaxWidth(0.8f)
.height(14.dp),
)
Spacer(modifier = Modifier.height(6.dp))
SkeletonBox(
modifier = Modifier
.fillMaxWidth(0.5f)
.height(10.dp),
)
Spacer(modifier = Modifier.height(8.dp))
SkeletonBox(
modifier = Modifier
.fillMaxWidth(0.6f)
.height(10.dp),
)
Spacer(modifier = Modifier.height(4.dp))
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
SkeletonBox(
modifier = Modifier
.width(40.dp)
.height(18.dp),
cornerRadius = 4.dp,
)
SkeletonBox(
modifier = Modifier
.width(52.dp)
.height(18.dp),
cornerRadius = 4.dp,
)
}
}
}
}
}
// ── Grid skeleton ──
@Composable
fun GridSkeletonItem(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
SkeletonBox(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(5f / 7f),
cornerRadius = 4.dp,
)
Spacer(modifier = Modifier.height(4.dp))
SkeletonBox(
modifier = Modifier
.fillMaxWidth(0.8f)
.height(12.dp),
)
}
}
@Composable
fun GridSkeletonModule(
modifier: Modifier = Modifier,
columns: Int = 3,
rows: Int = 2,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
repeat(rows) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
repeat(columns) {
GridSkeletonItem(modifier = Modifier.weight(1f))
}
}
}
}
}
// ── Banner skeleton ──
@Composable
fun BannerSkeletonModule(
modifier: Modifier = Modifier,
itemCount: Int = 6,
) {
LazyRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(itemCount) {
SkeletonBox(
modifier = Modifier
.width(96.dp)
.aspectRatio(5f / 7f),
cornerRadius = 12.dp,
)
}
}
}
// ── Card skeleton ──
@Composable
fun CardSkeletonItem(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.width(120.dp)
.clip(RoundedCornerShape(16.dp))
.background(LegadoTheme.colorScheme.surfaceContainerLow),
) {
SkeletonBox(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(5f / 7f),
cornerRadius = 0.dp,
)
Column(modifier = Modifier.padding(8.dp)) {
SkeletonBox(
modifier = Modifier
.fillMaxWidth()
.height(14.dp),
)
Spacer(modifier = Modifier.height(4.dp))
SkeletonBox(
modifier = Modifier
.fillMaxWidth(0.7f)
.height(14.dp),
)
}
}
}
@Composable
fun CardSkeletonModule(
modifier: Modifier = Modifier,
itemCount: Int = 5,
) {
LazyRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(itemCount) {
CardSkeletonItem()
}
}
}
// ── Ranking skeleton ──
@Composable
fun RankingSkeletonModule(
modifier: Modifier = Modifier,
rows: Int = 5,
) {
GlassCard(
modifier = modifier,
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
cornerRadius = 16.dp,
) {
Column(modifier = Modifier.padding(top = 12.dp)) {
repeat(rows) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp, vertical = 4.dp),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
) {
SkeletonBox(
modifier = Modifier
.size(18.dp),
)
Spacer(modifier = Modifier.width(14.dp))
SkeletonBox(
modifier = Modifier
.width(52.dp)
.aspectRatio(5f / 7f),
)
Spacer(modifier = Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
SkeletonBox(
modifier = Modifier
.fillMaxWidth(0.7f)
.height(14.dp),
)
Spacer(modifier = Modifier.height(4.dp))
SkeletonBox(
modifier = Modifier
.fillMaxWidth(0.4f)
.height(10.dp),
)
}
}
}
}
}
}
// ── GridRanking skeleton ──
@Composable
fun GridRankingSkeletonModule(
modifier: Modifier = Modifier,
rows: Int = 4,
) {
GlassCard(
modifier = modifier,
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
cornerRadius = 20.dp,
) {
Column(modifier = Modifier.padding(12.dp)) {
repeat(rows) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
) {
SkeletonBox(
modifier = Modifier
.width(48.dp)
.aspectRatio(5f / 7f),
)
Spacer(modifier = Modifier.width(8.dp))
SkeletonBox(
modifier = Modifier
.size(18.dp),
)
Spacer(modifier = Modifier.width(4.dp))
Column(modifier = Modifier.weight(1f)) {
SkeletonBox(
modifier = Modifier
.fillMaxWidth(0.8f)
.height(14.dp),
)
Spacer(modifier = Modifier.height(4.dp))
SkeletonBox(
modifier = Modifier
.fillMaxWidth(0.5f)
.height(10.dp),
)
}
}
}
}
}
}
// ── Dispatcher ──
@Composable
fun HomepageModuleSkeleton(
type: HomepageModuleType,
modifier: Modifier = Modifier,
columns: Int = 3,
rows: Int = 2,
) {
when (type) {
HomepageModuleType.Waterfall -> {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
repeat(2) {
WaterfallSkeletonItem(modifier = Modifier.fillMaxWidth())
}
}
}
HomepageModuleType.InfiniteGrid -> {
GridSkeletonModule(
modifier = modifier,
columns = columns,
rows = rows,
)
}
HomepageModuleType.Grid -> {
GridSkeletonModule(
modifier = modifier,
columns = columns,
rows = rows,
)
}
HomepageModuleType.Banner -> {
BannerSkeletonModule(modifier = modifier)
}
HomepageModuleType.Card -> {
CardSkeletonModule(modifier = modifier)
}
HomepageModuleType.Ranking -> {
RankingSkeletonModule(modifier = modifier)
}
HomepageModuleType.GridRanking -> {
GridRankingSkeletonModule(modifier = modifier)
}
HomepageModuleType.ButtonGroup -> {
// ButtonGroup loads instantly (no network), no skeleton needed
}
HomepageModuleType.Unknown -> {}
}
}