首页骨架屏:替换占位提示为EmptyMessage,模块加载骨架屏及状态切换动画
This commit is contained in:
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 -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user