Compare commits

..

No commits in common. "new" and "master" have entirely different histories.
new ... master

8 changed files with 202 additions and 359 deletions

View file

@ -179,6 +179,17 @@
<option name="screenX" value="1080" /> <option name="screenX" value="1080" />
<option name="screenY" value="2400" /> <option name="screenY" value="2400" />
</PersistentDeviceSelectionData> </PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="q2q" />
<option name="id" value="q2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold3" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1768" />
<option name="screenY" value="2208" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData> <PersistentDeviceSelectionData>
<option name="api" value="34" /> <option name="api" value="34" />
<option name="brand" value="samsung" /> <option name="brand" value="samsung" />

View file

@ -5,14 +5,14 @@ plugins {
android { android {
namespace = "eu.m724.coincounter" namespace = "eu.m724.coincounter"
compileSdk = 35 compileSdk = 34
defaultConfig { defaultConfig {
applicationId = "eu.m724.coincounter" applicationId = "eu.m724.coincounter"
minSdk = 30 minSdk = 34
targetSdk = 35 targetSdk = 34
versionCode = 3 versionCode = 3
versionName = "2.0" versionName = "1.1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -59,7 +59,7 @@ dependencies {
implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.datastore) implementation(libs.androidx.datastore.preferences)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View file

@ -5,18 +5,18 @@
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.CoinCounter" android:theme="@style/Theme.CoinCounter"
android:enableOnBackInvokedCallback="true"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/title_activity_main" android:label="@string/app_name"
android:theme="@style/Theme.CoinCounter"> android:theme="@style/Theme.CoinCounter">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View file

@ -1,297 +1,237 @@
package eu.m724.coincounter package eu.m724.coincounter
import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background
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.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
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.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
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.MutableIntState
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.asIntState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.FontScaling
import androidx.compose.ui.unit.FontScalingLinear
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import eu.m724.coincounter.ui.theme.CoinCounterTheme import eu.m724.coincounter.ui.theme.CoinCounterTheme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var viewModel: MainViewModel val Context.dataStore by preferencesDataStore(name = "preferences")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel = MainViewModel(application) val increments = listOf(1, 2, 5, 10, 20, 50, 100, 200, 500)
viewModel.init()
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
AppView(increments, dataStore)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppView(increments: List<Int>, dataStore: DataStore<Preferences>) {
val BALANCE_KEY = intPreferencesKey("balance")
val balanceFlow: Flow<Int> =
dataStore.data.map { preferences ->
preferences[BALANCE_KEY] ?: 0
}
val balance = balanceFlow.collectAsState(initial = 0).value
println("Loading balance: $balance")
val balanceState = rememberSaveable { mutableIntStateOf(0) }
balanceState.intValue = balance // this is required for some reason
CoinCounterTheme { CoinCounterTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box( Column(modifier = Modifier
modifier = Modifier.padding(innerPadding) .fillMaxSize()
) { .padding(innerPadding)) {
App(viewModel) Box(modifier = Modifier
} .fillMaxWidth()
} .weight(.25f),
contentAlignment = Alignment.Center) {
StatusPart(balanceState)
} }
Column(modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalArrangement = Arrangement.Center) {
ControlPart(increments, balanceState, dataStore)
} }
} }
override fun onStop() { }
super.onStop()
viewModel.saveAll()
} }
} }
@Composable @Composable
fun App(viewModel: MainViewModel) { fun StatusPart(balanceState: MutableState<Int>) {
val total by viewModel.totalBalance.collectAsState() val value by balanceState.asIntState()
val wallets by viewModel.wallets.collectAsState() Row(verticalAlignment = Alignment.Bottom) {
Column {
BalanceView(total)
WalletList(
wallets = wallets,
onAdd = { viewModel.addWallet(it) }
)
}
}
@Composable
fun BalanceView(balance: Int) {
Column(
modifier = Modifier
.height(150.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.Bottom
) {
Text( Text(
text = "%.2f".format(balance / 100.0), text = "%.2f".format(value / 100.0),
fontSize = 32.sp fontSize = 48.sp
) )
Text( Text(
text = "", text = "",
fontSize = 14.sp fontSize = 32.sp,
) modifier = Modifier.alpha(.7f))
}
} }
} }
@Composable @Composable
fun AddWalletBox( fun ControlPart(increments: List<Int>, balanceState: MutableIntState, dataStore: DataStore<Preferences>) {
onAdd: (String) -> Unit val multiplier = remember { mutableIntStateOf(1) }
) { val scope = rememberCoroutineScope()
var name by remember { mutableStateOf("") } val BALANCE_KEY = intPreferencesKey("balance")
Box( suspend fun setBalance(value: Int) {
modifier = Modifier.fillMaxWidth(), dataStore.edit { preferences ->
contentAlignment = Alignment.CenterEnd preferences[BALANCE_KEY] = value
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 2.dp),
) {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(16.dp))
BasicTextField(
value = name,
onValueChange = {
name = it
},
modifier = Modifier
.fillMaxWidth()
.weight(1f),
singleLine = true,
textStyle = TextStyle.Default
)
IconButton(
onClick = {
if (name.isNotBlank()) {
onAdd(name)
name = ""
}
},
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Add wallet"
)
} }
} }
}
}
}
@Composable
fun WalletList(
wallets: List<Wallet>,
onAdd: (String) -> Unit
) {
LazyColumn(
modifier = Modifier.width(500.dp)
) {
items(wallets) { wallet ->
WalletCard(wallet)
}
item {
AddWalletBox(onAdd)
}
}
}
@Composable
fun WalletCard(wallet: Wallet) {
var expanded by rememberSaveable { mutableStateOf(false) }
val balance by wallet.balance.collectAsState()
Button(
onClick = {
expanded = !expanded
},
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 2.dp),
colors = ButtonDefaults.buttonColors().copy(
containerColor = CardDefaults.cardColors().containerColor,
contentColor = CardDefaults.cardColors().contentColor
),
contentPadding = PaddingValues(8.dp, 2.dp)
) {
Column {
AnimatedVisibility(visible = !expanded) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 6.dp)
) {
Text(wallet.name)
Spacer(modifier = Modifier
.fillMaxWidth()
.weight(1f))
Text("%.2f zł".format(balance / 100.0))
}
}
AnimatedVisibility(visible = expanded) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(wallet.name)
Text(
text = "%.2f zł".format(balance / 100.0),
fontSize = 32.sp
)
UnitButtons(
onClick = {
wallet.fund(it)
}
)
}
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun UnitButtons(
onClick: (Int) -> Unit
) {
val increments = listOf(1, 2, 5, 10, 20, 50, 100, 200, 500)
var add by rememberSaveable { mutableStateOf(true) }
Row { Row {
TextButton( Column(
onClick = { add = false },
modifier = Modifier modifier = Modifier
.size(48.dp) .fillMaxSize()
.aspectRatio(1f), .weight(0.5f),
border = if (!add) BorderStroke(1.dp, Color.Red) else null, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Column(
text = "-", modifier = Modifier.clip(CircleShape).background(if (multiplier.intValue == 1) Color.Green else Color.Red)
fontSize = 24.sp ) {
) Button(
} onClick = { multiplier.intValue = -1 },
Spacer(Modifier.width(5.dp))
TextButton(
onClick = { add = true },
modifier = Modifier modifier = Modifier
.size(48.dp) .size(64.dp)
.aspectRatio(1f), .aspectRatio(1f)
border = if (add) BorderStroke(1.dp, Color.Green) else null, .alpha(if (multiplier.intValue == -1) 1f else 0.7f),
) { contentPadding = PaddingValues(0.dp),
Text( colors = ButtonDefaults.buttonColors(
text = "+", containerColor = Color.Red,
fontSize = 24.sp contentColor = Color.White
) )
) {
Text(text = "-", fontSize = 24.sp)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { multiplier.intValue = 1 },
modifier = Modifier
.size(64.dp)
.aspectRatio(1f)
.alpha(if (multiplier.intValue == 1) 1f else 0.7f),
contentPadding = PaddingValues(0.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Green,
contentColor = Color.Black
)
) {
Text(text = "+", fontSize = 24.sp)
}
}
}
Column(
modifier = Modifier
.fillMaxHeight()
.weight(1f),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 64.dp),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(increments) { increment ->
Button(
onClick = {
balanceState.intValue += increment * multiplier.intValue
scope.launch {
println("Saved")
setBalance(balanceState.intValue)
}
},
modifier = Modifier
.aspectRatio(1f),
contentPadding = PaddingValues(0.dp),
colors = ButtonDefaults.buttonColors(containerColor = currencyColor(increment), contentColor = Color.Black),
) {
CurrencyLabel(value = increment)
} }
} }
FlowRow {
increments.forEach {
TextButton(onClick = {
onClick(if (add) it else -it)
}) {
Text(formatCurrency(it))
} }
} }
} }
} }
fun formatCurrency(units: Int): String { fun currencyColor(value: Int): Color {
if (units < 100) { return if (value < 10) {
return "$units gr" Color(244,164,96)
} } else if (value < 200) {
if (units % 100 == 0) { Color(226, 226, 255)
return "%d zł".format(units / 100) } else
} Color(240, 230, 140)
return "%.2f zł".format(units / 100.0)
} }
@Composable
fun CurrencyLabel(value: Int) {
Row(verticalAlignment = Alignment.Bottom) {
Text(text = (if (value >= 100) value / 100 else value).toString(), fontSize = 18.sp)
Text(text = if (value < 100) "gr" else "", fontSize = 10.sp, lineHeight = 4.sp)
}
}

