5 Commits
1.0.0 ... 1.1.0

8 changed files with 366 additions and 124 deletions

6
.idea/appInsightsSettings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="selectedTabId" value="Firebase Crashlytics" />
</component>
</project>

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

View File

@@ -1,2 +1,18 @@
# MagicCounter # MagicCounter
This is a FOSS Android app meant to allow MTG Commander players to keep track of player health and commander damage. This app is offline-only and requires no special permissions to run. Its built using Jetpack Compose with Material You support.
## Download
You have two options:
1. Download the latest APK from the Released page
2. Use <a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.atridad.magiccounter%22%2C%22url%22%3A%22https%3A%2F%2Fgit.atri.dad%2Fatridad%2FMagicCounter%2Freleases%22%2C%22author%22%3A%22git.atri.dad%22%2C%22name%22%3A%22MagicCounter%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla%2F5.0%20(Linux%3B%20Android%2010%3B%20K)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F114.0.0.0%20Mobile%20Safari%2F537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22partialAPKHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D">Obtainium</a>
## Requirements
- Android 15+
## Contribution
As this is on my private git this will be difficult to do easily. Get in touch and I can figure something out.

View File

@@ -7,21 +7,21 @@ plugins {
android { android {
namespace = "com.atridad.magiccounter" namespace = "com.atridad.magiccounter"
compileSdk = 35 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "com.atridad.magiccounter" applicationId = "com.atridad.magiccounter"
minSdk = 35 minSdk = 35
targetSdk = 35 targetSdk = 36
versionCode = 1 versionCode = 2
versionName = "1.0.0" versionName = "1.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"

View File

@@ -6,15 +6,19 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.size
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.History import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FloatingActionButton
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
@@ -42,11 +46,11 @@ 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.settings.MatchRecord
import com.atridad.magiccounter.ui.theme.MagicCounterTheme import com.atridad.magiccounter.ui.theme.MagicCounterTheme
import androidx.compose.material3.FilterChip
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.layout.Spacer
private sealed class Screen { private sealed class Screen {
data object Home : Screen() data object Home : Screen()
@@ -63,6 +67,8 @@ fun MagicCounterApp() {
val theme = settingsVm.themeMode.collectAsState() val theme = settingsVm.themeMode.collectAsState()
val historyState = settingsVm.matchHistory.collectAsState() val historyState = settingsVm.matchHistory.collectAsState()
// State for clear history confirmation
var showClearConfirm by remember { mutableStateOf(false) }
MagicCounterTheme(themeMode = theme.value) { MagicCounterTheme(themeMode = theme.value) {
Scaffold( Scaffold(
@@ -77,11 +83,28 @@ fun MagicCounterApp() {
} }
}, },
actions = { actions = {
// Show Clear History icon only on home screen
if (screenStack.last() is Screen.Home) {
IconButton(onClick = { showClearConfirm = true }) {
Icon(Icons.Default.Clear, contentDescription = "Clear history")
}
}
IconButton(onClick = { screenStack.add(Screen.Settings) }) { IconButton(onClick = { screenStack.add(Screen.Settings) }) {
Icon(Icons.Default.Settings, contentDescription = "App settings") Icon(Icons.Default.Settings, contentDescription = "App settings")
} }
} }
) )
},
floatingActionButton = {
// Show FAB only on home screen when no active game is running
val currentScreen = screenStack.last()
if (currentScreen is Screen.Home && historyState.value.none { it.ongoing }) {
FloatingActionButton(
onClick = { screenStack.add(Screen.Setup) }
) {
Icon(Icons.Default.Add, contentDescription = "New game")
}
}
} }
) { paddingValues -> ) { paddingValues ->
val currentScreen = screenStack.last() val currentScreen = screenStack.last()
@@ -92,13 +115,17 @@ fun MagicCounterApp() {
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize(), .fillMaxSize(),
history = historyState.value, history = historyState.value,
onNewGame = { screenStack.add(Screen.Setup) },
onResume = { record -> screenStack.add(Screen.Game(record.id)) }, onResume = { record -> screenStack.add(Screen.Game(record.id)) },
onDelete = { id -> onDelete = { id ->
val newList = historyState.value.filterNot { it.id == id } val newList = historyState.value.filterNot { it.id == id }
settingsVm.saveHistory(newList) settingsVm.saveHistory(newList)
}, },
onClear = { settingsVm.saveHistory(emptyList()) } showClearConfirm = showClearConfirm,
onClearConfirm = { showClearConfirm = false },
onClear = {
settingsVm.saveHistory(emptyList())
showClearConfirm = false
}
) )
is Screen.Setup -> SetupScreen( is Screen.Setup -> SetupScreen(
modifier = Modifier modifier = Modifier
@@ -171,41 +198,109 @@ fun MagicCounterApp() {
onSelect = { settingsVm.setTheme(it) } onSelect = { settingsVm.setTheme(it) }
) )
} }
// Global clear history confirmation dialog
if (showClearConfirm) {
AlertDialog(
onDismissRequest = { showClearConfirm = false },
title = { Text("Clear history?") },
text = { Text("This will remove all matches. This action cannot be undone.") },
confirmButton = {
TextButton(onClick = {
settingsVm.saveHistory(emptyList())
showClearConfirm = false
}) { Text("Clear") }
},
dismissButton = {
TextButton(onClick = { showClearConfirm = false }) { Text("Cancel") }
}
)
}
} }
} }
} }
@Composable @Composable
private fun SettingsContent(modifier: Modifier, current: ThemeMode, onSelect: (ThemeMode) -> Unit) { private fun SettingsContent(modifier: Modifier, current: ThemeMode, onSelect: (ThemeMode) -> Unit) {
Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { Column(
Text("Theme") modifier = modifier.padding(24.dp),
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text(
"Appearance",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary
)
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
"Theme",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth()
) {
ThemeMode.entries.forEach { mode -> ThemeMode.entries.forEach { mode ->
FilterChip( androidx.compose.material3.FilterChip(
selected = current == mode, selected = current == mode,
onClick = { onSelect(mode) }, onClick = { onSelect(mode) },
label = { Text(mode.name) } label = { Text(mode.name) },
modifier = Modifier.weight(1f)
) )
} }
} }
} }
} }
}
@Composable @Composable
private fun HomeScreen( private fun HomeScreen(
modifier: Modifier, modifier: Modifier,
history: List<MatchRecord>, history: List<MatchRecord>,
onNewGame: () -> Unit,
onResume: (MatchRecord) -> Unit, onResume: (MatchRecord) -> Unit,
onDelete: (String) -> Unit, onDelete: (String) -> Unit,
showClearConfirm: Boolean,
onClearConfirm: () -> Unit,
onClear: () -> Unit onClear: () -> Unit
) { ) {
Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
var pendingDeleteId by remember { mutableStateOf<String?>(null) } var pendingDeleteId by remember { mutableStateOf<String?>(null) }
var showClearConfirm by remember { mutableStateOf(false) }
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { // Only show action buttons if there are ongoing games
Button(onClick = onNewGame) { Text("New game") } if (history.any { it.ongoing }) {
Button(onClick = { showClearConfirm = true }) { Text("Clear history") } Column(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
"Quick Actions",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth()
) {
androidx.compose.material3.OutlinedButton(
onClick = { onResume(history.first { it.ongoing }) },
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.PlayArrow, contentDescription = null)
Spacer(modifier = Modifier.padding(4.dp))
Text("Resume Game")
}
androidx.compose.material3.OutlinedButton(
onClick = { pendingDeleteId = history.first { it.ongoing }.id },
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.Delete, contentDescription = null)
Spacer(modifier = Modifier.padding(4.dp))
Text("Delete Game")
}
}
}
} }
if (history.isEmpty()) { if (history.isEmpty()) {
@@ -213,57 +308,120 @@ private fun HomeScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(
Icon(Icons.Default.History, contentDescription = "Empty history") horizontalAlignment = Alignment.CenterHorizontally,
Text("Nothing to see here") verticalArrangement = Arrangement.spacedBy(16.dp)
Text("Start a new game to begin.") ) {
Icon(
Icons.Default.History,
contentDescription = "Empty history",
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
"No games yet",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
"Start your first Magic: The Gathering game to begin tracking life totals and more.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
} }
} }
} else { } else {
LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) { LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
item { item {
Column(modifier = Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 4.dp)) { Column(modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 12.dp)) {
Text( Text(
"Ongoing", "Ongoing Games",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
HorizontalDivider() HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
} }
} }
items(history.filter { it.ongoing }, key = { it.id }) { rec -> items(history.filter { it.ongoing }, key = { it.id }) { rec ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { androidx.compose.material3.Card(
Column { modifier = Modifier.fillMaxWidth(),
Text(rec.name) colors = androidx.compose.material3.CardDefaults.cardColors(
Text("Started ${java.text.DateFormat.getDateTimeInstance().format(java.util.Date(rec.startedAtEpochMs))}") containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
rec.name,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
"Started ${java.text.DateFormat.getDateTimeInstance().format(java.util.Date(rec.startedAtEpochMs))}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton(onClick = { onResume(rec) }) {
Icon(Icons.Default.PlayArrow, contentDescription = "Resume game")
}
IconButton(onClick = { pendingDeleteId = rec.id }) {
Icon(Icons.Default.Delete, contentDescription = "Delete")
}
} }
Row {
IconButton(onClick = { onResume(rec) }) { Icon(Icons.Default.PlayArrow, contentDescription = "Resume game") }
IconButton(onClick = { pendingDeleteId = rec.id }) { Icon(Icons.Default.Delete, contentDescription = "Delete") }
} }
} }
} }
item { item {
Column(modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 4.dp)) { Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 12.dp)) {
Text( Text(
"Finished", "Finished Games",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
HorizontalDivider() HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
} }
} }
items(history.filter { !it.ongoing }, key = { it.id }) { rec -> items(history.filter { !it.ongoing }, key = { it.id }) { rec ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { androidx.compose.material3.Card(
Column { modifier = Modifier.fillMaxWidth(),
Text(rec.name) colors = androidx.compose.material3.CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
rec.name,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
val winner = rec.winnerPlayerId?.let { "Winner: Player ${it + 1}" } ?: "" val winner = rec.winnerPlayerId?.let { "Winner: Player ${it + 1}" } ?: ""
Text("Finished • $winner") Text(
"Finished • $winner",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton(onClick = { onResume(rec) }) {
Icon(Icons.Default.Visibility, contentDescription = "View match")
}
IconButton(onClick = { pendingDeleteId = rec.id }) {
Icon(Icons.Default.Delete, contentDescription = "Delete")
}
} }
Row {
IconButton(onClick = { onResume(rec) }) { Icon(Icons.Default.Visibility, contentDescription = "View match") }
IconButton(onClick = { pendingDeleteId = rec.id }) { Icon(Icons.Default.Delete, contentDescription = "Delete") }
} }
} }
} }
@@ -290,16 +448,13 @@ private fun HomeScreen(
// Confirm clear dialog // Confirm clear dialog
if (showClearConfirm) { if (showClearConfirm) {
AlertDialog( AlertDialog(
onDismissRequest = { showClearConfirm = false }, onDismissRequest = { onClearConfirm() },
title = { Text("Clear history?") }, title = { Text("Clear history?") },
text = { Text("This will remove all matches. This action cannot be undone.") }, text = { Text("This will remove all matches. This action cannot be undone.") },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = { onClear() }) { Text("Clear") }
onClear()
showClearConfirm = false
}) { Text("Clear") }
}, },
dismissButton = { TextButton(onClick = { showClearConfirm = false }) { Text("Cancel") } } dismissButton = { TextButton(onClick = { onClearConfirm() }) { Text("Cancel") } }
) )
} }
} }

