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