diff --git a/android/app/src/main/java/com/tsmobile/app/ui/components/ServerDetailCard.kt b/android/app/src/main/java/com/tsmobile/app/ui/components/ServerDetailCard.kt index 23f0f24..ace72bb 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/components/ServerDetailCard.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/components/ServerDetailCard.kt @@ -2,25 +2,36 @@ package com.tsmobile.app.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +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.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.LinkOff import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.tsmobile.app.data.ConnectionState import com.tsmobile.app.data.ServerInfo @@ -36,27 +47,84 @@ fun ServerDetailCard( onDismiss: () -> Unit, ) { var showDisconnectDialog by remember { mutableStateOf(false) } - val maxHeight = (LocalConfiguration.current.screenHeightDp * 0.8f).dp - DetailSheetScaffold( - title = serverName.ifEmpty { "服务器详情" }, - subtitle = serverAddress, - onDismiss = onDismiss, - modifier = Modifier.padding(UiTokens.Spacing.Large), - maxHeight = maxHeight, - content = { ServerInfoSection(serverInfo, connectionState) }, - footer = { + ModalDrawerSheet { + Column(modifier = Modifier.fillMaxSize()) { + // ═══ 顶部:服务器名称 + 地址 + 关闭按钮 ═══ + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = UiTokens.Spacing.Large, + end = UiTokens.Spacing.Small, + top = UiTokens.Spacing.Large, + bottom = UiTokens.Spacing.Medium, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = serverName.ifEmpty { "服务器详情" }, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + if (serverAddress.isNotEmpty()) { + Spacer(Modifier.height(UiTokens.Spacing.ExtraSmall)) + Text( + text = serverAddress, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + IconButton(onClick = onDismiss) { + Icon(Icons.Default.Close, contentDescription = "关闭") + } + } + + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + + // ═══ 中部:欢迎消息 + 服务器信息(可滚动) ═══ + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + .padding(UiTokens.Spacing.Large), + verticalArrangement = Arrangement.spacedBy(UiTokens.Spacing.Medium), + ) { + // 欢迎消息 + if (serverInfo != null && serverInfo.welcomeMessage.isNotEmpty()) { + SectionCard(title = "欢迎消息") { + Text( + text = serverInfo.welcomeMessage, + style = MaterialTheme.typography.bodySmall, + ) + } + } + + // 服务器详细信息 + SectionCard(title = "服务器信息") { + ServerInfoContent(serverInfo, connectionState) + } + } + + // ═══ 底部:断开连接按钮 ═══ + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) OutlinedButton( onClick = { showDisconnectDialog = true }, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error), + modifier = Modifier + .fillMaxWidth() + .padding(UiTokens.Spacing.Large), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error + ), ) { Icon(Icons.Default.LinkOff, contentDescription = null) Spacer(Modifier.width(UiTokens.Spacing.Small)) Text("断开连接") } - }, - ) + } + } if (showDisconnectDialog) { DisconnectConfirmDialog( @@ -67,7 +135,34 @@ fun ServerDetailCard( } @Composable -private fun ServerInfoSection(serverInfo: ServerInfo?, connectionState: ConnectionState?) { +private fun SectionCard( + title: String, + content: @Composable ColumnScope.() -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surfaceVariant, + tonalElevation = UiTokens.Elevation.Raised, + ) { + Column(modifier = Modifier.padding(UiTokens.Spacing.Medium)) { + Text( + text = title, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.Medium, + ) + Spacer(Modifier.height(UiTokens.Spacing.Small)) + content() + } + } +} + +@Composable +private fun ServerInfoContent( + serverInfo: ServerInfo?, + connectionState: ConnectionState?, +) { Column(verticalArrangement = Arrangement.spacedBy(UiTokens.Spacing.Small)) { DetailInfoRow("状态", when (connectionState) { is ConnectionState.Connected -> "已连接" @@ -78,17 +173,18 @@ private fun ServerInfoSection(serverInfo: ServerInfo?, connectionState: Connecti null -> "未连接" }) if (serverInfo == null) { - Text("正在加载服务器信息...", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + "正在加载服务器信息...", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } else { DetailInfoRow("在线人数", "${serverInfo.clientsOnline} / ${serverInfo.maxClients}") DetailInfoRow("频道数", "${serverInfo.channelsOnline}") if (serverInfo.version.isNotEmpty()) DetailInfoRow("版本", serverInfo.version) if (serverInfo.platform.isNotEmpty()) DetailInfoRow("平台", serverInfo.platform) - (serverInfo.uptime.toLongOrNull() ?: 0).takeIf { it > 0 }?.let { DetailInfoRow("运行时长", formatUptime(it)) } - if (serverInfo.welcomeMessage.isNotEmpty()) { - Spacer(Modifier.height(UiTokens.Spacing.Small)) - Text("欢迎消息", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) - Text(serverInfo.welcomeMessage, style = MaterialTheme.typography.bodySmall) + (serverInfo.uptime.toLongOrNull() ?: 0).takeIf { it > 0 }?.let { + DetailInfoRow("运行时长", formatUptime(it)) } } } @@ -98,5 +194,9 @@ private fun formatUptime(seconds: Long): String { val days = seconds / 86400 val hours = (seconds % 86400) / 3600 val minutes = (seconds % 3600) / 60 - return when { days > 0 -> "${days}天${hours}小时"; hours > 0 -> "${hours}小时${minutes}分钟"; else -> "${minutes}分钟" } + return when { + days > 0 -> "${days}天${hours}小时" + hours > 0 -> "${hours}小时${minutes}分钟" + else -> "${minutes}分钟" + } } diff --git a/android/app/src/main/java/com/tsmobile/app/ui/screens/ChannelListScreen.kt b/android/app/src/main/java/com/tsmobile/app/ui/screens/ChannelListScreen.kt index b78d0ab..0f681a3 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/screens/ChannelListScreen.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/screens/ChannelListScreen.kt @@ -110,12 +110,52 @@ fun ChannelListScreen( // 防抖:服务器详情按钮上次点击时间 var lastServerDetailClickTime by remember { mutableLongStateOf(0L) } - Box(modifier = Modifier.fillMaxSize().systemBarsPadding()) { + // 服务器状态(提前声明,供侧栏 drawerContent 读取) + val serverInfo by serverViewModel.serverInfo.collectAsState() + val serverState by serverViewModel.state.collectAsState() + val themeMode by serverViewModel.themeMode.collectAsState() + + // 侧栏抽屉状态 + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + + // 同步 showServerDetail 与抽屉开合 + LaunchedEffect(showServerDetail) { + if (showServerDetail) drawerState.open() else drawerState.close() + } + // 手势滑动打开时同步状态并加载数据 + LaunchedEffect(drawerState.isOpen) { + if (drawerState.isOpen && !showServerDetail) { + serverViewModel.fetchServerInfo() + showServerDetail = true + } + } + // 手势滑动关闭或编程关闭后同步状态 + LaunchedEffect(drawerState.isClosed) { + if (drawerState.isClosed && showServerDetail) { + showServerDetail = false + } + } + + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ServerDetailCard( + serverInfo = serverInfo, + serverName = serverInfo?.name ?: "", + serverAddress = serverState.address, + connectionState = connectionState, + onDisconnect = { + showServerDetail = false + serverViewModel.disconnect() + onNavigateToServerConfig() + }, + onDismiss = { showServerDetail = false }, + ) + }, + ) { + Box(modifier = Modifier.fillMaxSize().systemBarsPadding()) { Column(modifier = Modifier.fillMaxSize()) { // 头部 - val serverInfo by serverViewModel.serverInfo.collectAsState() - val serverState by serverViewModel.state.collectAsState() - val themeMode by serverViewModel.themeMode.collectAsState() ChannelListHeader( connectionState = connectionState, serverName = serverInfo?.name ?: "", @@ -182,6 +222,7 @@ fun ChannelListScreen( } } + } // ModalNavigationDrawer // ─── 弹窗层 ─── @@ -201,28 +242,6 @@ fun ChannelListScreen( } } - // 服务器详情卡弹窗(BottomSheet) - if (showServerDetail) { - val serverInfo by serverViewModel.serverInfo.collectAsState() - val serverState by serverViewModel.state.collectAsState() - ModalBottomSheet( - onDismissRequest = { showServerDetail = false }, - ) { - ServerDetailCard( - serverInfo = serverInfo, - serverName = serverInfo?.name ?: "", - serverAddress = serverState.address, - connectionState = connectionState, - onDisconnect = { - showServerDetail = false - serverViewModel.disconnect() - onNavigateToServerConfig() - }, - onDismiss = { showServerDetail = false }, - ) - } - } - // 频道详情卡弹窗(BottomSheet) if (showChannelDetailCard) { ModalBottomSheet( diff --git a/android/app/src/main/java/com/tsmobile/app/viewmodel/ServerViewModel.kt b/android/app/src/main/java/com/tsmobile/app/viewmodel/ServerViewModel.kt index 6f940c3..7d1f60a 100644 --- a/android/app/src/main/java/com/tsmobile/app/viewmodel/ServerViewModel.kt +++ b/android/app/src/main/java/com/tsmobile/app/viewmodel/ServerViewModel.kt @@ -192,6 +192,7 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) lastChannelId = chId } _connectionState.value = ConnectionState.WaitingForNetwork() + ConnectionService.stop(getApplication()) if (networkMonitor.isAvailable.value) { startReconnect("网络变化,正在重连...") } @@ -1071,11 +1072,13 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) return } - // 已在 WaitingForNetwork 或 Reconnecting 状态 → 我们的响应式监控已处理 + // 已在 WaitingForNetwork 或 Reconnecting 状态 → 响应式监控已处理 + // 但仍需更新通知栏(否则断连后仍显示"已连接") if (_connectionState.value is ConnectionState.WaitingForNetwork || _connectionState.value is ConnectionState.Reconnecting ) { - android.util.Log.d(TAG, "onDisconnected while already handling reconnect, ignoring") + android.util.Log.d(TAG, "onDisconnected while already handling reconnect, stopping notification") + ConnectionService.stop(getApplication()) return }