diff --git a/app/src/main/java/com/unogame/MainActivity.kt b/app/src/main/java/com/unogame/MainActivity.kt index 9734028..68ff3d1 100644 --- a/app/src/main/java/com/unogame/MainActivity.kt +++ b/app/src/main/java/com/unogame/MainActivity.kt @@ -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( diff --git a/app/src/main/java/com/unogame/ui/navigation/Screen.kt b/app/src/main/java/com/unogame/ui/navigation/Screen.kt index 3c9a667..fca5fb2 100644 --- a/app/src/main/java/com/unogame/ui/navigation/Screen.kt +++ b/app/src/main/java/com/unogame/ui/navigation/Screen.kt @@ -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") } diff --git a/app/src/main/java/com/unogame/ui/screens/LobbyMenuScreen.kt b/app/src/main/java/com/unogame/ui/screens/LobbyMenuScreen.kt new file mode 100644 index 0000000..c4eb857 --- /dev/null +++ b/app/src/main/java/com/unogame/ui/screens/LobbyMenuScreen.kt @@ -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 + ) + } + } + } + } +} diff --git a/app/src/main/java/com/unogame/ui/screens/MainMenuScreen.kt b/app/src/main/java/com/unogame/ui/screens/MainMenuScreen.kt index 922d780..2becf7b 100644 --- a/app/src/main/java/com/unogame/ui/screens/MainMenuScreen.kt +++ b/app/src/main/java/com/unogame/ui/screens/MainMenuScreen.kt @@ -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,15 +40,13 @@ fun MainMenuScreen( horizontalAlignment = Alignment.CenterHorizontally ) { // Title - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = "UNO", - fontSize = 72.sp, - fontWeight = FontWeight.Black, - color = GoldAccent, - textAlign = TextAlign.Center - ) - } + Text( + text = "UNO", + fontSize = 72.sp, + fontWeight = FontWeight.Black, + 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 - ) - } } diff --git a/app/src/main/java/com/unogame/ui/screens/SettingsScreen.kt b/app/src/main/java/com/unogame/ui/screens/SettingsScreen.kt new file mode 100644 index 0000000..1832341 --- /dev/null +++ b/app/src/main/java/com/unogame/ui/screens/SettingsScreen.kt @@ -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)) +}