439 lines
21 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.unogame.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
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 androidx.compose.ui.window.Popup
import com.unogame.model.Card
import com.unogame.model.CardColor
import com.unogame.model.CardType
import com.unogame.ui.theme.*
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CardView(
card: Card,
modifier: Modifier = Modifier,
selected: Boolean = false,
playable: Boolean = true,
theme: CardTheme = LocalCardTheme.current,
onClick: () -> Unit = {}
) {
val cardBg = getCardBgColor(card.color.name)
val isWild = card.color == CardColor.WILD
var showInfo by remember { mutableStateOf(false) }
Box {
when (theme) {
CardTheme.CLASSIC -> ClassicCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.ELEGANT -> ElegantCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.MIDNIGHT -> MidnightCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.NEON -> NeonCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.PASTEL -> PastelCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.FOREST -> ForestCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.OCEAN -> OceanCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
}
if (showInfo) {
CardInfoPopup(card) { showInfo = false }
}
}
}
// ── Classic ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ClassicCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-30).dp) else Modifier)
.shadow(if (selected) 8.dp else 2.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.background(
if (isWild) Brush.verticalGradient(listOf(CardRedBg, CardBlueBg, CardGreenBg, CardYellowBg))
else Brush.verticalGradient(listOf(cardBg, cardBg.copy(alpha = 0.8f)))
)
.border(2.dp, if (selected) Color.White else Color.Transparent, RoundedCornerShape(10.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) { CardContent(card, isWild) }
}
// ── Elegant ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ElegantCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
val borderColor = if (isWild) GoldAccent else cardBg.copy(alpha = 0.5f)
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-30).dp) else Modifier)
.shadow(if (selected) 8.dp else 3.dp, RoundedCornerShape(14.dp))
.clip(RoundedCornerShape(14.dp))
.background(Color.White)
.border(2.dp, if (selected) GoldAccent else borderColor, RoundedCornerShape(14.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.fillMaxWidth(0.82f).fillMaxHeight(0.78f)
.clip(RoundedCornerShape(50))
.background(
if (isWild) Brush.verticalGradient(listOf(CardRedBg, CardBlueBg, CardGreenBg, CardYellowBg))
else Brush.linearGradient(listOf(cardBg, cardBg.copy(alpha = 0.6f)))
),
contentAlignment = Alignment.Center
) {
Text(card.displayText, color = Color.White,
fontSize = if (card.type == CardType.NUMBER && card.number >= 10) 16.sp else 22.sp,
fontWeight = FontWeight.Black)
}
Text(card.displayText, modifier = Modifier.align(Alignment.TopStart).padding(6.dp, 4.dp),
color = if (isWild) Color.White else cardBg, fontSize = 10.sp, fontWeight = FontWeight.Bold)
Text(card.displayText, modifier = Modifier.align(Alignment.BottomEnd).padding(6.dp, 4.dp),
color = if (isWild) Color.White else cardBg, fontSize = 10.sp, fontWeight = FontWeight.Bold)
}
}
// ── Midnight ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun MidnightCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
val glow = if (isWild) Color.Magenta.copy(alpha = 0.6f) else cardBg.copy(alpha = 0.6f)
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-30).dp) else Modifier)
.shadow(if (selected) 12.dp else 4.dp, RoundedCornerShape(12.dp), ambientColor = glow, spotColor = glow)
.clip(RoundedCornerShape(12.dp))
.background(DarkCard)
.border(1.5.dp, glow, RoundedCornerShape(12.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Text(card.displayText, color = if (isWild) Color.Magenta else getCardColor(card.color.name),
fontSize = 24.sp, fontWeight = FontWeight.Black)
if (card.color != CardColor.WILD) {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
repeat(3) { Box(Modifier.size(3.dp).clip(CircleShape).background(getCardColor(card.color.name))) }
}
}
}
}
}
// ── Neon ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun NeonCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
val neonColor = if (isWild) Color.Magenta else cardBg
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-30).dp) else Modifier)
.shadow(if (selected) 10.dp else 4.dp, RoundedCornerShape(8.dp), ambientColor = neonColor, spotColor = neonColor)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF0A0A0A))
.border(2.dp, neonColor, RoundedCornerShape(8.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
Text(card.displayText, color = neonColor, fontSize = 26.sp, fontWeight = FontWeight.Black)
if (!isWild) {
Text(card.displayText, modifier = Modifier.align(Alignment.TopStart).padding(6.dp, 4.dp),
color = neonColor.copy(alpha = 0.6f), fontSize = 9.sp)
Text(card.displayText, modifier = Modifier.align(Alignment.BottomEnd).padding(6.dp, 4.dp),
color = neonColor.copy(alpha = 0.6f), fontSize = 9.sp)
}
}
}
// ── Pastel ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PastelCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-30).dp) else Modifier)
.shadow(if (selected) 6.dp else 2.dp, RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(16.dp))
.background(
if (isWild) Brush.horizontalGradient(listOf(UnoRed.copy(alpha = 0.15f), UnoBlue.copy(alpha = 0.15f),
UnoGreen.copy(alpha = 0.15f), UnoYellow.copy(alpha = 0.15f)))
else Brush.verticalGradient(listOf(Color.White, cardBg.copy(alpha = 0.08f)))
)
.border(1.5.dp, cardBg.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(card.displayText, color = cardBg, fontSize = 26.sp, fontWeight = FontWeight.Bold)
if (isWild) {
Spacer(modifier = Modifier.height(2.dp))
Row(Modifier.fillMaxWidth(0.6f), horizontalArrangement = Arrangement.SpaceEvenly) {
listOf(UnoRed, UnoBlue, UnoGreen, UnoYellow).forEach { c ->
Box(Modifier.size(5.dp).clip(CircleShape).background(c))
}
}
}
}
}
}
// ── Forest ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ForestCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-30).dp) else Modifier)
.shadow(if (selected) 6.dp else 2.dp, RoundedCornerShape(6.dp))
.clip(RoundedCornerShape(6.dp))
.background(
if (isWild) Brush.verticalGradient(listOf(Color(0xFF2E7D32), Color(0xFF1B5E20)))
else Brush.verticalGradient(listOf(Color(0xFFE8F5E9), Color(0xFFC8E6C9)))
)
.border(1.5.dp, if (isWild) Color(0xFF81C784) else Color(0xFF388E3C).copy(alpha = 0.3f), RoundedCornerShape(6.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
Text(card.displayText, color = if (isWild) Color.White else cardBg, fontSize = 24.sp, fontWeight = FontWeight.Black)
}
}
// ── Ocean ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun OceanCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-30).dp) else Modifier)
.shadow(if (selected) 6.dp else 3.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.background(
if (isWild) Brush.verticalGradient(listOf(Color(0xFF0277BD), Color(0xFF00BCD4), Color(0xFF0288D1)))
else Brush.verticalGradient(listOf(cardBg.copy(alpha = 0.7f), cardBg))
)
.border(1.dp, Color.White.copy(alpha = 0.3f), RoundedCornerShape(10.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(card.displayText, color = Color.White, fontSize = 26.sp, fontWeight = FontWeight.Black)
if (!isWild) {
Spacer(modifier = Modifier.height(2.dp))
Row(horizontalArrangement = Arrangement.spacedBy(3.dp)) {
repeat(3) { Box(Modifier.size(4.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.5f))) }
}
}
}
}
}
@Composable
fun CardInfoPopup(card: Card, onDismiss: () -> Unit) {
val effectText = when (card.type) {
CardType.SKIP -> "跳过下家出牌"
CardType.REVERSE -> "反转出牌方向2人局=跳过)"
CardType.DRAW_TWO -> "下家摸2张并跳过"
CardType.WILD -> "指定任一颜色继续"
CardType.WILD_DRAW_FOUR -> "下家摸4张并跳过指定颜色"
CardType.FLIP -> "翻转所有牌到另一面"
CardType.DRAW_FIVE -> "下家摸5张并跳过"
CardType.SKIP_ALL -> "其他玩家各摸1张跳过下家"
CardType.WILD_DRAW_TWO -> "下家摸2张并跳过指定颜色"
CardType.DRAW_SIX -> "下家摸6张并跳过"
CardType.DRAW_TEN -> "下家摸10张并跳过"
CardType.WILD_DRAW_FOUR_REVERSE -> "方向反转下家摸4张指定颜色"
CardType.DISCARD_COLOR -> "弃掉手中所有同颜色牌"
CardType.WILD_DRAW_COLOR -> "跳过下家,指定颜色"
CardType.NUMBER -> "数字牌,配对颜色或数字"
}
Popup(onDismissRequest = onDismiss) {
Card(
modifier = Modifier.padding(32.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = DarkSurface)
) {
Column(modifier = Modifier.padding(20.dp)) {
Text("卡牌说明", color = GoldAccent, fontSize = 18.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(12.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
CardView(card, selected = false, playable = false, onClick = {})
Spacer(modifier = Modifier.width(16.dp))
Column {
Text("颜色: ${card.color.displayName}", color = getCardColor(card.color.name), fontSize = 14.sp, fontWeight = FontWeight.Bold)
Text("类型: ${card.displayText}", color = Color.White, fontSize = 14.sp)
if (card.type == CardType.NUMBER) {
Text("数字: ${card.number}", color = Color.White.copy(alpha = 0.7f), fontSize = 13.sp)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(effectText, color = Color.White.copy(alpha = 0.8f), fontSize = 13.sp, lineHeight = 18.sp)
Spacer(modifier = Modifier.height(12.dp))
TextButton(onClick = onDismiss, modifier = Modifier.align(Alignment.End)) {
Text("关闭", color = GoldAccent)
}
}
}
}
}
@Composable
private fun CardContent(card: Card, isWild: Boolean) {
if (isWild) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Text(if (card.type == CardType.WILD_DRAW_FOUR) "+4" else "W", color = Color.White,
fontSize = 18.sp, fontWeight = FontWeight.Black)
Spacer(modifier = Modifier.height(4.dp))
Row(Modifier.fillMaxWidth(0.7f), horizontalArrangement = Arrangement.SpaceEvenly) {
listOf(UnoRed, UnoBlue, UnoGreen, UnoYellow).forEach { c ->
Box(Modifier.size(6.dp).clip(CircleShape).background(c))
}
}
}
} else {
Column(Modifier.fillMaxSize().padding(4.dp), horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start) {
Text(card.displayText, color = Color.White.copy(alpha = 0.8f), fontSize = 10.sp, fontWeight = FontWeight.Bold)
}
Text(card.displayText, color = Color.White, fontSize = 26.sp, fontWeight = FontWeight.Black)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
Text(card.displayText, color = Color.White.copy(alpha = 0.8f), fontSize = 10.sp, fontWeight = FontWeight.Bold)
}
}
}
}
@Composable
fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.current) {
when (theme) {
CardTheme.CLASSIC -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(2.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.background(DarkCard)
.border(2.dp, GoldAccent, RoundedCornerShape(10.dp)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("U", color = GoldAccent, fontSize = 28.sp, fontWeight = FontWeight.Black)
Text("N", color = GoldAccent, fontSize = 28.sp, fontWeight = FontWeight.Black)
Text("O", color = GoldAccent, fontSize = 28.sp, fontWeight = FontWeight.Black)
}
}
}
CardTheme.ELEGANT -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(3.dp, RoundedCornerShape(14.dp))
.clip(RoundedCornerShape(14.dp))
.background(Color.White)
.border(2.dp, GoldAccent, RoundedCornerShape(14.dp)),
contentAlignment = Alignment.Center
) {
Box(Modifier.fillMaxWidth(0.75f).fillMaxHeight(0.7f).clip(RoundedCornerShape(40)).background(DarkCard),
contentAlignment = Alignment.Center) {
Text("UNO", color = GoldAccent, fontSize = 16.sp, fontWeight = FontWeight.Black)
}
}
}
CardTheme.MIDNIGHT -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(4.dp, RoundedCornerShape(12.dp), ambientColor = Color.Magenta.copy(alpha = 0.4f))
.clip(RoundedCornerShape(12.dp))
.background(DarkCard)
.border(1.5.dp, Color.Magenta.copy(alpha = 0.5f), RoundedCornerShape(12.dp)),
contentAlignment = Alignment.Center
) { Text("U", color = Color.Magenta, fontSize = 32.sp, fontWeight = FontWeight.Black) }
}
CardTheme.NEON -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(4.dp, RoundedCornerShape(8.dp), ambientColor = Color.Cyan)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF0A0A0A))
.border(2.dp, Color.Cyan, RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center
) { Text("UNO", color = Color.Cyan, fontSize = 14.sp, fontWeight = FontWeight.Black) }
}
CardTheme.PASTEL -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(2.dp, RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(16.dp))
.background(Brush.verticalGradient(listOf(Color(0xFFFFF3E0), Color(0xFFFCE4EC))))
.border(1.5.dp, Color(0xFFE0C0C0), RoundedCornerShape(16.dp)),
contentAlignment = Alignment.Center
) { Text("UNO", color = Color(0xFFCC9999), fontSize = 14.sp, fontWeight = FontWeight.Black) }
}
CardTheme.FOREST -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(2.dp, RoundedCornerShape(6.dp))
.clip(RoundedCornerShape(6.dp))
.background(Brush.verticalGradient(listOf(Color(0xFF2E7D32), Color(0xFF1B5E20))))
.border(1.5.dp, Color(0xFF81C784), RoundedCornerShape(6.dp)),
contentAlignment = Alignment.Center
) { Text("UNO", color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Black) }
}
CardTheme.OCEAN -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(3.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.background(Brush.verticalGradient(listOf(Color(0xFF0277BD), Color(0xFF00BCD4))))
.border(1.dp, Color.White.copy(alpha = 0.3f), RoundedCornerShape(10.dp)),
contentAlignment = Alignment.Center
) { Text("UNO", color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Black) }
}
}
}