package com.example.myapplication.screens import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize 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.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.CircularProgressIndicator import com.example.myapplication.ui.GlassButton import com.example.myapplication.ui.GlassDialog import com.example.myapplication.ui.GlassInputField import com.example.myapplication.ui.GlassPopup import com.example.myapplication.ui.GlassPopupItem import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.CompositionLocalProvider import com.example.myapplication.ui.AppLanguage import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.window.Dialog import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.res.stringResource import com.example.myapplication.R as Res import top.yukonga.miuix.kmp.basic.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.GridView import androidx.compose.material.icons.filled.ViewAgenda import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.MoreHoriz import androidx.compose.material.icons.outlined.PhotoLibrary import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Image import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff import com.example.myapplication.data.EmbyServer import com.example.myapplication.data.IconItem import com.example.myapplication.data.IconLibrary import com.example.myapplication.data.IconLibraryRepository import com.example.myapplication.ui.GlassSurface import com.example.myapplication.ui.GlassTokens import com.example.myapplication.ui.LiquidGlassTopBar import com.example.myapplication.ui.bottomBarContentPadding import coil.compose.AsyncImage import coil.request.ImageRequest import kotlinx.coroutines.delay import kotlinx.coroutines.launch private val serverTints = listOf( Color(0xFF9CC4FF), Color(0xFFA8E6C0), Color(0xFFD6B8FF), Color(0xFFFFB8D0), ) private val onlineGreen = Color(0xFF34C759) private val offlineRed = Color(0xFFFF3B30) @Composable fun LibraryTopBar( isGrid: Boolean, onViewToggle: () -> Unit, onAdd: () -> Unit, onOpenIconLibraryManager: () -> Unit, modifier: Modifier = Modifier, ) { LiquidGlassTopBar(modifier) { Row( Modifier.fillMaxWidth().padding(horizontal = 18.dp, vertical = 12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Text(text = "\u8D44\u6E90\u5E93", color = GlassTokens.text, fontSize = 20.sp, fontWeight = FontWeight.Bold) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Box(Modifier.size(38.dp).clip(CircleShape).background(GlassTokens.glassFill).border(1.dp, GlassTokens.glassBorder, CircleShape).clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { onViewToggle() }, contentAlignment = Alignment.Center) { Icon(imageVector = if (isGrid) Icons.Filled.GridView else Icons.Filled.ViewAgenda, contentDescription = "\u89C6\u56FE\u5207\u6362", tint = GlassTokens.text, modifier = Modifier.size(21.dp)) } Box(Modifier.size(38.dp).clip(CircleShape).background(GlassTokens.glassFill).border(1.dp, GlassTokens.glassBorder, CircleShape).clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { onOpenIconLibraryManager() }, contentAlignment = Alignment.Center) { Icon(imageVector = Icons.Outlined.PhotoLibrary, contentDescription = "\u56FE\u6807\u5E93\u7BA1\u7406", tint = GlassTokens.text, modifier = Modifier.size(21.dp)) } Box(Modifier.size(38.dp).clip(CircleShape).background(GlassTokens.glassFill).border(1.dp, GlassTokens.glassBorder, CircleShape).clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { onAdd() }, contentAlignment = Alignment.Center) { Icon(imageVector = Icons.Outlined.Add, contentDescription = "\u6DFB\u52A0\u670D\u52A1\u5668", tint = GlassTokens.text, modifier = Modifier.size(21.dp)) } } } } } /* ------------------------------------------------------------------ */ /* Icon Library Section */ /* ------------------------------------------------------------------ */ @Composable private fun ServerCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onChangeIcon: () -> Unit, onDelete: () -> Unit, onEdit: () -> Unit) { var showPopup by remember { mutableStateOf(false) } var clicked by remember { mutableStateOf(false) } val pressBg by animateColorAsState(targetValue = if (clicked) Color.White.copy(alpha = 0.18f) else Color.Transparent, animationSpec = tween(durationMillis = if (clicked) 80 else 200), label = "pb") LaunchedEffect(clicked) { if (clicked) { delay(150); clicked = false } } GlassSurface(modifier = Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { clicked = true; onClick() }, fill = tint.copy(alpha = 0.50f), cornerRadius = 20.dp, padding = PaddingValues(0.dp)) { Row(modifier = Modifier.fillMaxWidth().background(pressBg).padding(14.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) { if (server.iconUrl.isNotBlank()) { AsyncImage(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 { Box(Modifier.size(46.dp).clip(RoundedCornerShape(14.dp)).background(Brush.linearGradient(listOf(tint, tint.copy(alpha = 0.7f)))), contentAlignment = Alignment.Center) { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null, tint = Color.White, modifier = Modifier.size(26.dp)) } } 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 { "\u5728\u7EBF" } else "\u79BB\u7EBF", color = GlassTokens.subtext, fontSize = 12.sp) } 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), ) } } } Box { Icon(imageVector = Icons.Outlined.MoreHoriz, contentDescription = "\u64CD\u4F5C", tint = GlassTokens.subtext, modifier = Modifier.size(32.dp).clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { showPopup = true }.padding(4.dp)) GlassPopup(expanded = showPopup, onDismissRequest = { showPopup = false }) { GlassPopupItem(text = "\u7F16\u8F91", icon = Icons.Outlined.Edit, onClick = { showPopup = false; onEdit() }); GlassPopupItem(text = "\u66F4\u6362\u56FE\u6807", icon = Icons.Outlined.Image, onClick = { showPopup = false; onChangeIcon() }); GlassPopupItem(text = "\u5220\u9664", icon = Icons.Outlined.Delete, tint = offlineRed, onClick = { showPopup = false; onDelete() }) } } } } } /* ------------------------------------------------------------------ */ /* Icon Library Section */ /* ------------------------------------------------------------------ */ @Composable private fun ServerGridCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onChangeIcon: () -> Unit, onDelete: () -> Unit, onEdit: () -> Unit) { var showPopup by remember { mutableStateOf(false) } var clicked by remember { mutableStateOf(false) } val pressBg by animateColorAsState(targetValue = if (clicked) Color.White.copy(alpha = 0.18f) else Color.Transparent, animationSpec = tween(durationMillis = if (clicked) 80 else 200), label = "pb") LaunchedEffect(clicked) { if (clicked) { delay(150); clicked = false } } GlassSurface(modifier = Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { clicked = true; onClick() }, fill = tint.copy(alpha = 0.50f), cornerRadius = 20.dp, padding = PaddingValues(0.dp)) { Column(modifier = Modifier.fillMaxWidth().background(pressBg).padding(14.dp), horizontalAlignment = Alignment.Start) { Row(verticalAlignment = Alignment.CenterVertically) { if (server.iconUrl.isNotBlank()) { AsyncImage( model = ImageRequest.Builder(LocalContext.current).data(server.iconUrl).crossfade(true).build(), contentDescription = null, modifier = Modifier.size(48.dp).clip(RoundedCornerShape(14.dp)), contentScale = ContentScale.Crop, ) } else { Box( Modifier.size(48.dp).clip(RoundedCornerShape(14.dp)).background(Brush.linearGradient(listOf(tint, tint.copy(alpha = 0.7f)))), contentAlignment = Alignment.Center, ) { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null, tint = Color.White, modifier = Modifier.size(26.dp)) } } Spacer(Modifier.width(10.dp)) Column(Modifier.weight(1f)) { Text( text = server.name.ifBlank { server.displayHost }, color = GlassTokens.text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, 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 { "\u5728\u7EBF" } else "\u79BB\u7EBF", color = GlassTokens.subtext, fontSize = 11.sp) } } } Box { Icon(imageVector = Icons.Outlined.MoreHoriz, contentDescription = null, tint = GlassTokens.subtext, modifier = Modifier.size(28.dp).clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { showPopup = true }.padding(4.dp)) GlassPopup(expanded = showPopup, onDismissRequest = { showPopup = false }) { GlassPopupItem(text = "\u7F16\u8F91", icon = Icons.Outlined.Edit, onClick = { showPopup = false; onEdit() }); GlassPopupItem(text = "\u66F4\u6362\u56FE\u6807", icon = Icons.Outlined.Image, onClick = { showPopup = false; onChangeIcon() }); GlassPopupItem(text = "\u5220\u9664", icon = Icons.Outlined.Delete, tint = offlineRed, onClick = { showPopup = false; onDelete() }) } } } 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), ) } } } } /* ------------------------------------------------------------------ */ /* Icon Library Section */ /* ------------------------------------------------------------------ */ @Composable fun LibraryScreen( servers: List, selectedServer: EmbyServer? = null, onSelectServer: (String) -> Unit = {}, onAddServer: (EmbyServer, (String?) -> Unit) -> Unit = { _, _ -> }, onDeleteServer: (String) -> Unit = {}, onUpdateServer: (EmbyServer, (String?) -> Unit) -> Unit = { _, _ -> }, showAddDialog: Boolean = false, onDismissAddDialog: () -> Unit = {}, isGrid: Boolean = false, onChangeIconTarget: EmbyServer? = null, onDismissChangeIcon: () -> Unit = {}, onPickIcon: (EmbyServer, String) -> Unit = { _, _ -> }, onRequestChangeIcon: (EmbyServer) -> Unit = {}, ) { var editingServer by remember { mutableStateOf(null) } if (showAddDialog) { AddServerDialog(onDismiss = onDismissAddDialog, onConfirm = { srv, onResult -> onAddServer(srv, onResult) }) } if (editingServer != null) { AddServerDialog( initial = editingServer, onDismiss = { editingServer = null }, onConfirm = { updated, onResult -> onUpdateServer(updated, onResult) }, ) } if (onChangeIconTarget != null) { IconPickerDialog( server = onChangeIconTarget, onDismiss = onDismissChangeIcon, onPick = { srv, url -> onPickIcon(srv, url); onDismissChangeIcon() }, ) } LazyVerticalGrid( columns = if (isGrid) GridCells.Fixed(2) else GridCells.Fixed(1), modifier = Modifier.fillMaxSize().padding(horizontal = if (isGrid) 16.dp else 0.dp), contentPadding = PaddingValues(top = 16.dp, bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()), verticalArrangement = Arrangement.spacedBy(if (isGrid) 12.dp else 10.dp), horizontalArrangement = Arrangement.spacedBy(if (isGrid) 12.dp else 0.dp), ) { // Servers section items(servers.size) { index -> val server = servers[index] val tint = serverTints[index % serverTints.size] if (isGrid) { ServerGridCard( server = server, tint = tint, onClick = { onSelectServer(server.id) }, onChangeIcon = { onRequestChangeIcon(server) }, onDelete = { onDeleteServer(server.id) }, onEdit = { editingServer = server }, ) } else { ServerCard( server = server, tint = tint, onClick = { onSelectServer(server.id) }, onChangeIcon = { onRequestChangeIcon(server) }, onDelete = { onDeleteServer(server.id) }, onEdit = { editingServer = server }, ) } } } } /* ------------------------------------------------------------------ */ /* Icon Library Section */ /* ------------------------------------------------------------------ */ @Composable fun IconLibraryManagerScreen( libraries: List, modifier: Modifier = Modifier, onBack: () -> Unit = {}, onAddIconLibrary: (String, String, List, Boolean) -> Unit = { _, _, _, _ -> }, onUpdateIconLibrary: (IconLibrary) -> Unit = { _ -> }, onDeleteIconLibrary: (String) -> Unit = {}, onRefreshIconLibrary: (String) -> Unit = {}, ) { var showAddDialog by remember { mutableStateOf(false) } var editingLibraryId by remember { mutableStateOf(null) } val editingLibrary = editingLibraryId?.let { id -> libraries.find { it.id == id } } Column(modifier.fillMaxSize()) { LiquidGlassTopBar { Row( Modifier.fillMaxWidth().padding(horizontal = 18.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { Box(Modifier.width(86.dp)) { Row( Modifier.clip(RoundedCornerShape(20.dp)) .background(GlassTokens.accent.copy(alpha = 0.15f)) .border(1.dp, GlassTokens.accent.copy(alpha = 0.38f), RoundedCornerShape(20.dp)) .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { showAddDialog = true } .padding(horizontal = 10.dp, vertical = 7.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(3.dp), ) { Icon(Icons.Outlined.Add, contentDescription = null, tint = GlassTokens.accent, modifier = Modifier.size(17.dp)) Text("\u6DFB\u52A0", color = GlassTokens.accent, fontSize = 13.sp, fontWeight = FontWeight.SemiBold) } } Box(Modifier.weight(1f), contentAlignment = Alignment.Center) { Text("\u56FE\u6807\u5E93\u7BA1\u7406", color = GlassTokens.text, fontSize = 19.sp, fontWeight = FontWeight.Bold) } Box(Modifier.width(86.dp), contentAlignment = Alignment.CenterEnd) { Box( Modifier.size(34.dp) .clip(CircleShape) .background(Color.White.copy(alpha = 0.42f)) .border(1.dp, Color.White.copy(alpha = 0.55f), CircleShape) .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { onBack() }, contentAlignment = Alignment.Center, ) { Icon(Icons.Outlined.Close, contentDescription = "\u8FD4\u56DE", tint = GlassTokens.text, modifier = Modifier.size(19.dp)) } } } } LazyVerticalGrid( columns = GridCells.Fixed(2), modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp), contentPadding = PaddingValues(top = 16.dp, bottom = 28.dp), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { if (libraries.isEmpty()) { item(span = { GridItemSpan(2) }) { Box( Modifier.fillMaxWidth() .clip(RoundedCornerShape(22.dp)) .background(Color.White.copy(alpha = 0.45f)) .border(0.5.dp, Color.White.copy(alpha = 0.58f), RoundedCornerShape(22.dp)) .padding(vertical = 54.dp), contentAlignment = Alignment.Center, ) { Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) { Icon(Icons.Outlined.Image, contentDescription = null, tint = GlassTokens.subtext, modifier = Modifier.size(32.dp)) Text("\u8FD8\u6CA1\u6709\u56FE\u6807\u5E93", color = GlassTokens.subtext, fontSize = 15.sp) Text("\u70B9\u51FB\u5DE6\u4E0A\u89D2\u6DFB\u52A0", color = GlassTokens.subtext.copy(alpha = 0.75f), fontSize = 12.sp) } } } } else { items(libraries) { library -> IconLibraryManagerCard(library = library, onClick = { editingLibraryId = library.id }) } } } } if (showAddDialog) { AddIconLibraryDialog( onDismiss = { showAddDialog = false }, onSave = { name, url, icons, isDefault -> onAddIconLibrary(name, url, icons, isDefault) showAddDialog = false }, ) } if (editingLibrary != null) { ManageIconLibraryDialog( library = editingLibrary, onDismiss = { editingLibraryId = null }, onUpdate = { updated -> onUpdateIconLibrary(updated) editingLibraryId = null }, onDelete = { onDeleteIconLibrary(editingLibrary.id) editingLibraryId = null }, onRefresh = { onRefreshIconLibrary(editingLibrary.id) }, ) } } @Composable private fun IconLibraryManagerCard(library: IconLibrary, onClick: () -> Unit) { Box( Modifier.fillMaxWidth() .clip(RoundedCornerShape(20.dp)) .background( Brush.verticalGradient( 0f to Color.White.copy(alpha = 0.74f), 1f to Color.White.copy(alpha = 0.50f), ) ) .border(0.5.dp, Color.White.copy(alpha = 0.62f), RoundedCornerShape(20.dp)) .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = onClick) .padding(14.dp), ) { Column { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { Box( Modifier.size(38.dp).clip(RoundedCornerShape(12.dp)) .background(Brush.linearGradient(listOf(Color(0xFFFF6B9D), Color(0xFFFF8A65)))), contentAlignment = Alignment.Center, ) { Icon(Icons.Outlined.Image, contentDescription = null, tint = Color.White, modifier = Modifier.size(19.dp)) } Column(Modifier.weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) { Text(library.name, color = GlassTokens.text, fontSize = 15.sp, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) if (library.isDefault) { Box( Modifier.clip(RoundedCornerShape(50)) .background(GlassTokens.accent.copy(alpha = 0.14f)) .border(0.5.dp, GlassTokens.accent.copy(alpha = 0.35f), RoundedCornerShape(50)) .padding(horizontal = 6.dp, vertical = 2.dp) ) { Text(stringResource(Res.string.icon_default_badge), color = GlassTokens.accent, fontSize = 10.sp, fontWeight = FontWeight.SemiBold) } } } Text("${library.iconCount} \u4E2A\u56FE\u6807", color = GlassTokens.subtext, fontSize = 11.sp) } } Spacer(Modifier.height(11.dp)) Row(horizontalArrangement = Arrangement.spacedBy((-5).dp), verticalAlignment = Alignment.CenterVertically) { library.icons.take(4).forEach { icon -> Box( Modifier.size(34.dp).clip(RoundedCornerShape(9.dp)) .background(Color.White.copy(alpha = 0.55f)) .border(0.5.dp, Color.White.copy(alpha = 0.60f), RoundedCornerShape(9.dp)), contentAlignment = Alignment.Center, ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current).data(icon.url).crossfade(true).build(), contentDescription = icon.name, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize().padding(4.dp).clip(RoundedCornerShape(6.dp)), ) } } if (library.iconCount > 0 && library.icons.isEmpty()) { Box( Modifier.weight(1f).height(34.dp).clip(RoundedCornerShape(9.dp)) .background(Color.White.copy(alpha = 0.30f)), contentAlignment = Alignment.Center, ) { Text("\u56fe\u6807\u672a\u52a0\u8f7d", color = GlassTokens.subtext, fontSize = 11.sp) } } else if (library.icons.isEmpty()) { Box( Modifier.weight(1f).height(34.dp).clip(RoundedCornerShape(9.dp)) .background(Color.White.copy(alpha = 0.30f)), contentAlignment = Alignment.Center, ) { Text("\u6682\u65E0\u56FE\u6807", color = GlassTokens.subtext, fontSize = 11.sp) } } else if (library.iconCount > 4) { Box( Modifier.size(34.dp).clip(RoundedCornerShape(9.dp)) .background(GlassTokens.accent.copy(alpha = 0.12f)), contentAlignment = Alignment.Center, ) { Text("+${library.iconCount - 4}", color = GlassTokens.accent, fontSize = 10.sp, fontWeight = FontWeight.Bold) } } } Spacer(Modifier.height(11.dp)) Box( Modifier.fillMaxWidth().clip(RoundedCornerShape(12.dp)) .background(Color.White.copy(alpha = 0.35f)) .border(0.5.dp, Color.White.copy(alpha = 0.48f), RoundedCornerShape(12.dp)) .padding(vertical = 7.dp), contentAlignment = Alignment.Center, ) { Text("\u9884\u89C8 \u00B7 \u4FEE\u6539", color = GlassTokens.text, fontSize = 12.sp, fontWeight = FontWeight.Medium) } } } } @Composable private fun IconLibrarySection( libraries: List, isGrid: Boolean, onClick: (IconLibrary) -> Unit, onManage: (IconLibrary) -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Text("\u56FE\u6807\u5E93", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = GlassTokens.text) Text("${libraries.size} \u4E2A", fontSize = 13.sp, color = GlassTokens.subtext) } if (isGrid) { // Grid layout for icon libraries — use Column+Row to avoid nested scrolling Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { libraries.chunked(2).forEach { row -> Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { row.forEach { lib -> IconLibraryCard( library = lib, modifier = Modifier.weight(1f), onClick = { onClick(lib) }, onLongClick = { onManage(lib) }, ) } // Fill remaining space if odd number if (row.size < 2) { Spacer(Modifier.weight(1f)) } } } } } else { // List layout for icon libraries — use Row to avoid nested scrolling Row( horizontalArrangement = Arrangement.spacedBy(12.dp), ) { libraries.forEach { lib -> IconLibraryCard( library = lib, modifier = Modifier.width(140.dp), onClick = { onClick(lib) }, onLongClick = { onManage(lib) }, ) } } } } } @Composable private fun IconLibraryCard( library: IconLibrary, modifier: Modifier = Modifier, onClick: () -> Unit, onLongClick: () -> Unit, ) { Box( modifier = modifier .clip(RoundedCornerShape(18.dp)) .background( Brush.verticalGradient( 0f to Color.White.copy(alpha = 0.72f), 1f to Color.White.copy(alpha = 0.48f), ) ) .border(0.5.dp, Color.White.copy(alpha = 0.6f), RoundedCornerShape(18.dp)) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = onClick, ) .padding(12.dp), ) { Column { // Icon preview row Row( horizontalArrangement = Arrangement.spacedBy((-6).dp), verticalAlignment = Alignment.CenterVertically, ) { library.icons.take(3).forEach { icon -> Box( Modifier.size(30.dp).clip(RoundedCornerShape(8.dp)) .background(Brush.verticalGradient(listOf(Color(0xFFFF6B9D).copy(alpha = 0.3f), Color(0xFFFF8A65).copy(alpha = 0.3f)))) .border(0.5.dp, Color.White.copy(alpha = 0.5f), RoundedCornerShape(8.dp)), contentAlignment = Alignment.Center, ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current).data(icon.url).crossfade(true).build(), contentDescription = icon.name, contentScale = ContentScale.Crop, modifier = Modifier.size(24.dp).clip(RoundedCornerShape(6.dp)), ) } } if (library.iconCount > 3) { Box( Modifier.size(30.dp).clip(RoundedCornerShape(8.dp)) .background(Brush.verticalGradient(listOf(Color(0xFF9B9B9B).copy(alpha = 0.2f), Color(0xFF9B9B9B).copy(alpha = 0.1f)))) .border(0.5.dp, Color.White.copy(alpha = 0.5f), RoundedCornerShape(8.dp)), contentAlignment = Alignment.Center, ) { Text("+${library.iconCount - 3}", fontSize = 10.sp, color = GlassTokens.subtext, fontWeight = FontWeight.Medium) } } } Spacer(Modifier.height(8.dp)) Text(library.name, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = GlassTokens.text, maxLines = 1, overflow = TextOverflow.Ellipsis) Text("${library.iconCount} \u4E2A\u56FE\u6807", fontSize = 11.sp, color = GlassTokens.subtext) Spacer(Modifier.height(6.dp)) // Manage button Box( modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp)) .background(Brush.verticalGradient(listOf(GlassTokens.accent.copy(alpha = 0.12f), GlassTokens.accent.copy(alpha = 0.06f)))) .border(0.5.dp, GlassTokens.accent.copy(alpha = 0.2f), RoundedCornerShape(10.dp)) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = onLongClick, ) .padding(vertical = 6.dp), contentAlignment = Alignment.Center, ) { Text("\u7BA1\u7406", fontSize = 12.sp, fontWeight = FontWeight.Medium, color = GlassTokens.accent) } } } } /* ------------------------------------------------------------------ */ /* Icon Library Detail Dialog — browse icons */ /* ------------------------------------------------------------------ */ @Composable private fun IconLibraryDetailDialog(library: IconLibrary, onDismiss: () -> Unit) { Dialog(onDismissRequest = onDismiss) { val dialogCtx = LocalContext.current val localizedCtx = remember(AppLanguage.language) { AppLanguage.getLocalizedContext(dialogCtx) } CompositionLocalProvider(LocalContext provides localizedCtx) { Box( Modifier .width(340.dp) .shadow(24.dp, RoundedCornerShape(28.dp), ambientColor = Color.White.copy(alpha = 0.18f), spotColor = Color.Black.copy(alpha = 0.05f)) .clip(RoundedCornerShape(28.dp)) .background( Brush.verticalGradient(0f to Color.White.copy(alpha = 0.88f), 0.5f to Color.White.copy(alpha = 0.75f), 1f to Color.White.copy(alpha = 0.65f)) ) .border(0.5.dp, Color.White.copy(alpha = 0.6f), RoundedCornerShape(28.dp)) .padding(20.dp) ) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { // Header Row(verticalAlignment = Alignment.CenterVertically) { Box( Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)) .background(Brush.linearGradient(listOf(Color(0xFFFF6B9D), Color(0xFFFF8A65)))), contentAlignment = Alignment.Center, ) { Icon(Icons.Outlined.Image, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp)) } Spacer(Modifier.width(12.dp)) Column(Modifier.weight(1f)) { Text(library.name, fontWeight = FontWeight.Bold, fontSize = 18.sp, color = GlassTokens.text) Text("${library.iconCount} \u4E2A\u56FE\u6807", fontSize = 12.sp, color = GlassTokens.subtext) } } if (library.iconCount > 0 && library.icons.isEmpty()) { Box( modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.35f), 1f to Color.White.copy(alpha = 0.20f))) .border(0.5.dp, Color.White.copy(alpha = 0.4f), RoundedCornerShape(16.dp)) .padding(vertical = 36.dp), contentAlignment = Alignment.Center, ) { Text("\u56fe\u6807\u672a\u52a0\u8f7d", color = GlassTokens.subtext, fontSize = 14.sp) } } else if (library.icons.isEmpty()) { Box( modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.35f), 1f to Color.White.copy(alpha = 0.20f))) .border(0.5.dp, Color.White.copy(alpha = 0.4f), RoundedCornerShape(16.dp)) .padding(vertical = 36.dp), contentAlignment = Alignment.Center, ) { Text("\u6682\u65E0\u56FE\u6807", color = GlassTokens.subtext, fontSize = 14.sp) } } else { Box( Modifier.fillMaxWidth().clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.35f), 1f to Color.White.copy(alpha = 0.20f))) .border(0.5.dp, Color.White.copy(alpha = 0.4f), RoundedCornerShape(16.dp)) .padding(8.dp) ) { LazyVerticalGrid( columns = GridCells.Fixed(5), modifier = Modifier.heightIn(min = 120.dp, max = 400.dp), horizontalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp), ) { items(library.icons) { icon -> Box( modifier = Modifier.aspectRatio(1f).clip(RoundedCornerShape(10.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.55f), 1f to Color.White.copy(alpha = 0.35f))) .border(0.5.dp, Color.White.copy(alpha = 0.5f), RoundedCornerShape(10.dp)) .padding(4.dp), contentAlignment = Alignment.Center, ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current).data(icon.url).crossfade(true).build(), contentDescription = icon.name, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(8.dp)), ) } } } } } // Close button Box( modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.45f), 1f to Color.White.copy(alpha = 0.25f))) .border(0.5.dp, Color.White.copy(alpha = 0.5f), RoundedCornerShape(16.dp)) .clickable { onDismiss() } .padding(vertical = 14.dp), contentAlignment = Alignment.Center, ) { Text("\u5173\u95ED", color = GlassTokens.subtext, fontSize = 15.sp, fontWeight = FontWeight.Medium) } } } } } } /* ------------------------------------------------------------------ */ /* Manage Icon Library Dialog — rename, refresh, delete */ /* ------------------------------------------------------------------ */ @Composable private fun ManageIconLibraryDialog( library: IconLibrary, onDismiss: () -> Unit, onUpdate: (IconLibrary) -> Unit, onDelete: () -> Unit, onRefresh: () -> Unit, ) { var editName by remember { mutableStateOf(TextFieldValue(library.name)) } var editUrl by remember { mutableStateOf(TextFieldValue(library.sourceUrl)) } var setAsDefault by remember { mutableStateOf(library.isDefault) } var isRefreshing by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() GlassDialog(onDismissRequest = onDismiss) { Column( modifier = Modifier.width(300.dp) .heightIn(max = (LocalConfiguration.current.screenHeightDp * 0.78f).dp) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp), ) { // Header Row(verticalAlignment = Alignment.CenterVertically) { Box( Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)) .background(Brush.linearGradient(listOf(Color(0xFFFF6B9D), Color(0xFFFF8A65)))), contentAlignment = Alignment.Center, ) { Icon(Icons.Outlined.Image, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp)) } Spacer(Modifier.width(10.dp)) Text(text = "\u7BA1\u7406\u56FE\u6807\u5E93", fontWeight = FontWeight.Bold, fontSize = 18.sp, color = GlassTokens.text) } // Info Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { Column { Text("\u56FE\u6807\u6570\u91CF", fontSize = 12.sp, color = GlassTokens.subtext) Text("${library.iconCount}", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = GlassTokens.text) } } // Editable library metadata GlassInputField(label = "\u56FE\u6807\u5E93\u540D\u79F0", value = editName, placeholder = "\u56FE\u6807\u5E93\u540D\u79F0", onValueChange = { editName = it }) GlassInputField(label = "\u56FE\u6807\u5E93\u5730\u5740 (JSON)", value = editUrl, placeholder = "https://...", onValueChange = { editUrl = it }) // Default library switch Row( Modifier.fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(if (setAsDefault) GlassTokens.accent.copy(alpha = 0.12f) else Color.White.copy(alpha = 0.35f)) .border(0.5.dp, if (setAsDefault) GlassTokens.accent.copy(alpha = 0.35f) else Color.White.copy(alpha = 0.55f), RoundedCornerShape(16.dp)) .clickable { setAsDefault = !setAsDefault } .padding(horizontal = 14.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { Column(Modifier.weight(1f)) { Text(stringResource(Res.string.set_default_library), color = GlassTokens.text, fontSize = 14.sp, fontWeight = FontWeight.Medium) Text(stringResource(Res.string.set_default_library_sub), color = GlassTokens.subtext, fontSize = 11.sp) } Box( Modifier.size(width = 42.dp, height = 25.dp) .clip(CircleShape) .background(if (setAsDefault) GlassTokens.accent else Color.White.copy(alpha = 0.65f)) .border(0.5.dp, if (setAsDefault) Color.Transparent else Color.White.copy(alpha = 0.75f), CircleShape) .padding(3.dp), contentAlignment = if (setAsDefault) Alignment.CenterEnd else Alignment.CenterStart, ) { Box(Modifier.size(19.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.92f))) } } // Icon preview if (library.icons.isNotEmpty()) { Text("\u56FE\u6807\u9884\u89C8", fontSize = 13.sp, color = GlassTokens.subtext, fontWeight = FontWeight.Medium) LazyVerticalGrid( columns = GridCells.Fixed(6), modifier = Modifier.heightIn(min = 80.dp, max = 180.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { items(library.icons) { icon -> Box( modifier = Modifier.aspectRatio(1f).clip(RoundedCornerShape(8.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.55f), 1f to Color.White.copy(alpha = 0.35f))) .border(0.5.dp, Color.White.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) .padding(2.dp), contentAlignment = Alignment.Center, ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current).data(icon.url).crossfade(true).build(), contentDescription = icon.name, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(6.dp)), ) } } } } // Refresh button Box( modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.50f), 1f to Color.White.copy(alpha = 0.30f))) .border(0.5.dp, Color.White.copy(alpha = 0.55f), RoundedCornerShape(16.dp)) .clickable(enabled = !isRefreshing) { isRefreshing = true scope.launch { onRefresh() delay(500) isRefreshing = false } } .padding(vertical = 12.dp), contentAlignment = Alignment.Center, ) { if (isRefreshing) { CircularProgressIndicator(modifier = Modifier.size(18.dp), color = GlassTokens.accent, strokeWidth = 2.dp) } else { Text("\u5237\u65B0\u56FE\u6807", fontSize = 14.sp, fontWeight = FontWeight.Medium, color = GlassTokens.accent) } } // Save / Delete row Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { GlassButton( text = "\u5220\u9664", onClick = onDelete, modifier = Modifier.weight(1f), danger = true, ) GlassButton( text = "\u4FDD\u5B58", onClick = { if (editName.text.isNotBlank() && editUrl.text.isNotBlank()) { onUpdate(library.copy(name = editName.text.trim(), sourceUrl = editUrl.text.trim(), isDefault = setAsDefault)) } }, modifier = Modifier.weight(1f), accent = true, enabled = editName.text.isNotBlank() && editUrl.text.isNotBlank(), ) } } } } @Composable private fun AddIconLibraryDialog( onDismiss: () -> Unit, onSave: (String, String, List, Boolean) -> Unit = { _, _, _, _ -> } ) { var name by remember { mutableStateOf(TextFieldValue("")) } var url by remember { mutableStateOf(TextFieldValue("")) } var isDefault by remember { mutableStateOf(false) } var icons by remember { mutableStateOf>(emptyList()) } var isLoading by remember { mutableStateOf(false) } var error by remember { mutableStateOf(null) } var fetched by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() Dialog(onDismissRequest = onDismiss) { val dialogCtx = LocalContext.current val localizedCtx = remember(AppLanguage.language) { AppLanguage.getLocalizedContext(dialogCtx) } CompositionLocalProvider(LocalContext provides localizedCtx) { Box( Modifier .width(320.dp) .shadow(24.dp, RoundedCornerShape(28.dp), ambientColor = Color.White.copy(alpha = 0.18f), spotColor = Color.Black.copy(alpha = 0.05f)) .clip(RoundedCornerShape(28.dp)) .background( Brush.verticalGradient( 0f to Color.White.copy(alpha = 0.88f), 0.5f to Color.White.copy(alpha = 0.75f), 1f to Color.White.copy(alpha = 0.65f), ) ) .border(0.5.dp, Color.White.copy(alpha = 0.6f), RoundedCornerShape(28.dp)) .padding(20.dp) ) { Column( verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.verticalScroll(rememberScrollState()), ) { Row(verticalAlignment = Alignment.CenterVertically) { Box( Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)) .background(Brush.linearGradient(listOf(Color(0xFFFF6B9D), Color(0xFFFF8A65)))), contentAlignment = Alignment.Center, ) { Icon(Icons.Outlined.Image, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp)) } Spacer(Modifier.width(12.dp)) Text(text = "\u589E\u52A0\u56FE\u6807\u5E93", fontWeight = FontWeight.Bold, fontSize = 19.sp, color = GlassTokens.text) } Text("\u56FE\u6807\u5E93\u540D\u79F0", fontSize = 13.sp, color = GlassTokens.subtext, fontWeight = FontWeight.Medium) Box( Modifier.fillMaxWidth().clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.50f), 1f to Color.White.copy(alpha = 0.30f))) .border(0.5.dp, Color.White.copy(alpha = 0.55f), RoundedCornerShape(16.dp)) .padding(horizontal = 16.dp, vertical = 14.dp), ) { androidx.compose.foundation.text.BasicTextField( value = name, onValueChange = { name = it }, singleLine = true, textStyle = androidx.compose.ui.text.TextStyle(color = GlassTokens.text, fontSize = 16.sp), modifier = Modifier.fillMaxWidth(), cursorBrush = androidx.compose.ui.graphics.SolidColor(GlassTokens.accent), decorationBox = { inner -> Box(Modifier.fillMaxWidth().height(24.dp)) { if (name.text.isEmpty()) Text("\u4F8B\u5982: Emby \u56FE\u6807\u5E93", color = GlassTokens.subtext, fontSize = 16.sp) inner() } } ) } // Default library switch Row( Modifier.fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(if (isDefault) GlassTokens.accent.copy(alpha = 0.12f) else Color.White.copy(alpha = 0.35f)) .border(0.5.dp, if (isDefault) GlassTokens.accent.copy(alpha = 0.35f) else Color.White.copy(alpha = 0.55f), RoundedCornerShape(16.dp)) .clickable { isDefault = !isDefault } .padding(horizontal = 14.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { Column(Modifier.weight(1f)) { Text(stringResource(Res.string.set_default_library), color = GlassTokens.text, fontSize = 14.sp, fontWeight = FontWeight.Medium) Text(stringResource(Res.string.set_default_library_sub), color = GlassTokens.subtext, fontSize = 11.sp) } Box( Modifier.size(width = 42.dp, height = 25.dp) .clip(CircleShape) .background(if (isDefault) GlassTokens.accent else Color.White.copy(alpha = 0.65f)) .border(0.5.dp, if (isDefault) Color.Transparent else Color.White.copy(alpha = 0.75f), CircleShape) .padding(3.dp), contentAlignment = if (isDefault) Alignment.CenterEnd else Alignment.CenterStart, ) { Box(Modifier.size(19.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.92f))) } } Text("\u56FE\u6807\u5E93\u5730\u5740 (JSON)", fontSize = 13.sp, color = GlassTokens.subtext, fontWeight = FontWeight.Medium) Box( Modifier.fillMaxWidth().clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.50f), 1f to Color.White.copy(alpha = 0.30f))) .border(0.5.dp, Color.White.copy(alpha = 0.55f), RoundedCornerShape(16.dp)) .padding(horizontal = 16.dp, vertical = 14.dp), ) { androidx.compose.foundation.text.BasicTextField( value = url, onValueChange = { url = it; fetched = false; error = null; icons = emptyList() }, singleLine = true, textStyle = androidx.compose.ui.text.TextStyle(color = GlassTokens.text, fontSize = 16.sp), modifier = Modifier.fillMaxWidth(), cursorBrush = androidx.compose.ui.graphics.SolidColor(GlassTokens.accent), decorationBox = { inner -> Box(Modifier.fillMaxWidth().height(24.dp)) { if (url.text.isEmpty()) Text("https://...", color = GlassTokens.subtext, fontSize = 16.sp) inner() } } ) } Box( modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(16.dp)) .background( if (url.text.isNotBlank() && !isLoading) Brush.verticalGradient(listOf(GlassTokens.accent, Color(0xFF0A6DD4))) else Brush.verticalGradient(listOf(GlassTokens.accent.copy(alpha = 0.4f), GlassTokens.accent.copy(alpha = 0.3f))) ) .then(if (url.text.isNotBlank() && !isLoading) Modifier.border(0.5.dp, Color.White.copy(alpha = 0.3f), RoundedCornerShape(16.dp)) else Modifier) .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 = { e -> error = e.message ?: "\u83B7\u53D6\u5931\u8D25" } ) isLoading = false } } .padding(vertical = 14.dp), contentAlignment = Alignment.Center, ) { if (isLoading) { CircularProgressIndicator(modifier = Modifier.size(22.dp), color = Color.White, strokeWidth = 2.5.dp) } else { Text("\u83B7\u53D6\u9884\u89C8", color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) } } if (error != null) Text(text = error!!, color = offlineRed, fontSize = 13.sp) if (icons.isNotEmpty()) { Text("\u9884\u89C8 (${icons.size} \u4E2A\u56FE\u6807)", fontSize = 13.sp, color = GlassTokens.subtext, fontWeight = FontWeight.Medium) Box( Modifier.fillMaxWidth().clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.35f), 1f to Color.White.copy(alpha = 0.20f))) .border(0.5.dp, Color.White.copy(alpha = 0.4f), RoundedCornerShape(16.dp)) .padding(8.dp) ) { LazyVerticalGrid( columns = GridCells.Fixed(5), modifier = Modifier.heightIn(min = 80.dp, max = 320.dp), horizontalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp), ) { items(icons) { icon -> Box( modifier = Modifier.aspectRatio(1f).clip(RoundedCornerShape(12.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.55f), 1f to Color.White.copy(alpha = 0.35f))) .border(0.5.dp, Color.White.copy(alpha = 0.5f), RoundedCornerShape(12.dp)), contentAlignment = Alignment.Center, ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current).data(icon.url).crossfade(true).build(), contentDescription = icon.name, modifier = Modifier.fillMaxSize().padding(4.dp).clip(RoundedCornerShape(10.dp)), contentScale = ContentScale.Crop, ) } } } } Text("\u2713 \u5DF2\u52A0\u8F7D ${icons.size} \u4E2A\u56FE\u6807", fontSize = 12.sp, color = onlineGreen) } Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { Box( modifier = Modifier.weight(1f).clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.45f), 1f to Color.White.copy(alpha = 0.25f))) .border(0.5.dp, Color.White.copy(alpha = 0.5f), RoundedCornerShape(16.dp)) .clickable { onDismiss() } .padding(vertical = 14.dp), contentAlignment = Alignment.Center, ) { Text("\u53D6\u6D88", color = GlassTokens.subtext, fontSize = 15.sp, fontWeight = FontWeight.Medium) } Box( modifier = Modifier.weight(1f).clip(RoundedCornerShape(16.dp)) .background( if (icons.isNotEmpty() && name.text.isNotBlank()) Brush.verticalGradient(listOf(GlassTokens.accent, Color(0xFF0A6DD4))) else Brush.verticalGradient(listOf(GlassTokens.accent.copy(alpha = 0.4f), GlassTokens.accent.copy(alpha = 0.3f))) ) .then(if (icons.isNotEmpty() && name.text.isNotBlank()) Modifier.border(0.5.dp, Color.White.copy(alpha = 0.3f), RoundedCornerShape(16.dp)) else Modifier) .clickable(enabled = icons.isNotEmpty() && name.text.isNotBlank()) { onSave(name.text.trim(), url.text.trim(), icons, isDefault); onDismiss() } .padding(vertical = 14.dp), contentAlignment = Alignment.Center, ) { Text("\u4FDD\u5B58", color = if (icons.isNotEmpty() && name.text.isNotBlank()) Color.White else Color.White.copy(alpha = 0.6f), fontSize = 15.sp, fontWeight = FontWeight.SemiBold) } } } } } } } /* ------------------------------------------------------------------ */ /* Icon Library Section */ /* ------------------------------------------------------------------ */ @Composable private fun IconPickerDialog(server: EmbyServer, onDismiss: () -> Unit, onPick: (EmbyServer, String) -> Unit) { var allIcons by remember { mutableStateOf>(emptyList()) } var query by remember { mutableStateOf(TextFieldValue("")) } var isLoading by remember { mutableStateOf(false) } var error by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() val filteredIcons = remember(allIcons, query) { val keyword = query.text.trim() if (keyword.isEmpty()) allIcons else allIcons.filter { icon -> icon.name.contains(keyword, ignoreCase = true) } } LaunchedEffect(Unit) { scope.launch { isLoading = true allIcons = IconLibraryRepository.getPickerIcons() isLoading = false } } Dialog(onDismissRequest = onDismiss) { val dialogCtx = LocalContext.current val localizedCtx = remember(AppLanguage.language) { AppLanguage.getLocalizedContext(dialogCtx) } CompositionLocalProvider(LocalContext provides localizedCtx) { Box( Modifier .width(320.dp) .shadow(24.dp, RoundedCornerShape(28.dp), ambientColor = Color.White.copy(alpha = 0.18f), spotColor = Color.Black.copy(alpha = 0.05f)) .clip(RoundedCornerShape(28.dp)) .background( Brush.verticalGradient( 0f to Color.White.copy(alpha = 0.88f), 0.5f to Color.White.copy(alpha = 0.75f), 1f to Color.White.copy(alpha = 0.65f), ) ) .border(0.5.dp, Color.White.copy(alpha = 0.6f), RoundedCornerShape(28.dp)) .padding(20.dp) ) { Column( verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.heightIn(max = (LocalConfiguration.current.screenHeightDp * 0.80f).dp), ) { Row(verticalAlignment = Alignment.CenterVertically) { Box( Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)) .background(Brush.linearGradient(listOf(Color(0xFFFF6B9D), Color(0xFFFF8A65)))), contentAlignment = Alignment.Center, ) { Icon(Icons.Outlined.Image, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp)) } Spacer(Modifier.width(12.dp)) Text(text = "\u9009\u62E9\u56FE\u6807", fontWeight = FontWeight.Bold, fontSize = 19.sp, color = GlassTokens.text) } if (!isLoading && allIcons.isNotEmpty()) { Box( Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp)) .background(Color.White.copy(alpha = 0.42f)) .border(0.5.dp, Color.White.copy(alpha = 0.55f), RoundedCornerShape(14.dp)) .padding(horizontal = 12.dp, vertical = 9.dp), ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Icon( Icons.Outlined.Search, contentDescription = null, tint = GlassTokens.subtext, modifier = Modifier.size(18.dp), ) Spacer(Modifier.width(8.dp)) Box(Modifier.weight(1f)) { if (query.text.isEmpty()) { Text( text = "\u641C\u7D22\u56FE\u6807\u540D\u79F0", color = GlassTokens.subtext, fontSize = 14.sp, ) } BasicTextField( value = query, onValueChange = { query = it }, singleLine = true, textStyle = androidx.compose.ui.text.TextStyle(color = GlassTokens.text, fontSize = 14.sp), modifier = Modifier.fillMaxWidth(), cursorBrush = androidx.compose.ui.graphics.SolidColor(GlassTokens.accent), ) } if (query.text.isNotEmpty()) { Spacer(Modifier.width(8.dp)) Icon( Icons.Outlined.Close, contentDescription = "\u6E05\u9664\u641C\u7D22", tint = GlassTokens.subtext, modifier = Modifier .size(18.dp) .clickable { query = TextFieldValue("") }, ) } } } } if (isLoading) { Box(Modifier.fillMaxWidth().weight(1f), contentAlignment = Alignment.Center) { CircularProgressIndicator(modifier = Modifier.size(28.dp), color = GlassTokens.accent, strokeWidth = 2.5.dp) } } else if (allIcons.isEmpty()) { Box( modifier = Modifier.fillMaxWidth().weight(1f).clip(RoundedCornerShape(20.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.35f), 1f to Color.White.copy(alpha = 0.20f))) .border(0.5.dp, Color.White.copy(alpha = 0.4f), RoundedCornerShape(20.dp)) .padding(vertical = 36.dp), contentAlignment = Alignment.Center, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Box(Modifier.size(56.dp).clip(CircleShape) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.45f), 1f to Color.White.copy(alpha = 0.25f))) .border(0.5.dp, Color.White.copy(alpha = 0.45f), CircleShape), contentAlignment = Alignment.Center ) { Icon(Icons.Outlined.Image, contentDescription = null, tint = GlassTokens.subtext2, modifier = Modifier.size(28.dp)) } Spacer(Modifier.height(14.dp)) Text(text = "\u6682\u65E0\u56FE\u6807\u5E93", color = GlassTokens.text, fontSize = 16.sp, fontWeight = FontWeight.Medium) Spacer(Modifier.height(4.dp)) Text(text = "\u8BF7\u5148\u5728\u56FE\u6807\u5E93\u7BA1\u7406\u4E2D\u6DFB\u52A0\u56FE\u6807\u5E93", color = GlassTokens.subtext, fontSize = 13.sp) } } } else { Text("\u5DF2\u52A0\u8F7D ${allIcons.size} \u4E2A\u56FE\u6807\uFF0C\u70B9\u51FB\u9009\u62E9", fontSize = 13.sp, color = GlassTokens.subtext) Box( Modifier.fillMaxWidth().weight(1f).clip(RoundedCornerShape(20.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.35f), 1f to Color.White.copy(alpha = 0.20f))) .border(0.5.dp, Color.White.copy(alpha = 0.4f), RoundedCornerShape(20.dp)) .padding(8.dp) ) { LazyVerticalGrid( columns = GridCells.Fixed(5), modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp), ) { items(filteredIcons) { icon -> val isSelected = server.iconUrl == icon.url Box( modifier = Modifier.aspectRatio(1f).clip(RoundedCornerShape(12.dp)) .background( if (isSelected) Brush.verticalGradient(listOf(GlassTokens.accent.copy(alpha = 0.2f), GlassTokens.accent.copy(alpha = 0.1f))) else Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.55f), Color.White.copy(alpha = 0.35f))) ) .border(if (isSelected) 1.5.dp else 0.5.dp, if (isSelected) GlassTokens.accent else Color.White.copy(alpha = 0.5f), RoundedCornerShape(12.dp)) .clickable { onPick(server, icon.url) }, contentAlignment = Alignment.Center, ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current).data(icon.url).crossfade(true).build(), contentDescription = icon.name, modifier = Modifier.fillMaxSize().padding(4.dp).clip(RoundedCornerShape(10.dp)), contentScale = ContentScale.Crop, ) if (isSelected) { Box( Modifier.align(Alignment.TopEnd).padding(3.dp).size(18.dp).clip(CircleShape) .background(Brush.verticalGradient(listOf(GlassTokens.accent, Color(0xFF0A6DD4)))) .border(0.5.dp, Color.White.copy(alpha = 0.4f), CircleShape), contentAlignment = Alignment.Center, ) { Icon(Icons.Filled.GridView, contentDescription = null, tint = Color.White, modifier = Modifier.size(11.dp)) } } } } } } if (filteredIcons.isEmpty()) { Box( Modifier.fillMaxWidth().height(90.dp).clip(RoundedCornerShape(20.dp)) .background(Color.White.copy(alpha = 0.24f)) .border(0.5.dp, Color.White.copy(alpha = 0.35f), RoundedCornerShape(20.dp)), contentAlignment = Alignment.Center, ) { Text( text = "\u672A\u627E\u5230\u5339\u914D\u7684\u56FE\u6807", color = GlassTokens.subtext, fontSize = 14.sp, fontWeight = FontWeight.Medium, ) } } } Box( modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(0f to Color.White.copy(alpha = 0.45f), 1f to Color.White.copy(alpha = 0.25f))) .border(0.5.dp, Color.White.copy(alpha = 0.5f), RoundedCornerShape(16.dp)) .clickable { onDismiss() } .padding(vertical = 14.dp), contentAlignment = Alignment.Center, ) { Text(text = "\u5173\u95ED", color = GlassTokens.subtext, fontSize = 15.sp, fontWeight = FontWeight.Medium) } } } } } } /* ------------------------------------------------------------------ */ /* Icon Library Section */ /* ------------------------------------------------------------------ */ @Composable private fun AddServerDialog(initial: EmbyServer? = null, onDismiss: () -> Unit, onConfirm: (EmbyServer, (String?) -> Unit) -> Unit) { var name by remember { mutableStateOf(TextFieldValue(initial?.name ?: "")) } var host by remember { mutableStateOf(TextFieldValue(initial?.host ?: "")) } var port by remember { mutableStateOf(TextFieldValue(initial?.port ?: "")) } var basePath by remember { mutableStateOf(TextFieldValue(initial?.basePath ?: "")) } var username by remember { mutableStateOf(TextFieldValue(initial?.username ?: "")) } var password by remember { mutableStateOf(TextFieldValue(initial?.password ?: "")) } var note by remember { mutableStateOf(TextFieldValue(initial?.note ?: "")) } var isSaving by remember { mutableStateOf(false) } var authError by remember { mutableStateOf("") } var showPassword by remember { mutableStateOf(false) } var showValidation by remember { mutableStateOf(false) } val hostFocusRequester = remember { FocusRequester() } val portFocusRequester = remember { FocusRequester() } val usernameFocusRequester = remember { FocusRequester() } val serverInputValid = host.text.trim().isNotEmpty() && port.text.trim().isNotEmpty() && username.text.trim().isNotEmpty() val hostError = showValidation && host.text.trim().isEmpty() val portError = showValidation && port.text.trim().isEmpty() val usernameError = showValidation && username.text.trim().isEmpty() GlassDialog(onDismissRequest = onDismiss) { Column( modifier = Modifier.width(300.dp).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Row(verticalAlignment = Alignment.CenterVertically) { Box( Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)) .background(Brush.linearGradient(listOf(Color(0xFF6EA8FF), Color(0xFF9B6BFF)))), contentAlignment = Alignment.Center, ) { Icon(Icons.Outlined.Add, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp)) } Spacer(Modifier.width(10.dp)) Text(text = if (initial == null) "\u6DFB\u52A0\u670D\u52A1\u5668" else "\u7F16\u8F91\u670D\u52A1\u5668", fontWeight = FontWeight.Bold, fontSize = 18.sp, color = GlassTokens.text) } GlassInputField(label = "\u540D\u79F0", value = name, placeholder = "\u6211\u7684 Emby", onValueChange = { name = it }) GlassInputField( label = "\u5730\u5740", value = host, placeholder = "emby.example.com", onValueChange = { host = it }, isError = hostError, errorMessage = if (hostError) "\u8BF7\u8F93\u5165\u5730\u5740" else null, focusRequester = hostFocusRequester, ) Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { GlassInputField( label = "\u8DEF\u5F84", value = basePath, placeholder = "/emby", onValueChange = { basePath = it }, modifier = Modifier.weight(1f), ) GlassInputField( label = "\u7AEF\u53E3", value = port, placeholder = "8096", onValueChange = { port = it }, modifier = Modifier.weight(1f), isError = portError, errorMessage = if (portError) "\u8BF7\u8F93\u5165\u7AEF\u53E3" else null, focusRequester = portFocusRequester, ) } GlassInputField( label = "\u7528\u6237\u540D", value = username, placeholder = "\u7528\u6237\u540D", onValueChange = { username = it }, isError = usernameError, errorMessage = if (usernameError) "\u8BF7\u8F93\u5165\u7528\u6237\u540D" else null, focusRequester = usernameFocusRequester, ) GlassInputField( label = "\u5BC6\u7801", value = password, placeholder = "\u5BC6\u7801", onValueChange = { password = it }, visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), trailingContent = { Icon( imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, contentDescription = if (showPassword) "\u9690\u85CF\u5BC6\u7801" else "\u663E\u793A\u5BC6\u7801", tint = GlassTokens.subtext, modifier = Modifier .size(24.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, ) { showPassword = !showPassword }, ) }, ) GlassInputField(label = "\u5907\u6CE8", value = note, placeholder = "\u5907\u6CE8", onValueChange = { note = it }) if (authError.isNotBlank()) { Text(text = authError, color = offlineRed, fontSize = 13.sp, modifier = Modifier.padding(bottom = 4.dp)) } Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { GlassButton(text = "\u53D6\u6D88", onClick = onDismiss, modifier = Modifier.weight(1f)) GlassButton( text = if (isSaving) "\u4FDD\u5B58\u4E2D..." else "\u4FDD\u5B58", onClick = { if (isSaving) return@GlassButton authError = "" showValidation = true when { host.text.trim().isEmpty() -> runCatching { hostFocusRequester.requestFocus() } port.text.trim().isEmpty() -> runCatching { portFocusRequester.requestFocus() } username.text.trim().isEmpty() -> runCatching { usernameFocusRequester.requestFocus() } else -> { isSaving = true val server = (initial ?: EmbyServer(id = EmbyServer.generateId())).copy( name = name.text.trim(), host = host.text.trim(), port = port.text.trim(), basePath = basePath.text.trim().trim('/'), username = username.text.trim(), password = password.text.trim(), note = note.text.trim(), isOnline = initial?.isOnline ?: true, lastActive = initial?.lastActive ?: "\u521A\u521A" ) onConfirm(server) { error -> isSaving = false if (error != null) { authError = error } else { onDismiss() } } } } }, modifier = Modifier.weight(1f), accent = true, enabled = !isSaving, ) } } } }