feat: 出牌记录序号修正、关于页面感谢名单、积分明细查看、JSON导入导出备份
This commit is contained in:
parent
548fbef889
commit
2db44625bc
@ -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
|
||||
)
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ScoreEntry?>(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<ScoreEntry>::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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user