refactor: 主页重构,联机模式/游戏设置改为二级页面,昵称/主题/关于移至设置

This commit is contained in:
flykhan 2026-04-26 18:16:23 +08:00
parent 093a759ad4
commit d8191a44b1
5 changed files with 357 additions and 217 deletions

View File

@ -198,40 +198,17 @@ fun UnoApp() {
) {
composable(Screen.MainMenu.route) {
MainMenuScreen(
initialName = savedName,
onNameChanged = { name ->
savedName = name
prefs.edit().putString("player_name", name).apply()
},
onLocalGame = {
navController.navigate(Screen.ModeSelect.route)
},
onScoreboard = {
navController.navigate(Screen.Scoreboard.route)
},
onRules = {
navController.navigate(Screen.Rules.route)
},
currentTheme = cardTheme,
currentBg = tableBg,
onToggleTheme = {
val next = CardTheme.values()[(cardTheme.ordinal + 1) % CardTheme.values().size]
cardTheme = next
CardTheme.save(context, next)
},
onToggleBg = {
val next = TableBg.values()[(tableBg.ordinal + 1) % TableBg.values().size]
tableBg = next
TableBg.save(context, next)
},
onToggleOrientation = {
isLandscape = !isLandscape
prefs.edit().putBoolean("landscape", isLandscape).apply()
(context as? android.app.Activity)?.requestedOrientation =
if (isLandscape) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
},
isLandscape = isLandscape,
onLocalGame = { navController.navigate(Screen.ModeSelect.route) },
onOnlineGame = { navController.navigate(Screen.LobbyMenu.route) },
onScoreboard = { navController.navigate(Screen.Scoreboard.route) },
onRules = { navController.navigate(Screen.Rules.route) },
onSettings = { navController.navigate(Screen.Settings.route) }
)
}
composable(Screen.LobbyMenu.route) {
LobbyMenuScreen(
playerName = savedName,
onHostGame = { name ->
myName = name
isHost = true
@ -244,7 +221,6 @@ fun UnoApp() {
discoveryService = discovery
discovery.startAdvertising(name, 1)
navController.navigate(Screen.Lobby.createRoute(true))
// Host connects as a client too (to localhost) - on IO thread
scope.launch {
val client = GameClient()
if (client.connect("127.0.0.1", name)) {
@ -262,7 +238,6 @@ fun UnoApp() {
discoveryService = discovery
discovery.startDiscovery(name)
isDiscovering = true
scope.launch {
discovery.hosts.collect { hosts ->
discoveredHosts = hosts
@ -270,7 +245,8 @@ fun UnoApp() {
}
}
navController.navigate(Screen.Lobby.createRoute(false))
}
},
onBack = { navController.popBackStack() }
)
}
@ -424,6 +400,34 @@ fun UnoApp() {
RulesHelpScreen(onBack = { navController.popBackStack() })
}
composable(Screen.Settings.route) {
SettingsScreen(
initialName = savedName,
currentTheme = cardTheme,
currentBg = tableBg,
isLandscape = isLandscape,
onNameChanged = { name ->
savedName = name
prefs.edit().putString("player_name", name).apply()
},
onToggleTheme = {
val next = CardTheme.values()[(cardTheme.ordinal + 1) % CardTheme.values().size]
cardTheme = next
CardTheme.save(context, next)
},
onToggleBg = {
val next = TableBg.values()[(tableBg.ordinal + 1) % TableBg.values().size]
tableBg = next
TableBg.save(context, next)
},
onToggleOrientation = {
isLandscape = !isLandscape
prefs.edit().putBoolean("landscape", isLandscape).apply()
},
onBack = { navController.popBackStack() }
)
}
composable(Screen.Game.route) {
gameState?.let { state ->
GameScreen(

View File

@ -6,6 +6,7 @@ sealed class Screen(val route: String) {
fun createRoute(isHost: Boolean) = "lobby/$isHost"
}
data object ModeSelect : Screen("mode_select")
data object LobbyMenu : Screen("lobby_menu")
data object LocalSetup : Screen("local_setup/{modeName}") {
fun createRoute(mode: String) = "local_setup/$mode"
}
@ -20,4 +21,5 @@ sealed class Screen(val route: String) {
fun createRoute(winnerName: String, isYouWinner: Boolean) =
"game_over/$winnerName/$isYouWinner"
}
data object Settings : Screen("settings")
}

View File

@ -0,0 +1,101 @@
package com.unogame.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
import com.unogame.ui.theme.*
@Composable
fun LobbyMenuScreen(
playerName: String,
onHostGame: (String) -> Unit,
onJoinGame: (String) -> Unit,
onBack: () -> Unit
) {
Box(modifier = Modifier.fillMaxSize().background(LocalTableBg.current.color)) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, "返回", tint = Color.White)
}
Text("联机模式", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color.White)
}
Spacer(modifier = Modifier.height(40.dp))
// Create room
Button(
onClick = {
val name = playerName.ifBlank { "玩家" }
onHostGame(name)
},
modifier = Modifier.fillMaxWidth().height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = GoldAccent,
contentColor = Color.Black
)
) {
Icon(Icons.Default.Home, null)
Spacer(modifier = Modifier.width(12.dp))
Text("创建房间", fontSize = 18.sp, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(16.dp))
// Join room
Button(
onClick = {
val name = playerName.ifBlank { "玩家" }
onJoinGame(name)
},
modifier = Modifier.fillMaxWidth().height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = DarkSurface,
contentColor = Color.White
)
) {
Icon(Icons.Default.Search, null)
Spacer(modifier = Modifier.width(12.dp))
Text("加入房间", fontSize = 18.sp, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = DarkSurface.copy(alpha = 0.5f))
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("说明", color = GoldAccent, fontWeight = FontWeight.Bold, fontSize = 14.sp)
Spacer(modifier = Modifier.height(8.dp))
Text(
"创建房间:作为房主等待其他玩家加入\n加入房间搜索同一WiFi下的房间",
color = Color.White.copy(alpha = 0.6f),
fontSize = 13.sp,
lineHeight = 20.sp
)
}
}
}
}
}

View File

@ -1,6 +1,5 @@
package com.unogame.ui.screens
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@ -12,10 +11,8 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -23,23 +20,12 @@ import com.unogame.ui.theme.*
@Composable
fun MainMenuScreen(
initialName: String,
currentTheme: CardTheme,
currentBg: TableBg,
onHostGame: (String) -> Unit,
onJoinGame: (String) -> Unit,
onLocalGame: () -> Unit,
onOnlineGame: () -> Unit,
onScoreboard: () -> Unit,
onRules: () -> Unit,
onToggleTheme: () -> Unit,
onToggleBg: () -> Unit,
onToggleOrientation: () -> Unit,
isLandscape: Boolean,
onNameChanged: (String) -> Unit
onSettings: () -> Unit
) {
var playerName by remember { mutableStateOf(initialName) }
var showAbout by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
@ -54,7 +40,6 @@ fun MainMenuScreen(
horizontalAlignment = Alignment.CenterHorizontally
) {
// Title
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "UNO",
fontSize = 72.sp,
@ -62,7 +47,6 @@ fun MainMenuScreen(
color = GoldAccent,
textAlign = TextAlign.Center
)
}
Text(
text = "卡牌游戏",
fontSize = 18.sp,
@ -70,76 +54,9 @@ fun MainMenuScreen(
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(60.dp))
Spacer(modifier = Modifier.height(48.dp))
// Name input
OutlinedTextField(
value = playerName,
onValueChange = {
playerName = it.take(10)
onNameChanged(playerName.ifBlank { "玩家" })
},
label = { Text("你的昵称", color = Color.White.copy(alpha = 0.6f)) },
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedBorderColor = GoldAccent,
unfocusedBorderColor = Color.Gray,
cursorColor = GoldAccent
),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(32.dp))
// Host Game button
Button(
onClick = {
val name = playerName.ifBlank { "玩家" }
onNameChanged(name)
onHostGame(name)
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = DarkSurface,
contentColor = GoldAccent
)
) {
Icon(Icons.Default.Home, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text("创建房间", fontSize = 18.sp, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(16.dp))
// Join Game button
Button(
onClick = {
val name = playerName.ifBlank { "玩家" }
onNameChanged(name)
onJoinGame(name)
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = DarkSurface,
contentColor = Color.White
)
) {
Icon(Icons.Default.Search, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text("加入房间", fontSize = 18.sp, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(16.dp))
// Local Game button
// 1. 本地模式
Button(
onClick = onLocalGame,
modifier = Modifier
@ -158,7 +75,26 @@ fun MainMenuScreen(
Spacer(modifier = Modifier.height(16.dp))
// Scoreboard button
// 2. 联机模式
Button(
onClick = onOnlineGame,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = DarkSurface,
contentColor = GoldAccent
)
) {
Icon(Icons.Default.Wifi, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text("联机模式", fontSize = 18.sp, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(16.dp))
// 3. 排行榜
Button(
onClick = onScoreboard,
modifier = Modifier
@ -177,7 +113,7 @@ fun MainMenuScreen(
Spacer(modifier = Modifier.height(16.dp))
// Rules button
// 4. 规则说明
Button(
onClick = onRules,
modifier = Modifier
@ -196,92 +132,22 @@ fun MainMenuScreen(
Spacer(modifier = Modifier.height(16.dp))
// Theme toggle
TextButton(onClick = onToggleTheme) {
Icon(Icons.Default.Palette, null, tint = Color.White.copy(alpha = 0.5f))
Spacer(modifier = Modifier.width(6.dp))
Text("卡面: ${currentTheme.displayName}", color = Color.White.copy(alpha = 0.5f), fontSize = 14.sp)
Spacer(modifier = Modifier.width(4.dp))
Text("", color = GoldAccent, fontSize = 14.sp)
}
// Background toggle
TextButton(onClick = onToggleBg) {
Icon(Icons.Default.Wallpaper, null, tint = Color.White.copy(alpha = 0.5f))
Spacer(modifier = Modifier.width(6.dp))
Text("牌桌: ${currentBg.displayName}", color = Color.White.copy(alpha = 0.5f), fontSize = 14.sp)
Spacer(modifier = Modifier.width(4.dp))
Text("", color = GoldAccent, fontSize = 14.sp)
}
// Orientation toggle
TextButton(onClick = onToggleOrientation) {
Icon(if (isLandscape) Icons.Default.StayCurrentLandscape else Icons.Default.StayCurrentPortrait,
null, tint = Color.White.copy(alpha = 0.5f))
Spacer(modifier = Modifier.width(6.dp))
Text(if (isLandscape) "横屏" else "竖屏", color = Color.White.copy(alpha = 0.5f), fontSize = 14.sp)
Spacer(modifier = Modifier.width(4.dp))
Text("", color = GoldAccent, fontSize = 14.sp)
}
Spacer(modifier = Modifier.height(16.dp))
// About button
TextButton(onClick = { showAbout = true }) {
Icon(Icons.Default.Info, null, tint = Color.White.copy(alpha = 0.5f))
Spacer(modifier = Modifier.width(6.dp))
Text("关于", color = Color.White.copy(alpha = 0.5f), fontSize = 14.sp)
}
Spacer(modifier = Modifier.height(8.dp))
// Instructions
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = DarkSurface.copy(alpha = 0.5f))
// 5. 游戏设置
Button(
onClick = onSettings,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = DarkSurface,
contentColor = Color.White.copy(alpha = 0.5f)
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"游戏说明",
color = GoldAccent,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"• 创建房间:作为房主等待其他玩家加入\n• 加入房间搜索同一WiFi下的房间\n• 与同颜色、同数字/功能的牌匹配",
color = Color.White.copy(alpha = 0.7f),
fontSize = 12.sp,
lineHeight = 18.sp
)
Icon(Icons.Default.Settings, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp))
Text("游戏设置", fontSize = 18.sp, fontWeight = FontWeight.Bold)
}
}
}
}
// About dialog
if (showAbout) {
AlertDialog(
onDismissRequest = { showAbout = false },
title = { Text("关于", color = GoldAccent, fontWeight = FontWeight.Bold) },
text = {
Column {
Text("感谢名单", color = GoldAccent, fontSize = 16.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(12.dp))
Text("主创:刘博", color = Color.White, fontSize = 14.sp)
Spacer(modifier = Modifier.height(4.dp))
Text("最佳苦力opencode、deepseek v4 pro", color = Color.White, fontSize = 14.sp)
Spacer(modifier = Modifier.height(4.dp))
Text("金牌测试:张天弈、蔺明智", color = Color.White, fontSize = 14.sp)
}
},
confirmButton = {
TextButton(onClick = { showAbout = false }) {
Text("关闭", color = GoldAccent)
}
},
containerColor = DarkSurface
)
}
}

View File

@ -0,0 +1,167 @@
package com.unogame.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
import com.unogame.ui.theme.*
@Composable
fun SettingsScreen(
initialName: String,
currentTheme: CardTheme,
currentBg: TableBg,
isLandscape: Boolean,
onNameChanged: (String) -> Unit,
onToggleTheme: () -> Unit,
onToggleBg: () -> Unit,
onToggleOrientation: () -> Unit,
onBack: () -> Unit
) {
var playerName by remember { mutableStateOf(initialName) }
var showAbout by remember { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxSize().background(LocalTableBg.current.color)) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, "返回", tint = Color.White)
}
Text("游戏设置", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color.White)
}
Spacer(modifier = Modifier.height(32.dp))
// Nickname
Text("你的昵称", color = Color.White.copy(alpha = 0.6f), fontSize = 14.sp)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = playerName,
onValueChange = {
playerName = it.take(10)
onNameChanged(playerName.ifBlank { "玩家" })
},
singleLine = true,
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedBorderColor = GoldAccent,
unfocusedBorderColor = Color.Gray,
cursorColor = GoldAccent
)
)
Spacer(modifier = Modifier.height(28.dp))
// Card theme
Text("外观设置", color = Color.White.copy(alpha = 0.6f), fontSize = 14.sp)
Spacer(modifier = Modifier.height(12.dp))
SettingsRow(
icon = Icons.Default.Palette,
label = "卡面风格",
value = currentTheme.displayName,
onClick = onToggleTheme
)
SettingsRow(
icon = Icons.Default.Wallpaper,
label = "牌桌背景",
value = currentBg.displayName,
onClick = onToggleBg
)
SettingsRow(
icon = if (isLandscape) Icons.Default.StayCurrentLandscape else Icons.Default.StayCurrentPortrait,
label = "屏幕方向",
value = if (isLandscape) "横屏" else "竖屏",
onClick = onToggleOrientation
)
Spacer(modifier = Modifier.height(28.dp))
// About
Text("其他", color = Color.White.copy(alpha = 0.6f), fontSize = 14.sp)
Spacer(modifier = Modifier.height(12.dp))
SettingsRow(
icon = Icons.Default.Info,
label = "关于",
value = "",
onClick = { showAbout = true }
)
}
}
if (showAbout) {
AlertDialog(
onDismissRequest = { showAbout = false },
title = { Text("关于", color = GoldAccent, fontWeight = FontWeight.Bold) },
text = {
Column {
Text("感谢名单", color = GoldAccent, fontSize = 16.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(12.dp))
Text("主创:刘博", color = Color.White, fontSize = 14.sp)
Spacer(modifier = Modifier.height(4.dp))
Text("最佳苦力opencode、deepseek v4 pro", color = Color.White.copy(alpha = 0.7f), fontSize = 14.sp)
Spacer(modifier = Modifier.height(4.dp))
Text("金牌测试:张天羿、蔺明智", color = Color.White.copy(alpha = 0.7f), fontSize = 14.sp)
}
},
confirmButton = {
TextButton(onClick = { showAbout = false }) {
Text("关闭", color = GoldAccent)
}
},
containerColor = DarkSurface
)
}
}
@Composable
private fun SettingsRow(
icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String,
value: String,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() },
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = DarkSurface)
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(icon, null, tint = Color.White.copy(alpha = 0.6f), modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(12.dp))
Text(label, color = Color.White, fontSize = 15.sp, modifier = Modifier.weight(1f))
if (value.isNotEmpty()) {
Text(value, color = GoldAccent, fontSize = 14.sp)
Spacer(modifier = Modifier.width(4.dp))
Icon(Icons.Default.ChevronRight, null, tint = Color.White.copy(alpha = 0.3f), modifier = Modifier.size(18.dp))
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}