小调整

This commit is contained in:
beyond committed 2026-09-05 19:43:52 +08:00
1 parent 082c42e177
commit 1946319d22
4 files changed
+124 -59

No files matched your search

@@ -151,7 +151,16 @@ fun MainApp() {
try {
val authResult = EmbyRepository.authenticate(server)
if (authResult.success) {
ServerRepository.addServer(server.copy(apiKey = authResult.accessToken, userId = authResult.userId))
var finalServer = server.copy(apiKey = authResult.accessToken, userId = authResult.userId)
if (finalServer.name.isBlank()) {
val fetchedName = EmbyRepository.fetchServerName(finalServer, authResult.accessToken)
finalServer = if (!fetchedName.isNullOrBlank()) {
finalServer.copy(name = fetchedName)
} else {
finalServer.copy(name = finalServer.displayHost)
}
}
ServerRepository.addServer(finalServer)
if (selectedServerId.isBlank()) selectedServerId = server.id
onResult(null)
} else {
@@ -167,7 +176,15 @@ fun MainApp() {
try {
val authResult = EmbyRepository.authenticate(server)
if (authResult.success) {
val updatedServer = server.copy(apiKey = authResult.accessToken, userId = authResult.userId)
var updatedServer = server.copy(apiKey = authResult.accessToken, userId = authResult.userId)
if (updatedServer.name.isBlank()) {
val fetchedName = EmbyRepository.fetchServerName(updatedServer, authResult.accessToken)
updatedServer = if (!fetchedName.isNullOrBlank()) {
updatedServer.copy(name = fetchedName)
} else {
updatedServer.copy(name = updatedServer.displayHost)
}
}
ServerRepository.updateServer(updatedServer)
// Re-check online status after edit
ServerRepository.checkServerOnline(updatedServer.id)
@@ -894,6 +894,63 @@ object EmbyRepository {
return "$serverUrl/emby/Items/${person.id}/Images/Primary?tag=${person.primaryImageTag}&maxWidth=180&quality=70"
}
/**
* Fetch the server name from emby/System/Info/Public (no auth required).
* Falls back to emby/System/Info with token if public endpoint fails.
* Returns the server name or null if both fail.
*/
suspend fun fetchServerName(server: EmbyServer, token: String): String? = withContext(Dispatchers.IO) {
// Try public endpoint first (no auth needed, more reliable)
val publicName = fetchServerNameFromUrl("${server.fullUrl}/emby/System/Info/Public")
if (!publicName.isNullOrBlank()) return@withContext publicName
// Fall back to authenticated endpoint
val authUrl = "${server.fullUrl}/emby/System/Info"
try {
val request = Request.Builder()
.url(authUrl)
.header("X-Emby-Token", token)
.header("Accept", "application/json")
.get()
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@use null
val body = response.body?.string() ?: return@use null
parseServerName(body)
}
} catch (_: Exception) {
null
}
}
private fun fetchServerNameFromUrl(url: String): String? {
try {
val request = Request.Builder()
.url(url)
.header("Accept", "application/json")
.get()
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return null
val body = response.body?.string() ?: return null
return parseServerName(body)
}
} catch (_: Exception) {
return null
}
}
private fun parseServerName(jsonBody: String): String? {
return try {
val json = gson.fromJson(jsonBody, Map::class.java) as? Map<*, *>
// Try common field names across Emby/Jellyfin
val name = json?.get("ServerName")?.toString()
?: json?.get("Name")?.toString()
name?.takeIf { it.isNotBlank() }
} catch (_: Exception) {
null
}
}
suspend fun checkOnline(server: EmbyServer): Boolean = withContext(Dispatchers.IO) {
val url = "${server.fullUrl}/emby/System/Info/Public"
try {
@@ -188,40 +188,32 @@ private fun ServerCard(server: EmbyServer, tint: Color, nowMillis: Long, onClick
padding = PaddingValues(0.dp),
highlight = false,
) {
Column(modifier = Modifier.fillMaxWidth().background(pressBg).padding(14.dp)) {
Column(modifier = Modifier.fillMaxWidth().background(pressBg).padding(horizontal = 12.dp, vertical = 8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (server.iconUrl.isNotBlank()) {
LumaAsyncImage(model = ImageRequest.Builder(LocalContext.current).data(server.iconUrl).crossfade(true).build(), contentDescription = null, modifier = Modifier.size(46.dp).clip(RoundedCornerShape(14.dp)), contentScale = ContentScale.Crop)
LumaAsyncImage(model = ImageRequest.Builder(LocalContext.current).data(server.iconUrl).crossfade(true).build(), contentDescription = null, modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)), contentScale = ContentScale.Crop)
} else {
Box(Modifier.size(46.dp).clip(RoundedCornerShape(14.dp)).background(Brush.linearGradient(listOf(tint, tint.copy(alpha = 0.7f)))), contentAlignment = Alignment.Center) {
LumaIconView(LumaIcon.Server, contentDescription = null, tint = Color.White, modifier = Modifier.size(26.dp))
Box(Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(Brush.linearGradient(listOf(tint, tint.copy(alpha = 0.7f)))), contentAlignment = Alignment.Center) {
LumaIconView(LumaIcon.Server, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
}
}
Spacer(Modifier.width(12.dp))
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = server.name.ifBlank { server.displayHost }, color = GlassTokens.text, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(
text = activeLineName,
color = GlassTokens.accent,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (server.note.isNotBlank()) {
Text(
text = server.note,
color = GlassTokens.subtext,
fontSize = 11.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(top = 2.dp),
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = server.name.ifBlank { server.displayHost }, color = GlassTokens.text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(0.85f))
ServerStatusLine(server = server, nowMillis = nowMillis, compact = true)
}
Row(Modifier.padding(top = 4.dp), verticalAlignment = Alignment.CenterVertically) {
ServerMediaCounts(server = server)
if (server.note.isNotBlank()) {
Spacer(Modifier.width(6.dp))
Text(text = server.note, color = GlassTokens.subtext, fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
}
Box(
Modifier
.size(48.dp)
.size(32.dp)
.onGloballyPositioned(onMorePositioned)
.clickable(
indication = null,
@@ -230,17 +222,9 @@ private fun ServerCard(server: EmbyServer, tint: Color, nowMillis: Long, onClick
) { onMoreClick() },
contentAlignment = Alignment.Center,
) {
LumaIconView(LumaIcon.More, contentDescription = stringResource(Res.string.server_actions), tint = GlassTokens.subtext, modifier = Modifier.size(24.dp))
LumaIconView(LumaIcon.More, contentDescription = stringResource(Res.string.server_actions), tint = GlassTokens.subtext, modifier = Modifier.size(18.dp))
}
}
ServerStatusLine(server = server, nowMillis = nowMillis, compact = false, modifier = Modifier.padding(top = 8.dp))
// Fixed-height error line to keep card heights consistent
Box(modifier = Modifier.height(14.dp)) {
if (!server.isOnline && server.authError.isNotBlank()) {
Text(text = server.authError, color = offlineRed, fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
ServerMediaCounts(server = server, modifier = Modifier.padding(top = 4.dp))
}
}
}
@@ -269,39 +253,39 @@ private fun ServerGridCard(server: EmbyServer, tint: Color, nowMillis: Long, onC
padding = PaddingValues(0.dp),
highlight = false,
) {
Column(modifier = Modifier.fillMaxWidth().background(pressBg).padding(14.dp), horizontalAlignment = Alignment.Start) {
Column(modifier = Modifier.fillMaxWidth().background(pressBg).padding(10.dp), horizontalAlignment = Alignment.Start) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (server.iconUrl.isNotBlank()) {
LumaAsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(server.iconUrl).crossfade(true).build(),
contentDescription = null,
modifier = Modifier.size(48.dp).clip(RoundedCornerShape(14.dp)),
modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)),
contentScale = ContentScale.Crop,
)
} else {
Box(
Modifier.size(48.dp).clip(RoundedCornerShape(14.dp)).background(Brush.linearGradient(listOf(tint, tint.copy(alpha = 0.7f)))),
Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(Brush.linearGradient(listOf(tint, tint.copy(alpha = 0.7f)))),
contentAlignment = Alignment.Center,
) {
LumaIconView(LumaIcon.Server, contentDescription = null, tint = Color.White, modifier = Modifier.size(26.dp))
LumaIconView(LumaIcon.Server, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
}
}
Spacer(Modifier.width(10.dp))
Spacer(Modifier.width(8.dp))
Column(Modifier.weight(1f)) {
Text(
text = server.name.ifBlank { server.displayHost },
color = GlassTokens.text,
fontSize = 14.sp,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Spacer(Modifier.height(3.dp))
Text(text = activeLineName, color = GlassTokens.accent, fontSize = 11.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis)
Spacer(Modifier.height(2.dp))
Text(text = activeLineName, color = GlassTokens.accent, fontSize = 10.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Box(
Modifier
.size(36.dp)
.size(30.dp)
.onGloballyPositioned(onMorePositioned)
.clickable(
indication = null,
@@ -310,17 +294,16 @@ private fun ServerGridCard(server: EmbyServer, tint: Color, nowMillis: Long, onC
) { onMoreClick() },
contentAlignment = Alignment.Center,
) {
LumaIconView(LumaIcon.More, contentDescription = stringResource(Res.string.server_actions), tint = GlassTokens.subtext, modifier = Modifier.size(20.dp))
LumaIconView(LumaIcon.More, contentDescription = stringResource(Res.string.server_actions), tint = GlassTokens.subtext, modifier = Modifier.size(18.dp))
}
}
ServerStatusLine(server = server, nowMillis = nowMillis, compact = true, modifier = Modifier.padding(top = 8.dp))
// Fixed-height area for error + note to keep card heights consistent
Box(modifier = Modifier.height(14.dp)) {
if (!server.isOnline && server.authError.isNotBlank()) {
Text(text = server.authError, color = offlineRed, fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
} else if (server.note.isNotBlank()) {
Text(text = server.note, color = GlassTokens.subtext, fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
ServerStatusLine(server = server, nowMillis = nowMillis, compact = true, modifier = Modifier.padding(top = 6.dp))
if (!server.isOnline && server.authError.isNotBlank()) {
Text(text = server.authError, color = offlineRed, fontSize = 9.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(top = 4.dp))
} else if (server.note.isNotBlank()) {
Text(text = server.note, color = GlassTokens.subtext, fontSize = 9.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(top = 4.dp))
} else {
Spacer(Modifier.height(16.dp))
}
ServerMediaCounts(server = server, modifier = Modifier.padding(top = 4.dp))
}
@@ -607,6 +590,7 @@ private fun ServerStatusLine(server: EmbyServer, nowMillis: Long, compact: Boole
fontSize = if (compact) 10.sp else 11.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = if (compact) Modifier.widthIn(min = 96.dp) else Modifier,
)
}
}
@@ -1297,11 +1281,10 @@ private fun IconPickerDialog(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
Modifier.size(36.dp).clip(RoundedCornerShape(10.dp))
.background(Brush.linearGradient(listOf(Color(0xFFFF6B9D), Color(0xFFFF8A65)))),
Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)),
contentAlignment = Alignment.Center,
) {
LumaIconView(LumaIcon.Image, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
LumaIconView(LumaIcon.Image, contentDescription = null, tint = GlassTokens.accent, modifier = Modifier.size(20.dp))
}
Spacer(Modifier.width(12.dp))
Text(text = stringResource(Res.string.icon_picker_title), fontWeight = FontWeight.Bold, fontSize = 19.sp, color = GlassTokens.text)
@@ -1457,7 +1440,7 @@ private fun IconPickerDialog(
Box(
modifier = Modifier.fillMaxWidth()
.lumaGlass(radius = 16.dp, level = LumaGlassLevel.Interactive)
.lumaGlass(radius = 16.dp, level = LumaGlassLevel.Clear)
.clickable { onDismiss() }
.padding(vertical = 14.dp),
contentAlignment = Alignment.Center,
@@ -831,12 +831,20 @@ fun GlassSheet(
)
.then(
surfaceOverlay?.let {
Modifier.background(it, RoundedCornerShape(28.dp))
Modifier.background(it, RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp, bottomStart = 0.dp, bottomEnd = 0.dp))
} ?: Modifier,
)
.padding(20.dp),
.padding(horizontal = 20.dp, vertical = 20.dp),
content = content,
)
if (surfaceOverlay != null) {
Box(
Modifier
.fillMaxWidth()
.navigationBarsPadding()
.background(surfaceOverlay),
)
}
}
}
}