View file

@ -1,66 +0,0 @@
package eu.m724.coincounter
import android.app.Application
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class MainViewModel(
application: Application
) : AndroidViewModel(application) {
private val _totalBalance: MutableStateFlow<Int> = MutableStateFlow(0)
val totalBalance: StateFlow<Int> = _totalBalance.asStateFlow()
private val _wallets: MutableStateFlow<List<Wallet>> = MutableStateFlow(listOf())
val wallets: StateFlow<List<Wallet>> = _wallets.asStateFlow()
private val dataPreferences = application.getSharedPreferences("data", Application.MODE_PRIVATE)
private val walletsPreferences = application.getSharedPreferences("wallets", Application.MODE_PRIVATE)
private var highestId = 0
fun init() {
val savedWallets = dataPreferences.getStringSet("walletIds", null)
if (savedWallets != null) {
val savedWalletIds = savedWallets.map { it.toInt() }.sorted()
val w = arrayListOf<Wallet>()
savedWalletIds.forEach { id ->
val wallet = Wallet.fromPreferences(id, walletsPreferences) { wa, mo -> onWalletUpdate(wa, mo) }
w.add(wallet)
_totalBalance.value += wallet.balance.value
if (wallet.id > highestId)
highestId = wallet.id
}
_wallets.value = w
} else {
_wallets.value = listOf(Wallet.default { wa, mo -> onWalletUpdate(wa, mo) })
_totalBalance.value = 2000
}
}
fun saveAll() {
dataPreferences.edit(true) {
putStringSet("walletIds", wallets.value.map { it.id.toString() }.toSet())
}
walletsPreferences.edit(true) {
wallets.value.forEach {
it.toPreferences(this)
}
}
}
fun addWallet(name: String) {
_wallets.value += Wallet(++highestId, name, 0) { wa, mo -> onWalletUpdate(wa, mo) }
}
private fun onWalletUpdate(wallet: Wallet, money: Int) {
_totalBalance.value += money
}
}

