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

This commit is contained in:
HapeLee
2026-05-27 19:30:47 +08:00
parent 134118011a
commit d3d107a4f8
2 changed files with 601 additions and 171 deletions
@@ -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 -> {}
}
}