feat: add dark mode with toggle in top bar
- Created UnoTheme.kt with light/dark color schemes - Added dark mode toggle (moon/sun icon) in GameInfoBar - Wrapped all entry points (Desktop, Android, Web) with UnoTheme - Fixed Web entry point to use CanvasBasedWindow - Fixed Math.random() for WASM compatibility
This commit is contained in:
@@ -13,6 +13,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import com.uno.game.GameEngine
|
import com.uno.game.GameEngine
|
||||||
import com.uno.game.model.*
|
import com.uno.game.model.*
|
||||||
import com.uno.shared.UnoGameScreen
|
import com.uno.shared.UnoGameScreen
|
||||||
|
import com.uno.shared.UnoTheme
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -20,7 +21,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
MaterialTheme {
|
UnoTheme {
|
||||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
UnoApp()
|
UnoApp()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.ui.window.application
|
|||||||
import com.uno.game.GameEngine
|
import com.uno.game.GameEngine
|
||||||
import com.uno.game.model.*
|
import com.uno.game.model.*
|
||||||
import com.uno.shared.UnoGameScreen
|
import com.uno.shared.UnoGameScreen
|
||||||
|
import com.uno.shared.UnoTheme
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
@@ -49,31 +50,33 @@ fun UnoDesktopApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gameStarted) {
|
UnoTheme {
|
||||||
DesktopStartScreen(onStartGame = { playerCount ->
|
if (!gameStarted) {
|
||||||
gameState = engine.startGame(playerCount)
|
DesktopStartScreen(onStartGame = { playerCount ->
|
||||||
gameStarted = true
|
gameState = engine.startGame(playerCount)
|
||||||
})
|
gameStarted = true
|
||||||
} else {
|
})
|
||||||
UnoGameScreen(
|
} else {
|
||||||
state = gameState,
|
UnoGameScreen(
|
||||||
onPlayCard = { card, color ->
|
state = gameState,
|
||||||
gameState = engine.playCard(gameState, 0, card, color)
|
onPlayCard = { card, color ->
|
||||||
},
|
gameState = engine.playCard(gameState, 0, card, color)
|
||||||
onDrawCard = {
|
},
|
||||||
gameState = engine.drawCard(gameState, 0)
|
onDrawCard = {
|
||||||
},
|
gameState = engine.drawCard(gameState, 0)
|
||||||
onCallUno = { gameState = engine.callUno(gameState, 0) }
|
},
|
||||||
)
|
onCallUno = { gameState = engine.callUno(gameState, 0) }
|
||||||
|
|
||||||
if (gameState.isFinished) {
|
|
||||||
DesktopGameOverScreen(
|
|
||||||
winner = gameState.winner,
|
|
||||||
onPlayAgain = {
|
|
||||||
gameState = engine.startGame()
|
|
||||||
gameStarted = false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (gameState.isFinished) {
|
||||||
|
DesktopGameOverScreen(
|
||||||
|
winner = gameState.winner,
|
||||||
|
onPlayAgain = {
|
||||||
|
gameState = engine.startGame()
|
||||||
|
gameStarted = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ private class FlyingCardState {
|
|||||||
animatableX = Animatable(fromX)
|
animatableX = Animatable(fromX)
|
||||||
animatableY = Animatable(fromY)
|
animatableY = Animatable(fromY)
|
||||||
animatableScale = Animatable(0.3f)
|
animatableScale = Animatable(0.3f)
|
||||||
animatableRotation = Animatable((Math.random() * 60 - 30).toFloat())
|
animatableRotation = Animatable((kotlin.random.Random.nextFloat() * 60 - 30).toFloat())
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
launch {
|
launch {
|
||||||
@@ -274,6 +274,7 @@ fun UnoGameScreen(
|
|||||||
var previousLastAction by remember { mutableStateOf(state.lastAction) }
|
var previousLastAction by remember { mutableStateOf(state.lastAction) }
|
||||||
var previousTopCard by remember { mutableStateOf(state.deck.topCard) }
|
var previousTopCard by remember { mutableStateOf(state.deck.topCard) }
|
||||||
var lastPlayedByPlayerIndex by remember { mutableIntStateOf(0) }
|
var lastPlayedByPlayerIndex by remember { mutableIntStateOf(0) }
|
||||||
|
var isDarkMode by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val flyingState = rememberFlyingCardState()
|
val flyingState = rememberFlyingCardState()
|
||||||
|
|
||||||
@@ -317,14 +318,15 @@ fun UnoGameScreen(
|
|||||||
previousLastAction = state.lastAction
|
previousLastAction = state.lastAction
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
UnoTheme(darkTheme = isDarkMode) {
|
||||||
Column(
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
modifier = Modifier
|
Column(
|
||||||
.fillMaxSize()
|
modifier = Modifier
|
||||||
.padding(16.dp),
|
.fillMaxSize()
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
.padding(16.dp),
|
||||||
) {
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
GameInfoBar(state)
|
) {
|
||||||
|
GameInfoBar(state, isDarkMode, { isDarkMode = it })
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
OpponentHands(state)
|
OpponentHands(state)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
@@ -417,12 +419,13 @@ fun UnoGameScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// region --- GameInfoBar ---
|
// region --- GameInfoBar ---
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GameInfoBar(state: GameState) {
|
private fun GameInfoBar(state: GameState, isDarkMode: Boolean, onDarkModeToggle: (Boolean) -> Unit) {
|
||||||
val directionRotation by animateFloatAsState(
|
val directionRotation by animateFloatAsState(
|
||||||
targetValue = if (state.direction == GameDirection.CLOCKWISE) 0f else 180f,
|
targetValue = if (state.direction == GameDirection.CLOCKWISE) 0f else 180f,
|
||||||
animationSpec = tween(500, easing = FastOutSlowInEasing),
|
animationSpec = tween(500, easing = FastOutSlowInEasing),
|
||||||
@@ -462,6 +465,13 @@ private fun GameInfoBar(state: GameState) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Dark mode toggle
|
||||||
|
IconButton(onClick = { onDarkModeToggle(!isDarkMode) }) {
|
||||||
|
Text(
|
||||||
|
text = if (isDarkMode) "\u2600\uFE0F" else "\uD83C\uDF19",
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.uno.shared
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
private val LightColorScheme = lightColorScheme(
|
||||||
|
primary = Color(0xFF1976D2),
|
||||||
|
onPrimary = Color.White,
|
||||||
|
primaryContainer = Color(0xFFBBDEFB),
|
||||||
|
onPrimaryContainer = Color(0xFF0D47A1),
|
||||||
|
secondary = Color(0xFFFF6D00),
|
||||||
|
onSecondary = Color.White,
|
||||||
|
secondaryContainer = Color(0xFFFFE0B2),
|
||||||
|
onSecondaryContainer = Color(0xFFE65100),
|
||||||
|
background = Color(0xFFF5F5F5),
|
||||||
|
onBackground = Color(0xFF1C1B1F),
|
||||||
|
surface = Color.White,
|
||||||
|
onSurface = Color(0xFF1C1B1F),
|
||||||
|
surfaceVariant = Color(0xFFE7E0EC),
|
||||||
|
onSurfaceVariant = Color(0xFF49454F),
|
||||||
|
error = Color(0xFFE53935),
|
||||||
|
onError = Color.White,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DarkColorScheme = darkColorScheme(
|
||||||
|
primary = Color(0xFF90CAF9),
|
||||||
|
onPrimary = Color(0xFF0D47A1),
|
||||||
|
primaryContainer = Color(0xFF1565C0),
|
||||||
|
onPrimaryContainer = Color(0xFFBBDEFB),
|
||||||
|
secondary = Color(0xFFFFB74D),
|
||||||
|
onSecondary = Color(0xFF3E2723),
|
||||||
|
secondaryContainer = Color(0xFFE65100),
|
||||||
|
onSecondaryContainer = Color(0xFFFFE0B2),
|
||||||
|
background = Color(0xFF121212),
|
||||||
|
onBackground = Color(0xFFE6E1E5),
|
||||||
|
surface = Color(0xFF1E1E1E),
|
||||||
|
onSurface = Color(0xFFE6E1E5),
|
||||||
|
surfaceVariant = Color(0xFF2C2C2C),
|
||||||
|
onSurfaceVariant = Color(0xFFCAC4D0),
|
||||||
|
error = Color(0xFFEF5350),
|
||||||
|
onError = Color(0xFF601010),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UnoTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,26 +4,25 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.CanvasBasedWindow
|
||||||
import com.uno.game.GameEngine
|
import com.uno.game.GameEngine
|
||||||
import com.uno.game.model.*
|
import com.uno.game.model.*
|
||||||
import com.uno.shared.UnoGameScreen
|
import com.uno.shared.UnoGameScreen
|
||||||
import kotlinx.browser.document
|
import com.uno.shared.UnoTheme
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.w3c.dom.HTMLElement
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
fun main() {
|
fun main() {
|
||||||
val root = document.getElementById("root") as HTMLElement
|
CanvasBasedWindow(canvasElementId = "ComposeTarget") {
|
||||||
root.innerHTML = ""
|
UnoApp()
|
||||||
val container = document.createElement("div")
|
}
|
||||||
container.id = "app"
|
|
||||||
root.appendChild(container)
|
|
||||||
composeWebRoot()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun composeWebRoot() {
|
fun UnoApp() {
|
||||||
val engine = remember { GameEngine() }
|
val engine = remember { GameEngine() }
|
||||||
var gameState by remember { mutableStateOf(engine.startGame()) }
|
var gameState by remember { mutableStateOf(engine.startGame()) }
|
||||||
var gameStarted by remember { mutableStateOf(false) }
|
var gameStarted by remember { mutableStateOf(false) }
|
||||||
@@ -45,31 +44,33 @@ fun composeWebRoot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gameStarted) {
|
UnoTheme {
|
||||||
WebStartScreen(onStartGame = { playerCount ->
|
if (!gameStarted) {
|
||||||
gameState = engine.startGame(playerCount)
|
WebStartScreen(onStartGame = { playerCount ->
|
||||||
gameStarted = true
|
gameState = engine.startGame(playerCount)
|
||||||
})
|
gameStarted = true
|
||||||
} else {
|
})
|
||||||
UnoGameScreen(
|
} else {
|
||||||
state = gameState,
|
UnoGameScreen(
|
||||||
onPlayCard = { card, color ->
|
state = gameState,
|
||||||
gameState = engine.playCard(gameState, 0, card, color)
|
onPlayCard = { card, color ->
|
||||||
},
|
gameState = engine.playCard(gameState, 0, card, color)
|
||||||
onDrawCard = {
|
},
|
||||||
gameState = engine.drawCard(gameState, 0)
|
onDrawCard = {
|
||||||
},
|
gameState = engine.drawCard(gameState, 0)
|
||||||
onCallUno = { gameState = engine.callUno(gameState, 0) }
|
},
|
||||||
)
|
onCallUno = { gameState = engine.callUno(gameState, 0) }
|
||||||
|
|
||||||
if (gameState.isFinished) {
|
|
||||||
WebGameOverScreen(
|
|
||||||
winner = gameState.winner,
|
|
||||||
onPlayAgain = {
|
|
||||||
gameState = engine.startGame()
|
|
||||||
gameStarted = false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (gameState.isFinished) {
|
||||||
|
WebGameOverScreen(
|
||||||
|
winner = gameState.winner,
|
||||||
|
onPlayAgain = {
|
||||||
|
gameState = engine.startGame()
|
||||||
|
gameStarted = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user