1.1.0 - Better Material Design Compliance

This commit is contained in:
2025-08-22 00:14:01 -06:00
parent 54d9239dfe
commit 98ca6e676c
7 changed files with 349 additions and 123 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">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

View File

@@ -7,14 +7,14 @@ plugins {
android {
namespace = "com.atridad.magiccounter"
compileSdk = 35
compileSdk = 36
defaultConfig {
applicationId = "com.atridad.magiccounter"
minSdk = 35
targetSdk = 35
versionCode = 1
versionName = "1.0.0"
targetSdk = 36
versionCode = 2
versionName = "1.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -6,15 +6,19 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.PlayArrow
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.filled.Delete
import androidx.compose.material3.Button
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.MatchRecord
import com.atridad.magiccounter.ui.theme.MagicCounterTheme
import androidx.compose.material3.FilterChip
import androidx.activity.compose.BackHandler
import androidx.compose.material3.HorizontalDivider
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.layout.Spacer
private sealed class Screen {
data object Home : Screen()
@@ -63,6 +67,8 @@ fun MagicCounterApp() {
val theme = settingsVm.themeMode.collectAsState()
val historyState = settingsVm.matchHistory.collectAsState()
// State for clear history confirmation
var showClearConfirm by remember { mutableStateOf(false) }
MagicCounterTheme(themeMode = theme.value) {
Scaffold(
@@ -77,11 +83,28 @@ fun MagicCounterApp() {
}
},
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) }) {
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 ->
val currentScreen = screenStack.last()
@@ -92,13 +115,17 @@ fun MagicCounterApp() {
.padding(paddingValues)
.fillMaxSize(),
history = historyState.value,
onNewGame = { screenStack.add(Screen.Setup) },
onResume = { record -> screenStack.add(Screen.Game(record.id)) },
onDelete = { id ->
val newList = historyState.value.filterNot { it.id == id }
settingsVm.saveHistory(newList)
},
onClear = { settingsVm.saveHistory(emptyList()) }
showClearConfirm = showClearConfirm,
onClearConfirm = { showClearConfirm = false },
onClear = {
settingsVm.saveHistory(emptyList())
showClearConfirm = false
}
)
is Screen.Setup -> SetupScreen(
modifier = Modifier
@@ -171,41 +198,109 @@ fun MagicCounterApp() {
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
private fun SettingsContent(modifier: Modifier, current: ThemeMode, onSelect: (ThemeMode) -> Unit) {
Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text("Theme")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Column(
modifier = modifier.padding(24.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 ->
FilterChip(
androidx.compose.material3.FilterChip(
selected = current == mode,
onClick = { onSelect(mode) },
label = { Text(mode.name) }
label = { Text(mode.name) },
modifier = Modifier.weight(1f)
)
}
}
}
}
}
@Composable
private fun HomeScreen(
modifier: Modifier,
history: List<MatchRecord>,
onNewGame: () -> Unit,
onResume: (MatchRecord) -> Unit,
onDelete: (String) -> Unit,
showClearConfirm: Boolean,
onClearConfirm: () -> Unit,
onClear: () -> Unit
) {
Column(modifier = modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
var pendingDeleteId by remember { mutableStateOf<String?>(null) }
var showClearConfirm by remember { mutableStateOf(false) }
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = onNewGame) { Text("New game") }
Button(onClick = { showClearConfirm = true }) { Text("Clear history") }
// Only show action buttons if there are ongoing games
if (history.any { it.ongoing }) {
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()) {
@@ -213,57 +308,120 @@ private fun HomeScreen(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.History, contentDescription = "Empty history")
Text("Nothing to see here")
Text("Start a new game to begin.")
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
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 {
LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
item {
Column(modifier = Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 4.dp)) {
Column(modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 12.dp)) {
Text(
"Ongoing",
"Ongoing Games",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
HorizontalDivider()
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
}
}
items(history.filter { it.ongoing }, key = { it.id }) { rec ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Column {
Text(rec.name)
Text("Started ${java.text.DateFormat.getDateTimeInstance().format(java.util.Date(rec.startedAtEpochMs))}")
androidx.compose.material3.Card(
modifier = Modifier.fillMaxWidth(),
colors = androidx.compose.material3.CardDefaults.cardColors(
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 {
Column(modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 4.dp)) {
Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 12.dp)) {
Text(
"Finished",
"Finished Games",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
HorizontalDivider()
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
}
}
items(history.filter { !it.ongoing }, key = { it.id }) { rec ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Column {
Text(rec.name)
androidx.compose.material3.Card(
modifier = Modifier.fillMaxWidth(),
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}" } ?: ""
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
if (showClearConfirm) {
AlertDialog(
onDismissRequest = { showClearConfirm = false },
onDismissRequest = { onClearConfirm() },
title = { Text("Clear history?") },
text = { Text("This will remove all matches. This action cannot be undone.") },
confirmButton = {
TextButton(onClick = {
onClear()
showClearConfirm = false
}) { Text("Clear") }
TextButton(onClick = { onClear() }) { 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.rememberScrollState
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.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
@@ -49,11 +52,24 @@ fun SetupScreen(
Column(
modifier = modifier
.padding(16.dp)
.padding(24.dp)
.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(
value = startingLife.toFloat(),
onValueChange = {
@@ -63,41 +79,85 @@ fun SetupScreen(
valueRange = 10f..40f,
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) {
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) {
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(
modifier = Modifier.fillMaxWidth(),
value = matchName,
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 ->
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = value,
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()
Button(onClick = {
Button(
onClick = {
val players = names.mapIndexed { index, name ->
PlayerState(
id = index,
@@ -116,8 +176,14 @@ fun SetupScreen(
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]
agp = "8.9.0"
agp = "8.12.1"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"

View File

@@ -1,6 +1,6 @@
#Sat Aug 09 23:53:05 MDT 2025
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists