From f9dc62d148ca7d69dbd8fa8621c614b3ea1c6101 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Thu, 27 Feb 2025 20:15:28 +0100 Subject: [PATCH] Add timer --- .tool-versions | 2 +- .../kotlin/ch/dissem/android/MainActivity.kt | 4 +- .../kotlin/ch/dissem/yaep/ui/common/App.kt | 23 ++++++-- .../ch/dissem/yaep/ui/common/GameTimer.kt | 57 +++++++++++++++++++ .../kotlin/ch/dissem/yaep/domain/Game.kt | 26 +++++++++ .../kotlin/ch/dissem/yaep/domain/GameRow.kt | 15 ++++- gradle/libs.versions.toml | 19 ++++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 8 files changed, 128 insertions(+), 20 deletions(-) create mode 100644 commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/GameTimer.kt diff --git a/.tool-versions b/.tool-versions index bc2afb2..c1b0546 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -java temurin-17.0.12+7 +java temurin-21 diff --git a/android/src/main/kotlin/ch/dissem/android/MainActivity.kt b/android/src/main/kotlin/ch/dissem/android/MainActivity.kt index c82e371..1ffdd89 100644 --- a/android/src/main/kotlin/ch/dissem/android/MainActivity.kt +++ b/android/src/main/kotlin/ch/dissem/android/MainActivity.kt @@ -19,7 +19,7 @@ class MainActivity : ComponentActivity() { topBar = { } - ) {insets -> + ) { insets -> App(modifier = Modifier.padding(insets)) } } @@ -30,4 +30,4 @@ class MainActivity : ComponentActivity() { @Composable fun AppAndroidPreview() { App() -} \ No newline at end of file +} diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt index 10c1159..ec84291 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/App.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -58,12 +59,26 @@ class DisplayClue(val clue: C) { @Composable fun App(modifier: Modifier = Modifier, game: Game = remember { generateGame() }) { - val horizontalClues = remember { game.horizontalClues.map { DisplayClue(it) } } - val verticalClues = remember { game.verticalClues.map { DisplayClue(it) } } - val time = "00:00:00" // TODO + val horizontalClues = remember(game) { game.horizontalClues.map { DisplayClue(it) } } + val verticalClues = remember(game) { game.verticalClues.map { DisplayClue(it) } } + val timer = remember(game) { GameTimer() } + val time by timer.elapsedTime.collectAsState("00:00") + LaunchedEffect(game) { + game.onStart { + timer.start() + } + + game.onSolved { + timer.stop() + } + } + Row(modifier = modifier) { PuzzleGrid( - modifier = Modifier.aspectRatio(1f).weight(0.6f).fillMaxHeight(), + modifier = Modifier + .aspectRatio(1f) + .weight(0.6f) + .fillMaxHeight(), grid = game.grid, onUpdate = { horizontalClues.forEach { it.update(game.grid) } diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/GameTimer.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/GameTimer.kt new file mode 100644 index 0000000..51b07bf --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/GameTimer.kt @@ -0,0 +1,57 @@ +package ch.dissem.yaep.ui.common + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map + +class GameTimer { + + private var startTime: Long = 0L + private var stopTime: Long = 0L + + val isRunning: Boolean + get() = startTime != 0L && stopTime == 0L + + val elapsedSeconds: Flow = flow { + emit(0L) + while (startTime == 0L) { + delay(100) + } + var previousSeconds = 0L + while (stopTime == 0L) { + val elapsedSeconds = System.currentTimeMillis() / 1000 - startTime + if (elapsedSeconds != previousSeconds) { + emit(elapsedSeconds) + } + delay(100) + previousSeconds = elapsedSeconds + } + emit(stopTime - startTime) + } + + val elapsedTime: Flow = elapsedSeconds.map { seconds -> + val minutes = seconds / 60 + val hours = minutes / 60 + val remainingMinutes = minutes % 60 + val remainingSeconds = seconds % 60 + if (hours == 0L) { + "%02d:%02d".format(minutes, remainingSeconds) + } else { + "%02d:%02d:%02d".format(hours, remainingMinutes, remainingSeconds) + } + } + + fun start() { + if (startTime == 0L) { + startTime = System.currentTimeMillis() / 1000 + } + } + + fun stop() { + if (isRunning) { + stopTime = System.currentTimeMillis() / 1000 + } + } + +} \ No newline at end of file diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Game.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Game.kt index 6ba68f9..6db2ef5 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Game.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/Game.kt @@ -7,6 +7,9 @@ class Game( val horizontalClues = clues.filterIsInstance() val verticalClues = clues.filterIsInstance, ItemClass<*>>>() + private val onStartListeners = mutableListOf<() -> Unit>() + private val onSolvedListeners = mutableListOf<() -> Unit>() + init { for (position in clues.filterIsInstance>>()) { val row = grid[position.item.itemType.companion] @@ -18,6 +21,29 @@ class Game( } } } + grid.forEach { row -> + row.forEach { cell -> + cell.optionsRemovedListeners.add { + if (onStartListeners.isNotEmpty()) { + onStartListeners.forEach { it() } + onStartListeners.clear() + } + } + } + row.onSolved { + if (onSolvedListeners.isNotEmpty() && isSolved) { + onSolvedListeners.forEach { it() } + } + } + } + } + + fun onStart(listener: () -> Unit) { + onStartListeners.add(listener) + } + + fun onSolved(listener: () -> Unit) { + onSolvedListeners.add(listener) } /** diff --git a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt index 93d3def..d58a1a1 100644 --- a/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt +++ b/domain/src/commonMain/kotlin/ch/dissem/yaep/domain/GameRow.kt @@ -6,13 +6,22 @@ class GameRow>( val cells: List> ) : List> by cells { var isSolved = false - private set + private set(value) { + field = value + if (value) { + onSolvedListener() + } + } + var onSolvedListener = {} + + fun onSolved(listener: () -> Unit) { + onSolvedListener = listener + } fun indexOf(element: C) = indexOfFirst { it.selection?.itemType == element } fun cleanupOptions() { - if ( - isSolved && all { + if (isSolved && all { it.solution != null && it.options.size == 1 && it.options.single() == it.selection diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3bb62b6..7fb09a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,14 @@ [versions] -agp = "8.5.2" -jdk = "11" -android-compileSdk = "34" +agp = "8.8.2" +jdk = "21" +android-compileSdk = "35" android-minSdk = "24" -android-targetSdk = "34" -androidx-activityCompose = "1.9.1" -androidx-compose = "1.6.8" -compose-plugin = "1.6.11" -kotlin = "2.0.20" -coreKtx = "1.13.1" +android-targetSdk = "35" +androidx-activityCompose = "1.10.1" +androidx-compose = "1.7.8" +compose-plugin = "1.7.0" +kotlin = "2.1.0" +coreKtx = "1.15.0" atrium = "1.2.0" [libraries] @@ -18,6 +18,7 @@ androidx-compose-foundation = { module = "androidx.compose.foundation:foundation kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } atrium = { module = "ch.tutteli.atrium:atrium-fluent", version.ref = "atrium" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } +kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.1" } logging = { module = "io.github.oshai:kotlin-logging", version = "7.0.0" } [plugins] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9355b41..df97d72 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME