new version

This commit is contained in:
Minecon724 2024-08-08 16:59:33 +02:00
parent 74bb305253
commit a7c9957512
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
8 changed files with 362 additions and 205 deletions

View file

@ -179,17 +179,6 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</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>
<option name="api" value="34" />
<option name="brand" value="samsung" />

View file

@ -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)

View file

@ -5,18 +5,18 @@
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
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.CoinCounter"
android:enableOnBackInvokedCallback="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:label="@string/title_activity_main"
android:theme="@style/Theme.CoinCounter">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -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<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 {
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<Int>) {
val value by balanceState.asIntState()
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = "%.2f".format(value / 100.0),
fontSize = 48.sp
)
Text(
text = "",
fontSize = 32.sp,
modifier = Modifier.alpha(.7f))
}
}
@Composable
fun ControlPart(increments: List<Int>, balanceState: MutableIntState, dataStore: DataStore<Preferences>) {
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 "", 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 = "",
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<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 {
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)
}

View file

@ -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<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

@ -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<Int> = MutableStateFlow(balance)
val balance: StateFlow<Int> = _balance.asStateFlow()
fun fund(money: Int) {
_balance.value += money
onUpdate(this, money)
}
}

View file

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

View file

@ -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" }