This commit is contained in:
2025-08-10 00:50:14 -06:00
parent bde85ad2ae
commit 2b7752c5c3
63 changed files with 1642 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+58
View File
@@ -0,0 +1,58 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.atridad.magiccounter"
compileSdk = 35
defaultConfig {
applicationId = "com.atridad.magiccounter"
minSdk = 35
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.core.ktx)
implementation(libs.material)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.datastore.preferences)
debugImplementation(libs.androidx.compose.ui.tooling)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.atridad.magiccounter
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.atridad.magiccounter", appContext.packageName)
}
}
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MagicCounter"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MagicCounter">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,24 @@
package com.atridad.magiccounter
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.Surface
import androidx.compose.material3.MaterialTheme
import com.atridad.magiccounter.ui.MagicCounterApp
import com.atridad.magiccounter.ui.theme.MagicCounterTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MagicCounterTheme {
Surface(color = MaterialTheme.colorScheme.background) {
MagicCounterApp()
}
}
}
}
}
@@ -0,0 +1,110 @@
package com.atridad.magiccounter.ui
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.atridad.magiccounter.ui.screens.GameScreen
import com.atridad.magiccounter.ui.screens.SetupScreen
import com.atridad.magiccounter.ui.state.GameState
import androidx.lifecycle.viewmodel.compose.viewModel
import com.atridad.magiccounter.ui.settings.AppSettingsViewModel
import com.atridad.magiccounter.ui.settings.ThemeMode
import com.atridad.magiccounter.ui.theme.MagicCounterTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.FilterChip
// no-op
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MagicCounterApp() {
var gameState by remember { mutableStateOf<GameState?>(null) }
var showSettings by remember { mutableStateOf(false) }
val settingsVm: AppSettingsViewModel = viewModel()
val theme = settingsVm.themeMode.collectAsState()
MagicCounterTheme(themeMode = theme.value) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Magic Counter", style = MaterialTheme.typography.titleLarge) },
actions = {
if (gameState != null) {
IconButton(onClick = { gameState = null }) {
Icon(Icons.Default.Refresh, contentDescription = "New game")
}
}
IconButton(onClick = { showSettings = true }) {
Icon(Icons.Default.Settings, contentDescription = "App settings")
}
}
)
}
) { paddingValues ->
if (gameState == null) {
SetupScreen(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
onStart = { state -> gameState = state }
)
} else {
GameScreen(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
state = gameState!!,
onEnd = { gameState = null }
)
}
if (showSettings) {
ModalBottomSheet(onDismissRequest = { showSettings = false }) {
SettingsSheet(
current = theme.value,
onSelect = { settingsVm.setTheme(it) }
)
}
}
}
}
}
@Composable
private fun SettingsSheet(current: ThemeMode, onSelect: (ThemeMode) -> Unit) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text("Theme")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ThemeMode.values().forEach { mode ->
FilterChip(
selected = current == mode,
onClick = { onSelect(mode) },
label = { Text(mode.name) }
)
}
}
}
}
@@ -0,0 +1,220 @@
package com.atridad.magiccounter.ui.screens
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.atridad.magiccounter.ui.state.GameState
import com.atridad.magiccounter.ui.state.PlayerState
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GameScreen(
modifier: Modifier = Modifier,
state: GameState,
onEnd: () -> Unit
) {
// Local editable state per player
val lifeTotals = remember { mutableStateMapOf<Int, Int>() }
val poisonTotals = remember { mutableStateMapOf<Int, Int>() }
val energyTotals = remember { mutableStateMapOf<Int, Int>() }
val experienceTotals = remember { mutableStateMapOf<Int, Int>() }
val commanderDamages = remember { mutableStateMapOf<Int, MutableMap<Int, Int>>() }
// Initialize
state.players.forEach { p ->
lifeTotals.putIfAbsent(p.id, p.life)
poisonTotals.putIfAbsent(p.id, p.poison)
energyTotals.putIfAbsent(p.id, p.energy)
experienceTotals.putIfAbsent(p.id, p.experience)
commanderDamages.putIfAbsent(p.id, p.commanderDamages.toMutableMap())
}
// Single column for maximum width per request
val numColumns = 1
Column(modifier = modifier.padding(12.dp)) {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(state.players, key = { _, item -> item.id }) { index, player ->
val accent = seatAccentColor(index, MaterialTheme.colorScheme)
val perPlayerCommander: Map<Int, Int> = commanderDamages[player.id]?.toMap() ?: emptyMap()
PlayerCard(
player = player,
opponents = state.players.map { it.id }.filter { it != player.id },
life = lifeTotals[player.id] ?: state.startingLife,
onLifeChange = { lifeTotals[player.id] = it },
poison = poisonTotals[player.id] ?: 0,
onPoisonChange = { poisonTotals[player.id] = it },
energy = energyTotals[player.id] ?: 0,
onEnergyChange = { energyTotals[player.id] = it },
experience = experienceTotals[player.id] ?: 0,
onExperienceChange = { experienceTotals[player.id] = it },
trackPoison = state.trackPoison,
trackEnergy = state.trackEnergy,
trackExperience = state.trackExperience,
trackCommanderDamage = state.trackCommanderDamage,
commanderDamages = perPlayerCommander,
onCommanderDamageChange = { fromId, dmg ->
val newMap = (commanderDamages[player.id] ?: mutableMapOf()).toMutableMap()
newMap[fromId] = dmg
commanderDamages[player.id] = newMap
},
rotation = 0f,
accentColor = accent
)
}
}
}
}
private fun seatAccentColor(index: Int, scheme: androidx.compose.material3.ColorScheme): Color = when (index % 6) {
0 -> scheme.primary
1 -> scheme.tertiary
2 -> scheme.secondary
3 -> scheme.error
4 -> scheme.primaryContainer
else -> scheme.tertiaryContainer
}
@Composable
private fun PlayerCard(
player: PlayerState,
opponents: List<Int>,
life: Int,
onLifeChange: (Int) -> Unit,
poison: Int,
onPoisonChange: (Int) -> Unit,
energy: Int,
onEnergyChange: (Int) -> Unit,
experience: Int,
onExperienceChange: (Int) -> Unit,
trackPoison: Boolean,
trackEnergy: Boolean,
trackExperience: Boolean,
trackCommanderDamage: Boolean,
commanderDamages: Map<Int, Int>,
onCommanderDamageChange: (fromId: Int, damage: Int) -> Unit,
rotation: Float,
accentColor: Color
) {
Card(
modifier = Modifier
.padding(4.dp)
.fillMaxWidth()
.graphicsLayer { rotationZ = rotation },
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
border = BorderStroke(2.dp, accentColor)
) {
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(player.name, style = MaterialTheme.typography.titleMedium, color = accentColor)
BigLifeRow(value = life, onChange = onLifeChange)
if (trackPoison) ChipRow(label = "Poison", value = poison, onChange = onPoisonChange)
if (trackEnergy) ChipRow(label = "Energy", value = energy, onChange = onEnergyChange)
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) })
}
}
}
}
}
}
private fun playerOpponents(selfId: Int, ids: Collection<Int>): List<Int> =
ids.filter { it != selfId }.sorted()
@Composable
private fun ChipRow(
label: String,
value: Int,
onChange: (Int) -> Unit,
emphasized: Boolean = false
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(label)
Row(verticalAlignment = Alignment.CenterVertically) {
CounterIconButton(Icons.Default.Remove, "decrement") { onChange(value - 1) }
Text(
text = value.toString(),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(horizontal = 12.dp),
textAlign = TextAlign.Center
)
CounterIconButton(Icons.Default.Add, "increment") { onChange(value + 1) }
}
}
}
@Composable
private fun BigLifeRow(value: Int, onChange: (Int) -> Unit) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Life", style = MaterialTheme.typography.titleMedium)
Row(verticalAlignment = Alignment.CenterVertically) {
CounterIconButton(Icons.Default.Remove, "decrement life") { onChange(value - 1) }
Text(
text = value.toString(),
style = MaterialTheme.typography.displaySmall,
modifier = Modifier.padding(horizontal = 16.dp),
textAlign = TextAlign.Center
)
CounterIconButton(Icons.Default.Add, "increment life") { onChange(value + 1) }
}
}
}
@Composable
private fun CounterIconButton(icon: ImageVector, contentDescription: String, onClick: () -> Unit) {
FilledTonalIconButton(onClick = onClick, modifier = Modifier.size(40.dp)) {
Icon(icon, contentDescription = contentDescription)
}
}
@@ -0,0 +1,123 @@
package com.atridad.magiccounter.ui.screens
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
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.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.atridad.magiccounter.ui.state.GameState
import com.atridad.magiccounter.ui.state.PlayerState
import com.atridad.magiccounter.ui.state.defaultPlayerName
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
fun SetupScreen(
modifier: Modifier = Modifier,
onStart: (GameState) -> Unit
) {
var playerCount by remember { mutableIntStateOf(4) }
var startingLife by remember { mutableIntStateOf(40) }
var trackPoison by remember { mutableStateOf(true) }
var trackEnergy by remember { mutableStateOf(false) }
var trackExperience by remember { mutableStateOf(false) }
var trackCommander by remember { mutableStateOf(true) }
val names = remember { mutableStateListOf<String>() }
LaunchedEffect(playerCount) {
while (names.size < playerCount) names.add(defaultPlayerName(names.size))
while (names.size > playerCount) names.removeLast()
}
Column(
modifier = modifier
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Starting life: $startingLife")
Slider(value = startingLife.toFloat(), onValueChange = { startingLife = it.toInt() }, valueRange = 20f..60f, steps = 20)
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)
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = trackCommander, onCheckedChange = { trackCommander = it })
Text("Track commander damage")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = trackPoison, onCheckedChange = { trackPoison = it })
Text("Track poison")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = trackEnergy, onCheckedChange = { trackEnergy = it })
Text("Track energy")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = trackExperience, onCheckedChange = { trackExperience = it })
Text("Track experience")
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
names.forEachIndexed { index, value ->
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = value,
onValueChange = { names[index] = it },
label = { Text("Player ${index + 1} name") }
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
val players = names.mapIndexed { index, name ->
PlayerState(
id = index,
name = name.ifBlank { defaultPlayerName(index) },
life = startingLife,
poison = 0,
energy = 0,
experience = 0,
commanderDamages = emptyMap()
)
}
onStart(
GameState(
players = players,
startingLife = startingLife,
trackPoison = trackPoison,
trackEnergy = trackEnergy,
trackExperience = trackExperience,
trackCommanderDamage = trackCommander
)
)
}) {
Text("Start game")
}
}
}
@@ -0,0 +1,38 @@
package com.atridad.magiccounter.ui.settings
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
enum class ThemeMode { System, Light, Dark }
private val Context.dataStore by preferencesDataStore(name = "app_settings")
object AppSettingsRepository {
private val THEME_MODE = intPreferencesKey("theme_mode")
fun themeMode(context: Context): Flow<ThemeMode> =
context.dataStore.data.map { prefs ->
when (prefs[THEME_MODE]) {
1 -> ThemeMode.Light
2 -> ThemeMode.Dark
else -> ThemeMode.System
}
}
suspend fun setThemeMode(context: Context, mode: ThemeMode) {
context.dataStore.edit { prefs ->
prefs[THEME_MODE] = when (mode) {
ThemeMode.System -> 0
ThemeMode.Light -> 1
ThemeMode.Dark -> 2
}
}
}
}
@@ -0,0 +1,26 @@
package com.atridad.magiccounter.ui.settings
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class AppSettingsViewModel(application: Application) : AndroidViewModel(application) {
val themeMode: StateFlow<ThemeMode> =
AppSettingsRepository.themeMode(application).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ThemeMode.System
)
fun setTheme(mode: ThemeMode) {
viewModelScope.launch {
AppSettingsRepository.setThemeMode(getApplication(), mode)
}
}
}
@@ -0,0 +1,34 @@
package com.atridad.magiccounter.ui.state
import androidx.compose.runtime.Immutable
@Immutable
data class CommanderDamage(
val fromPlayerId: Int,
val damage: Int
)
@Immutable
data class PlayerState(
val id: Int,
val name: String,
val life: Int,
val poison: Int,
val energy: Int,
val experience: Int,
val commanderDamages: Map<Int, Int>
)
@Immutable
data class GameState(
val players: List<PlayerState>,
val startingLife: Int,
val trackPoison: Boolean,
val trackEnergy: Boolean,
val trackExperience: Boolean,
val trackCommanderDamage: Boolean
)
fun defaultPlayerName(index: Int): String = "Player ${index + 1}"
@@ -0,0 +1,45 @@
package com.atridad.magiccounter.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.atridad.magiccounter.ui.settings.ThemeMode
private val LightColors = lightColorScheme()
private val DarkColors = darkColorScheme()
@Composable
fun MagicCounterTheme(
themeMode: ThemeMode = ThemeMode.System,
useDarkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val dark = when (themeMode) {
ThemeMode.System -> useDarkTheme
ThemeMode.Light -> false
ThemeMode.Dark -> true
}
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (dark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
dark -> DarkColors
else -> LightColors
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,7 @@
package com.atridad.magiccounter.ui.theme
import androidx.compose.material3.Typography
val Typography = Typography()
@@ -0,0 +1,170 @@
<?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>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<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">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
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>
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" />
<path
android:fillColor="#FF000000"
android:pathData="M9,2L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2H9zm3,15c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M22,16V4c0,-1.1 -0.9,-2 -2,-2H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zm-11,-4l2.03,2.71L16,11l4,5H8l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2H4V6H2z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6zm16,-4H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zm-8,12.5v-9l6,4.5 -6,4.5z" />
</vector>
@@ -0,0 +1,9 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="135"
android:centerColor="#009688"
android:endColor="#00695C"
android:startColor="#4DB6AC"
android:type="linear" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

+3
View File
@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<style name="Theme.MagicCounter" parent="Theme.Material3.DayNight.NoActionBar" />
</resources>
@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">200dp</dimen>
</resources>
@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
+8
View File
@@ -0,0 +1,8 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>
+13
View File
@@ -0,0 +1,13 @@
<resources>
<string name="app_name">MagicCounter</string>
<string name="navigation_drawer_open">Open navigation drawer</string>
<string name="navigation_drawer_close">Close navigation drawer</string>
<string name="nav_header_title">Android Studio</string>
<string name="nav_header_subtitle">android.studio@android.com</string>
<string name="nav_header_desc">Navigation header</string>
<string name="action_settings">Settings</string>
<string name="menu_home">Home</string>
<string name="menu_gallery">Gallery</string>
<string name="menu_slideshow">Slideshow</string>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<style name="Theme.MagicCounter" parent="Theme.Material3.DayNight.NoActionBar" />
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package com.atridad.magiccounter
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}