View File

@@ -9,8 +9,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -49,11 +52,24 @@ fun SetupScreen(
Column( Column(
modifier = modifier modifier = modifier
.padding(16.dp) .padding(24.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(24.dp)
) { ) {
Text("Starting life: $startingLife") // Game Settings Section
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
"Game Settings",
style = androidx.compose.material3.MaterialTheme.typography.headlineSmall,
color = androidx.compose.material3.MaterialTheme.colorScheme.primary
)
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
"Starting life: $startingLife",
style = androidx.compose.material3.MaterialTheme.typography.titleMedium
)
Slider( Slider(
value = startingLife.toFloat(), value = startingLife.toFloat(),
onValueChange = { onValueChange = {
@@ -63,41 +79,85 @@ fun SetupScreen(
valueRange = 10f..40f, valueRange = 10f..40f,
steps = 5 steps = 5
) )
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Text("Players: $playerCount")
} }
Slider(value = playerCount.toFloat(), onValueChange = { playerCount = it.toInt() }, valueRange = 2f..8f, steps = 6)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
"Players: $playerCount",
style = androidx.compose.material3.MaterialTheme.typography.titleMedium
)
Slider(
value = playerCount.toFloat(),
onValueChange = { playerCount = it.toInt() },
valueRange = 2f..8f,
steps = 6
)
}
}
}
// Game Options Section
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
"Game Options",
style = androidx.compose.material3.MaterialTheme.typography.headlineSmall,
color = androidx.compose.material3.MaterialTheme.colorScheme.primary
)
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = trackCommander, onCheckedChange = { trackCommander = it }) Checkbox(checked = trackCommander, onCheckedChange = { trackCommander = it })
Text("Track commander damage") Text(
"Track commander damage",
style = androidx.compose.material3.MaterialTheme.typography.bodyLarge
)
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = trackPoison, onCheckedChange = { trackPoison = it }) Checkbox(checked = trackPoison, onCheckedChange = { trackPoison = it })
Text("Track poison") Text(
"Track poison",
style = androidx.compose.material3.MaterialTheme.typography.bodyLarge
)
} }
}
}
// Match Details Section
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
"Match Details",
style = androidx.compose.material3.MaterialTheme.typography.headlineSmall,
color = androidx.compose.material3.MaterialTheme.colorScheme.primary
)
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = matchName, value = matchName,
onValueChange = { matchName = it }, onValueChange = { matchName = it },
label = { Text("Match name") } label = { Text("Match name") },
singleLine = true
) )
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
names.forEachIndexed { index, value -> names.forEachIndexed { index, value ->
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = value, value = value,
onValueChange = { names[index] = it }, onValueChange = { names[index] = it },
label = { Text("Player ${index + 1} name") } label = { Text("Player ${index + 1} name") },
singleLine = true
) )
} }
} }
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(16.dp))
// Note: The actual start button is now handled by a FAB in the parent Scaffold
// This button is kept for accessibility and as a fallback
val canStart = matchName.isNotBlank() val canStart = matchName.isNotBlank()
Button(onClick = { Button(
onClick = {
val players = names.mapIndexed { index, name -> val players = names.mapIndexed { index, name ->
PlayerState( PlayerState(
id = index, id = index,
@@ -116,8 +176,14 @@ fun SetupScreen(
trackCommanderDamage = trackCommander trackCommanderDamage = trackCommander
) )
) )
}, enabled = canStart) { },
Text("Start game") enabled = canStart,
modifier = Modifier.fillMaxWidth(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp)
) {
Icon(Icons.Default.PlayArrow, contentDescription = null)
Spacer(modifier = Modifier.padding(8.dp))
Text("Start Game", style = androidx.compose.material3.MaterialTheme.typography.titleMedium)
} }
} }
} }

View File

@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.9.0" agp = "8.12.1"
kotlin = "2.0.21" kotlin = "2.0.21"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"

View File

@@ -1,6 +1,6 @@
#Sat Aug 09 23:53:05 MDT 2025 #Sat Aug 09 23:53:05 MDT 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists