diff --git a/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt b/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt index ee7a686..bdaf84b 100644 --- a/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt +++ b/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt @@ -38,7 +38,8 @@ data class ScoreEntry( val duration: Int, val playerCount: Int, val turnNumber: Int, - val date: Long + val date: Long, + val scoreDetail: String = "" ) object Scoreboard { @@ -66,7 +67,8 @@ object Scoreboard { difficulty: String, duration: Int, playerCount: Int, - turnNumber: Int + turnNumber: Int, + scoreDetail: String = "" ) { val scores = loadScores(context).toMutableList() scores.add( @@ -78,7 +80,8 @@ object Scoreboard { duration = duration, playerCount = playerCount, turnNumber = turnNumber, - date = System.currentTimeMillis() + date = System.currentTimeMillis(), + scoreDetail = scoreDetail ) ) saveScores(context, scores.sortedByDescending { it.points }.take(50)) @@ -158,6 +161,15 @@ fun LocalGameScreen( active.score } } + val scoreDetails = state.players.filter { it.id != myPlayerId }.flatMap { player -> + player.cards.map { card -> + val active = if (state.flipped && card.flipSide != null) card.flipSide else card + val label = if (active.type == CardType.NUMBER) "${active.color.displayName}${active.number}" + else "${active.color.displayName}${active.type.symbol}" + "${label}(${active.score}分)" + } + } + val detailStr = scoreDetails.joinToString(", ") Scoreboard.addEntry( context = context, name = humanPlayerName, @@ -166,7 +178,8 @@ fun LocalGameScreen( difficulty = gameDifficulty, duration = gameDuration, playerCount = totalPlayers, - turnNumber = gameTurnNumber + turnNumber = gameTurnNumber, + scoreDetail = detailStr ) } } @@ -315,7 +328,7 @@ fun LocalGameScreen( gameLog.forEachIndexed { index, msg -> Row(modifier = Modifier.padding(vertical = 2.dp)) { Text( - "${index + 1}. ", + "${gameLog.size - index}. ", color = Color.White.copy(alpha = 0.3f), fontSize = 12.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 dcc31b9..922d780 100644 --- a/app/src/main/java/com/unogame/ui/screens/MainMenuScreen.kt +++ b/app/src/main/java/com/unogame/ui/screens/MainMenuScreen.kt @@ -38,6 +38,7 @@ fun MainMenuScreen( onNameChanged: (String) -> Unit ) { var playerName by remember { mutableStateOf(initialName) } + var showAbout by remember { mutableStateOf(false) } Box( modifier = Modifier @@ -225,6 +226,15 @@ fun MainMenuScreen( 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(), @@ -249,4 +259,29 @@ fun MainMenuScreen( } } } + + // 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/ScoreboardScreen.kt b/app/src/main/java/com/unogame/ui/screens/ScoreboardScreen.kt index 856382d..516d5ec 100644 --- a/app/src/main/java/com/unogame/ui/screens/ScoreboardScreen.kt +++ b/app/src/main/java/com/unogame/ui/screens/ScoreboardScreen.kt @@ -1,12 +1,18 @@ package com.unogame.ui.screens +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.widget.Toast import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed 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.* @@ -19,6 +25,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.google.gson.Gson import com.unogame.game.GameMode import com.unogame.ui.theme.* import java.text.SimpleDateFormat @@ -28,10 +35,16 @@ import java.util.* @Composable fun ScoreboardScreen(onBack: () -> Unit) { val context = LocalContext.current - val scores = remember { Scoreboard.loadScores(context) } + var scores by remember { mutableStateOf(Scoreboard.loadScores(context)) } var selectedFilter by remember { mutableStateOf("全部") } val modes = listOf("全部") + GameMode.values().map { it.displayName } + var showDetailDialog by remember { mutableStateOf(false) } + var detailEntry by remember { mutableStateOf(null) } + var showImportDialog by remember { mutableStateOf(false) } + var importJson by remember { mutableStateOf("") } + val gson = remember { Gson() } + val filtered = if (selectedFilter == "全部") scores else scores.filter { it.mode == selectedFilter } @@ -49,6 +62,38 @@ fun ScoreboardScreen(onBack: () -> Unit) { Spacer(modifier = Modifier.height(16.dp)) + // Import / Export buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedButton( + onClick = { + val json = gson.toJson(scores) + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText("uno_scoreboard", json)) + Toast.makeText(context, "已复制备份JSON到剪贴板", Toast.LENGTH_SHORT).show() + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White.copy(alpha = 0.7f)) + ) { + Icon(Icons.Default.FileUpload, null, modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.width(4.dp)) + Text("导出备份", fontSize = 12.sp) + } + OutlinedButton( + onClick = { showImportDialog = true }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White.copy(alpha = 0.7f)) + ) { + Icon(Icons.Default.FileDownload, null, modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.width(4.dp)) + Text("导入备份", fontSize = 12.sp) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + // Mode filter chips if (scores.isNotEmpty()) { Row( @@ -106,7 +151,12 @@ fun ScoreboardScreen(onBack: () -> Unit) { else "${entry.duration}秒" Card( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .clickable { + detailEntry = entry + showDetailDialog = true + }, shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors(containerColor = if (index < 3) DarkSurface else DarkCard) ) { @@ -179,4 +229,102 @@ fun ScoreboardScreen(onBack: () -> Unit) { } } } + + // Detail dialog + if (showDetailDialog && detailEntry != null) { + val entry = detailEntry!! + val grouped = entry.scoreDetail.split(", ").filter { it.isNotEmpty() } + .groupBy { it } + .mapValues { it.value.size } + AlertDialog( + onDismissRequest = { showDetailDialog = false }, + title = { + Text("${entry.name} — ${entry.mode}", color = GoldAccent, fontWeight = FontWeight.Bold, fontSize = 18.sp) + }, + text = { + Column( + modifier = Modifier.heightIn(max = 400.dp) + .verticalScroll(rememberScrollState()) + ) { + Text("总分: ${entry.points}分", color = GoldAccent, fontSize = 16.sp, fontWeight = FontWeight.Black) + Spacer(modifier = Modifier.height(4.dp)) + Text("难度: ${entry.difficulty} | ${entry.playerCount}人 | ${entry.turnNumber}轮", + color = Color.White.copy(alpha = 0.5f), fontSize = 12.sp) + Spacer(modifier = Modifier.height(12.dp)) + Text("分数明细", color = GoldAccent, fontSize = 14.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(6.dp)) + if (grouped.isEmpty()) { + Text("无明细数据", color = Color.White.copy(alpha = 0.4f), fontSize = 13.sp) + } + grouped.forEach { (cardLabel, count) -> + val cardScore = cardLabel.substringAfter("(").substringBefore("分)").toIntOrNull() ?: 0 + val subtotal = cardScore * count + Row(modifier = Modifier.padding(vertical = 2.dp)) { + Text( + "$cardLabel × $count = ${subtotal}分", + color = Color.White.copy(alpha = 0.75f), + fontSize = 13.sp + ) + } + } + } + }, + confirmButton = { + TextButton(onClick = { showDetailDialog = false }) { + Text("关闭", color = GoldAccent) + } + }, + containerColor = DarkSurface + ) + } + + // Import dialog + if (showImportDialog) { + AlertDialog( + onDismissRequest = { showImportDialog = false }, + title = { Text("导入备份", color = GoldAccent) }, + text = { + Column { + Text("粘贴之前导出的JSON数据:", color = Color.White.copy(alpha = 0.7f), fontSize = 14.sp) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = importJson, + onValueChange = { importJson = it }, + modifier = Modifier.fillMaxWidth().heightIn(min = 100.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = Color.White, + unfocusedTextColor = Color.White, + focusedBorderColor = GoldAccent, + unfocusedBorderColor = Color.Gray, + cursorColor = GoldAccent + ) + ) + } + }, + confirmButton = { + TextButton(onClick = { + try { + val imported = gson.fromJson(importJson, Array::class.java)?.toList() ?: emptyList() + if (imported.isNotEmpty()) { + val merged = (scores + imported).distinctBy { "${it.name}_${it.mode}_${it.date}" } + .sortedByDescending { it.points }.take(50) + Scoreboard.saveScores(context, merged) + scores = merged + Toast.makeText(context, "成功导入 ${imported.size} 条记录", Toast.LENGTH_SHORT).show() + } + showImportDialog = false + importJson = "" + } catch (e: Exception) { + Toast.makeText(context, "JSON格式错误: ${e.message}", Toast.LENGTH_LONG).show() + } + }) { Text("导入", color = GoldAccent) } + }, + dismissButton = { + TextButton(onClick = { showImportDialog = false; importJson = "" }) { + Text("取消", color = Color.White.copy(alpha = 0.5f)) + } + }, + containerColor = DarkSurface + ) + } }