2 Commits
Author SHA1 Message Date
sansen 18e2f730e7 1. 改服务详情卡片为侧边滑出卡片
2. 修复重连导致的通知栏信息不变更问题
2026-07-24 11:43:05 +08:00
sansen ebc46233e5 1. 改服务详情卡片为侧边滑出卡片
2. 修复重连导致的通知栏信息不变更问题
2026-07-24 11:42:46 +08:00
4 changed files with 174 additions and 52 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "com.tsmobile.app"
minSdk = 26
targetSdk = 35
versionCode = 7
versionName = "1.0.7"
versionCode = 8
versionName = "1.0.8"
}
signingConfigs {
@@ -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}分钟"
}
}
@@ -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(
@@ -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
}