diff --git a/README.md b/README.md index c66eae7..a8e7690 100644 --- a/README.md +++ b/README.md @@ -1,3 +1 @@ -# lumaplayer - -播放器 \ No newline at end of file +# LumaPlayer \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 38074f1..3c3b3e8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,7 +12,7 @@ android { minSdk = 24 targetSdk = 35 versionCode = 1 - versionName = "1.0" + versionName = "1.0.0" } buildTypes { diff --git a/app/src/main/kotlin/com/example/myapplication/MainApp.kt b/app/src/main/kotlin/com/example/myapplication/MainApp.kt index 3399593..9c1d769 100644 --- a/app/src/main/kotlin/com/example/myapplication/MainApp.kt +++ b/app/src/main/kotlin/com/example/myapplication/MainApp.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -46,6 +47,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.example.myapplication.R as Res import com.example.myapplication.data.EmbyRepository import com.example.myapplication.data.EmbyServer import com.example.myapplication.data.IconLibrary @@ -65,6 +67,7 @@ import com.example.myapplication.ui.AppTheme import com.example.myapplication.ui.GlassTokens import com.example.myapplication.ui.LiquidAppBottomBar import com.example.myapplication.ui.LiquidGlassBackground +import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeSource @@ -81,7 +84,7 @@ fun MainApp() { var recordsSegment by remember { mutableIntStateOf(0) } var searchQuery by remember { mutableStateOf("") } var showLibraryAddDialog by remember { mutableStateOf(false) } - var libraryIsGrid by remember { mutableStateOf(false) } + var libraryIsGrid by remember { mutableStateOf(true) } var showIconLibraryManager by remember { mutableStateOf(false) } var showCacheDetail by remember { mutableStateOf(false) } var homeDetailVisible by remember { mutableStateOf(false) } @@ -249,6 +252,7 @@ fun MainApp() { showAddDialog = showLibraryAddDialog, onDismissAddDialog = { showLibraryAddDialog = false }, isGrid = libraryIsGrid, + showTopBar = false, onChangeIconTarget = changeIconTarget, onDismissChangeIcon = { changeIconTarget = null }, onPickIcon = { server, iconUrl -> ServerRepository.setRuntimeIcon(server.id, iconUrl) }, diff --git a/app/src/main/kotlin/com/example/myapplication/data/IconLibrary.kt b/app/src/main/kotlin/com/example/myapplication/data/IconLibrary.kt index 19e36b7..19596ad 100644 --- a/app/src/main/kotlin/com/example/myapplication/data/IconLibrary.kt +++ b/app/src/main/kotlin/com/example/myapplication/data/IconLibrary.kt @@ -57,7 +57,14 @@ object IconLibraryRepository { suspend fun addLibrary(library: IconLibrary) { save(library) - setRuntimeIcons(library.id, library.icons) + if (library.icons.isNotEmpty()) { + setRuntimeIcons(library.id, library.icons) + cachedIcons[library.id] = library.icons + saveIconsToCache(library.id, library.icons) + withContext(Dispatchers.IO) { + database?.iconLibraryDao()?.updateIconCount(library.id, library.icons.size) + } + } } suspend fun removeLibrary(id: String) { diff --git a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt index b00e11d..f360319 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt @@ -513,7 +513,11 @@ fun HomeScreen( }, label = "HomeLoadingTransition" ) { isLoading -> - if (isLoading) { + if (activeServer == null) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + EmptyHomeState() + } + } else if (isLoading) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator(color = adaptiveAccent) } @@ -579,6 +583,25 @@ fun HomeScreen( } } +@Composable +private fun EmptyHomeState() { + Column( + Modifier.fillMaxWidth().padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + Modifier.size(64.dp).clip(CircleShape).background(homeSecondaryColor().copy(alpha = 0.16f)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Filled.PlayArrow, contentDescription = null, tint = homeSecondaryColor(), modifier = Modifier.size(32.dp)) + } + Spacer(Modifier.height(16.dp)) + Text("没有可用的媒体库", color = homeSecondaryColor(), fontSize = 15.sp) + Spacer(Modifier.height(6.dp)) + Text("请在资源库中添加服务器", color = homeSecondaryColor().copy(alpha = 0.72f), fontSize = 12.sp) + } +} + @Composable private fun HomeOverlayTopBar(name: String, iconUrl: String, modifier: Modifier = Modifier) { LiquidGlassTopBar(modifier) { 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 3e760cf..60a981c 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/LibraryScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/LibraryScreen.kt @@ -12,6 +12,8 @@ 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.gestures.detectHorizontalDragGestures +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -45,6 +47,8 @@ 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 @@ -96,6 +100,9 @@ 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) @@ -113,81 +120,146 @@ fun LibraryTopBar( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Text(text = "资源库", color = GlassTokens.text, fontSize = 20.sp, fontWeight = FontWeight.Bold) + 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(Color.White).border(0.5.dp, Color.LightGray, CircleShape).clickable { onViewToggle() }, contentAlignment = Alignment.Center) { - Icon(imageVector = if (isGrid) Icons.Filled.GridView else Icons.Filled.ViewAgenda, contentDescription = null, tint = Color(0xFF374151), modifier = Modifier.size(20.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(Color.White).border(0.5.dp, Color.LightGray, CircleShape).clickable { onOpenIconLibraryManager() }, contentAlignment = Alignment.Center) { - Icon(imageVector = Icons.Outlined.PhotoLibrary, contentDescription = null, tint = Color(0xFF374151), modifier = Modifier.size(20.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(Color.White).border(0.5.dp, Color.LightGray, CircleShape).clickable { onAdd() }, contentAlignment = Alignment.Center) { - Icon(imageVector = Icons.Outlined.Add, contentDescription = null, tint = Color(0xFF374151), modifier = Modifier.size(20.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, onClick: () -> Unit, onChangeIcon: () -> Unit, onDelete: () -> Unit, onEdit: () -> Unit) { +private fun ServerCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onChangeIcon: () -> Unit, onDelete: () -> Unit, onEdit: () -> Unit) { var showPopup by remember { mutableStateOf(false) } - GlassSurface(modifier = Modifier.clickable { onClick() }, fill = Color.White, cornerRadius = 16.dp, padding = PaddingValues(12.dp), border = Color.LightGray) { - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - if (server.iconUrl.isNotBlank()) { - AsyncImage(model = ImageRequest.Builder(LocalContext.current).data(server.iconUrl).crossfade(true).build(), contentDescription = null, modifier = Modifier.size(44.dp).clip(RoundedCornerShape(10.dp)), contentScale = ContentScale.Crop) - } else { - Box(Modifier.size(44.dp).clip(RoundedCornerShape(10.dp)).background(Color(0xFFF3F4F6)), contentAlignment = Alignment.Center) { - Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null, tint = Color(0xFF374151), modifier = Modifier.size(24.dp)) + 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), + ) + } } - } - Spacer(Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { - Text(text = server.name.ifBlank { server.displayHost }, color = Color(0xFF1F2937), fontSize = 15.sp, fontWeight = FontWeight.Bold) - Text(text = if (server.isOnline) "在线" else "离线", color = if (server.isOnline) onlineGreen else offlineRed, fontSize = 11.sp) } Box { - Icon(imageVector = Icons.Outlined.MoreHoriz, contentDescription = null, tint = Color(0xFF6B7280), modifier = Modifier.size(28.dp).clickable { showPopup = true }) - GlassPopup(expanded = showPopup, onDismissRequest = { showPopup = false }) { - GlassPopupItem(text = "编辑", icon = Icons.Outlined.Edit, onClick = { showPopup = false; onEdit() }) - GlassPopupItem(text = "更换图标", icon = Icons.Outlined.Image, onClick = { showPopup = false; onChangeIcon() }) - GlassPopupItem(text = "删除", icon = Icons.Outlined.Delete, tint = offlineRed, onClick = { showPopup = false; onDelete() }) - } + 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, onClick: () -> Unit, onChangeIcon: () -> Unit, onDelete: () -> Unit, onEdit: () -> Unit) { +private fun ServerGridCard(server: EmbyServer, tint: Color, onClick: () -> Unit, onChangeIcon: () -> Unit, onDelete: () -> Unit, onEdit: () -> Unit) { var showPopup by remember { mutableStateOf(false) } - GlassSurface(modifier = Modifier.clickable { onClick() }, fill = Color.White, cornerRadius = 16.dp, padding = PaddingValues(12.dp), border = Color.LightGray) { - Column(modifier = Modifier.fillMaxWidth()) { + 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(40.dp).clip(RoundedCornerShape(8.dp)), contentScale = ContentScale.Crop) + 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(40.dp).clip(RoundedCornerShape(8.dp)).background(Color(0xFFF3F4F6)), contentAlignment = Alignment.Center) { - Icon(Icons.Filled.PlayArrow, null, tint = Color(0xFF374151), modifier = Modifier.size(20.dp)) + 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.weight(1f)) - Box { - Icon(Icons.Outlined.MoreHoriz, null, tint = Color(0xFF6B7280), modifier = Modifier.size(24.dp).clickable { showPopup = true }) - GlassPopup(expanded = showPopup, onDismissRequest = { showPopup = false }) { - GlassPopupItem(text = "编辑", icon = Icons.Outlined.Edit, onClick = { showPopup = false; onEdit() }) - GlassPopupItem(text = "更换图标", icon = Icons.Outlined.Image, onClick = { showPopup = false; onChangeIcon() }) - GlassPopupItem(text = "删除", icon = Icons.Outlined.Delete, tint = offlineRed, onClick = { showPopup = false; onDelete() }) + 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() }) } + } + } + Box(Modifier.height(22.dp)) { + Text( + text = server.note, + color = GlassTokens.subtext, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 8.dp), + ) } - Spacer(Modifier.height(8.dp)) - Text(text = server.name.ifBlank { server.displayHost }, color = Color(0xFF1F2937), fontSize = 14.sp, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) - Text(text = if (server.isOnline) "在线" else "离线", color = if (server.isOnline) onlineGreen else offlineRed, fontSize = 10.sp) } } } +/* ------------------------------------------------------------------ */ +/* Icon Library Section */ +/* ------------------------------------------------------------------ */ + @Composable fun LibraryScreen( servers: List, @@ -203,35 +275,114 @@ fun LibraryScreen( onDismissChangeIcon: () -> Unit = {}, onPickIcon: (EmbyServer, String) -> Unit = { _, _ -> }, onRequestChangeIcon: (EmbyServer) -> Unit = {}, + onViewToggle: () -> Unit = {}, + onAdd: () -> Unit = {}, + onOpenIconLibraryManager: () -> Unit = {}, + showTopBar: Boolean = true, ) { var editingServer by remember { mutableStateOf(null) } if (showAddDialog) { - AddServerDialog(onDismiss = onDismissAddDialog, onConfirm = { srv, onResult -> onAddServer(srv, onResult) }) + AddServerDialog(onDismiss = onDismissAddDialog, onConfirm = { srv, onResult -> + onAddServer(srv, onResult) + }) } if (editingServer != null) { - AddServerDialog(initial = editingServer, onDismiss = { editingServer = null }, onConfirm = { updated, onResult -> onUpdateServer(updated, onResult) }) + 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() }) + 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 = 16.dp), - contentPadding = PaddingValues(top = 16.dp, bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - items(servers) { server -> - if (isGrid) { - ServerGridCard(server = server, onClick = { onSelectServer(server.id) }, onChangeIcon = { onRequestChangeIcon(server) }, onDelete = { onDeleteServer(server.id) }, onEdit = { editingServer = server }) - } else { - ServerCard(server = server, onClick = { onSelectServer(server.id) }, onChangeIcon = { onRequestChangeIcon(server) }, onDelete = { onDeleteServer(server.id) }, onEdit = { editingServer = server }) + Box(Modifier.fillMaxSize()) { + + if (servers.isEmpty()) { + Box( + Modifier.fillMaxSize().padding(bottom = bottomBarContentPadding(90.dp).calculateBottomPadding()), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + Modifier + .size(64.dp) + .clip(CircleShape) + .background(GlassTokens.glassFill) + .border(1.dp, GlassTokens.glassBorder, CircleShape), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = null, + tint = GlassTokens.subtext2, + modifier = Modifier.size(32.dp), + ) + } + Spacer(Modifier.height(16.dp)) + Text(text = stringResource(Res.string.empty_servers), color = GlassTokens.subtext, fontSize = 15.sp) + Spacer(Modifier.height(6.dp)) + Text(text = stringResource(Res.string.empty_servers_sub), color = GlassTokens.subtext, fontSize = 12.sp) + } } + } else { + LazyVerticalGrid( + columns = if (isGrid) GridCells.Fixed(2) else GridCells.Fixed(1), + modifier = Modifier.fillMaxSize().statusBarsPadding().padding(horizontal = if (isGrid) 16.dp else 0.dp, vertical = 0.dp).padding(top = 78.dp), + contentPadding = PaddingValues(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 }, + ) + } + } + } + } + + if (showTopBar) { + LibraryTopBar( + isGrid = isGrid, + onViewToggle = onViewToggle, + onAdd = onAdd, + onOpenIconLibraryManager = onOpenIconLibraryManager, + modifier = Modifier.align(Alignment.TopCenter), + ) } } } +/* ------------------------------------------------------------------ */ +/* Icon Library Section */ +/* ------------------------------------------------------------------ */ + @Composable fun IconLibraryManagerScreen( libraries: List, @@ -244,127 +395,1158 @@ fun IconLibraryManagerScreen( ) { 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.size(34.dp).clip(CircleShape).background(Color.White).border(0.5.dp, Color.LightGray, CircleShape).clickable { onBack() }, contentAlignment = Alignment.Center) { - Icon(Icons.Outlined.Close, null, tint = Color(0xFF374151), modifier = Modifier.size(20.dp)) + Column(modifier.fillMaxSize() + .pointerInput(Unit) { + var totalDrag = 0f + detectHorizontalDragGestures( + onDragStart = { totalDrag = 0f }, + onDragEnd = { + if (totalDrag > 300f) onBack() + }, + ) { _, dragAmount -> + totalDrag += dragAmount } - Spacer(Modifier.width(16.dp)) - Text("图标库管理", color = GlassTokens.text, fontSize = 18.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) - Box(Modifier.size(34.dp).clip(CircleShape).background(Color.White).border(0.5.dp, Color.LightGray, CircleShape).clickable { showAddDialog = true }, contentAlignment = Alignment.Center) { - Icon(Icons.Outlined.Add, null, tint = Color(0xFF374151), modifier = Modifier.size(20.dp)) + } + ) { + LiquidGlassTopBar { + Row( + Modifier.fillMaxWidth().padding(horizontal = 18.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(Modifier.width(86.dp)) { + 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)) + } + } + 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) { + 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) + } } } } - 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)) { - items(libraries) { library -> - IconLibraryManagerCard(library = library, onClick = { editingLibraryId = library.id }) + 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\u53F3\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 }) + 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) }) + 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(16.dp)).background(Color.White).border(0.5.dp, Color.LightGray, RoundedCornerShape(16.dp)).clickable { onClick() }.padding(12.dp)) { + 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 { - Text(library.name, color = Color(0xFF1F2937), fontSize = 14.sp, fontWeight = FontWeight.Bold) - Text("${library.iconCount} 图标", color = Color(0xFF6B7280), fontSize = 11.sp) + 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(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, 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) } + } + } + + 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 IconPickerDialog(server: EmbyServer, onDismiss: () -> Unit, onPick: (EmbyServer, String) -> Unit) { - var allIcons by remember { mutableStateOf>(emptyList()) } - var isLoading by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - isLoading = true - allIcons = IconLibraryRepository.getPickerIcons() - isLoading = false - } - - Dialog(onDismissRequest = onDismiss) { - Box(Modifier.width(320.dp).clip(RoundedCornerShape(24.dp)).background(Color.White).padding(20.dp)) { - Column(verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.heightIn(max = 500.dp)) { - Text("选择图标", fontWeight = FontWeight.Bold, fontSize = 18.sp, color = Color(0xFF1F2937)) - if (isLoading) { - CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally)) - } else { - LazyVerticalGrid(columns = GridCells.Fixed(5), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) { - items(allIcons) { icon -> - AsyncImage(model = icon.url, contentDescription = null, modifier = Modifier.aspectRatio(1f).clip(RoundedCornerShape(8.dp)).background(Color(0xFFF3F4F6)).clickable { onPick(server, icon.url) }, contentScale = ContentScale.Crop) +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)) } } } - GlassButton(text = "取消", onClick = onDismiss, modifier = Modifier.fillMaxWidth()) + } + } 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)), + ) + } + } + + } + 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(5), + modifier = Modifier.heightIn(min = 80.dp, max = 320.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.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)) { - Text(if (initial == null) "添加服务器" else "编辑服务器", fontWeight = FontWeight.Bold, fontSize = 18.sp, color = Color(0xFF1F2937)) - GlassInputField(label = "名称", value = name, placeholder = "我的 Emby", onValueChange = { name = it }) - GlassInputField(label = "地址", value = host, placeholder = "emby.example.com", onValueChange = { host = it }) - GlassInputField(label = "端口", value = port, placeholder = "8096", onValueChange = { port = it }) - GlassInputField(label = "用户名", value = username, placeholder = "用户名", onValueChange = { username = it }) - GlassInputField(label = "密码", value = password, placeholder = "密码", onValueChange = { password = it }, visualTransformation = PasswordVisualTransformation()) + 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 = "取消", onClick = onDismiss, modifier = Modifier.weight(1f)) - GlassButton(text = "保存", onClick = { - val server = (initial ?: EmbyServer(id = EmbyServer.generateId())).copy(name = name.text, host = host.text, port = port.text, username = username.text, password = password.text) - onConfirm(server) { onDismiss() } - }, modifier = Modifier.weight(1f), accent = true) - } - } - } -} - -@Composable -private fun AddIconLibraryDialog(onDismiss: () -> Unit, onSave: (String, String, List, Boolean) -> Unit) { - Dialog(onDismissRequest = onDismiss) { - Box(Modifier.width(300.dp).clip(RoundedCornerShape(24.dp)).background(Color.White).padding(20.dp)) { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text("添加图标库", fontWeight = FontWeight.Bold, fontSize = 18.sp, color = Color(0xFF1F2937)) - // ... rest of implementation ... - GlassButton(text = "关闭", onClick = onDismiss, modifier = Modifier.fillMaxWidth()) - } - } - } -} - -@Composable -private fun ManageIconLibraryDialog(library: IconLibrary, onDismiss: () -> Unit, onUpdate: (IconLibrary) -> Unit, onDelete: () -> Unit, onRefresh: () -> Unit) { - Dialog(onDismissRequest = onDismiss) { - Box(Modifier.width(300.dp).clip(RoundedCornerShape(24.dp)).background(Color.White).padding(20.dp)) { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text("管理图标库", fontWeight = FontWeight.Bold, fontSize = 18.sp, color = Color(0xFF1F2937)) - Text(library.name, color = Color(0xFF374151)) - GlassButton(text = "删除", onClick = onDelete, danger = true, modifier = Modifier.fillMaxWidth()) - GlassButton(text = "关闭", onClick = onDismiss, modifier = Modifier.fillMaxWidth()) + 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, + ) } } } 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 2063294..a801c0d 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt @@ -6,26 +6,21 @@ 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.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.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember 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.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -59,7 +54,7 @@ fun SearchTopBar( GlassSearchField( value = query, onValueChange = onQueryChange, - placeholder = stringResource(Res.string.search_hint), + placeholder = stringResource(Res.string.search_hint), onGearClick = onGearClick, ) } @@ -67,7 +62,11 @@ fun SearchTopBar( } @Composable -fun SearchScreen(query: String) { +fun SearchScreen( + query: String, + onQueryChange: (String) -> Unit = {}, + onGearClick: () -> Unit = {}, +) { val allMedia: List = sampleMovies() + sampleTvShows() val results = remember(query) { val q = query.trim() @@ -76,71 +75,80 @@ fun SearchScreen(query: String) { } } - Column( - Modifier - .fillMaxSize() - .padding(horizontal = 16.dp, vertical = 16.dp), - ) { - if (query.isBlank()) { - Box( - Modifier - .fillMaxSize() - .padding(top = 80.dp), - contentAlignment = Alignment.TopCenter, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp), + val bottomPadding = bottomBarContentPadding(90.dp).calculateBottomPadding() + + Box(Modifier.fillMaxSize()) { + // Content area (full size, same as HomeScreen) + Box( + Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .padding(bottom = bottomPadding), + ) { + if (query.isBlank()) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, ) { - Box( - Modifier - .size(72.dp) - .clip(CircleShape) - .background(GlassTokens.glassFill) - .border(1.dp, GlassTokens.glassBorder, CircleShape), - contentAlignment = Alignment.Center, + Column( + horizontalAlignment = Alignment.CenterHorizontally, ) { - androidx.compose.material3.Icon( - imageVector = Icons.Outlined.Search, - contentDescription = null, - tint = GlassTokens.subtext2, - modifier = Modifier.size(34.dp), - ) + Box( + Modifier + .size(64.dp) + .clip(CircleShape) + .background(GlassTokens.glassFill) + .border(1.dp, GlassTokens.glassBorder, CircleShape), + contentAlignment = Alignment.Center, + ) { + androidx.compose.material3.Icon( + imageVector = Icons.Outlined.Search, + contentDescription = null, + tint = GlassTokens.subtext2, + modifier = Modifier.size(32.dp), + ) + } + Spacer(Modifier.height(16.dp)) + Text(text = stringResource(Res.string.search_title), color = GlassTokens.text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold) + Spacer(Modifier.height(6.dp)) + Text(text = stringResource(Res.string.search_empty_input), color = GlassTokens.subtext, fontSize = 12.sp) } - Text(text = stringResource(Res.string.search_title), color = GlassTokens.text, fontSize = 16.sp, fontWeight = FontWeight.SemiBold) - Text(text = stringResource(Res.string.search_empty_input), color = GlassTokens.subtext, fontSize = 13.sp) } - } - } else if (results.isEmpty()) { - Box( - Modifier - .fillMaxSize() - .padding(top = 80.dp), - contentAlignment = Alignment.TopCenter, - ) { - Text(text = stringResource(Res.string.search_no_result_prefix) + "$query" + stringResource(Res.string.search_no_result_suffix), color = GlassTokens.subtext, fontSize = 14.sp) - } - } else { - LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - items(results) { item -> - Box(Modifier.fillMaxWidth()) { - PosterBox( - colors = item.posterColors, - title = item.title, - subtitle = item.subtitle, - height = 190.dp, - ) + } else if (results.isEmpty()) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text(text = stringResource(Res.string.search_no_result_prefix) + "$query" + stringResource(Res.string.search_no_result_suffix), color = GlassTokens.subtext, fontSize = 14.sp) + } + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + items(results) { item -> + Box(Modifier.fillMaxWidth()) { + PosterBox( + colors = item.posterColors, + title = item.title, + subtitle = item.subtitle, + height = 190.dp, + ) + } } } } } + + // Overlay top bar (same pattern as HomeScreen) + SearchTopBar( + query = query, + onQueryChange = onQueryChange, + onGearClick = onGearClick, + modifier = Modifier.align(Alignment.TopCenter), + ) } -} - - +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/example/myapplication/ui/AppBottomBar.kt b/app/src/main/kotlin/com/example/myapplication/ui/AppBottomBar.kt index 023e67c..ce762b9 100644 --- a/app/src/main/kotlin/com/example/myapplication/ui/AppBottomBar.kt +++ b/app/src/main/kotlin/com/example/myapplication/ui/AppBottomBar.kt @@ -14,7 +14,15 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Storage +import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.Storage import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -22,6 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -34,6 +43,19 @@ import com.example.myapplication.ui.liquidglass.LocalLiquidBottomTabsPillValue import com.kyant.backdrop.Backdrop import top.yukonga.miuix.kmp.basic.Text +private data class MainTab( + val selectedIcon: ImageVector, + val unselectedIcon: ImageVector, + val labelRes: Int, +) + +private val mainTabs = listOf( + MainTab(Icons.Filled.Home, Icons.Outlined.Home, Res.string.nav_home), + MainTab(Icons.Filled.Favorite, Icons.Outlined.FavoriteBorder, Res.string.nav_records), + MainTab(Icons.Filled.Storage, Icons.Outlined.Storage, Res.string.nav_library), + MainTab(Icons.Filled.Settings, Icons.Outlined.Settings, Res.string.nav_settings), +) + @Composable fun LiquidAppBottomBar( selectedTab: Int, diff --git a/app/src/main/kotlin/com/example/myapplication/ui/AppLanguage.kt b/app/src/main/kotlin/com/example/myapplication/ui/AppLanguage.kt index 50da752..cf7d862 100644 --- a/app/src/main/kotlin/com/example/myapplication/ui/AppLanguage.kt +++ b/app/src/main/kotlin/com/example/myapplication/ui/AppLanguage.kt @@ -54,6 +54,14 @@ object AppLanguage { return context as? Activity } + fun getLocalizedContext(context: Context): Context { + val tag = if (language == 0) "zh-CN" else "en" + val locale = java.util.Locale.forLanguageTag(tag) + val config = android.content.res.Configuration(context.resources.configuration) + config.setLocale(locale) + return context.createConfigurationContext(config) + } + private fun preferences(context: Context): SharedPreferences { return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) } 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 71af70b..6b41d2b 100644 --- a/app/src/main/kotlin/com/example/myapplication/ui/LiquidGlass.kt +++ b/app/src/main/kotlin/com/example/myapplication/ui/LiquidGlass.kt @@ -40,6 +40,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.runtime.remember +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -489,6 +491,9 @@ fun GlassDialog( content: @Composable () -> Unit, ) { Dialog(onDismissRequest = onDismissRequest) { + val dialogCtx = LocalContext.current + val localizedCtx = remember(AppLanguage.language) { AppLanguage.getLocalizedContext(dialogCtx) } + CompositionLocalProvider(LocalContext provides localizedCtx) { Box( modifier .width(IntrinsicSize.Min) @@ -498,6 +503,7 @@ fun GlassDialog( ) { content() } + } } } diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..66137cb 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -4,167 +4,15 @@ android:height="108dp" android:viewportWidth="108" android:viewportHeight="108"> + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:fillColor="#1A321946" + android:pathData="M54,54m-36,0a36,36 0,1 1,72 0a36,36 0,1 1,-72 0" /> + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 2b068d1..858b757 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,30 +1,18 @@ + - - - - - - - - + + - \ No newline at end of file + android:fillColor="#FFFFFFFF" + android:pathData="M54,54m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0" /> + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eba48dc..8ae0e29 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eba48dc..8ae0e29 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index a072795..2eb7187 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png index 33a2f35..b3116f3 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index c946fff..d223faa 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index 7816585..2eb7187 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index 51542d7..532b6d7 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png index 652c73e..1bd7a18 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index 62d3170..c19d868 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index b776493..532b6d7 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 1fb4ac0..444de49 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png index 95b4a4b..84ac7d1 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index 4841f19..bb151cb 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 6f51950..444de49 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 8d31334..72b74d9 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png index 61d4093..7a8f2d9 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index 2662ffa..14ea78f 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 7b8afe5..72b74d9 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index cbcbe74..6364588 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png index dde9f80..5481380 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index dac2e1d..0a2abd2 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 0a7e774..6364588 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index e7889b9..b397bac 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -46,7 +46,7 @@ Continue Watching Default Set as default icon library - Used first when changing library icons My Application + Used first when changing library icons LumaPlayer Cold War 2 Jul 8, 2016 Crime / Mystery TV - Korea @@ -92,7 +92,96 @@ Failed to load icons Search Search settings - Luma Player + LumaPlayer + + No media libraries available + Add a server in the server tab + No media content + The current server has no content available + No favorites yet + Browse media and add to favorites + No servers yet + Tap the add button to get started + No calendar data + Calendar feature coming soon + + + Icon Library Manager + Add + No icon libraries yet + Tap top-left to add + Icons not loaded + No icons + Preview · Edit + Icon Libraries + Manage + Close + %d icons + %d + + + Manage Icon Library + Icon Count + Library Name + Library URL (JSON) + Icon Preview + Refresh Icons + Delete + Save + + + Add Icon Library + e.g. Emby Icon Library + Fetch Preview + Fetch failed + Preview (%d icons) + ✓ Loaded %d icons + Cancel + + + Select Icon + Search icon name + Clear search + No icon libraries + Add icon libraries in the library manager first + Loaded %d icons, tap to select + No matching icons found + + + Add Server + Edit Server + Name + My Emby + URL + Please enter URL + Path + Port + Please enter port + Username + Username + Please enter username + Password + Password + Hide password + Show password + Note + Note + Saving… + Save + Cancel + Online + Offline + Actions + Edit + Change Icon + Just now + Toggle View + Icon Library + Add Server + + Login failed + Network error: %s + Unknown diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e9fda5..0d8397f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - My Application + LumaPlayer 首页 Vision 服务器 @@ -92,7 +92,96 @@ 图标加载失败 搜索 搜索设置 - Luma Player + LumaPlayer + + 暂无可用的媒体库 + 请在资源库中添加服务器 + 暂无媒体内容 + 当前服务器暂无可用内容 + 暂无收藏内容 + 浏览媒体并添加收藏 + 暂无服务器 + 点击右上角添加服务器 + 暂无日历数据 + 日历功能即将推出 + + + 图标库管理 + 添加 + 还没有图标库 + 点击左上角添加 + 图标未加载 + 暂无图标 + 预览 · 修改 + 图标库 + 管理 + 关闭 + %d 个图标 + %d 个 + + + 管理图标库 + 图标数量 + 图标库名称 + 图标库地址 (JSON) + 图标预览 + 刷新图标 + 删除 + 保存 + + + 增加图标库 + 例如: Emby 图标库 + 获取预览 + 获取失败 + 预览 (%d 个图标) + ✓ 已加载 %d 个图标 + 取消 + + + 选择图标 + 搜索图标名称 + 清除搜索 + 暂无图标库 + 请先在图标库管理中添加图标库 + 已加载 %d 个图标,点击选择 + 未找到匹配的图标 + + + 添加服务器 + 编辑服务器 + 名称 + 我的 Emby + 地址 + 请输入地址 + 路径 + 端口 + 请输入端口 + 用户名 + 用户名 + 请输入用户名 + 密码 + 密码 + 隐藏密码 + 显示密码 + 备注 + 备注 + 保存中… + 保存 + 取消 + 在线 + 离线 + 操作 + 编辑 + 更换图标 + 刚刚 + 视图切换 + 图标库管理 + 添加服务器 + + 登录失败 + 网络错误: %s + 未知 diff --git a/settings.gradle.kts b/settings.gradle.kts index 68ae130..482b3b9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,5 +27,5 @@ dependencyResolutionManagement { } } -rootProject.name = "EmbyPlayer" +rootProject.name = "LumaPlayer" include(":app")