首页骨架屏:替换占位提示为EmptyMessage,模块加载骨架屏及状态切换动画
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
package io.legado.app.ui.main.homepage
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
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.layout.Arrangement
|
||||
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.GridModule
|
||||
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.WaterfallItem
|
||||
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.card.GlassCard
|
||||
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.EmptyMessage
|
||||
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.GlassTopAppBarDefaults
|
||||
@@ -198,9 +205,10 @@ fun HomepageScreen(
|
||||
.padding(paddingValues),
|
||||
) {
|
||||
if (selectedSets.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
AppText(stringResource(R.string.homepage_no_source_sets_selected))
|
||||
}
|
||||
EmptyMessage(
|
||||
messageResId = R.string.homepage_no_source_sets_selected,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
} else {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
@@ -394,100 +402,103 @@ private fun ModuleList(
|
||||
|
||||
// 2. 内容正文
|
||||
when (val state = moduleUi.state) {
|
||||
is ModuleLoadState.Loading -> {
|
||||
item(
|
||||
key = "loading_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
AppCircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ModuleLoadState.Loading,
|
||||
is ModuleLoadState.Error -> {
|
||||
item(
|
||||
key = "error_${moduleUi.globalId}",
|
||||
key = "status_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
GlassCard(
|
||||
onClick = { onErrorClick(state.message) },
|
||||
containerColor = LegadoTheme.colorScheme.errorContainer.copy(
|
||||
alpha = 0.6f
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
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)
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
transitionSpec = {
|
||||
fadeIn(tween(300)) togetherWith fadeOut(tween(300))
|
||||
},
|
||||
contentKey = { it::class },
|
||||
) { targetState ->
|
||||
when (targetState) {
|
||||
is ModuleLoadState.Loading -> {
|
||||
HomepageModuleSkeleton(
|
||||
type = moduleUi.type,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
viewModel.retryModule(moduleUi.globalId)
|
||||
}
|
||||
.padding(vertical = 10.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
is ModuleLoadState.Error -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
GlassCard(
|
||||
onClick = { onErrorClick(targetState.message) },
|
||||
containerColor = LegadoTheme.colorScheme.errorContainer.copy(
|
||||
alpha = 0.6f
|
||||
),
|
||||
) {
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.error
|
||||
)
|
||||
Column(
|
||||
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 = "重试",
|
||||
color = LegadoTheme.colorScheme.error,
|
||||
style = LegadoTheme.typography.labelMedium
|
||||
)
|
||||
AppText(
|
||||
text = targetState.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(
|
||||
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,
|
||||
"home:${moduleUi.globalId}:waterfall:$index"
|
||||
)
|
||||
WaterfallItem(
|
||||
item = item,
|
||||
onClick = {
|
||||
viewModel.onBookClick(
|
||||
item.book,
|
||||
sharedCoverKey
|
||||
)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||
WaterfallItem(
|
||||
item = item,
|
||||
onClick = {
|
||||
viewModel.onBookClick(
|
||||
item.book,
|
||||
sharedCoverKey
|
||||
)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item(
|
||||
@@ -556,20 +569,22 @@ private fun ModuleList(
|
||||
item.book.bookUrl,
|
||||
"home:${moduleUi.globalId}:infinite:$index"
|
||||
)
|
||||
SearchBookGridItem(
|
||||
book = item.book,
|
||||
shelfState = item.shelfState,
|
||||
onClick = {
|
||||
viewModel.onBookClick(
|
||||
item.book,
|
||||
sharedCoverKey
|
||||
)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||
SearchBookGridItem(
|
||||
book = item.book,
|
||||
shelfState = item.shelfState,
|
||||
onClick = {
|
||||
viewModel.onBookClick(
|
||||
item.book,
|
||||
sharedCoverKey
|
||||
)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item(
|
||||
@@ -592,19 +607,21 @@ private fun ModuleList(
|
||||
key = "content_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
GridModule(
|
||||
books = state.books,
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
columns = columns,
|
||||
maxRows = rows,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid",
|
||||
)
|
||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||
GridModule(
|
||||
books = state.books,
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
columns = columns,
|
||||
maxRows = rows,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,17 +632,19 @@ private fun ModuleList(
|
||||
key = "content_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
BannerModule(
|
||||
books = state.books,
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:banner",
|
||||
)
|
||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||
BannerModule(
|
||||
books = state.books,
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:banner",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,17 +653,19 @@ private fun ModuleList(
|
||||
key = "content_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
RankingModule(
|
||||
books = state.books,
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:ranking",
|
||||
)
|
||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||
RankingModule(
|
||||
books = state.books,
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:ranking",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,18 +674,20 @@ private fun ModuleList(
|
||||
key = "content_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
GridRankingModule(
|
||||
books = state.books,
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
rows = config["layout_rows"]?.toIntOrNull() ?: 4,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid-ranking",
|
||||
)
|
||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||
GridRankingModule(
|
||||
books = state.books,
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
rows = config["layout_rows"]?.toIntOrNull() ?: 4,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid-ranking",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,17 +696,19 @@ private fun ModuleList(
|
||||
key = "content_${moduleUi.globalId}",
|
||||
span = StaggeredGridItemSpan.FullLine
|
||||
) {
|
||||
CardModule(
|
||||
books = state.books,
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKeySourceId = "home:${moduleUi.globalId}:card",
|
||||
)
|
||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||
CardModule(
|
||||
books = state.books,
|
||||
onClick = { book, sharedCoverKey ->
|
||||
viewModel.onBookClick(book, sharedCoverKey)
|
||||
},
|
||||
onLongClick = onBookLongClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
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 -> {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user