首页优化

This commit is contained in:
beyond committed 2026-09-07 17:13:05 +08:00
1 parent dd1c62f0f0
commit 0dfb3788e8
3 files changed
+51 -13

No files matched your search

@@ -295,7 +295,7 @@ object EmbyRepository {
limit: Int = 6,
): EmbyLibraryPage = withContext(Dispatchers.IO) {
try {
val url = "${server.fullUrl}/emby/Users/${server.userId}/Items?ParentId=$libraryId&SortBy=$sortBy&SortOrder=$sortOrder&StartIndex=${startIndex.coerceAtLeast(0)}&Limit=${limit.coerceAtLeast(1)}&IncludeItemTypes=Movie,Series&Fields=PrimaryImageAspectRatio,Overview,ProductionYear,Genres,RecursiveChildCount,ChildCount,DateCreated,RunTimeTicks,UserData&Recursive=true"
val url = "${server.fullUrl}/emby/Users/${server.userId}/Items?ParentId=$libraryId&SortBy=$sortBy&SortOrder=$sortOrder&StartIndex=${startIndex.coerceAtLeast(0)}&Limit=${limit.coerceAtLeast(1)}&IncludeItemTypes=Movie,Series&Fields=PrimaryImageAspectRatio,Overview,ProductionYear,Genres,RecursiveChildCount,ChildCount,DateCreated,RunTimeTicks,UserData,CommunityRating,OfficialRating&Recursive=true"
val request = Request.Builder()
.url(url)
.header("X-Emby-Token", server.apiKey)
@@ -490,9 +490,17 @@ private fun MediaPoster(
ImageRequest.Builder(context).data(it).crossfade(true).build()
}
}
val subtitle = listOfNotNull(
item.communityRating?.takeIf { it > 0.0 }?.let { "★ ${String.format("%.1f", it)}" },
item.productionYear?.let { "$it" },
).takeIf { it.isNotEmpty() }?.joinToString(" ")
LumaMediaCard(
model = imageRequest,
title = item.seriesName ?: item.name,
subtitle = subtitle,
subtitleColor = homeSecondaryColor(),
subtitleStarColor = LumaColors.ratingStar,
modifier = modifier.width(115.dp),
imageAspectRatio = 115f / 165f,
titleColor = homeContentColor(),
@@ -590,6 +598,7 @@ private fun LazyLibrarySection(
refreshToken: Int,
onItemClick: (EmbyItem) -> Unit = {},
onViewAll: () -> Unit = {},
onEmpty: () -> Unit = {},
) {
val cacheKey = remember(activeServer.id, activeServer.fullUrl, activeServer.userId) {
homeCacheKey(activeServer)
@@ -616,6 +625,7 @@ private fun LazyLibrarySection(
EmbyLibraryPage()
}.also { loaded ->
HomeContentCache.putLibraryPage(cacheKey, folder.id, loaded)
if (loaded.items.isEmpty()) onEmpty()
}
}
// Fetch accurate episode counts for series items (parallel)
@@ -637,7 +647,7 @@ private fun LazyLibrarySection(
}
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
SectionTitle(title = folder.name, count = page?.totalCount ?: 0, onMoreClick = onViewAll)
val list = page?.items
if (list == null) {
@@ -676,13 +686,12 @@ private fun SectionTitle(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(text = title, color = homeContentColor(), fontSize = 18.sp, fontWeight = FontWeight.Bold)
Text(text = title, color = homeContentColor(), fontSize = 15.sp, fontWeight = FontWeight.Bold)
if (count > 0) {
Box(
Modifier
.heightIn(min = 48.dp)
.clickable(onClick = { onMoreClick?.invoke() })
.padding(horizontal = 8.dp),
.padding(horizontal = 8.dp, vertical = 4.dp),
contentAlignment = Alignment.Center,
) {
Text(
@@ -698,7 +707,7 @@ private fun SectionTitle(
@Composable
private fun MyMediaGrid(libraries: List<EmbyMediaFolder>, serverUrl: String, onSelect: (EmbyMediaFolder) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
SectionTitle(title = stringResource(Res.string.section_library))
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
items(libraries, key = { it.id }) { folder ->
@@ -898,6 +907,7 @@ fun HomeScreen(
var reloadToken by remember(homeKey) { mutableIntStateOf(0) }
var showResourceStats by remember(homeKey) { mutableStateOf(false) }
var serverMenuVisible by remember(homeKey) { mutableStateOf(false) }
val emptyLibraries = remember(homeKey) { mutableStateMapOf<String, Boolean>() }
var serverMenuMounted by remember(homeKey) { mutableStateOf(false) }
var serverAnchorBounds by remember { mutableStateOf<Rect?>(null) }
var homeRootOrigin by remember { mutableStateOf(Offset.Zero) }
@@ -1064,8 +1074,8 @@ fun HomeScreen(
if (resumeItems.isNotEmpty()) {
item(key = "resume", contentType = "resume") {
HomeSection(first = true) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
HomeSection {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
SectionTitle(title = stringResource(Res.string.continue_watching))
LazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
items(resumeItems, key = { it.id }) { item ->
@@ -1081,7 +1091,7 @@ fun HomeScreen(
if (libraries.isNotEmpty()) {
item(key = "media", contentType = "media") {
HomeSection(first = resumeItems.isEmpty()) {
HomeSection {
MyMediaGrid(libraries, server.fullUrl) { folder ->
onViewAll(server.id, folder.id, folder.name)
}
@@ -1089,7 +1099,8 @@ fun HomeScreen(
}
}
items(libraries, key = { "library_${it.id}" }, contentType = { "library" }) { folder ->
val visibleLibraries = libraries.filter { emptyLibraries[it.id] != true }
items(visibleLibraries, key = { "library_${it.id}" }, contentType = { "library" }) { folder ->
HomeSection {
LazyLibrarySection(
folder = folder,
@@ -1097,12 +1108,13 @@ fun HomeScreen(
refreshToken = reloadToken,
onItemClick = { onOpenDetail(it.id) },
onViewAll = { onViewAll(server.id, folder.id, folder.name) },
onEmpty = { emptyLibraries[folder.id] = true },
)
}
}
item(key = "stats", contentType = "stats") {
HomeSection(first = resumeItems.isEmpty() && libraries.isEmpty()) {
HomeSection {
ResourceStatsCard(
stats = resourceStats,
onClick = { showResourceStats = true },
@@ -1172,12 +1184,12 @@ fun HomeScreen(
}
@Composable
private fun HomeSection(first: Boolean = false, content: @Composable () -> Unit) {
private fun HomeSection(content: @Composable () -> Unit) {
Box(
Modifier.fillMaxWidth().padding(
start = LumaSpacing.md,
end = LumaSpacing.md,
top = if (first) LumaSpacing.md else LumaSpacing.xl,
top = LumaSpacing.md,
),
) {
content()
@@ -192,6 +192,9 @@ fun LumaMediaCard(
titleSize: TextUnit = LumaTypography.body,
titleWeight: FontWeight = FontWeight.Medium,
titleSpacing: Dp = LumaSpacing.xs,
subtitle: String? = null,
subtitleColor: Color = titleColor.copy(alpha = 0.6f),
subtitleStarColor: Color = subtitleColor,
progress: Float? = null,
onClick: (() -> Unit)? = null,
overlay: @Composable BoxScope.() -> Unit = {},
@@ -283,6 +286,29 @@ fun LumaMediaCard(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (!subtitle.isNullOrBlank()) {
val parts = subtitle.split(" ", limit = 2)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = parts[0],
color = subtitleStarColor,
fontSize = (titleSize.value - 1).sp,
fontWeight = FontWeight.Normal,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (parts.size > 1) {
Text(
text = " ${parts[1]}",
color = subtitleColor,
fontSize = (titleSize.value - 1).sp,
fontWeight = FontWeight.Normal,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
}