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) } } } }