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.model.*
import com.uno.shared.UnoGameScreen
import com.uno.shared.UnoTheme
import kotlinx.coroutines.delay
class MainActivity : ComponentActivity() {
@@ -20,7 +21,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
UnoTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
UnoApp()
}
@@ -13,6 +13,7 @@ import androidx.compose.ui.window.application
import com.uno.game.GameEngine
import com.uno.game.model.*
import com.uno.shared.UnoGameScreen
import com.uno.shared.UnoTheme
import kotlinx.coroutines.delay
fun main() = application {
@@ -49,6 +50,7 @@ fun UnoDesktopApp() {
}
}
UnoTheme {
if (!gameStarted) {
DesktopStartScreen(onStartGame = { playerCount ->
gameState = engine.startGame(playerCount)
@@ -76,6 +78,7 @@ fun UnoDesktopApp() {
)
}
}
}
}
@Composable
@@ -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,6 +318,7 @@ fun UnoGameScreen(
previousLastAction = state.lastAction
}
UnoTheme(darkTheme = isDarkMode) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
@@ -324,7 +326,7 @@ fun UnoGameScreen(
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
GameInfoBar(state)
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
)
}
@@ -4,26 +4,25 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.CanvasBasedWindow
import com.uno.game.GameEngine
import com.uno.game.model.*
import com.uno.shared.UnoGameScreen
import kotlinx.browser.document
import com.uno.shared.UnoTheme
import kotlinx.coroutines.delay
import org.w3c.dom.HTMLElement
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
val root = document.getElementById("root") as HTMLElement
root.innerHTML = ""
val container = document.createElement("div")
container.id = "app"
root.appendChild(container)
composeWebRoot()
CanvasBasedWindow(canvasElementId = "ComposeTarget") {
UnoApp()
}
}
@Composable
fun composeWebRoot() {
fun UnoApp() {
val engine = remember { GameEngine() }
var gameState by remember { mutableStateOf(engine.startGame()) }
var gameStarted by remember { mutableStateOf(false) }
@@ -45,6 +44,7 @@ fun composeWebRoot() {
}
}
UnoTheme {
if (!gameStarted) {
WebStartScreen(onStartGame = { playerCount ->
gameState = engine.startGame(playerCount)
@@ -72,6 +72,7 @@ fun composeWebRoot() {
)
}
}
}
}
@Composable