View file

@ -1,41 +0,0 @@
package eu.m724.coincounter
import android.content.SharedPreferences
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class Wallet(
val id: Int,
val name: String,
balance: Int,
val onUpdate: (Wallet, Int) -> Unit
) {
companion object {
fun default(onUpdate: (Wallet, Int) -> Unit): Wallet {
return Wallet(0, "Primary", 2000, onUpdate)
}
fun fromPreferences(id: Int, sharedPreferences: SharedPreferences, onUpdate: (Wallet, Int) -> Unit): Wallet {
return Wallet(
id,
sharedPreferences.getString("${id}_name", "Wallet")!!,
sharedPreferences.getInt("${id}_balance", 0),
onUpdate
)
}
}
fun toPreferences(editor: SharedPreferences.Editor) {
editor.putString("${id}_name", name)
editor.putInt("${id}_balance", balance.value)
}
private val _balance: MutableStateFlow<Int> = MutableStateFlow(balance)
val balance: StateFlow<Int> = _balance.asStateFlow()
fun fund(money: Int) {
_balance.value += money
onUpdate(this, money)
}
}

View file

@ -1,4 +1,3 @@
<resources> <resources>
<string name="app_name">Coin Counter</string> <string name="app_name">Coin Counter</string>
<string name="title_activity_main">MainActivity</string>
</resources> </resources>

View file

@ -1,6 +1,6 @@
[versions] [versions]
agp = "8.5.1" agp = "8.5.1"
datastore = "1.1.1" datastorePreferences = "1.1.1"
kotlin = "1.9.0" kotlin = "1.9.0"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"
@ -12,7 +12,7 @@ composeBom = "2024.04.01"
[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" }
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }