0.1.0
This commit is contained in:
@@ -2,6 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -13,7 +14,7 @@ android {
|
|||||||
minSdk = 35
|
minSdk = 35
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "0.1.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
implementation(libs.ktorx.serialization.json)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|||||||
BIN
app/release/app-release.aab
Normal file
BIN
app/release/app-release.aab
Normal file
Binary file not shown.
@@ -10,6 +10,11 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.filled.History
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -31,19 +36,27 @@ import com.atridad.magiccounter.ui.state.GameState
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.atridad.magiccounter.ui.settings.AppSettingsViewModel
|
import com.atridad.magiccounter.ui.settings.AppSettingsViewModel
|
||||||
import com.atridad.magiccounter.ui.settings.ThemeMode
|
import com.atridad.magiccounter.ui.settings.ThemeMode
|
||||||
|
import com.atridad.magiccounter.ui.settings.MatchRecord
|
||||||
import com.atridad.magiccounter.ui.theme.MagicCounterTheme
|
import com.atridad.magiccounter.ui.theme.MagicCounterTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
// no-op
|
// no-op
|
||||||
|
|
||||||
|
// Top-level navigation destinations
|
||||||
|
private enum class Screen { Home, Setup, Game }
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MagicCounterApp() {
|
fun MagicCounterApp() {
|
||||||
var gameState by remember { mutableStateOf<GameState?>(null) }
|
var gameState by remember { mutableStateOf<GameState?>(null) }
|
||||||
var showSettings by remember { mutableStateOf(false) }
|
var showSettings by remember { mutableStateOf(false) }
|
||||||
|
var activeMatchId by remember { mutableStateOf<String?>(null) }
|
||||||
|
var screen by remember { mutableStateOf(Screen.Home) }
|
||||||
val settingsVm: AppSettingsViewModel = viewModel()
|
val settingsVm: AppSettingsViewModel = viewModel()
|
||||||
val theme = settingsVm.themeMode.collectAsState()
|
val theme = settingsVm.themeMode.collectAsState()
|
||||||
|
val historyState = settingsVm.matchHistory.collectAsState()
|
||||||
|
|
||||||
|
|
||||||
MagicCounterTheme(themeMode = theme.value) {
|
MagicCounterTheme(themeMode = theme.value) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -51,9 +64,9 @@ fun MagicCounterApp() {
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Magic Counter", style = MaterialTheme.typography.titleLarge) },
|
title = { Text("Magic Counter", style = MaterialTheme.typography.titleLarge) },
|
||||||
actions = {
|
actions = {
|
||||||
if (gameState != null) {
|
if (screen != Screen.Home) {
|
||||||
IconButton(onClick = { gameState = null }) {
|
IconButton(onClick = { screen = Screen.Home }) {
|
||||||
Icon(Icons.Default.Refresh, contentDescription = "New game")
|
Icon(Icons.Default.Home, contentDescription = "Home")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IconButton(onClick = { showSettings = true }) {
|
IconButton(onClick = { showSettings = true }) {
|
||||||
@@ -63,20 +76,78 @@ fun MagicCounterApp() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
if (gameState == null) {
|
when (screen) {
|
||||||
SetupScreen(
|
Screen.Home -> HomeScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
onStart = { state -> gameState = state }
|
history = historyState.value,
|
||||||
|
onNewGame = { screen = Screen.Setup },
|
||||||
|
onResume = { record ->
|
||||||
|
activeMatchId = record.id
|
||||||
|
gameState = record.state
|
||||||
|
screen = Screen.Game
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else {
|
Screen.Setup -> SetupScreen(
|
||||||
GameScreen(
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize(),
|
||||||
|
onStart = { name, state ->
|
||||||
|
// Create and persist a new MatchRecord
|
||||||
|
val newId = java.util.UUID.randomUUID().toString()
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val record = MatchRecord(
|
||||||
|
id = newId,
|
||||||
|
name = name,
|
||||||
|
startedAtEpochMs = now,
|
||||||
|
lastUpdatedEpochMs = now,
|
||||||
|
ongoing = true,
|
||||||
|
winnerPlayerId = null,
|
||||||
|
state = state
|
||||||
|
)
|
||||||
|
val updated = historyState.value.toMutableList().apply { add(0, record) }
|
||||||
|
settingsVm.saveHistory(updated)
|
||||||
|
activeMatchId = newId
|
||||||
|
gameState = state
|
||||||
|
screen = Screen.Game
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Screen.Game -> GameScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
state = gameState!!,
|
state = gameState!!,
|
||||||
onEnd = { gameState = null }
|
onEnd = { screen = Screen.Home },
|
||||||
|
onProgress = { updated ->
|
||||||
|
val current = historyState.value
|
||||||
|
val id = activeMatchId
|
||||||
|
if (id != null) {
|
||||||
|
val idx = current.indexOfFirst { it.id == id }
|
||||||
|
if (idx >= 0) {
|
||||||
|
val updatedRec = current[idx].copy(state = updated, lastUpdatedEpochMs = System.currentTimeMillis())
|
||||||
|
val newList = current.toMutableList().apply { set(idx, updatedRec) }
|
||||||
|
settingsVm.saveHistory(newList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onWinner = { winnerId, finalState ->
|
||||||
|
val current = historyState.value
|
||||||
|
val id = activeMatchId
|
||||||
|
if (id != null) {
|
||||||
|
val idx = current.indexOfFirst { it.id == id }
|
||||||
|
if (idx >= 0) {
|
||||||
|
val updatedRec = current[idx].copy(
|
||||||
|
state = finalState,
|
||||||
|
lastUpdatedEpochMs = System.currentTimeMillis(),
|
||||||
|
ongoing = false,
|
||||||
|
winnerPlayerId = winnerId
|
||||||
|
)
|
||||||
|
val newList = current.toMutableList().apply { set(idx, updatedRec) }
|
||||||
|
settingsVm.saveHistory(newList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showSettings) {
|
if (showSettings) {
|
||||||
@@ -107,4 +178,59 @@ private fun SettingsSheet(current: ThemeMode, onSelect: (ThemeMode) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HistorySheet(history: List<MatchRecord>, onResume: (MatchRecord) -> Unit) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
Text("Match history")
|
||||||
|
history.forEach { rec ->
|
||||||
|
Row(modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Column {
|
||||||
|
Text(rec.name)
|
||||||
|
val status = if (rec.ongoing) "Ongoing" else "Finished"
|
||||||
|
Text("$status • Started ${java.text.DateFormat.getDateTimeInstance().format(java.util.Date(rec.startedAtEpochMs))}")
|
||||||
|
rec.winnerPlayerId?.let { Text("Winner: Player ${it + 1}") }
|
||||||
|
}
|
||||||
|
IconButton(onClick = { onResume(rec) }) {
|
||||||
|
Icon(Icons.Default.Refresh, contentDescription = "Resume")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HomeScreen(
|
||||||
|
modifier: Modifier,
|
||||||
|
history: List<MatchRecord>,
|
||||||
|
onNewGame: () -> Unit,
|
||||||
|
onResume: (MatchRecord) -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Button(onClick = onNewGame) { Text("New game") }
|
||||||
|
|
||||||
|
Text("Ongoing")
|
||||||
|
history.filter { it.ongoing }.forEach { rec ->
|
||||||
|
Row(modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Column {
|
||||||
|
Text(rec.name)
|
||||||
|
Text("Started ${java.text.DateFormat.getDateTimeInstance().format(java.util.Date(rec.startedAtEpochMs))}")
|
||||||
|
}
|
||||||
|
IconButton(onClick = { onResume(rec) }) { Icon(Icons.Default.PlayArrow, contentDescription = "Resume game") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Finished")
|
||||||
|
history.filter { !it.ongoing }.forEach { rec ->
|
||||||
|
Row(modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Column {
|
||||||
|
Text(rec.name)
|
||||||
|
val winner = rec.winnerPlayerId?.let { "Winner: Player ${it + 1}" } ?: ""
|
||||||
|
Text("Finished • $winner")
|
||||||
|
}
|
||||||
|
IconButton(onClick = { onResume(rec) }) { Icon(Icons.Default.Visibility, contentDescription = "View match") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
package com.atridad.magiccounter.ui.screens
|
package com.atridad.magiccounter.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Flag
|
||||||
import androidx.compose.material.icons.filled.Remove
|
import androidx.compose.material.icons.filled.Remove
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -21,25 +26,31 @@ import androidx.compose.material3.FilledTonalIconButton
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
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.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.atridad.magiccounter.ui.state.GameState
|
import com.atridad.magiccounter.ui.state.GameState
|
||||||
import com.atridad.magiccounter.ui.state.PlayerState
|
import com.atridad.magiccounter.ui.state.PlayerState
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun GameScreen(
|
fun GameScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
state: GameState,
|
state: GameState,
|
||||||
onEnd: () -> Unit
|
onEnd: () -> Unit,
|
||||||
|
onProgress: ((GameState) -> Unit)? = null,
|
||||||
|
onWinner: ((Int, GameState) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
// Local editable state per player
|
// Local editable state per player
|
||||||
val lifeTotals = remember { mutableStateMapOf<Int, Int>() }
|
val lifeTotals = remember { mutableStateMapOf<Int, Int>() }
|
||||||
@@ -47,6 +58,8 @@ fun GameScreen(
|
|||||||
val energyTotals = remember { mutableStateMapOf<Int, Int>() }
|
val energyTotals = remember { mutableStateMapOf<Int, Int>() }
|
||||||
val experienceTotals = remember { mutableStateMapOf<Int, Int>() }
|
val experienceTotals = remember { mutableStateMapOf<Int, Int>() }
|
||||||
val commanderDamages = remember { mutableStateMapOf<Int, MutableMap<Int, Int>>() }
|
val commanderDamages = remember { mutableStateMapOf<Int, MutableMap<Int, Int>>() }
|
||||||
|
val eliminated = remember { mutableStateMapOf<Int, Boolean>() }
|
||||||
|
val gameLocked = remember { mutableStateMapOf<String, Boolean>() } // single key used to freeze when winner determined
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
state.players.forEach { p ->
|
state.players.forEach { p ->
|
||||||
@@ -55,43 +68,113 @@ fun GameScreen(
|
|||||||
energyTotals.putIfAbsent(p.id, p.energy)
|
energyTotals.putIfAbsent(p.id, p.energy)
|
||||||
experienceTotals.putIfAbsent(p.id, p.experience)
|
experienceTotals.putIfAbsent(p.id, p.experience)
|
||||||
commanderDamages.putIfAbsent(p.id, p.commanderDamages.toMutableMap())
|
commanderDamages.putIfAbsent(p.id, p.commanderDamages.toMutableMap())
|
||||||
|
eliminated.putIfAbsent(p.id, p.scooped)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single column for maximum width per request
|
fun checkElimination(playerId: Int) {
|
||||||
|
val life = lifeTotals[playerId] ?: state.startingLife
|
||||||
|
if (life <= 0) {
|
||||||
|
eliminated[playerId] = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val fromMap: Map<Int, Int> = commanderDamages[playerId] ?: emptyMap()
|
||||||
|
if (fromMap.values.any { it >= 21 }) {
|
||||||
|
eliminated[playerId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single column for maximum width
|
||||||
val numColumns = 1
|
val numColumns = 1
|
||||||
|
|
||||||
|
fun snapshotState(): GameState = state.copy(
|
||||||
|
players = state.players.map { p ->
|
||||||
|
PlayerState(
|
||||||
|
id = p.id,
|
||||||
|
name = p.name,
|
||||||
|
life = lifeTotals[p.id] ?: p.life,
|
||||||
|
poison = poisonTotals[p.id] ?: p.poison,
|
||||||
|
energy = energyTotals[p.id] ?: p.energy,
|
||||||
|
experience = experienceTotals[p.id] ?: p.experience,
|
||||||
|
commanderDamages = commanderDamages[p.id]?.toMap() ?: p.commanderDamages,
|
||||||
|
scooped = eliminated[p.id] == true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Column(modifier = modifier.padding(12.dp)) {
|
Column(modifier = modifier.padding(12.dp)) {
|
||||||
|
// Lock game when only one active player remains
|
||||||
|
val aliveCount = state.players.count { eliminated[it.id] != true }
|
||||||
|
if (aliveCount == 1) {
|
||||||
|
val winnerId = state.players.first { eliminated[it.id] != true }.id
|
||||||
|
if (gameLocked["locked"] != true) {
|
||||||
|
gameLocked["locked"] = true
|
||||||
|
onWinner?.invoke(winnerId, snapshotState())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val displayPlayers = state.players.sortedBy { eliminated[it.id] == true }
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
contentPadding = PaddingValues(4.dp),
|
contentPadding = PaddingValues(4.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(state.players, key = { _, item -> item.id }) { index, player ->
|
itemsIndexed(displayPlayers, key = { _, item -> item.id }) { index, player ->
|
||||||
val accent = seatAccentColor(index, MaterialTheme.colorScheme)
|
val accent = seatAccentColor(index, MaterialTheme.colorScheme)
|
||||||
val perPlayerCommander: Map<Int, Int> = commanderDamages[player.id]?.toMap() ?: emptyMap()
|
val perPlayerCommander: Map<Int, Int> = commanderDamages[player.id]?.toMap() ?: emptyMap()
|
||||||
PlayerCard(
|
PlayerCard(
|
||||||
player = player,
|
player = player,
|
||||||
opponents = state.players.map { it.id }.filter { it != player.id },
|
opponents = state.players.map { it.id }.filter { it != player.id },
|
||||||
life = lifeTotals[player.id] ?: state.startingLife,
|
life = lifeTotals[player.id] ?: state.startingLife,
|
||||||
onLifeChange = { lifeTotals[player.id] = it },
|
onLifeChange = { new ->
|
||||||
|
if (gameLocked["locked"] == true) return@PlayerCard
|
||||||
|
lifeTotals[player.id] = new
|
||||||
|
checkElimination(player.id)
|
||||||
|
onProgress?.invoke(snapshotState())
|
||||||
|
},
|
||||||
poison = poisonTotals[player.id] ?: 0,
|
poison = poisonTotals[player.id] ?: 0,
|
||||||
onPoisonChange = { poisonTotals[player.id] = it },
|
onPoisonChange = {
|
||||||
|
if (gameLocked["locked"] != true) {
|
||||||
|
poisonTotals[player.id] = it
|
||||||
|
onProgress?.invoke(snapshotState())
|
||||||
|
}
|
||||||
|
},
|
||||||
energy = energyTotals[player.id] ?: 0,
|
energy = energyTotals[player.id] ?: 0,
|
||||||
onEnergyChange = { energyTotals[player.id] = it },
|
onEnergyChange = {
|
||||||
|
if (gameLocked["locked"] != true) {
|
||||||
|
energyTotals[player.id] = it
|
||||||
|
onProgress?.invoke(snapshotState())
|
||||||
|
}
|
||||||
|
},
|
||||||
experience = experienceTotals[player.id] ?: 0,
|
experience = experienceTotals[player.id] ?: 0,
|
||||||
onExperienceChange = { experienceTotals[player.id] = it },
|
onExperienceChange = {
|
||||||
|
if (gameLocked["locked"] != true) {
|
||||||
|
experienceTotals[player.id] = it
|
||||||
|
onProgress?.invoke(snapshotState())
|
||||||
|
}
|
||||||
|
},
|
||||||
trackPoison = state.trackPoison,
|
trackPoison = state.trackPoison,
|
||||||
trackEnergy = state.trackEnergy,
|
trackEnergy = state.trackEnergy,
|
||||||
trackExperience = state.trackExperience,
|
trackExperience = state.trackExperience,
|
||||||
trackCommanderDamage = state.trackCommanderDamage,
|
trackCommanderDamage = state.trackCommanderDamage,
|
||||||
commanderDamages = perPlayerCommander,
|
commanderDamages = perPlayerCommander,
|
||||||
onCommanderDamageChange = { fromId, dmg ->
|
onCommanderDamageChange = { fromId, dmg ->
|
||||||
val newMap = (commanderDamages[player.id] ?: mutableMapOf()).toMutableMap()
|
if (gameLocked["locked"] != true) {
|
||||||
newMap[fromId] = dmg
|
val newMap = (commanderDamages[player.id] ?: mutableMapOf()).toMutableMap()
|
||||||
commanderDamages[player.id] = newMap
|
newMap[fromId] = dmg
|
||||||
|
commanderDamages[player.id] = newMap
|
||||||
|
checkElimination(player.id)
|
||||||
|
onProgress?.invoke(snapshotState())
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rotation = 0f,
|
rotation = 0f,
|
||||||
accentColor = accent
|
accentColor = accent,
|
||||||
|
isEliminated = eliminated[player.id] == true,
|
||||||
|
onScoop = {
|
||||||
|
if (gameLocked["locked"] != true) {
|
||||||
|
eliminated[player.id] = true
|
||||||
|
onProgress?.invoke(snapshotState())
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,35 +209,59 @@ private fun PlayerCard(
|
|||||||
commanderDamages: Map<Int, Int>,
|
commanderDamages: Map<Int, Int>,
|
||||||
onCommanderDamageChange: (fromId: Int, damage: Int) -> Unit,
|
onCommanderDamageChange: (fromId: Int, damage: Int) -> Unit,
|
||||||
rotation: Float,
|
rotation: Float,
|
||||||
accentColor: Color
|
accentColor: Color,
|
||||||
|
isEliminated: Boolean,
|
||||||
|
onScoop: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val targetContainer = if (isEliminated) MaterialTheme.colorScheme.errorContainer else MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val containerColor = animateColorAsState(targetValue = targetContainer, label = "eliminationColor")
|
||||||
|
val overlayAlpha = animateFloatAsState(targetValue = if (isEliminated) 1f else 0f, label = "overlayAlpha")
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.graphicsLayer { rotationZ = rotation },
|
.graphicsLayer { rotationZ = rotation },
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
colors = CardDefaults.cardColors(containerColor = containerColor.value),
|
||||||
border = BorderStroke(2.dp, accentColor)
|
border = BorderStroke(2.dp, accentColor)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Text(player.name, style = MaterialTheme.typography.titleMedium, color = accentColor)
|
Column(modifier = Modifier.padding(12.dp).alpha(if (isEliminated) 0.45f else 1f), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||||
BigLifeRow(value = life, onChange = onLifeChange)
|
Text(player.name, style = MaterialTheme.typography.titleMedium, color = accentColor)
|
||||||
|
TextButton(onClick = onScoop, enabled = !isEliminated) {
|
||||||
if (trackPoison) ChipRow(label = "Poison", value = poison, onChange = onPoisonChange)
|
Icon(Icons.Default.Flag, contentDescription = "Scoop")
|
||||||
if (trackEnergy) ChipRow(label = "Energy", value = energy, onChange = onEnergyChange)
|
Text("Scoop")
|
||||||
if (trackExperience) ChipRow(label = "Experience", value = experience, onChange = onExperienceChange)
|
|
||||||
|
|
||||||
if (trackCommanderDamage) {
|
|
||||||
Divider()
|
|
||||||
Text("Commander damage", style = MaterialTheme.typography.titleSmall)
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
||||||
opponents.forEach { fromId ->
|
|
||||||
val value = commanderDamages[fromId] ?: 0
|
|
||||||
ChipRow(label = "From P${fromId + 1}", value = value, onChange = { onCommanderDamageChange(fromId, it) })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BigLifeRow(value = life, onChange = onLifeChange, enabled = !isEliminated)
|
||||||
|
|
||||||
|
if (trackPoison) ChipRow(label = "Poison", value = poison, onChange = onPoisonChange, enabled = !isEliminated)
|
||||||
|
if (trackEnergy) ChipRow(label = "Energy", value = energy, onChange = onEnergyChange, enabled = !isEliminated)
|
||||||
|
if (trackExperience) ChipRow(label = "Experience", value = experience, onChange = onExperienceChange, enabled = !isEliminated)
|
||||||
|
|
||||||
|
if (trackCommanderDamage) {
|
||||||
|
Divider()
|
||||||
|
Text("Commander damage", style = MaterialTheme.typography.titleSmall)
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
opponents.forEach { fromId ->
|
||||||
|
val value = commanderDamages[fromId] ?: 0
|
||||||
|
ChipRow(label = "From P${fromId + 1}", value = value, onChange = { onCommanderDamageChange(fromId, it) }, enabled = !isEliminated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEliminated) {
|
||||||
|
// Skull overlay
|
||||||
|
Text(
|
||||||
|
text = "☠️",
|
||||||
|
fontSize = 64.sp,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.align(Alignment.Center).alpha(overlayAlpha.value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,7 +275,8 @@ private fun ChipRow(
|
|||||||
label: String,
|
label: String,
|
||||||
value: Int,
|
value: Int,
|
||||||
onChange: (Int) -> Unit,
|
onChange: (Int) -> Unit,
|
||||||
emphasized: Boolean = false
|
emphasized: Boolean = false,
|
||||||
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -177,20 +285,20 @@ private fun ChipRow(
|
|||||||
) {
|
) {
|
||||||
Text(label)
|
Text(label)
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
CounterIconButton(Icons.Default.Remove, "decrement") { onChange(value - 1) }
|
CounterIconButton(Icons.Default.Remove, "decrement", enabled = enabled) { onChange(value - 1) }
|
||||||
Text(
|
Text(
|
||||||
text = value.toString(),
|
text = value.toString(),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
modifier = Modifier.padding(horizontal = 12.dp),
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
CounterIconButton(Icons.Default.Add, "increment") { onChange(value + 1) }
|
CounterIconButton(Icons.Default.Add, "increment", enabled = enabled) { onChange(value + 1) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BigLifeRow(value: Int, onChange: (Int) -> Unit) {
|
private fun BigLifeRow(value: Int, onChange: (Int) -> Unit, enabled: Boolean = true) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -198,21 +306,21 @@ private fun BigLifeRow(value: Int, onChange: (Int) -> Unit) {
|
|||||||
) {
|
) {
|
||||||
Text("Life", style = MaterialTheme.typography.titleMedium)
|
Text("Life", style = MaterialTheme.typography.titleMedium)
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
CounterIconButton(Icons.Default.Remove, "decrement life") { onChange(value - 1) }
|
CounterIconButton(Icons.Default.Remove, "decrement life", enabled = enabled) { onChange(value - 1) }
|
||||||
Text(
|
Text(
|
||||||
text = value.toString(),
|
text = value.toString(),
|
||||||
style = MaterialTheme.typography.displaySmall,
|
style = MaterialTheme.typography.displaySmall,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
CounterIconButton(Icons.Default.Add, "increment life") { onChange(value + 1) }
|
CounterIconButton(Icons.Default.Add, "increment life", enabled = enabled) { onChange(value + 1) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CounterIconButton(icon: ImageVector, contentDescription: String, onClick: () -> Unit) {
|
private fun CounterIconButton(icon: ImageVector, contentDescription: String, enabled: Boolean = true, onClick: () -> Unit) {
|
||||||
FilledTonalIconButton(onClick = onClick, modifier = Modifier.size(40.dp)) {
|
FilledTonalIconButton(onClick = onClick, enabled = enabled, modifier = Modifier.size(40.dp)) {
|
||||||
Icon(icon, contentDescription = contentDescription)
|
Icon(icon, contentDescription = contentDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import com.atridad.magiccounter.ui.state.defaultPlayerName
|
|||||||
@Composable
|
@Composable
|
||||||
fun SetupScreen(
|
fun SetupScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onStart: (GameState) -> Unit
|
onStart: (String, GameState) -> Unit
|
||||||
) {
|
) {
|
||||||
var playerCount by remember { mutableIntStateOf(4) }
|
var playerCount by remember { mutableIntStateOf(4) }
|
||||||
var startingLife by remember { mutableIntStateOf(40) }
|
var startingLife by remember { mutableIntStateOf(40) }
|
||||||
@@ -43,6 +43,7 @@ fun SetupScreen(
|
|||||||
var trackEnergy by remember { mutableStateOf(false) }
|
var trackEnergy by remember { mutableStateOf(false) }
|
||||||
var trackExperience by remember { mutableStateOf(false) }
|
var trackExperience by remember { mutableStateOf(false) }
|
||||||
var trackCommander by remember { mutableStateOf(true) }
|
var trackCommander by remember { mutableStateOf(true) }
|
||||||
|
var matchName by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val names = remember { mutableStateListOf<String>() }
|
val names = remember { mutableStateListOf<String>() }
|
||||||
LaunchedEffect(playerCount) {
|
LaunchedEffect(playerCount) {
|
||||||
@@ -80,6 +81,13 @@ fun SetupScreen(
|
|||||||
Text("Track experience")
|
Text("Track experience")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = matchName,
|
||||||
|
onValueChange = { matchName = it },
|
||||||
|
label = { Text("Match name") }
|
||||||
|
)
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
names.forEachIndexed { index, value ->
|
names.forEachIndexed { index, value ->
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -105,6 +113,7 @@ fun SetupScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onStart(
|
onStart(
|
||||||
|
matchName.ifBlank { "Game ${System.currentTimeMillis()}" },
|
||||||
GameState(
|
GameState(
|
||||||
players = players,
|
players = players,
|
||||||
startingLife = startingLife,
|
startingLife = startingLife,
|
||||||
|
|||||||
@@ -4,9 +4,25 @@ import android.content.Context
|
|||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MatchRecord(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val startedAtEpochMs: Long,
|
||||||
|
val lastUpdatedEpochMs: Long,
|
||||||
|
val ongoing: Boolean,
|
||||||
|
val winnerPlayerId: Int? = null,
|
||||||
|
val state: com.atridad.magiccounter.ui.state.GameState
|
||||||
|
)
|
||||||
|
|
||||||
enum class ThemeMode { System, Light, Dark }
|
enum class ThemeMode { System, Light, Dark }
|
||||||
|
|
||||||
@@ -14,6 +30,7 @@ private val Context.dataStore by preferencesDataStore(name = "app_settings")
|
|||||||
|
|
||||||
object AppSettingsRepository {
|
object AppSettingsRepository {
|
||||||
private val THEME_MODE = intPreferencesKey("theme_mode")
|
private val THEME_MODE = intPreferencesKey("theme_mode")
|
||||||
|
private val MATCH_HISTORY = stringPreferencesKey("match_history_json")
|
||||||
|
|
||||||
fun themeMode(context: Context): Flow<ThemeMode> =
|
fun themeMode(context: Context): Flow<ThemeMode> =
|
||||||
context.dataStore.data.map { prefs ->
|
context.dataStore.data.map { prefs ->
|
||||||
@@ -33,6 +50,21 @@ object AppSettingsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Very small JSON blob to persist match history
|
||||||
|
fun readMatchHistory(context: Context): Flow<List<MatchRecord>> =
|
||||||
|
context.dataStore.data.map { prefs ->
|
||||||
|
prefs[MATCH_HISTORY]?.let {
|
||||||
|
runCatching { Json.decodeFromString<List<MatchRecord>>(it) }.getOrDefault(emptyList())
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun writeMatchHistory(context: Context, history: List<MatchRecord>) {
|
||||||
|
val json = Json.encodeToString(history)
|
||||||
|
context.dataStore.edit { prefs ->
|
||||||
|
prefs[MATCH_HISTORY] = json
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.atridad.magiccounter.ui.settings
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -21,6 +22,19 @@ class AppSettingsViewModel(application: Application) : AndroidViewModel(applicat
|
|||||||
AppSettingsRepository.setThemeMode(getApplication(), mode)
|
AppSettingsRepository.setThemeMode(getApplication(), mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val matchHistory: StateFlow<List<MatchRecord>> =
|
||||||
|
AppSettingsRepository.readMatchHistory(application).stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000),
|
||||||
|
initialValue = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun saveHistory(history: List<MatchRecord>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
AppSettingsRepository.writeMatchHistory(getApplication(), history)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
package com.atridad.magiccounter.ui.state
|
package com.atridad.magiccounter.ui.state
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.builtins.MapSerializer
|
||||||
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@Serializable
|
||||||
data class CommanderDamage(
|
data class CommanderDamage(
|
||||||
val fromPlayerId: Int,
|
val fromPlayerId: Int,
|
||||||
val damage: Int
|
val damage: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@Serializable
|
||||||
data class PlayerState(
|
data class PlayerState(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -16,10 +21,12 @@ data class PlayerState(
|
|||||||
val poison: Int,
|
val poison: Int,
|
||||||
val energy: Int,
|
val energy: Int,
|
||||||
val experience: Int,
|
val experience: Int,
|
||||||
val commanderDamages: Map<Int, Int>
|
val commanderDamages: Map<Int, Int>,
|
||||||
|
val scooped: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@Serializable
|
||||||
data class GameState(
|
data class GameState(
|
||||||
val players: List<PlayerState>,
|
val players: List<PlayerState>,
|
||||||
val startingLife: Int,
|
val startingLife: Int,
|
||||||
|
|||||||
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,30 +1,15 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:width="108dp"
|
<!-- Inset the bitmap to avoid clipping on circular masks -->
|
||||||
android:height="108dp"
|
<item
|
||||||
android:viewportWidth="108"
|
android:top="18dp"
|
||||||
android:viewportHeight="108">
|
android:bottom="18dp"
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
android:left="18dp"
|
||||||
<aapt:attr name="android:fillColor">
|
android:right="18dp">
|
||||||
<gradient
|
<bitmap
|
||||||
android:endX="85.84757"
|
android:src="@drawable/ic_launcher"
|
||||||
android:endY="92.4963"
|
android:gravity="center" />
|
||||||
android:startX="42.9492"
|
</item>
|
||||||
android:startY="49.59793"
|
</layer-list>
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@android:color/transparent" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@android:color/transparent" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -17,6 +17,7 @@ activityCompose = "1.9.2"
|
|||||||
lifecycleRuntimeCompose = "2.8.6"
|
lifecycleRuntimeCompose = "2.8.6"
|
||||||
lifecycleViewmodelCompose = "2.8.6"
|
lifecycleViewmodelCompose = "2.8.6"
|
||||||
datastore = "1.1.1"
|
datastore = "1.1.1"
|
||||||
|
serialization = "1.7.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -40,9 +41,11 @@ androidx-material-icons-extended = { group = "androidx.compose.material", name =
|
|||||||
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
|
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
|
||||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
|
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
|
||||||
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||||
|
ktorx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user