Make app run on Android (WIP)

This commit is contained in:
Christian Basler
2025-07-14 21:34:13 +02:00
parent 1fb3b39590
commit 1800174087
6 changed files with 72 additions and 32 deletions

View File

@@ -17,6 +17,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import ch.dissem.yaep.domain.Game import ch.dissem.yaep.domain.Game
import ch.dissem.yaep.domain.generateGame import ch.dissem.yaep.domain.generateGame
import ch.dissem.yaep.ui.common.App import ch.dissem.yaep.ui.common.App
@@ -63,6 +64,8 @@ class MainActivity : ComponentActivity() {
) { insets -> ) { insets ->
App( App(
modifier = Modifier.padding(insets), modifier = Modifier.padding(insets),
spacing = 4.dp,
selectDirectly = false,
game = game, game = game,
onNewGame = { game = generateGame() }, onNewGame = { game = generateGame() },
resetCluesBeacon = resetCluesBeacon resetCluesBeacon = resetCluesBeacon
@@ -75,11 +78,13 @@ class MainActivity : ComponentActivity() {
@Preview @Preview
@Composable @Composable
fun AppAndroidPreview() { fun AppAndroidPreview() {
var game by remember { mutableStateOf<Game>(generateGame()) } var game by remember { mutableStateOf(generateGame()) }
var resetCluesBeacon by remember { mutableStateOf(Any()) } var resetCluesBeacon by remember { mutableStateOf(Any()) }
App( App(
game = game, game = game,
spacing = 4.dp,
selectDirectly = false,
onNewGame = { game = generateGame() }, onNewGame = { game = generateGame() },
resetCluesBeacon = resetCluesBeacon resetCluesBeacon = resetCluesBeacon
) )

View File

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
@@ -24,6 +25,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -59,6 +61,8 @@ class DisplayClue<C : Clue>(val clue: C) {
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
fun App( fun App(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selectDirectly: Boolean,
spacing: Dp,
game: Game, game: Game,
onNewGame: () -> Unit, onNewGame: () -> Unit,
resetCluesBeacon: Any = Any() resetCluesBeacon: Any = Any()
@@ -89,6 +93,8 @@ fun App(
grid = { grid = {
PuzzleGrid( PuzzleGrid(
grid = game.grid, grid = game.grid,
spacing = spacing,
selectDirectly = selectDirectly,
onUpdate = { onUpdate = {
horizontalClues.forEach { it.update(game.grid) } horizontalClues.forEach { it.update(game.grid) }
verticalClues.forEach { it.update(game.grid) } verticalClues.forEach { it.update(game.grid) }
@@ -98,7 +104,8 @@ fun App(
horizontalClues = { horizontalClues = {
for (clue in horizontalClues) { for (clue in horizontalClues) {
HorizontalClue( HorizontalClue(
modifier = Modifier.forClue(clue), modifier = Modifier.forClue(clue, spacing),
spacing = spacing,
clue = clue.clue, clue = clue.clue,
isClueViolated = clue.isViolated isClueViolated = clue.isViolated
) )
@@ -107,7 +114,8 @@ fun App(
verticalClues = { verticalClues = {
for (clue in verticalClues) { for (clue in verticalClues) {
VerticalClue( VerticalClue(
modifier = Modifier.forClue(clue), modifier = Modifier.forClue(clue, spacing),
spacing = spacing,
clue = clue.clue, clue = clue.clue,
isClueViolated = clue.isViolated isClueViolated = clue.isViolated
) )
@@ -119,7 +127,8 @@ fun App(
fontSize = TextUnit(4f, TextUnitType.Companion.Em), fontSize = TextUnit(4f, TextUnitType.Companion.Em),
textAlign = TextAlign.End textAlign = TextAlign.End
) )
} },
spacing = spacing
) )
EndOfGame(isSolved = isSolved, time = time, onRestart = onNewGame) EndOfGame(isSolved = isSolved, time = time, onRestart = onNewGame)
} }
@@ -128,6 +137,8 @@ fun App(
@Composable @Composable
fun PuzzleGrid( fun PuzzleGrid(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selectDirectly: Boolean,
spacing: Dp = 8.dp,
grid: Grid, grid: Grid,
onUpdate: () -> Unit onUpdate: () -> Unit
) { ) {
@@ -155,8 +166,10 @@ fun PuzzleGrid(
} }
Selector( Selector(
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(spacing)
.weight(1f), .weight(1f),
spacing,
selectDirectly = selectDirectly,
options = options, options = options,
onOptionRemoved = { onOptionRemoved = {
grid.snapshot() grid.snapshot()
@@ -188,43 +201,49 @@ fun PuzzleGrid(
} }
} }
private fun Modifier.forClue(clue: DisplayClue<out Clue>): Modifier = this private fun Modifier.forClue(clue: DisplayClue<out Clue>, padding: Dp = 8.dp): Modifier = this
.alpha(if (clue.isActive) 1f else 0.2f) .alpha(if (clue.isActive) 1f else 0.2f)
.padding(8.dp) .padding(padding)
.onEitherPointerAction { clue.isActive = !clue.isActive } .onEitherPointerAction { clue.isActive = !clue.isActive }
@Composable @Composable
fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue, isClueViolated: Boolean) { fun HorizontalClue(
modifier: Modifier = Modifier,
spacing: Dp,
clue: HorizontalClue,
isClueViolated: Boolean
) {
ClueCard( ClueCard(
modifier = modifier, modifier = modifier,
spacing = spacing,
isClueViolated = isClueViolated isClueViolated = isClueViolated
) { ) {
Row { Row {
when (clue) { when (clue) {
is NeighbourClue<*, *> -> { is NeighbourClue<*, *> -> {
DrawItem(modifier = Modifier.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.a)
Image( Image(
modifier = Modifier.aspectRatio(1f).weight(1f), modifier = Modifier.aspectRatio(1f).weight(1f),
painter = painterResource(Res.drawable.neighbour), painter = painterResource(Res.drawable.neighbour),
contentDescription = null contentDescription = null
) )
DrawItem(modifier = Modifier.weight(1f), clue.b) DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.b)
} }
is OrderClue<*, *> -> { is OrderClue<*, *> -> {
DrawItem(modifier = Modifier.weight(1f), clue.left) DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.left)
Image( Image(
modifier = Modifier.aspectRatio(1f).weight(1f), modifier = Modifier.aspectRatio(1f).weight(1f),
painter = painterResource(Res.drawable.order), painter = painterResource(Res.drawable.order),
contentDescription = null contentDescription = null
) )
DrawItem(modifier = Modifier.weight(1f), clue.right) DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.right)
} }
is TripletClue<*, *, *> -> { is TripletClue<*, *, *> -> {
DrawItem(modifier = Modifier.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.a)
DrawItem(modifier = Modifier.weight(1f), clue.b) DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.b)
DrawItem(modifier = Modifier.weight(1f), clue.c) DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.c)
} }
} }
} }
@@ -234,16 +253,18 @@ fun HorizontalClue(modifier: Modifier = Modifier, clue: HorizontalClue, isClueVi
@Composable @Composable
fun VerticalClue( fun VerticalClue(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
spacing: Dp,
clue: SameColumnClue<*, *>, clue: SameColumnClue<*, *>,
isClueViolated: Boolean = false isClueViolated: Boolean = false
) { ) {
ClueCard( ClueCard(
modifier = modifier.aspectRatio(0.5f), modifier = modifier.aspectRatio(0.5f),
spacing = spacing,
isClueViolated = isClueViolated isClueViolated = isClueViolated
) { ) {
Column { Column {
DrawItem(modifier = Modifier.weight(1f), clue.a) DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.a)
DrawItem(modifier = Modifier.weight(1f), clue.b) DrawItem(modifier = Modifier.weight(1f), spacing = spacing, item = clue.b)
} }
} }
} }
@@ -253,6 +274,7 @@ expect fun CoroutineScope.logGame(game: Game)
@Composable @Composable
fun ClueCard( fun ClueCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
spacing: Dp,
isClueViolated: Boolean, isClueViolated: Boolean,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
@@ -268,6 +290,7 @@ fun ClueCard(
} else { } else {
modifier modifier
}, },
shape = RoundedCornerShape(spacing),
border = if (isClueViolated) { border = if (isClueViolated) {
remember { BorderStroke(1.0.dp, colors.error) } remember { BorderStroke(1.0.dp, colors.error) }
} else { } else {

View File

@@ -9,6 +9,7 @@ import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Constraints.Companion.fixed import androidx.compose.ui.unit.Constraints.Companion.fixed
import androidx.compose.ui.unit.Constraints.Companion.fixedWidth import androidx.compose.ui.unit.Constraints.Companion.fixedWidth
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ch.dissem.yaep.ui.common.AspectRatio.LANDSCAPE import ch.dissem.yaep.ui.common.AspectRatio.LANDSCAPE
import ch.dissem.yaep.ui.common.AspectRatio.PORTRAIT import ch.dissem.yaep.ui.common.AspectRatio.PORTRAIT
@@ -17,8 +18,6 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
internal val spacing = 8.dp
private enum class AspectRatio { private enum class AspectRatio {
PORTRAIT, LANDSCAPE, SQUARISH; PORTRAIT, LANDSCAPE, SQUARISH;
@@ -44,7 +43,8 @@ fun AdaptiveGameLayout(
horizontalClues: @Composable () -> Unit, horizontalClues: @Composable () -> Unit,
verticalClues: @Composable () -> Unit, verticalClues: @Composable () -> Unit,
time: @Composable () -> Unit, time: @Composable () -> Unit,
divider: @Composable () -> Unit = { HorizontalDivider() } divider: @Composable () -> Unit = { HorizontalDivider() },
spacing: Dp = 8.dp
) { ) {
Layout( Layout(
contents = listOf(grid, horizontalClues, verticalClues, time, divider, divider), contents = listOf(grid, horizontalClues, verticalClues, time, divider, divider),

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -18,6 +19,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Dp
import ch.dissem.yaep.domain.Item import ch.dissem.yaep.domain.Item
import ch.dissem.yaep.domain.ItemClass import ch.dissem.yaep.domain.ItemClass
import ch.dissem.yaep.ui.common.theme.emojiFontFamily import ch.dissem.yaep.ui.common.theme.emojiFontFamily
@@ -26,16 +28,26 @@ import kotlin.math.min
@Composable @Composable
fun <C : ItemClass<C>> Selector( fun <C : ItemClass<C>> Selector(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
spacing: Dp,
selectDirectly: Boolean,
options: List<Toggleable<Item<C>>>, options: List<Toggleable<Item<C>>>,
onOptionRemoved: (Item<C>) -> Unit, onOptionRemoved: (Item<C>) -> Unit,
onOptionAdded: (Item<C>) -> Unit, onOptionAdded: (Item<C>) -> Unit,
selectedItem: Item<C>?, selectedItem: Item<C>?,
onSelectItem: (Item<C>?) -> Unit, onSelectItem: (Item<C>?) -> Unit,
) { ) {
val radius = spacing * 1.5f
if (selectedItem != null) { if (selectedItem != null) {
DrawItem(item = selectedItem, modifier = modifier.clickable { onSelectItem(null) }) DrawItem(
item = selectedItem,
modifier = modifier.clickable { onSelectItem(null) },
spacing = radius
)
} else { } else {
OutlinedCard(modifier = modifier.aspectRatio(1f)) { OutlinedCard(
modifier = modifier.aspectRatio(1f),
shape = RoundedCornerShape(radius)
) {
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(3), columns = GridCells.Fixed(3),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
@@ -58,7 +70,8 @@ fun <C : ItemClass<C>> Selector(
onOptionAdded(option.item) onOptionAdded(option.item)
} }
} }
) ),
spacing = radius
) )
} }
} }
@@ -70,9 +83,10 @@ fun <C : ItemClass<C>> Selector(
@Composable @Composable
fun <C : ItemClass<C>> DrawItem( fun <C : ItemClass<C>> DrawItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
spacing: Dp,
item: Item<C> item: Item<C>
) { ) {
OutlinedCard(modifier = modifier.aspectRatio(1f)) { OutlinedCard(modifier = modifier.aspectRatio(1f), shape = RoundedCornerShape(spacing)) {
val emoji = item.symbol val emoji = item.symbol
val textMeasurer = rememberTextMeasurer() val textMeasurer = rememberTextMeasurer()

View File

@@ -6,10 +6,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.io.path.Path import kotlin.io.path.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.createFile import kotlin.io.path.createFile
import kotlin.io.path.isDirectory import kotlin.io.path.isDirectory
import kotlin.io.path.notExists
import kotlin.io.path.writeText import kotlin.io.path.writeText
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@@ -21,17 +19,15 @@ actual fun CoroutineScope.logGame(game: Game) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val dirName = """${System.getProperty("user.home")}/.yaep""" val dirName = """${System.getProperty("user.home")}/.yaep"""
val dir = Path(dirName) val dir = Path(dirName)
if (dir.notExists()) { if (dir.isDirectory()) {
dir.createDirectories()
} else if (!dir.isDirectory()) {
log.error { "Yaep data directory already exists and is not a directory: $dir" }
log.debug { "Game: $game" }
} else {
val fileName = "$dirName/${Clock.System.now()}.yaep" val fileName = "$dirName/${Clock.System.now()}.yaep"
Path(fileName) Path(fileName)
.createFile() .createFile()
.writeText(game.toString()) .writeText(game.toString())
log.info { "Saved game to $fileName" } log.info { "Saved game to $fileName" }
} else {
log.debug { "Yaep data directory does not exist or is not a directory: $dir" }
log.debug { "Game: $game" }
} }
} }
} }

View File

@@ -68,6 +68,8 @@ fun main(): Unit = application {
) { ) {
App( App(
modifier = Modifier.padding(it), modifier = Modifier.padding(it),
spacing = 8.dp,
selectDirectly = true,
game = game, game = game,
onNewGame = { game = generateGame() }, onNewGame = { game = generateGame() },
resetCluesBeacon = resetCluesBeacon resetCluesBeacon = resetCluesBeacon