refactoring

This commit is contained in:
Minecon724 2024-10-02 17:46:59 +02:00
parent fb565c1a9d
commit dad71108d8
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
17 changed files with 394 additions and 297 deletions

View file

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">

View file

@ -25,6 +25,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="id" value="TB370FU" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />

View file

@ -23,9 +23,6 @@ android {
vectorDrawables {
useSupportLibrary = true
}
buildConfigField("String", "CURRENCY_UNIT", "\"zł\"")
buildConfigField("String", "CURRENCY_CENT", "\"gr\"")
}
buildTypes {

View file

@ -20,9 +20,9 @@
android:label="@string/title_activity_wallet"
android:theme="@style/Theme.CoinCounter" />
<activity
android:name=".MainActivity"
android:name=".home.HomeActivity"
android:exported="true"
android:label="@string/title_activity_main"
android:label="@string/title_activity_home"
android:theme="@style/Theme.CoinCounter">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -0,0 +1,54 @@
package eu.m724.coincounter
import android.icu.number.LocalizedNumberFormatter
import android.icu.number.Notation
import android.icu.number.NumberFormatter
import android.icu.number.Precision
import android.icu.util.Currency
import java.util.Locale
class CurrencyUtils {
companion object {
private val locale: Locale = Locale.getDefault() // TODO
private val currency: Currency = Currency.getInstance(locale)
private val currencyFormatter: LocalizedNumberFormatter = NumberFormatter
.withLocale(locale)
.notation(Notation.compactShort())
.unit(currency)
.precision(Precision.fixedFraction(2))!! // TODO too
private val numberFormatter: LocalizedNumberFormatter = NumberFormatter
.withLocale(locale) // TODO
.notation(Notation.compactShort())
.precision(Precision.fixedFraction(2))!! // TODO this too
/**
* Short currency symbol, like
*/
val currencySymbol: String = currency.symbol
/**
* Currency name, like euro
*/
val currencyName: String = currency.displayName
/**
* Formats a currency value, like 10,00
*
* @param units cents, 100 = 1 unit
*/
fun formatCurrency(units: Int): String {
return currencyFormatter.format(units / 100.0).toString()
}
/**
* Formats a number without currency unit, like 10,00
*
* @param units cents, 100 = 1 unit
*/
fun formatNoCurrency(units: Int): String {
return numberFormatter.unit(null).format(units / 100.0).toString()
}
}
}

View file

@ -1,279 +0,0 @@
package eu.m724.coincounter
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
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.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.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import eu.m724.coincounter.data.entity.Wallet
import eu.m724.coincounter.ui.theme.CoinCounterTheme
import eu.m724.coincounter.wallet.WalletActivity
import kotlinx.coroutines.launch
// TODO modularize
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
viewModel.openEvent.collect {
openWallet(it)
}
}
enableEdgeToEdge()
setContent {
CoinCounterTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box(
modifier = Modifier.padding(innerPadding)
) {
App(
viewModel = viewModel,
onClick = {
openWallet(it.id)
}
)
}
}
}
}
}
private fun openWallet(walletId: Long) {
val intent = Intent(application.applicationContext, WalletActivity::class.java)
intent.putExtra("walletId", walletId)
startActivity(intent)
}
}
@Composable
fun App(
viewModel: MainViewModel,
onClick: (Wallet) -> Unit
) {
val total by viewModel.totalBalance.collectAsState(initial = 0)
val wallets by viewModel.wallets.collectAsState(initial = listOf())
Column {
BalanceView(total)
WalletList(
wallets = wallets,
onClick = {
onClick(it)
},
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 = BuildConfig.CURRENCY_UNIT,
fontSize = 14.sp
)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun WalletList(
wallets: List<Wallet>,
onClick: (Wallet) -> Unit,
onAdd: (String) -> Unit
) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
maxItemsInEachRow = 3
) {
wallets.forEach { wallet ->
WalletCard(
wallet = wallet,
onClick = {
onClick(wallet)
}
)
}
AddWalletButton(
onAdd = onAdd
)
}
}
@Composable
fun WalletCard(
wallet: Wallet,
onClick: () -> Unit
) {
Button(
modifier = Modifier.padding(8.dp),
onClick = onClick,
colors = ButtonDefaults.buttonColors().copy(
containerColor = CardDefaults.cardColors().containerColor,
contentColor = CardDefaults.cardColors().contentColor
),
shape = RoundedCornerShape(30),
contentPadding = PaddingValues(16.dp, 4.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(wallet.label)
Text(formatCurrency(wallet.balance))
}
}
}
@Composable
fun AddWalletButton(
onAdd: (String) -> Unit
) {
var label by remember { mutableStateOf("") }
var expanded by remember { mutableStateOf(false) }
val angle by animateFloatAsState(
targetValue = if (expanded && label.isBlank()) 45f else 0f,
label = "Add button rotation"
)
val focusRequester = remember { FocusRequester() }
Card(
modifier = Modifier
.padding(8.dp)
.height(48.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(
visible = expanded
) {
BasicTextField(
value = label,
onValueChange = { label = it },
modifier = Modifier
.padding(start = 16.dp)
.focusRequester(focusRequester),
textStyle = LocalTextStyle.current,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
expanded = false
onAdd(label)
label = ""
}
),
singleLine = true
)
LaunchedEffect(expanded) {
focusRequester.requestFocus()
}
}
Button(
modifier = Modifier.fillMaxHeight(),
onClick = {
if (!expanded) {
expanded = true
} else {
if (label.isBlank()) {
expanded = false
} else {
onAdd(label)
expanded = false
}
label = ""
}
},
colors = ButtonDefaults.buttonColors().copy(
containerColor = CardDefaults.cardColors().containerColor,
contentColor = CardDefaults.cardColors().contentColor
),
shape = RoundedCornerShape(30),
contentPadding = PaddingValues(16.dp, 4.dp)
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = stringResource(R.string.home_add_wallet),
modifier = Modifier.rotate(angle)
)
}
}
}
}
fun formatCurrency(units: Int): String {
if (units < 100) {
return "$units ${BuildConfig.CURRENCY_CENT}"
}
return "%.2f %s".format(units / 100.0, BuildConfig.CURRENCY_UNIT)
}

View file

@ -0,0 +1,94 @@
package eu.m724.coincounter.home
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import eu.m724.coincounter.CurrencyUtils
import eu.m724.coincounter.home.compose.HomeActivityView
import eu.m724.coincounter.ui.theme.CoinCounterTheme
import eu.m724.coincounter.wallet.WalletActivity
import kotlinx.coroutines.launch
// TODO modularize
@AndroidEntryPoint
class HomeActivity : ComponentActivity() {
private val viewModel: HomeViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
viewModel.openEvent.collect {
openWallet(it)
}
}
enableEdgeToEdge()
setContent {
CoinCounterTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box(
modifier = Modifier.padding(innerPadding)
) {
HomeActivityView(
viewModel = viewModel,
onClick = {
openWallet(it.id)
}
)
}
}
}
}
}
private fun openWallet(walletId: Long) {
val intent = Intent(application.applicationContext, WalletActivity::class.java)
intent.putExtra("walletId", walletId)
startActivity(intent)
}
}
@Composable
fun BalanceView(balance: Int) {
Column(
modifier = Modifier
.height(150.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.Bottom
) {
Text(
text = CurrencyUtils.formatNoCurrency(balance),
fontSize = 32.sp
)
Text(
text = CurrencyUtils.currencySymbol,
fontSize = 14.sp
)
}
}
}

View file

@ -1,4 +1,4 @@
package eu.m724.coincounter
package eu.m724.coincounter.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -13,7 +13,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
class HomeViewModel @Inject constructor(
private val repository: WalletRepository
) : ViewModel() {
val wallets: Flow<List<Wallet>> = repository.getAllWallets()

View file

@ -0,0 +1,114 @@
package eu.m724.coincounter.home.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import eu.m724.coincounter.R
@Composable
fun CreateWalletButton(
onCreate: (String) -> Unit
) {
var label by remember { mutableStateOf("") }
var expanded by remember { mutableStateOf(false) }
val angle by animateFloatAsState(
targetValue = if (expanded && label.isBlank()) 45f else 0f,
label = "Add button rotation"
)
val focusRequester = remember { FocusRequester() }
Card(
modifier = Modifier
.padding(8.dp)
.height(48.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(
visible = expanded
) {
BasicTextField(
value = label,
onValueChange = { label = it },
modifier = Modifier
.padding(start = 16.dp)
.focusRequester(focusRequester),
textStyle = LocalTextStyle.current,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
expanded = false
onCreate(label)
label = ""
}
),
singleLine = true
)
LaunchedEffect(expanded) {
focusRequester.requestFocus()
}
}
Button(
modifier = Modifier.fillMaxHeight(),
onClick = {
if (!expanded) {
expanded = true
} else {
if (label.isBlank()) {
expanded = false
} else {
onCreate(label)
expanded = false
}
label = ""
}
},
colors = ButtonDefaults.buttonColors().copy(
containerColor = CardDefaults.cardColors().containerColor,
contentColor = CardDefaults.cardColors().contentColor
),
shape = RoundedCornerShape(30),
contentPadding = PaddingValues(16.dp, 4.dp)
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = stringResource(R.string.home_add_wallet),
modifier = Modifier.rotate(angle)
)
}
}
}
}

View file

@ -0,0 +1,31 @@
package eu.m724.coincounter.home.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import eu.m724.coincounter.data.entity.Wallet
import eu.m724.coincounter.home.BalanceView
import eu.m724.coincounter.home.HomeViewModel
@Composable
fun HomeActivityView(
viewModel: HomeViewModel,
onClick: (Wallet) -> Unit
) {
val total by viewModel.totalBalance.collectAsState(initial = 0)
val wallets by viewModel.wallets.collectAsState(initial = listOf())
Column {
BalanceView(total)
WalletList(
wallets = wallets,
onClick = {
onClick(it)
},
onCreate = {
viewModel.addWallet(it)
}
)
}
}

View file

@ -0,0 +1,40 @@
package eu.m724.coincounter.home.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.m724.coincounter.CurrencyUtils.Companion.formatCurrency
import eu.m724.coincounter.data.entity.Wallet
@Composable
fun WalletCard(
wallet: Wallet,
onClick: () -> Unit
) {
Button(
modifier = Modifier.padding(8.dp),
onClick = onClick,
colors = ButtonDefaults.buttonColors().copy(
containerColor = CardDefaults.cardColors().containerColor,
contentColor = CardDefaults.cardColors().contentColor
),
shape = RoundedCornerShape(30),
contentPadding = PaddingValues(16.dp, 4.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(wallet.label)
Text(formatCurrency(wallet.balance))
}
}
}

View file

@ -0,0 +1,35 @@
package eu.m724.coincounter.home.compose
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.m724.coincounter.data.entity.Wallet
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun WalletList(
wallets: List<Wallet>,
onClick: (Wallet) -> Unit,
onCreate: (String) -> Unit
) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
maxItemsInEachRow = 3
) {
wallets.forEach { wallet ->
WalletCard(
wallet = wallet,
onClick = {
onClick(wallet)
}
)
}
CreateWalletButton(
onCreate = onCreate
)
}
}

View file

@ -40,6 +40,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import eu.m724.coincounter.CurrencyUtils
import eu.m724.coincounter.R
/**
@ -107,7 +108,7 @@ fun TransactionDialog(
.focusRequester(secondFocus),
supportingText = {
Text(
text = stringResource(R.string.create_transaction_value),
text = stringResource(R.string.create_transaction_value, CurrencyUtils.currencyName),
color = if (!valueValid) MaterialTheme.colorScheme.error else Color.Unspecified
)
},

View file

@ -17,7 +17,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import eu.m724.coincounter.BuildConfig
import eu.m724.coincounter.CurrencyUtils
import eu.m724.coincounter.wallet.WalletViewModel
import java.text.DateFormat
import java.util.Date
@ -54,7 +54,7 @@ fun TransactionList(
.weight(1f))
Text(
text = "%.2f %s".format(transaction.value / 100.0, BuildConfig.CURRENCY_UNIT),
text = CurrencyUtils.formatCurrency(transaction.value),
color = if (transaction.value < 0) MaterialTheme.colorScheme.error else Color.Unspecified
)
}

View file

@ -20,7 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.m724.coincounter.BuildConfig
import eu.m724.coincounter.CurrencyUtils
import eu.m724.coincounter.wallet.WalletViewModel
@Composable
@ -86,11 +86,11 @@ fun WalletActivityView(
verticalAlignment = Alignment.Bottom,
) {
Text(
text = "%.2f".format(wallet.balance / 100.0),
text = CurrencyUtils.formatNoCurrency(wallet.balance),
fontSize = 32.sp
)
Text(
text = BuildConfig.CURRENCY_UNIT,
text = CurrencyUtils.currencySymbol,
fontSize = 14.sp
)
}

View file

@ -1,13 +1,13 @@
<resources>
<string name="app_name">Coin Counter</string>
<string name="title_activity_main">Coin Counter</string>
<string name="title_activity_home">Coin Counter</string>
<string name="title_activity_wallet">WalletActivity</string>
<string name="create_transaction_confirm">Utwórz transakcję</string>
<string name="create_transaction_nan">Wartość musi być liczbą</string>
<string name="create_transaction_cancel">Anuluj</string>
<string name="create_transaction_absolute">Bezwzględna</string>
<string name="create_transaction_value_error">Błąd</string>
<string name="create_transaction_value">Wartość</string>
<string name="create_transaction_value">Wartość (%1$s)</string>
<string name="create_transaction_label">Etykieta</string>
<string name="wallet_actions_rename">Zmień nazwę portfela</string>
<string name="wallet_actions_delete">Usuń portfel</string>

View file

@ -1,13 +1,13 @@
<resources>
<string name="app_name">Coin Counter</string>
<string name="title_activity_main">Coin Counter</string>
<string name="title_activity_home">Coin Counter</string>
<string name="title_activity_wallet">WalletActivity</string>
<string name="create_transaction_confirm">Create transaction</string>
<string name="create_transaction_nan">Value must be a number</string>
<string name="create_transaction_cancel">Cancel</string>
<string name="create_transaction_absolute">Absolute</string>
<string name="create_transaction_value_error">Error</string>
<string name="create_transaction_value">Value</string>
<string name="create_transaction_value">Value (%1$s)</string>
<string name="create_transaction_label">Label</string>
<string name="wallet_actions_rename">Rename wallet</string>
<string name="wallet_actions_delete">Delete wallet</string>