From 7aad0982df5dd2c4bebefb6d90f74671455ae681 Mon Sep 17 00:00:00 2001
From: beyond
Date: Sun, 30 Aug 2026 17:13:57 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=9C=8D=E5=8A=A1=E5=99=A8?=
=?UTF-8?q?=E4=B8=AD=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=E8=B0=83=E6=95=B4?=
=?UTF-8?q?=E5=92=8C=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../myapplication/data/AppDatabase.java | 18 +-
.../myapplication/data/ServerEntity.java | 14 +-
.../com/example/myapplication/MainApp.kt | 6 +
.../example/myapplication/data/EmbyServer.kt | 34 +-
.../myapplication/data/ServerRepository.kt | 86 +++-
.../myapplication/navigation/AppNavHost.kt | 4 +-
.../myapplication/screens/LibraryScreen.kt | 455 +++++++++++++-----
.../myapplication/screens/SearchScreen.kt | 4 +-
.../example/myapplication/ui/LiquidGlass.kt | 218 ++++++---
.../myapplication/ui/design/LumaComponents.kt | 9 +-
app/src/main/res/values-en/strings.xml | 12 +-
app/src/main/res/values/strings.xml | 12 +-
.../data/EmbyServerAddressTest.kt | 36 ++
13 files changed, 656 insertions(+), 252 deletions(-)
diff --git a/app/src/main/java/com/example/myapplication/data/AppDatabase.java b/app/src/main/java/com/example/myapplication/data/AppDatabase.java
index b1c5cde..1f09df0 100644
--- a/app/src/main/java/com/example/myapplication/data/AppDatabase.java
+++ b/app/src/main/java/com/example/myapplication/data/AppDatabase.java
@@ -9,7 +9,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
@Database(
entities = {IconLibraryEntity.class, ServerEntity.class},
- version = 7,
+ version = 9,
exportSchema = false
)
public abstract class AppDatabase extends RoomDatabase {
@@ -90,6 +90,20 @@ public abstract class AppDatabase extends RoomDatabase {
}
};
+ private static final Migration MIGRATION_7_8 = new Migration(7, 8) {
+ @Override
+ public void migrate(SupportSQLiteDatabase db) {
+ db.execSQL("ALTER TABLE servers ADD COLUMN activeAddressId TEXT");
+ }
+ };
+
+ private static final Migration MIGRATION_8_9 = new Migration(8, 9) {
+ @Override
+ public void migrate(SupportSQLiteDatabase db) {
+ db.execSQL("ALTER TABLE servers ADD COLUMN lastAccessedAt INTEGER NOT NULL DEFAULT 0");
+ }
+ };
+
public static AppDatabase getInstance(Context context) {
if (instance == null) {
synchronized (AppDatabase.class) {
@@ -98,7 +112,7 @@ public static AppDatabase getInstance(Context context) {
context.getApplicationContext(),
AppDatabase.class,
"emby_player.db"
- ).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7).build();
+ ).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9).build();
}
}
}
diff --git a/app/src/main/java/com/example/myapplication/data/ServerEntity.java b/app/src/main/java/com/example/myapplication/data/ServerEntity.java
index 171221e..8def77c 100644
--- a/app/src/main/java/com/example/myapplication/data/ServerEntity.java
+++ b/app/src/main/java/com/example/myapplication/data/ServerEntity.java
@@ -23,6 +23,8 @@ public class ServerEntity {
public String iconUrl;
public int isSelected;
public String alternateAddresses;
+ public String activeAddressId;
+ public long lastAccessedAt;
@Ignore
public ServerEntity(
@@ -37,7 +39,7 @@ public class ServerEntity {
String accessToken,
String userId
) {
- this(id, name, "https", host, basePath, port, username, password, note, accessToken, userId, "", 0, "[]");
+ this(id, name, "https", host, basePath, port, username, password, note, accessToken, userId, "", 0, "[]", "", 0L);
}
@Ignore
@@ -54,7 +56,7 @@ public class ServerEntity {
String userId,
String iconUrl
) {
- this(id, name, "https", host, basePath, port, username, password, note, accessToken, userId, iconUrl, 0, "[]");
+ this(id, name, "https", host, basePath, port, username, password, note, accessToken, userId, iconUrl, 0, "[]", "", 0L);
}
@Ignore
@@ -72,7 +74,7 @@ public class ServerEntity {
String iconUrl,
int isSelected
) {
- this(id, name, "https", host, basePath, port, username, password, note, accessToken, userId, iconUrl, isSelected, "[]");
+ this(id, name, "https", host, basePath, port, username, password, note, accessToken, userId, iconUrl, isSelected, "[]", "", 0L);
}
public ServerEntity(
@@ -89,7 +91,9 @@ public class ServerEntity {
String userId,
String iconUrl,
int isSelected,
- String alternateAddresses
+ String alternateAddresses,
+ String activeAddressId,
+ long lastAccessedAt
) {
this.id = id;
this.name = name;
@@ -105,5 +109,7 @@ public class ServerEntity {
this.iconUrl = iconUrl == null ? "" : iconUrl;
this.isSelected = isSelected;
this.alternateAddresses = alternateAddresses == null ? "[]" : alternateAddresses;
+ this.activeAddressId = activeAddressId == null ? "" : activeAddressId;
+ this.lastAccessedAt = lastAccessedAt;
}
}
diff --git a/app/src/main/kotlin/com/example/myapplication/MainApp.kt b/app/src/main/kotlin/com/example/myapplication/MainApp.kt
index db7cab5..d1262b4 100644
--- a/app/src/main/kotlin/com/example/myapplication/MainApp.kt
+++ b/app/src/main/kotlin/com/example/myapplication/MainApp.kt
@@ -206,6 +206,12 @@ fun MainApp() {
onDismissChangeIcon = { changeIconTarget = null },
onPickIcon = { server, iconUrl -> ServerRepository.setRuntimeIcon(server.id, iconUrl) },
onRequestChangeIcon = { server -> changeIconTarget = server },
+ onSwitchServerAddress = { serverId, addressId ->
+ coroutineScope.launch {
+ ServerRepository.setActiveAddress(serverId, addressId)
+ ServerRepository.checkServerOnline(serverId)
+ }
+ },
onAddIconLibrary = { name, url, icons, isDefault ->
val library = IconLibrary(
id = "lib_${System.currentTimeMillis()}",
diff --git a/app/src/main/kotlin/com/example/myapplication/data/EmbyServer.kt b/app/src/main/kotlin/com/example/myapplication/data/EmbyServer.kt
index 3adc861..c78bc3d 100644
--- a/app/src/main/kotlin/com/example/myapplication/data/EmbyServer.kt
+++ b/app/src/main/kotlin/com/example/myapplication/data/EmbyServer.kt
@@ -41,33 +41,21 @@ data class EmbyServer(
val authError: String = "",
val isSelected: Boolean = false,
val alternateAddresses: List = emptyList(),
+ val activeAddressId: String = "",
+ val lastAccessedAt: Long = 0L,
+ val movieCount: Int? = null,
+ val seriesCount: Int? = null,
) {
+ val activeAlternateAddress: EmbyServerAddress?
+ get() = alternateAddresses.firstOrNull { it.id == activeAddressId }
+
val fullUrl: String
- get() {
- // Strip protocol prefix from host if user already included it
- var cleanHost = host.trim()
- val effectiveProtocol = if (protocol.equals("http", ignoreCase = true)) "http" else "https"
- if (cleanHost.startsWith("https://", ignoreCase = true)) {
- cleanHost = cleanHost.substringAfter("://")
- } else if (cleanHost.startsWith("http://", ignoreCase = true)) {
- cleanHost = cleanHost.substringAfter("://")
- }
- // Strip trailing slashes and port if embedded in host
- cleanHost = cleanHost.trimEnd('/')
- if (port.isBlank()) {
- // Check if host already contains a port like "example.com:8096"
- val portMatch = Regex("^(.+):(\\d+)$").find(cleanHost)
- if (portMatch != null) {
- cleanHost = portMatch.groupValues[1]
- }
- }
- val base = if (port.isNotBlank()) "$effectiveProtocol://$cleanHost:$port" else "$effectiveProtocol://$cleanHost"
- val segments = listOf(base.trimEnd('/'), basePath.trim().trim('/')).filter { it.isNotBlank() }
- return segments.joinToString("/")
- }
+ get() = activeAlternateAddress?.fullUrl ?: buildServerUrl(protocol, host, port, basePath)
val displayHost: String
- get() = if (port.isNotBlank()) "$host:$port" else host
+ get() = activeAlternateAddress?.let { address ->
+ if (address.port.isNotBlank()) "${address.host}:${address.port}" else address.host
+ } ?: if (port.isNotBlank()) "$host:$port" else host
companion object {
fun generateId(): String = "server_${UUID.randomUUID()}"
diff --git a/app/src/main/kotlin/com/example/myapplication/data/ServerRepository.kt b/app/src/main/kotlin/com/example/myapplication/data/ServerRepository.kt
index f06d134..43e3618 100644
--- a/app/src/main/kotlin/com/example/myapplication/data/ServerRepository.kt
+++ b/app/src/main/kotlin/com/example/myapplication/data/ServerRepository.kt
@@ -79,32 +79,63 @@ object ServerRepository {
}
suspend fun checkAllServersOnline() {
- val currentServers = _servers.value.toList()
- for (server in currentServers) {
- val reachable = EmbyRepository.checkOnline(server)
- if (!reachable) {
- updateServerStatus(server.id, false, "无法连接到服务器")
- continue
- }
- val authResult = EmbyRepository.authenticate(server)
- if (authResult.success) {
- if (server.apiKey != authResult.accessToken) {
- val updated = server.copy(
- apiKey = authResult.accessToken,
- userId = authResult.userId,
- )
- updateServer(updated)
- }
- updateServerStatus(server.id, true, "")
+ _servers.value.toList().forEach { server ->
+ checkServerOnline(server.id)
+ }
+ }
+
+ suspend fun checkServerOnline(serverId: String) {
+ val server = _servers.value.find { it.id == serverId } ?: return
+ val reachable = EmbyRepository.checkOnline(server)
+ if (!reachable) {
+ updateServerStatus(server.id, false, "无法连接到服务器")
+ return
+ }
+ val authResult = EmbyRepository.authenticate(server)
+ if (!authResult.success) {
+ updateServerStatus(server.id, false, authResult.errorMessage ?: "登录失败")
+ return
+ }
+
+ val authenticatedServer = server.copy(
+ apiKey = authResult.accessToken,
+ userId = authResult.userId,
+ )
+ if (server.apiKey != authenticatedServer.apiKey || server.userId != authenticatedServer.userId) {
+ updateServer(authenticatedServer)
+ }
+ updateServerStatus(server.id, true, "", System.currentTimeMillis())
+ val stats = EmbyRepository.getResourceStats(authenticatedServer).withoutVersions
+ updateServerMediaCounts(server.id, stats.movieCount, stats.seriesCount)
+ }
+
+ fun updateServerStatus(serverId: String, isOnline: Boolean, error: String, lastAccessedAt: Long? = null) {
+ var changedServer: EmbyServer? = null
+ _servers.value = _servers.value.map { server ->
+ if (server.id == serverId) {
+ server.copy(
+ isOnline = isOnline,
+ authError = error,
+ lastAccessedAt = lastAccessedAt ?: server.lastAccessedAt,
+ ).also { changedServer = it }
} else {
- updateServerStatus(server.id, false, authResult.errorMessage ?: "登录失败")
+ server
+ }
+ }
+ if (lastAccessedAt != null) {
+ changedServer?.let { updated ->
+ repositoryScope.launch { database?.serverDao()?.insertServer(toEntity(updated)) }
}
}
}
- fun updateServerStatus(serverId: String, isOnline: Boolean, error: String) {
+ private fun updateServerMediaCounts(serverId: String, movieCount: Int, seriesCount: Int) {
_servers.value = _servers.value.map { server ->
- if (server.id == serverId) server.copy(isOnline = isOnline, authError = error) else server
+ if (server.id == serverId) {
+ server.copy(movieCount = movieCount, seriesCount = seriesCount)
+ } else {
+ server
+ }
}
}
@@ -116,6 +147,15 @@ object ServerRepository {
reload()
}
+ suspend fun setActiveAddress(serverId: String, addressId: String) {
+ val server = _servers.value.find { it.id == serverId } ?: return
+ val validAddressId = addressId.takeIf { candidate ->
+ candidate.isNotBlank() && server.alternateAddresses.any { it.id == candidate }
+ }.orEmpty()
+ if (server.activeAddressId == validAddressId) return
+ updateServer(server.copy(activeAddressId = validAddressId))
+ }
+
private suspend fun reload(): List {
val previousServers = _servers.value.toList()
val entities = withContext(Dispatchers.IO) {
@@ -129,6 +169,8 @@ object ServerRepository {
isOnline = prev?.isOnline ?: true,
lastActive = prev?.lastActive ?: "",
authError = prev?.authError ?: "",
+ movieCount = prev?.movieCount,
+ seriesCount = prev?.seriesCount,
)
)
}
@@ -186,6 +228,8 @@ object ServerRepository {
server.iconUrl,
if (server.isSelected) 1 else 0,
encodeAlternateAddresses(server.alternateAddresses),
+ server.activeAddressId,
+ server.lastAccessedAt,
)
}
@@ -205,6 +249,8 @@ object ServerRepository {
iconUrl = entity.iconUrl ?: "",
isSelected = entity.isSelected == 1,
alternateAddresses = decodeAlternateAddresses(entity.alternateAddresses),
+ activeAddressId = entity.activeAddressId ?: "",
+ lastAccessedAt = entity.lastAccessedAt,
)
}
diff --git a/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt b/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt
index 8bc20ed..4c3f242 100644
--- a/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt
+++ b/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt
@@ -61,6 +61,7 @@ fun AppNavHost(
onDismissChangeIcon: () -> Unit,
onPickIcon: (EmbyServer, String) -> Unit,
onRequestChangeIcon: (EmbyServer) -> Unit,
+ onSwitchServerAddress: (String, String) -> Unit,
onAddIconLibrary: (String, String, List, Boolean) -> Unit,
onUpdateIconLibrary: (IconLibrary) -> Unit,
onDeleteIconLibrary: (String) -> Unit,
@@ -116,7 +117,7 @@ fun AppNavHost(
}
composable(AppRoute.Library.pattern) {
- var isGrid by rememberSaveable { mutableStateOf(false) }
+ var isGrid by rememberSaveable { mutableStateOf(true) }
LibraryScreen(
servers = servers,
selectedServer = selectedServer,
@@ -128,6 +129,7 @@ fun AppNavHost(
onDismissChangeIcon = onDismissChangeIcon,
onPickIcon = onPickIcon,
onRequestChangeIcon = onRequestChangeIcon,
+ onSwitchServerAddress = onSwitchServerAddress,
onViewToggle = { isGrid = !isGrid },
onAdd = { navigateOnce(AppRoute.ServerEditor.create()) },
onOpenIconLibraryManager = { navigateOnce(AppRoute.IconLibraryManager.pattern) },
diff --git a/app/src/main/kotlin/com/example/myapplication/screens/LibraryScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/LibraryScreen.kt
index 034b512..e0a787f 100644
--- a/app/src/main/kotlin/com/example/myapplication/screens/LibraryScreen.kt
+++ b/app/src/main/kotlin/com/example/myapplication/screens/LibraryScreen.kt
@@ -45,6 +45,7 @@ import com.example.myapplication.ui.GlassDialog
import com.example.myapplication.ui.GlassSheet
import com.example.myapplication.ui.GlassInputField
import com.example.myapplication.ui.GlassPopupItem
+import com.example.myapplication.ui.LiquidGlassButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -143,7 +144,7 @@ fun LibraryTopBar(
iconSize = 21.dp,
)
LumaIconButton(
- icon = LumaIcon.Library,
+ icon = LumaIcon.Image,
contentDescription = stringResource(Res.string.server_library_manager),
onClick = onOpenIconLibraryManager,
tint = GlassTokens.text,
@@ -166,8 +167,10 @@ fun LibraryTopBar(
/* ------------------------------------------------------------------ */
@Composable
-private fun ServerCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onChangeIcon: () -> Unit, onDelete: () -> Unit, onEdit: () -> Unit, onMorePositioned: (LayoutCoordinates) -> Unit, onMoreClick: () -> Unit) {
+private fun ServerCard(server: EmbyServer, tint: Color, nowMillis: Long, onClick: () -> Unit, onChangeIcon: () -> Unit, onDelete: () -> Unit, onEdit: () -> Unit, onMorePositioned: (LayoutCoordinates) -> Unit, onMoreClick: () -> Unit) {
var clicked by remember { mutableStateOf(false) }
+ val activeLineName = server.activeAlternateAddress?.name?.takeIf { it.isNotBlank() }
+ ?: stringResource(Res.string.server_primary_line)
val pressBg by animateColorAsState(targetValue = if (clicked) LumaColors.glassHighlight.copy(alpha = 0.24f) else Color.Transparent, animationSpec = tween(durationMillis = if (clicked) 80 else 200), label = "pb")
LaunchedEffect(clicked) { if (clicked) { delay(150); clicked = false } }
GlassSurface(
@@ -178,13 +181,13 @@ private fun ServerCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onC
) { clicked = true; onClick() },
cornerRadius = 20.dp,
fill = Color.Transparent,
- border = Color.Unspecified,
+ border = GlassTokens.glassBorder,
shadowElevation = 0.dp,
padding = PaddingValues(0.dp),
highlight = false,
) {
- Row(modifier = Modifier.fillMaxWidth().background(pressBg).padding(14.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
+ Column(modifier = Modifier.fillMaxWidth().background(pressBg).padding(14.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)
} else {
@@ -194,16 +197,15 @@ private fun ServerCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onC
}
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(text = server.name.ifBlank { server.displayHost }, color = GlassTokens.text, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
- Spacer(Modifier.width(7.dp))
- Box(Modifier.size(8.dp).clip(CircleShape).background(if (server.isOnline) onlineGreen else offlineRed))
- }
- if (!server.isOnline && server.authError.isNotBlank()) {
- Text(text = server.authError, color = offlineRed, fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
- } else {
- Text(text = if (server.isOnline) server.lastActive.ifEmpty { stringResource(Res.string.server_online) } else stringResource(Res.string.server_offline), color = GlassTokens.subtext, fontSize = 12.sp)
- }
+ 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,
@@ -215,7 +217,6 @@ private fun ServerCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onC
)
}
}
- }
Box(
Modifier
.size(48.dp)
@@ -229,6 +230,12 @@ private fun ServerCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onC
) {
LumaIconView(LumaIcon.More, contentDescription = stringResource(Res.string.server_actions), tint = GlassTokens.subtext, modifier = Modifier.size(24.dp))
}
+ }
+ ServerStatusLine(server = server, nowMillis = nowMillis, compact = false, modifier = Modifier.padding(top = 8.dp))
+ if (!server.isOnline && server.authError.isNotBlank()) {
+ Text(text = server.authError, color = offlineRed, fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(top = 3.dp))
+ }
+ ServerMediaCounts(server = server, modifier = Modifier.padding(top = 8.dp))
}
}
}
@@ -238,8 +245,10 @@ private fun ServerCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onC
/* ------------------------------------------------------------------ */
@Composable
-private fun ServerGridCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onChangeIcon: () -> Unit, onDelete: () -> Unit, onEdit: () -> Unit, onMorePositioned: (LayoutCoordinates) -> Unit, onMoreClick: () -> Unit) {
+private fun ServerGridCard(server: EmbyServer, tint: Color, nowMillis: Long, onClick: () -> Unit, onChangeIcon: () -> Unit, onDelete: () -> Unit, onEdit: () -> Unit, onMorePositioned: (LayoutCoordinates) -> Unit, onMoreClick: () -> Unit) {
var clicked by remember { mutableStateOf(false) }
+ val activeLineName = server.activeAlternateAddress?.name?.takeIf { it.isNotBlank() }
+ ?: stringResource(Res.string.server_primary_line)
val pressBg by animateColorAsState(targetValue = if (clicked) LumaColors.glassHighlight.copy(alpha = 0.24f) else Color.Transparent, animationSpec = tween(durationMillis = if (clicked) 80 else 200), label = "pb")
LaunchedEffect(clicked) { if (clicked) { delay(150); clicked = false } }
GlassSurface(
@@ -250,7 +259,7 @@ private fun ServerGridCard(server: EmbyServer, tint: Color, onClick: () -> Unit,
) { clicked = true; onClick() },
cornerRadius = 20.dp,
fill = Color.Transparent,
- border = Color.Unspecified,
+ border = GlassTokens.glassBorder,
shadowElevation = 0.dp,
padding = PaddingValues(0.dp),
highlight = false,
@@ -283,19 +292,11 @@ private fun ServerGridCard(server: EmbyServer, tint: Color, onClick: () -> Unit,
overflow = TextOverflow.Ellipsis,
)
Spacer(Modifier.height(3.dp))
- Row(verticalAlignment = Alignment.CenterVertically) {
- Box(Modifier.size(7.dp).clip(CircleShape).background(if (server.isOnline) onlineGreen else offlineRed))
- Spacer(Modifier.width(4.dp))
- if (!server.isOnline && server.authError.isNotBlank()) {
- Text(text = server.authError, color = offlineRed, fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
- } else {
- Text(text = if (server.isOnline) server.lastActive.ifEmpty { stringResource(Res.string.server_online) } else stringResource(Res.string.server_offline), color = GlassTokens.subtext, fontSize = 11.sp)
- }
- }
+ Text(text = activeLineName, color = GlassTokens.accent, fontSize = 11.sp, fontWeight = FontWeight.Medium, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Box(
Modifier
- .size(48.dp)
+ .size(36.dp)
.onGloballyPositioned(onMorePositioned)
.clickable(
indication = null,
@@ -304,19 +305,24 @@ private fun ServerGridCard(server: EmbyServer, tint: Color, onClick: () -> Unit,
) { 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(20.dp))
}
}
- Box(Modifier.height(22.dp)) {
+ ServerStatusLine(server = server, nowMillis = nowMillis, compact = true, modifier = Modifier.padding(top = 8.dp))
+ if (!server.isOnline && server.authError.isNotBlank()) {
+ Text(text = server.authError, color = offlineRed, fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(top = 3.dp))
+ }
+ if (server.note.isNotBlank()) {
Text(
text = server.note,
color = GlassTokens.subtext,
fontSize = 11.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
- modifier = Modifier.padding(top = 8.dp),
+ modifier = Modifier.padding(top = 3.dp),
)
}
+ ServerMediaCounts(server = server, modifier = Modifier.padding(top = 8.dp))
}
}
}
@@ -337,6 +343,7 @@ fun LibraryScreen(
onDismissChangeIcon: () -> Unit = {},
onPickIcon: (EmbyServer, String) -> Unit = { _, _ -> },
onRequestChangeIcon: (EmbyServer) -> Unit = {},
+ onSwitchServerAddress: (String, String) -> Unit = { _, _ -> },
onViewToggle: () -> Unit = {},
onAdd: () -> Unit = {},
onOpenIconLibraryManager: () -> Unit = {},
@@ -346,9 +353,18 @@ fun LibraryScreen(
var popupAnchor by remember { mutableStateOf(null) }
var popupVisible by remember { mutableStateOf(false) }
var deleteServerTarget by remember { mutableStateOf(null) }
+ var addressPickerTarget by remember { mutableStateOf(null) }
var rootOrigin by remember { mutableStateOf(androidx.compose.ui.geometry.Offset.Zero) }
+ var nowMillis by remember { mutableStateOf(System.currentTimeMillis()) }
val libraryBackdrop = rememberLumaLayerBackdrop()
+ LaunchedEffect(Unit) {
+ while (true) {
+ delay(60_000L)
+ nowMillis = System.currentTimeMillis()
+ }
+ }
+
if (onChangeIconTarget != null) {
IconPickerDialog(
server = onChangeIconTarget,
@@ -396,7 +412,7 @@ fun LibraryScreen(
val tint = serverTints[index % serverTints.size]
if (isGrid) {
ServerGridCard(
- server = server, tint = tint,
+ server = server, tint = tint, nowMillis = nowMillis,
onClick = { onSelectServer(server.id) },
onChangeIcon = { onRequestChangeIcon(server) },
onDelete = { onDeleteServer(server.id) },
@@ -412,7 +428,7 @@ fun LibraryScreen(
)
} else {
ServerCard(
- server = server, tint = tint,
+ server = server, tint = tint, nowMillis = nowMillis,
onClick = { onSelectServer(server.id) },
onChangeIcon = { onRequestChangeIcon(server) },
onDelete = { onDeleteServer(server.id) },
@@ -453,6 +469,14 @@ fun LibraryScreen(
popupServer = null
popupAnchor = null
},
+ height = 200.dp,
+ autoPlaceAroundAnchor = true,
+ bottomInset = 86.dp,
+ containerColor = if (GlassTokens.isDark) {
+ Color(0xFF141A22).copy(alpha = 0.88f)
+ } else {
+ Color(0xFFE5EBF2).copy(alpha = 0.90f)
+ },
) {
GlassPopupItem(
text = stringResource(Res.string.server_edit),
@@ -461,9 +485,17 @@ fun LibraryScreen(
)
GlassPopupItem(
text = stringResource(Res.string.change_icon_action),
- icon = LumaIcon.Library,
+ icon = LumaIcon.Image,
onClick = { popupVisible = false; onRequestChangeIcon(currentPopupServer) },
)
+ GlassPopupItem(
+ text = stringResource(Res.string.server_switch_line),
+ icon = LumaIcon.Network,
+ onClick = {
+ popupVisible = false
+ addressPickerTarget = currentPopupServer
+ },
+ )
GlassPopupItem(
text = stringResource(Res.string.icon_lib_delete),
icon = LumaIcon.Delete,
@@ -490,6 +522,159 @@ fun LibraryScreen(
},
)
}
+
+ addressPickerTarget?.let { serverToSwitch ->
+ ServerAddressPickerDialog(
+ server = serverToSwitch,
+ backdrop = libraryBackdrop,
+ onDismiss = { addressPickerTarget = null },
+ onSelect = { addressId ->
+ onSwitchServerAddress(serverToSwitch.id, addressId)
+ addressPickerTarget = null
+ },
+ )
+ }
+ }
+ }
+}
+
+private fun managementDialogSurfaceColor(): Color = if (GlassTokens.isDark) {
+ Color(0xFF111923).copy(alpha = 0.92f)
+} else {
+ Color(0xFFDDE7F1).copy(alpha = 0.94f)
+}
+
+@Composable
+private fun ServerAddressPickerDialog(
+ server: EmbyServer,
+ backdrop: Backdrop?,
+ onDismiss: () -> Unit,
+ onSelect: (String) -> Unit,
+) {
+ val activeAddressId = server.activeAlternateAddress?.id.orEmpty()
+ GlassDialog(
+ backdrop = backdrop,
+ onDismissRequest = onDismiss,
+ modifier = Modifier.width(320.dp),
+ surfaceTint = Color.Transparent,
+ dimBehind = false,
+ useBackdropGlass = false,
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ .heightIn(max = (LocalConfiguration.current.screenHeightDp * 0.70f).dp)
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ LumaIconView(
+ icon = LumaIcon.Network,
+ contentDescription = null,
+ tint = GlassTokens.accent,
+ modifier = Modifier.size(22.dp),
+ )
+ Spacer(Modifier.width(10.dp))
+ Text(
+ text = stringResource(Res.string.server_switch_line),
+ color = GlassTokens.text,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+
+ ServerAddressOption(
+ name = stringResource(Res.string.server_primary_line),
+ address = if (server.port.isNotBlank()) "${server.host}:${server.port}" else server.host,
+ selected = activeAddressId.isBlank(),
+ onClick = { onSelect("") },
+ )
+ server.alternateAddresses.forEach { address ->
+ ServerAddressOption(
+ name = address.name.ifBlank { stringResource(Res.string.server_alternate_line) },
+ address = if (address.port.isNotBlank()) "${address.host}:${address.port}" else address.host,
+ selected = activeAddressId == address.id,
+ onClick = { onSelect(address.id) },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ServerStatusLine(server: EmbyServer, nowMillis: Long, compact: Boolean, modifier: Modifier = Modifier) {
+ val elapsed = (nowMillis - server.lastAccessedAt).coerceAtLeast(0L)
+ val accessText = when {
+ server.lastAccessedAt <= 0L -> null
+ elapsed < 60_000L -> stringResource(Res.string.server_access_just_now)
+ elapsed < 3_600_000L -> stringResource(Res.string.server_access_minutes_ago, elapsed / 60_000L)
+ elapsed < 86_400_000L -> stringResource(Res.string.server_access_hours_ago, elapsed / 3_600_000L)
+ else -> stringResource(Res.string.server_access_days_ago, elapsed / 86_400_000L)
+ }
+ Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
+ Box(Modifier.size(if (compact) 7.dp else 8.dp).clip(CircleShape).background(if (server.isOnline) onlineGreen else offlineRed))
+ Spacer(Modifier.width(5.dp))
+ Text(
+ text = listOfNotNull(
+ stringResource(if (server.isOnline) Res.string.server_online else Res.string.server_offline),
+ accessText,
+ ).joinToString(" · "),
+ color = GlassTokens.subtext,
+ fontSize = if (compact) 10.sp else 11.sp,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+}
+
+@Composable
+private fun ServerMediaCounts(server: EmbyServer, modifier: Modifier = Modifier) {
+ Text(
+ text = if (server.movieCount != null && server.seriesCount != null) {
+ stringResource(Res.string.server_media_counts, server.movieCount, server.seriesCount)
+ } else {
+ stringResource(Res.string.server_media_counts_pending)
+ },
+ modifier = modifier,
+ color = GlassTokens.subtext,
+ fontSize = 10.sp,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+}
+
+@Composable
+private fun ServerAddressOption(
+ name: String,
+ address: String,
+ selected: Boolean,
+ onClick: () -> Unit,
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ .background(if (selected) LumaColors.accentSoft else Color.Transparent)
+ .border(0.7.dp, if (selected) GlassTokens.accent.copy(alpha = 0.55f) else GlassTokens.glassBorder, RoundedCornerShape(16.dp))
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ role = Role.Button,
+ onClick = onClick,
+ )
+ .padding(horizontal = 14.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ Column(Modifier.weight(1f)) {
+ Text(name, color = GlassTokens.text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
+ Text(address, color = GlassTokens.subtext, fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ }
+ if (selected) {
+ LumaIconView(
+ icon = LumaIcon.Check,
+ contentDescription = stringResource(Res.string.server_current_line),
+ tint = GlassTokens.accent,
+ modifier = Modifier.size(18.dp),
+ )
}
}
}
@@ -598,7 +783,7 @@ internal fun IconLibraryManagerScreenContent(
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) {
- LumaIconView(LumaIcon.Library, contentDescription = null, tint = GlassTokens.subtext, modifier = Modifier.size(32.dp))
+ LumaIconView(LumaIcon.Image, contentDescription = null, tint = GlassTokens.subtext, modifier = Modifier.size(32.dp))
Text(stringResource(Res.string.icon_lib_empty), color = GlassTokens.subtext, fontSize = 15.sp)
Text(stringResource(Res.string.icon_lib_empty_hint), color = GlassTokens.subtext.copy(alpha = 0.75f), fontSize = 12.sp)
}
@@ -664,9 +849,9 @@ private fun IconLibraryManagerCard(library: IconLibrary, onClick: () -> Unit) {
Modifier.fillMaxWidth()
.lumaGlass(
radius = LumaShapes.large,
- level = com.example.myapplication.ui.design.LumaGlassLevel.Clear,
+ level = LumaGlassLevel.Regular,
tint = Color.Transparent,
- border = false,
+ borderColor = GlassTokens.glassBorder,
shadowElevation = 0.dp,
highlight = false,
)
@@ -688,7 +873,7 @@ private fun IconLibraryManagerCard(library: IconLibrary, onClick: () -> Unit) {
.background(Brush.linearGradient(listOf(Color(0xFFFF6B9D), Color(0xFFFF8A65)))),
contentAlignment = Alignment.Center,
) {
- LumaIconView(LumaIcon.Library, contentDescription = null, tint = Color.White, modifier = Modifier.size(19.dp))
+ LumaIconView(LumaIcon.Image, contentDescription = null, tint = Color.White, modifier = Modifier.size(19.dp))
}
Column(Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
@@ -713,7 +898,7 @@ private fun IconLibraryManagerCard(library: IconLibrary, onClick: () -> Unit) {
library.icons.take(4).forEach { icon ->
Box(
Modifier.size(34.dp)
- .lumaGlass(radius = 9.dp, level = LumaGlassLevel.Clear, shadowElevation = 0.dp),
+ .lumaGlass(radius = 9.dp, level = LumaGlassLevel.Clear, tint = Color.Transparent, shadowElevation = 0.dp, highlight = false),
contentAlignment = Alignment.Center,
) {
LumaAsyncImage(
@@ -727,13 +912,13 @@ private fun IconLibraryManagerCard(library: IconLibrary, onClick: () -> Unit) {
if (library.iconCount > 0 && library.icons.isEmpty()) {
Box(
Modifier.weight(1f).height(34.dp)
- .lumaGlass(radius = 9.dp, level = LumaGlassLevel.Clear, shadowElevation = 0.dp),
+ .lumaGlass(radius = 9.dp, level = LumaGlassLevel.Clear, tint = Color.Transparent, shadowElevation = 0.dp, highlight = false),
contentAlignment = Alignment.Center,
) { Text(stringResource(Res.string.icon_lib_not_loaded), color = GlassTokens.subtext, fontSize = 11.sp) }
} else if (library.icons.isEmpty()) {
Box(
Modifier.weight(1f).height(34.dp)
- .lumaGlass(radius = 9.dp, level = LumaGlassLevel.Clear, shadowElevation = 0.dp),
+ .lumaGlass(radius = 9.dp, level = LumaGlassLevel.Clear, tint = Color.Transparent, shadowElevation = 0.dp, highlight = false),
contentAlignment = Alignment.Center,
) { Text(stringResource(Res.string.icon_lib_no_icons), color = GlassTokens.subtext, fontSize = 11.sp) }
}
@@ -742,7 +927,7 @@ private fun IconLibraryManagerCard(library: IconLibrary, onClick: () -> Unit) {
Spacer(Modifier.height(11.dp))
Box(
Modifier.fillMaxWidth()
- .lumaGlass(radius = 12.dp, level = LumaGlassLevel.Clear, shadowElevation = 0.dp)
+ .lumaGlass(radius = 12.dp, level = LumaGlassLevel.Clear, tint = Color.Transparent, shadowElevation = 0.dp, highlight = false)
.padding(vertical = 7.dp),
contentAlignment = Alignment.Center,
) {
@@ -771,9 +956,14 @@ private fun ManageIconLibraryDialog(
var isRefreshing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
- GlassDialog(backdrop = backdrop, onDismissRequest = onDismiss) {
+ GlassDialog(
+ backdrop = backdrop,
+ onDismissRequest = onDismiss,
+ modifier = Modifier.width(320.dp),
+ surfaceOverlay = managementDialogSurfaceColor(),
+ ) {
Column(
- modifier = Modifier.width(300.dp)
+ modifier = Modifier.fillMaxWidth()
.heightIn(max = (LocalConfiguration.current.screenHeightDp * 0.78f).dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -785,7 +975,7 @@ private fun ManageIconLibraryDialog(
.background(Brush.linearGradient(listOf(Color(0xFFFF6B9D), Color(0xFFFF8A65)))),
contentAlignment = Alignment.Center,
) {
- LumaIconView(LumaIcon.Library, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
+ LumaIconView(LumaIcon.Image, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
}
Spacer(Modifier.width(10.dp))
Text(text = stringResource(Res.string.icon_lib_manage_dialog_title), fontWeight = FontWeight.Bold, fontSize = 18.sp, color = GlassTokens.text)
@@ -800,18 +990,15 @@ private fun ManageIconLibraryDialog(
}
// Editable library metadata
- GlassInputField(label = stringResource(Res.string.icon_lib_name_label), value = editName, placeholder = stringResource(Res.string.icon_lib_name_label), onValueChange = { editName = it })
- GlassInputField(label = stringResource(Res.string.icon_lib_url_label), value = editUrl, placeholder = "https://...", onValueChange = { editUrl = it })
+ GlassInputField(label = stringResource(Res.string.icon_lib_name_label), value = editName, placeholder = stringResource(Res.string.icon_lib_name_label), onValueChange = { editName = it }, containerTint = Color.Transparent, useBackdropGlass = false)
+ GlassInputField(label = stringResource(Res.string.icon_lib_url_label), value = editUrl, placeholder = "https://...", onValueChange = { editUrl = it }, containerTint = Color.Transparent, useBackdropGlass = false)
// Default library switch
Row(
Modifier.fillMaxWidth()
- .lumaGlass(
- radius = 16.dp,
- level = LumaGlassLevel.Interactive,
- tint = if (setAsDefault) LumaColors.accentSoft else LumaColors.clearTint,
- borderColor = if (setAsDefault) GlassTokens.accent.copy(alpha = 0.35f) else LumaColors.glassBorder,
- )
+ .clip(RoundedCornerShape(16.dp))
+ .background(if (setAsDefault) LumaColors.accentSoft else Color.Transparent)
+ .border(0.7.dp, if (setAsDefault) GlassTokens.accent.copy(alpha = 0.35f) else LumaColors.glassBorder, RoundedCornerShape(16.dp))
.clickable { setAsDefault = !setAsDefault }
.padding(horizontal = 14.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -837,14 +1024,15 @@ private fun ManageIconLibraryDialog(
Text(stringResource(Res.string.icon_lib_preview), fontSize = 13.sp, color = GlassTokens.subtext, fontWeight = FontWeight.Medium)
LazyVerticalGrid(
columns = GridCells.Fixed(5),
- modifier = Modifier.heightIn(min = 80.dp, max = 320.dp),
+ modifier = Modifier.heightIn(min = 80.dp, max = 160.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
items(library.icons) { icon ->
Box(
modifier = Modifier.aspectRatio(1f)
- .lumaGlass(radius = 8.dp, level = LumaGlassLevel.Clear, shadowElevation = 0.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .border(0.6.dp, GlassTokens.glassBorder, RoundedCornerShape(8.dp))
.padding(2.dp),
contentAlignment = Alignment.Center,
) {
@@ -862,11 +1050,8 @@ private fun ManageIconLibraryDialog(
// Refresh button
Box(
modifier = Modifier.fillMaxWidth()
- .lumaGlass(
- radius = 16.dp,
- level = LumaGlassLevel.Interactive,
- tint = if (isRefreshing) LumaColors.clearTint else LumaColors.regularTint,
- )
+ .clip(RoundedCornerShape(16.dp))
+ .border(0.7.dp, GlassTokens.glassBorder, RoundedCornerShape(16.dp))
.clickable(enabled = !isRefreshing) {
isRefreshing = true
scope.launch {
@@ -892,6 +1077,7 @@ private fun ManageIconLibraryDialog(
onClick = onDelete,
modifier = Modifier.weight(1f),
danger = true,
+ useBackdropGlass = false,
)
GlassButton(
text = stringResource(Res.string.icon_lib_save),
@@ -903,6 +1089,7 @@ private fun ManageIconLibraryDialog(
modifier = Modifier.weight(1f),
accent = true,
enabled = editName.text.isNotBlank() && editUrl.text.isNotBlank(),
+ useBackdropGlass = false,
)
}
}
@@ -924,7 +1111,12 @@ private fun AddIconLibraryDialog(
val scope = rememberCoroutineScope()
val fetchErrorFallback = stringResource(Res.string.icon_lib_fetch_error)
- GlassDialog(backdrop = backdrop, onDismissRequest = onDismiss, modifier = Modifier.width(320.dp)) {
+ GlassDialog(
+ backdrop = backdrop,
+ onDismissRequest = onDismiss,
+ modifier = Modifier.width(320.dp),
+ surfaceOverlay = managementDialogSurfaceColor(),
+ ) {
Box(Modifier.fillMaxWidth()) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -936,7 +1128,7 @@ private fun AddIconLibraryDialog(
.background(Brush.linearGradient(listOf(Color(0xFFFF6B9D), Color(0xFFFF8A65)))),
contentAlignment = Alignment.Center,
) {
- LumaIconView(LumaIcon.Library, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
+ LumaIconView(LumaIcon.Image, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
}
Spacer(Modifier.width(12.dp))
Text(text = stringResource(Res.string.icon_lib_add_title), fontWeight = FontWeight.Bold, fontSize = 19.sp, color = GlassTokens.text)
@@ -947,17 +1139,16 @@ private fun AddIconLibraryDialog(
value = name,
placeholder = stringResource(Res.string.icon_lib_name_hint),
onValueChange = { name = it },
+ containerTint = Color.Transparent,
+ useBackdropGlass = false,
)
// Default library switch
Row(
Modifier.fillMaxWidth()
- .lumaGlass(
- radius = 16.dp,
- level = LumaGlassLevel.Interactive,
- tint = if (isDefault) LumaColors.accentSoft else LumaColors.clearTint,
- borderColor = if (isDefault) GlassTokens.accent.copy(alpha = 0.35f) else LumaColors.glassBorder,
- )
+ .clip(RoundedCornerShape(16.dp))
+ .background(if (isDefault) LumaColors.accentSoft else Color.Transparent)
+ .border(0.7.dp, if (isDefault) GlassTokens.accent.copy(alpha = 0.35f) else LumaColors.glassBorder, RoundedCornerShape(16.dp))
.clickable { isDefault = !isDefault }
.padding(horizontal = 14.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -988,29 +1179,27 @@ private fun AddIconLibraryDialog(
error = null
icons = emptyList()
},
+ containerTint = Color.Transparent,
+ useBackdropGlass = false,
)
- Box(
- modifier = Modifier.fillMaxWidth()
- .lumaGlass(
- radius = 16.dp,
- level = LumaGlassLevel.Interactive,
- tint = if (url.text.isNotBlank() && !isLoading) GlassTokens.accent else LumaColors.accentSoft,
- borderColor = GlassTokens.accent.copy(alpha = 0.35f),
- )
- .clickable(enabled = url.text.isNotBlank() && !isLoading) {
- scope.launch {
- isLoading = true; error = null
- val result = IconLibraryRepository.fetchIcons(url.text)
- result.fold(
- onSuccess = { items -> icons = items; fetched = true },
- onFailure = { error = fetchErrorFallback }
- )
- isLoading = false
- }
+ LiquidGlassButton(
+ onClick = {
+ scope.launch {
+ isLoading = true; error = null
+ val result = IconLibraryRepository.fetchIcons(url.text.trim())
+ result.fold(
+ onSuccess = { items -> icons = items; fetched = true },
+ onFailure = { error = fetchErrorFallback }
+ )
+ isLoading = false
}
- .padding(vertical = 14.dp),
- contentAlignment = Alignment.Center,
+ },
+ modifier = Modifier.fillMaxWidth(),
+ cornerRadius = 16.dp,
+ enabled = url.text.isNotBlank() && !isLoading,
+ fill = GlassTokens.accent.copy(alpha = 0.78f),
+ useBackdropGlass = false,
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(22.dp), color = Color.White, strokeWidth = 2.5.dp)
@@ -1025,19 +1214,21 @@ private fun AddIconLibraryDialog(
Text(stringResource(Res.string.icon_lib_preview_count, icons.size), fontSize = 13.sp, color = GlassTokens.subtext, fontWeight = FontWeight.Medium)
Box(
Modifier.fillMaxWidth()
- .lumaGlass(radius = 16.dp, level = LumaGlassLevel.Clear)
+ .clip(RoundedCornerShape(16.dp))
+ .border(0.6.dp, GlassTokens.glassBorder, RoundedCornerShape(16.dp))
.padding(8.dp)
) {
LazyVerticalGrid(
columns = GridCells.Fixed(5),
- modifier = Modifier.heightIn(min = 80.dp, max = 320.dp),
+ modifier = Modifier.height(240.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
items(icons) { icon ->
Box(
modifier = Modifier.aspectRatio(1f)
- .lumaGlass(radius = 12.dp, level = LumaGlassLevel.Clear, shadowElevation = 0.dp),
+ .clip(RoundedCornerShape(12.dp))
+ .border(0.6.dp, GlassTokens.glassBorder, RoundedCornerShape(12.dp)),
contentAlignment = Alignment.Center,
) {
LumaAsyncImage(
@@ -1058,13 +1249,16 @@ private fun AddIconLibraryDialog(
text = stringResource(Res.string.icon_lib_cancel),
onClick = onDismiss,
modifier = Modifier.weight(1f),
+ clear = true,
+ useBackdropGlass = false,
)
GlassButton(
text = stringResource(Res.string.icon_lib_save),
onClick = { onSave(name.text.trim(), url.text.trim(), icons, isDefault); onDismiss() },
modifier = Modifier.weight(1f),
accent = true,
- enabled = icons.isNotEmpty() && name.text.isNotBlank(),
+ enabled = name.text.isNotBlank() && url.text.isNotBlank(),
+ useBackdropGlass = false,
)
}
}
@@ -1115,7 +1309,7 @@ private fun IconPickerDialog(
.background(Brush.linearGradient(listOf(Color(0xFFFF6B9D), Color(0xFFFF8A65)))),
contentAlignment = Alignment.Center,
) {
- LumaIconView(LumaIcon.Library, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
+ LumaIconView(LumaIcon.Image, contentDescription = null, tint = Color.White, 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)
@@ -1194,7 +1388,7 @@ private fun IconPickerDialog(
.lumaGlass(radius = 28.dp, level = LumaGlassLevel.Clear, shadowElevation = 0.dp),
contentAlignment = Alignment.Center
) {
- LumaIconView(LumaIcon.Library, contentDescription = null, tint = GlassTokens.subtext2, modifier = Modifier.size(28.dp))
+ LumaIconView(LumaIcon.Image, contentDescription = null, tint = GlassTokens.subtext2, modifier = Modifier.size(28.dp))
}
Spacer(Modifier.height(14.dp))
Text(text = stringResource(Res.string.icon_picker_no_lib), color = GlassTokens.text, fontSize = 16.sp, fontWeight = FontWeight.Medium)
@@ -1441,6 +1635,9 @@ private fun ServerFormContent(
basePath = address.basePath.text.trim().trim('/'),
)
},
+ activeAddressId = initial?.activeAddressId?.takeIf { activeId ->
+ normalizedAlternates.any { it.id == activeId }
+ }.orEmpty(),
)
onConfirm(server) { error ->
isSaving = false
@@ -1498,6 +1695,41 @@ private fun ServerFormContent(
focusRequester = portFocusRequester,
)
}
+ ServerFormInput(
+ label = stringResource(Res.string.server_username_label),
+ value = username,
+ placeholder = stringResource(Res.string.server_username_placeholder),
+ onValueChange = { username = it },
+ isError = usernameError,
+ errorMessage = if (usernameError) stringResource(Res.string.server_username_error) else null,
+ focusRequester = usernameFocusRequester,
+ )
+ ServerFormInput(
+ label = stringResource(Res.string.server_password_label),
+ value = password,
+ placeholder = stringResource(Res.string.server_password_placeholder),
+ onValueChange = { password = it },
+ visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
+ trailingContent = {
+ Box(
+ Modifier
+ .size(48.dp)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ ) { showPassword = !showPassword },
+ contentAlignment = Alignment.Center,
+ ) {
+ LumaIconView(
+ icon = if (showPassword) LumaIcon.EyeOff else LumaIcon.Eye,
+ contentDescription = stringResource(if (showPassword) Res.string.server_hide_password else Res.string.server_show_password),
+ tint = GlassTokens.subtext,
+ modifier = Modifier.size(24.dp),
+ )
+ }
+ },
+ )
+ ServerFormInput(label = stringResource(Res.string.server_note_label), value = note, placeholder = stringResource(Res.string.server_note_placeholder), onValueChange = { note = it })
Text(
text = stringResource(Res.string.server_alternate_title),
color = GlassTokens.text,
@@ -1628,41 +1860,6 @@ private fun ServerFormContent(
},
modifier = Modifier.fillMaxWidth(),
)
- ServerFormInput(
- label = stringResource(Res.string.server_username_label),
- value = username,
- placeholder = stringResource(Res.string.server_username_placeholder),
- onValueChange = { username = it },
- isError = usernameError,
- errorMessage = if (usernameError) stringResource(Res.string.server_username_error) else null,
- focusRequester = usernameFocusRequester,
- )
- ServerFormInput(
- label = stringResource(Res.string.server_password_label),
- value = password,
- placeholder = stringResource(Res.string.server_password_placeholder),
- onValueChange = { password = it },
- visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
- trailingContent = {
- Box(
- Modifier
- .size(48.dp)
- .clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = null,
- ) { showPassword = !showPassword },
- contentAlignment = Alignment.Center,
- ) {
- LumaIconView(
- icon = if (showPassword) LumaIcon.EyeOff else LumaIcon.Eye,
- contentDescription = stringResource(if (showPassword) Res.string.server_hide_password else Res.string.server_show_password),
- tint = GlassTokens.subtext,
- modifier = Modifier.size(24.dp),
- )
- }
- },
- )
- ServerFormInput(label = stringResource(Res.string.server_note_label), value = note, placeholder = stringResource(Res.string.server_note_placeholder), onValueChange = { note = it })
if (authError.isNotBlank()) {
Text(text = authError, color = offlineRed, fontSize = 13.sp, modifier = Modifier.padding(bottom = 4.dp))
}
diff --git a/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt
index c354b7a..15e37ec 100644
--- a/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt
+++ b/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt
@@ -68,6 +68,7 @@ fun SearchTopBar(
onValueChange = onQueryChange,
placeholder = stringResource(Res.string.search_hint),
onGearClick = onGearClick,
+ useBackdropGlass = false,
)
}
}
@@ -147,6 +148,7 @@ fun SearchScreen(
icon = LumaIcon.Search,
title = stringResource(Res.string.search_no_result_title),
description = stringResource(Res.string.search_no_result_prefix) + query.trim() + stringResource(Res.string.search_no_result_suffix),
+ showIconBackground = false,
)
}
else -> LazyVerticalGrid(
@@ -180,7 +182,7 @@ private fun SearchEmptyState(title: String, subtitle: String) {
.padding(bottom = bottomBarContentPadding(90.dp).calculateBottomPadding()),
contentAlignment = Alignment.Center,
) {
- LumaEmptyState(icon = LumaIcon.Search, title = title, description = subtitle)
+ LumaEmptyState(icon = LumaIcon.Search, title = title, description = subtitle, showIconBackground = false)
}
}
diff --git a/app/src/main/kotlin/com/example/myapplication/ui/LiquidGlass.kt b/app/src/main/kotlin/com/example/myapplication/ui/LiquidGlass.kt
index 29540fa..2c12562 100644
--- a/app/src/main/kotlin/com/example/myapplication/ui/LiquidGlass.kt
+++ b/app/src/main/kotlin/com/example/myapplication/ui/LiquidGlass.kt
@@ -1,5 +1,6 @@
package com.example.myapplication.ui
+import android.view.WindowManager
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
@@ -21,6 +22,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
@@ -51,7 +53,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawBehind
@@ -82,11 +83,13 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.window.DialogWindowProvider
import kotlinx.coroutines.delay
import kotlin.math.roundToInt
import androidx.compose.ui.zIndex
import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.asPaddingValues
@@ -157,6 +160,7 @@ fun LiquidGlassBackground(modifier: Modifier = Modifier) {
@Composable
fun GlassSurface(
modifier: Modifier = Modifier,
+ backdrop: Backdrop? = LocalGlassBackdrop.current,
cornerRadius: androidx.compose.ui.unit.Dp = 22.dp,
fill: Color = GlassTokens.glassFill,
border: Color = GlassTokens.glassBorder,
@@ -170,7 +174,7 @@ fun GlassSurface(
Box(
modifier
.lumaGlass(
- backdrop = LocalGlassBackdrop.current,
+ backdrop = backdrop,
radius = cornerRadius,
level = LumaGlassLevel.Regular,
border = border != Color.Unspecified,
@@ -313,25 +317,31 @@ fun GlassSearchField(
placeholder: String,
onGearClick: () -> Unit = {},
modifier: Modifier = Modifier,
+ useBackdropGlass: Boolean = true,
) {
val backdrop = LocalGlassBackdrop.current
- var focused by remember { mutableStateOf(false) }
- val borderColor by animateColorAsState(
- targetValue = if (focused) GlassTokens.accent.copy(alpha = 0.72f) else GlassTokens.glassBorder,
- animationSpec = tween(LumaMotion.fast),
- label = "searchFieldFocus",
- )
+ val borderColor = GlassTokens.glassBorder
Row(
modifier
.fillMaxWidth()
- .lumaGlass(
- backdrop = backdrop,
- radius = LumaShapes.large,
- level = LumaGlassLevel.Regular,
- borderColor = borderColor,
- shadowElevation = 0.dp,
+ .height(52.dp)
+ .then(
+ if (useBackdropGlass) {
+ Modifier.lumaGlass(
+ backdrop = backdrop,
+ radius = LumaShapes.large,
+ level = LumaGlassLevel.Regular,
+ borderColor = borderColor,
+ shadowElevation = 0.dp,
+ )
+ } else {
+ val shape = RoundedCornerShape(LumaShapes.large)
+ Modifier.clip(shape)
+ .background(Color.Transparent, shape)
+ .border(0.7.dp, borderColor, shape)
+ }
)
- .padding(horizontal = 14.dp, vertical = 13.dp),
+ .padding(start = 14.dp, end = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
LumaIconView(
@@ -349,7 +359,7 @@ fun GlassSearchField(
color = GlassTokens.text,
fontSize = 15.sp,
),
- modifier = Modifier.weight(1f).onFocusChanged { focused = it.isFocused },
+ modifier = Modifier.weight(1f),
decorationBox = { inner ->
Box {
if (value.isEmpty()) {
@@ -359,18 +369,25 @@ fun GlassSearchField(
}
},
)
- if (value.isNotEmpty()) {
- Box(
- Modifier
- .size(48.dp)
+ Box(
+ Modifier
+ .size(48.dp)
+ .then(
+ if (value.isNotEmpty()) {
+ Modifier
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
role = Role.Button,
onClick = { onValueChange("") },
- ),
- contentAlignment = Alignment.Center,
- ) {
+ )
+ } else {
+ Modifier
+ }
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ if (value.isNotEmpty()) {
LumaIconView(
icon = LumaIcon.Close,
contentDescription = stringResource(Res.string.clear_search),
@@ -395,7 +412,9 @@ fun LiquidGlassPopupOverlay(
height: Dp = 152.dp,
expandToRight: Boolean = false,
placeBelowAnchor: Boolean = false,
- anchorGap: Dp = 6.dp,
+ autoPlaceAroundAnchor: Boolean = false,
+ anchorGap: Dp = 3.dp,
+ bottomInset: Dp = 0.dp,
containerColor: Color? = null,
borderColor: Color? = null,
content: @Composable ColumnScope.() -> Unit,
@@ -429,27 +448,6 @@ fun LiquidGlassPopupOverlay(
val menuHeight = initialSize + (height - initialSize) * heightProgress
val contentProgress = ((rawProgress - 0.34f) / 0.66f).coerceIn(0f, 1f)
val cornerRadius = initialSize / 2f + (18.dp - initialSize / 2f) * rawProgress
- val initialSizePx = with(density) { initialSize.toPx() }
- val anchorCenterX = anchorBounds.center.x - rootOrigin.x
- val anchorCenterY = anchorBounds.center.y - rootOrigin.y
- val menuWidthPx = with(density) { menuWidth.toPx() }
- val menuLeft = if (expandToRight) {
- anchorCenterX - initialSizePx / 2f
- } else {
- anchorCenterX + initialSizePx / 2f - menuWidthPx
- }
- val menuTop = if (placeBelowAnchor) {
- anchorBounds.bottom - rootOrigin.y + with(density) { anchorGap.toPx() }
- } else {
- anchorCenterY - initialSizePx / 2f
- }
- val menuRect = Rect(
- menuLeft,
- menuTop,
- menuLeft + menuWidthPx,
- menuTop + with(density) { menuHeight.toPx() },
- )
- val currentMenuRect by androidx.compose.runtime.rememberUpdatedState(menuRect)
val shape = RoundedCornerShape(cornerRadius)
val surface = containerColor ?: if (GlassTokens.isDark) {
Color(0xFF14171D).copy(alpha = 0.42f)
@@ -458,7 +456,46 @@ fun LiquidGlassPopupOverlay(
}
val border = borderColor ?: GlassTokens.glassBorder
- Box(modifier.fillMaxSize().zIndex(20f)) {
+ BoxWithConstraints(modifier = modifier.fillMaxSize().zIndex(20f)) {
+ val containerWidthPx = with(density) { maxWidth.toPx() }
+ val containerHeightPx = with(density) { maxHeight.toPx() }
+ val bottomInsetPx = with(density) { bottomBarContentPadding(bottomInset).calculateBottomPadding().toPx() }
+ val anchorLeft = anchorBounds.left - rootOrigin.x
+ val anchorTop = anchorBounds.top - rootOrigin.y
+ val anchorRight = anchorBounds.right - rootOrigin.x
+ val anchorBottom = anchorBounds.bottom - rootOrigin.y
+ val menuWidthPx = with(density) { menuWidth.toPx() }
+ val menuHeightPx = with(density) { menuHeight.toPx() }
+ val gapPx = with(density) { anchorGap.toPx() }
+ val safeBottomPx = containerHeightPx - bottomInsetPx
+ val initialSizePx = with(density) { initialSize.toPx() }
+ val anchorCenterX = (anchorLeft + anchorRight) / 2f
+ val anchorCenterY = (anchorTop + anchorBottom) / 2f
+ val menuLeft = if (autoPlaceAroundAnchor) {
+ val expandRight = (containerWidthPx - anchorRight) >= anchorLeft
+ if (expandRight) anchorRight + gapPx else anchorLeft - gapPx - menuWidthPx
+ } else if (expandToRight) {
+ anchorCenterX - initialSizePx / 2f
+ } else {
+ anchorCenterX + initialSizePx / 2f - menuWidthPx
+ }
+ val menuTop = if (autoPlaceAroundAnchor) {
+ val openBelow = (safeBottomPx - anchorBottom) >= anchorTop
+ if (openBelow) anchorBottom + gapPx else anchorTop - gapPx - menuHeightPx
+ } else if (placeBelowAnchor) {
+ anchorBottom + gapPx
+ } else {
+ anchorCenterY - initialSizePx / 2f
+ }
+ val menuLeftClamped = menuLeft.coerceIn(0f, (containerWidthPx - menuWidthPx).coerceAtLeast(0f))
+ val menuTopClamped = menuTop.coerceIn(0f, (containerHeightPx - menuHeightPx - bottomInsetPx).coerceAtLeast(0f))
+ val menuRect = Rect(
+ menuLeftClamped,
+ menuTopClamped,
+ menuLeftClamped + menuWidthPx,
+ menuTopClamped + menuHeightPx,
+ )
+ val currentMenuRect by androidx.compose.runtime.rememberUpdatedState(menuRect)
Box(
Modifier
.fillMaxSize()
@@ -470,7 +507,7 @@ fun LiquidGlassPopupOverlay(
)
Box(
Modifier
- .offset { IntOffset(menuLeft.roundToInt(), menuTop.roundToInt()) }
+ .offset { IntOffset(menuLeftClamped.roundToInt(), menuTopClamped.roundToInt()) }
.width(menuWidth)
.height(menuHeight)
.clip(shape)
@@ -546,6 +583,7 @@ fun LiquidGlassButton(
cornerRadius: Dp = 14.dp,
enabled: Boolean = true,
fill: Color = GlassTokens.glassFillStrong,
+ useBackdropGlass: Boolean = true,
content: @Composable RowScope.() -> Unit,
) {
val backdrop = LocalGlassBackdrop.current
@@ -565,12 +603,20 @@ fun LiquidGlassButton(
scaleY = scale
alpha = if (enabled) 1f else 0.52f
}
- .lumaGlass(
- backdrop = backdrop,
- radius = cornerRadius,
- level = LumaGlassLevel.Interactive,
- tint = fill,
- borderColor = GlassTokens.glassBorder,
+ .then(
+ if (useBackdropGlass) {
+ Modifier.lumaGlass(
+ backdrop = backdrop,
+ radius = cornerRadius,
+ level = LumaGlassLevel.Interactive,
+ tint = fill,
+ borderColor = GlassTokens.glassBorder,
+ )
+ } else {
+ Modifier.clip(shape)
+ .background(fill, shape)
+ .border(0.7.dp, GlassTokens.glassBorder, shape)
+ }
)
.clickable(enabled = enabled, indication = null, interactionSource = interactionSource, onClick = onClick)
.padding(horizontal = 16.dp, vertical = 11.dp),
@@ -588,10 +634,13 @@ fun GlassButton(
enabled: Boolean = true,
accent: Boolean = false,
danger: Boolean = false,
+ clear: Boolean = false,
+ useBackdropGlass: Boolean = true,
) {
val fill = when {
danger -> GlassTokens.danger.copy(alpha = 0.84f)
accent -> GlassTokens.accent.copy(alpha = 0.78f)
+ clear -> Color.Transparent
else -> GlassTokens.glassFillStrong
}
LiquidGlassButton(
@@ -599,6 +648,7 @@ fun GlassButton(
modifier = modifier,
enabled = enabled,
fill = fill,
+ useBackdropGlass = useBackdropGlass,
) {
Text(
text = text,
@@ -615,10 +665,19 @@ fun GlassDialog(
backdrop: Backdrop? = null,
modifier: Modifier = Modifier,
surfaceTint: Color? = null,
+ surfaceOverlay: Color? = null,
blurRadius: Dp? = null,
+ dimBehind: Boolean = true,
+ useBackdropGlass: Boolean = true,
content: @Composable () -> Unit,
) {
Dialog(onDismissRequest = onDismissRequest) {
+ if (!dimBehind) {
+ val dialogWindow = (LocalView.current.parent as? DialogWindowProvider)?.window
+ SideEffect {
+ dialogWindow?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ }
+ }
val dialogCtx = LocalContext.current
val localizedCtx = remember(AppLanguage.language) { AppLanguage.getLocalizedContext(dialogCtx) }
val glassBackdrop = backdrop ?: LocalGlassBackdrop.current
@@ -628,15 +687,28 @@ fun GlassDialog(
) {
Box(
modifier
- .width(IntrinsicSize.Min)
- .lumaGlass(
- backdrop = glassBackdrop,
- radius = 24.dp,
- level = LumaGlassLevel.Strong,
- tint = surfaceTint,
- borderColor = GlassTokens.glassBorder,
- shadowElevation = 10.dp,
- blurOverride = blurRadius,
+ .then(
+ if (useBackdropGlass) {
+ Modifier.lumaGlass(
+ backdrop = glassBackdrop,
+ radius = 24.dp,
+ level = LumaGlassLevel.Strong,
+ tint = surfaceTint,
+ borderColor = GlassTokens.glassBorder,
+ shadowElevation = 10.dp,
+ blurOverride = blurRadius,
+ )
+ } else {
+ val shape = RoundedCornerShape(24.dp)
+ Modifier.clip(shape)
+ .background(surfaceTint ?: Color.Transparent, shape)
+ .border(0.7.dp, GlassTokens.glassBorder, shape)
+ }
+ )
+ .then(
+ surfaceOverlay?.let {
+ Modifier.background(it, RoundedCornerShape(24.dp))
+ } ?: Modifier,
)
.padding(20.dp),
) {
@@ -732,6 +804,8 @@ fun GlassInputField(
isError: Boolean = false,
errorMessage: String? = null,
focusRequester: FocusRequester? = null,
+ containerTint: Color = GlassTokens.glassFillSoft,
+ useBackdropGlass: Boolean = true,
) {
val backdrop = LocalGlassBackdrop.current
Column(modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
@@ -740,12 +814,18 @@ fun GlassInputField(
Box(
Modifier.fillMaxWidth()
.heightIn(min = 48.dp)
- .lumaGlass(
- backdrop = backdrop,
- radius = 12.dp,
- level = LumaGlassLevel.Clear,
- tint = GlassTokens.glassFillSoft,
- border = false,
+ .then(
+ if (useBackdropGlass) {
+ Modifier.lumaGlass(
+ backdrop = backdrop,
+ radius = 12.dp,
+ level = LumaGlassLevel.Clear,
+ tint = containerTint,
+ border = false,
+ )
+ } else {
+ Modifier.clip(shape).background(containerTint, shape)
+ }
)
.border(0.7.dp, if (isError) GlassTokens.danger else GlassTokens.glassBorder, shape)
.padding(horizontal = 14.dp, vertical = 12.dp),
diff --git a/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt b/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt
index 87ffd5e..3419e00 100644
--- a/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt
+++ b/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt
@@ -596,6 +596,7 @@ fun LumaEmptyState(
modifier: Modifier = Modifier,
actionLabel: String? = null,
onAction: (() -> Unit)? = null,
+ showIconBackground: Boolean = true,
) {
Column(
modifier,
@@ -605,7 +606,13 @@ fun LumaEmptyState(
Box(
Modifier
.size(72.dp)
- .lumaGlass(radius = 24.dp, level = LumaGlassLevel.Clear),
+ .then(
+ if (showIconBackground) {
+ Modifier.lumaGlass(radius = 24.dp, level = LumaGlassLevel.Clear)
+ } else {
+ Modifier
+ }
+ ),
contentAlignment = Alignment.Center,
) {
LumaIconView(icon, modifier = Modifier.size(30.dp), tint = LumaColors.textTertiary)
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index 51cae5e..1cbe6f8 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -258,7 +258,7 @@
Icon Library Manager
Add
No icon libraries yet
- Tap top-left to add
+ Tap top-right to add
Icons not loaded
No icons
Preview · Edit
@@ -334,7 +334,17 @@
Actions
Edit
Change Icon
+ Switch Line
+ Primary Line
+ Alternate Line
+ Current line
Just now
+ Accessed just now
+ Accessed %1$d min ago
+ Accessed %1$d hr ago
+ Accessed %1$d days ago
+ Movies %1$d · Shows %2$d
+ Movies -- · Shows --
Toggle View
Icon Library
Add Server
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f28c373..be0c68e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -258,7 +258,7 @@
图标库管理
添加
还没有图标库
- 点击左上角添加
+ 点击右上角添加
图标未加载
暂无图标
预览 · 修改
@@ -334,7 +334,17 @@
操作
编辑
更换图标
+ 切换线路
+ 主线路
+ 备用线路
+ 当前线路
刚刚
+ 刚刚访问
+ %1$d 分钟前访问
+ %1$d 小时前访问
+ %1$d 天前访问
+ 电影 %1$d · 节目 %2$d
+ 电影 -- · 节目 --
视图切换
图标库管理
添加服务器
diff --git a/app/src/test/kotlin/com/example/myapplication/data/EmbyServerAddressTest.kt b/app/src/test/kotlin/com/example/myapplication/data/EmbyServerAddressTest.kt
index 29139c4..7da9feb 100644
--- a/app/src/test/kotlin/com/example/myapplication/data/EmbyServerAddressTest.kt
+++ b/app/src/test/kotlin/com/example/myapplication/data/EmbyServerAddressTest.kt
@@ -53,4 +53,40 @@ class EmbyServerAddressTest {
assertEquals("https://emby.example.com:443/emby", server.fullUrl)
}
+
+ @Test
+ fun selectedAlternateAddressDrivesUrlAndDisplayHost() {
+ val alternate = EmbyServerAddress(
+ id = "remote",
+ name = "Remote",
+ protocol = "http",
+ host = "remote.example.com",
+ port = "8096",
+ basePath = "emby",
+ )
+ val server = EmbyServer(
+ id = "test",
+ host = "home.example.com",
+ port = "443",
+ alternateAddresses = listOf(alternate),
+ activeAddressId = alternate.id,
+ )
+
+ assertEquals("http://remote.example.com:8096/emby", server.fullUrl)
+ assertEquals("remote.example.com:8096", server.displayHost)
+ }
+
+ @Test
+ fun unknownAlternateAddressFallsBackToPrimary() {
+ val server = EmbyServer(
+ id = "test",
+ protocol = "https",
+ host = "home.example.com",
+ port = "443",
+ activeAddressId = "missing",
+ )
+
+ assertEquals("https://home.example.com:443", server.fullUrl)
+ assertEquals("home.example.com:443", server.displayHost)
+ }
}