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:
2026-08-02 18:52:25 +02:00
parent a7fb6ff94c
commit 8732c4142c
5 changed files with 140 additions and 68 deletions
@@ -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,6 +50,7 @@ fun UnoDesktopApp() {
} }
} }
UnoTheme {
if (!gameStarted) { if (!gameStarted) {
DesktopStartScreen(onStartGame = { playerCount -> DesktopStartScreen(onStartGame = { playerCount ->
gameState = engine.startGame(playerCount) gameState = engine.startGame(playerCount)
@@ -76,6 +78,7 @@ fun UnoDesktopApp() {
) )
} }
} }
}
} }
@Composable @Composable
@@ -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,6 +318,7 @@ fun UnoGameScreen(
previousLastAction = state.lastAction previousLastAction = state.lastAction
} }
UnoTheme(darkTheme = isDarkMode) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -324,7 +326,7 @@ fun UnoGameScreen(
.padding(16.dp), .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally 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,6 +44,7 @@ fun composeWebRoot() {
} }
} }
UnoTheme {
if (!gameStarted) { if (!gameStarted) {
WebStartScreen(onStartGame = { playerCount -> WebStartScreen(onStartGame = { playerCount ->
gameState = engine.startGame(playerCount) gameState = engine.startGame(playerCount)
@@ -72,6 +72,7 @@ fun composeWebRoot() {
) )
} }
} }
}
} }
@Composable @Composable