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
@@ -63,7 +63,7 @@ private class FlyingCardState {
animatableX = Animatable(fromX)
animatableY = Animatable(fromY)
animatableScale = Animatable(0.3f)
animatableRotation = Animatable((Math.random() * 60 - 30).toFloat())
animatableRotation = Animatable((kotlin.random.Random.nextFloat() * 60 - 30).toFloat())
coroutineScope {
launch {
@@ -274,6 +274,7 @@ fun UnoGameScreen(
var previousLastAction by remember { mutableStateOf(state.lastAction) }
var previousTopCard by remember { mutableStateOf(state.deck.topCard) }
var lastPlayedByPlayerIndex by remember { mutableIntStateOf(0) }
var isDarkMode by remember { mutableStateOf(false) }
val flyingState = rememberFlyingCardState()
@@ -317,14 +318,15 @@ fun UnoGameScreen(
previousLastAction = state.lastAction
}
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
GameInfoBar(state)
UnoTheme(darkTheme = isDarkMode) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
GameInfoBar(state, isDarkMode, { isDarkMode = it })
Spacer(modifier = Modifier.height(12.dp))
OpponentHands(state)
Spacer(modifier = Modifier.height(12.dp))
@@ -417,12 +419,13 @@ fun UnoGameScreen(
}
)
}
}
}
// region --- GameInfoBar ---
@Composable
private fun GameInfoBar(state: GameState) {
private fun GameInfoBar(state: GameState, isDarkMode: Boolean, onDarkModeToggle: (Boolean) -> Unit) {
val directionRotation by animateFloatAsState(
targetValue = if (state.direction == GameDirection.CLOCKWISE) 0f else 180f,
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
)
}