diff --git a/.idea/other.xml b/.idea/other.xml
index 0d3a1fb..4604c44 100644
--- a/.idea/other.xml
+++ b/.idea/other.xml
@@ -179,17 +179,6 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3953a16..22f5a9b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -5,14 +5,14 @@ plugins {
android {
namespace = "eu.m724.coincounter"
- compileSdk = 34
+ compileSdk = 35
defaultConfig {
applicationId = "eu.m724.coincounter"
- minSdk = 34
- targetSdk = 34
+ minSdk = 30
+ targetSdk = 35
versionCode = 3
- versionName = "1.1.1"
+ versionName = "2.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -59,7 +59,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
- implementation(libs.androidx.datastore.preferences)
+ implementation(libs.androidx.datastore)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0fe640b..804086b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,18 +5,18 @@
diff --git a/app/src/main/java/eu/m724/coincounter/MainActivity.kt b/app/src/main/java/eu/m724/coincounter/MainActivity.kt
index 61c4fde..a493a64 100644
--- a/app/src/main/java/eu/m724/coincounter/MainActivity.kt
+++ b/app/src/main/java/eu/m724/coincounter/MainActivity.kt
@@ -1,237 +1,297 @@
package eu.m724.coincounter
-import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.background
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
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.getValue
-import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.Shape
-import androidx.compose.ui.unit.FontScaling
-import androidx.compose.ui.unit.FontScalingLinear
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
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 kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-
class MainActivity : ComponentActivity() {
- val Context.dataStore by preferencesDataStore(name = "preferences")
+ private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val increments = listOf(1, 2, 5, 10, 20, 50, 100, 200, 500)
+ viewModel = MainViewModel(application)
+ viewModel.init()
enableEdgeToEdge()
setContent {
- AppView(increments, dataStore)
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun AppView(increments: List, dataStore: DataStore) {
- val BALANCE_KEY = intPreferencesKey("balance")
-
- val balanceFlow: Flow =
- 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 {
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Column(modifier = Modifier
- .fillMaxSize()
- .padding(innerPadding)) {
- Box(modifier = Modifier
- .fillMaxWidth()
- .weight(.25f),
- contentAlignment = Alignment.Center) {
- StatusPart(balanceState)
- }
- Column(modifier = Modifier
- .fillMaxWidth()
- .weight(1f),
- verticalArrangement = Arrangement.Center) {
- ControlPart(increments, balanceState, dataStore)
- }
- }
-
- }
- }
-}
-
-@Composable
-fun StatusPart(balanceState: MutableState) {
- val value by balanceState.asIntState()
- Row(verticalAlignment = Alignment.Bottom) {
- Text(
- text = "%.2f".format(value / 100.0),
- fontSize = 48.sp
- )
- Text(
- text = "zł",
- fontSize = 32.sp,
- modifier = Modifier.alpha(.7f))
- }
-}
-
-@Composable
-fun ControlPart(increments: List, balanceState: MutableIntState, dataStore: DataStore) {
- val multiplier = remember { mutableIntStateOf(1) }
- val scope = rememberCoroutineScope()
- val BALANCE_KEY = intPreferencesKey("balance")
-
- suspend fun setBalance(value: Int) {
- dataStore.edit { preferences ->
- preferences[BALANCE_KEY] = value
- }
- }
-
- Row {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .weight(0.5f),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Column(
- modifier = Modifier.clip(CircleShape).background(if (multiplier.intValue == 1) Color.Green else Color.Red)
- ) {
- 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.Red,
- 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),
+ CoinCounterTheme {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ Box(
+ modifier = Modifier.padding(innerPadding)
) {
- CurrencyLabel(value = increment)
+ App(viewModel)
}
}
}
}
}
-}
-fun currencyColor(value: Int): Color {
- return if (value < 10) {
- Color(244,164,96)
- } else if (value < 200) {
- Color(226, 226, 255)
- } else
- Color(240, 230, 140)
-}
-
-@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 "zł", fontSize = 10.sp, lineHeight = 4.sp)
+ override fun onStop() {
+ super.onStop()
+ viewModel.saveAll()
}
}
+@Composable
+fun App(viewModel: MainViewModel) {
+ val total by viewModel.totalBalance.collectAsState()
+ val wallets by viewModel.wallets.collectAsState()
+
+ 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 = "%.2f".format(balance / 100.0),
+ fontSize = 32.sp
+ )
+ Text(
+ text = "zł",
+ fontSize = 14.sp
+ )
+ }
+ }
+}
+
+@Composable
+fun AddWalletBox(
+ onAdd: (String) -> Unit
+) {
+ var name by remember { mutableStateOf("") }
+
+ Box(
+ modifier = Modifier.fillMaxWidth(),
+ contentAlignment = Alignment.CenterEnd
+ ) {
+ 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,
+ 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 {
+ TextButton(
+ onClick = { add = false },
+ modifier = Modifier
+ .size(48.dp)
+ .aspectRatio(1f),
+ border = if (!add) BorderStroke(1.dp, Color.Red) else null,
+ ) {
+ Text(
+ text = "-",
+ fontSize = 24.sp
+ )
+ }
+ Spacer(Modifier.width(5.dp))
+ TextButton(
+ onClick = { add = true },
+ modifier = Modifier
+ .size(48.dp)
+ .aspectRatio(1f),
+ border = if (add) BorderStroke(1.dp, Color.Green) else null,
+ ) {
+ Text(
+ text = "+",
+ fontSize = 24.sp
+ )
+ }
+ }
+ FlowRow {
+ increments.forEach {
+ TextButton(onClick = {
+ onClick(if (add) it else -it)
+ }) {
+ Text(formatCurrency(it))
+ }
+ }
+ }
+}
+
+fun formatCurrency(units: Int): String {
+ if (units < 100) {
+ return "$units gr"
+ }
+ if (units % 100 == 0) {
+ return "%d zł".format(units / 100)
+ }
+ return "%.2f zł".format(units / 100.0)
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/MainViewModel.kt b/app/src/main/java/eu/m724/coincounter/MainViewModel.kt
new file mode 100644
index 0000000..5884569
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/MainViewModel.kt
@@ -0,0 +1,66 @@
+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 = MutableStateFlow(0)
+ val totalBalance: StateFlow = _totalBalance.asStateFlow()
+
+ private val _wallets: MutableStateFlow> = MutableStateFlow(listOf())
+ val wallets: StateFlow> = _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()
+
+ 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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/Wallet.kt b/app/src/main/java/eu/m724/coincounter/Wallet.kt
new file mode 100644
index 0000000..e548b2b
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/Wallet.kt
@@ -0,0 +1,41 @@
+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 = MutableStateFlow(balance)
+ val balance: StateFlow = _balance.asStateFlow()
+
+ fun fund(money: Int) {
+ _balance.value += money
+ onUpdate(this, money)
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5326c53..a915b7b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
Coin Counter
+ MainActivity
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e4ab77a..1d6d8b3 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
agp = "8.5.1"
-datastorePreferences = "1.1.1"
+datastore = "1.1.1"
kotlin = "1.9.0"
coreKtx = "1.10.1"
junit = "4.13.2"
@@ -12,7 +12,7 @@ composeBom = "2024.04.01"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
-androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
+androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }