lost motivation :(
This commit is contained in:
Minecon724 2024-09-30 19:10:36 +02:00
parent 7f9eccba34
commit 97f1905179
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
17 changed files with 651 additions and 422 deletions

View file

@ -69,6 +69,28 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
@ -91,6 +113,17 @@
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="id" value="e1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
@ -102,6 +135,17 @@
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
@ -146,6 +190,17 @@
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
@ -235,6 +290,17 @@
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />

View file

@ -15,7 +15,7 @@
android:theme="@style/Theme.CoinCounter"
tools:targetApi="35">
<activity
android:name=".WalletActivity"
android:name=".wallet.WalletActivity"
android:exported="false"
android:label="@string/title_activity_wallet"
android:theme="@style/Theme.CoinCounter" />

View file

@ -53,8 +53,10 @@ 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()

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import eu.m724.coincounter.data.entity.Wallet
import eu.m724.coincounter.wallet.WalletRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow

View file

@ -5,7 +5,6 @@ import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
}

View file

@ -1,399 +0,0 @@
package eu.m724.coincounter
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.gestures.detectTapGestures
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Create
import androidx.compose.material3.Card
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import dagger.hilt.android.AndroidEntryPoint
import eu.m724.coincounter.ui.theme.CoinCounterTheme
import java.text.DateFormat
import java.util.Date
import java.util.Locale
@AndroidEntryPoint
class WalletActivity : ComponentActivity() {
private val viewModel: WalletViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val walletId = intent.getLongExtra("walletId", 0)
viewModel.init(walletId)
enableEdgeToEdge()
setContent {
CoinCounterTheme {
var transactionDialogShown by rememberSaveable { mutableStateOf(false) }
if (transactionDialogShown) {
TransactionDialog(
onDismiss = { transactionDialogShown = false },
onConfirm = { label, value ->
transactionDialogShown = false
viewModel.transact(value, label)
}
)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
floatingActionButton = {
FloatingActionButton(onClick = {
transactionDialogShown = true
}) {
Icon(Icons.Filled.Create, "New transaction")
}
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
App(
viewModel = viewModel,
finish = { finish() }
)
}
}
}
}
}
}
@Composable
fun App(
viewModel: WalletViewModel,
finish: () -> Unit
) {
val walletState by viewModel.wallet.collectAsState(initial = null)
val wallet = walletState
var actionsVisible by rememberSaveable { mutableStateOf(false) }
var renameVisible by rememberSaveable { mutableStateOf(false) }
if (actionsVisible) {
ActionsDialog(
onRename = {
actionsVisible = false
renameVisible = true
},
onDismiss = {
actionsVisible = false
},
onDelete = {
viewModel.delete()
finish()
}
)
}
if (renameVisible) {
RenameDialog(
name = wallet!!.label,
onDismiss = { renameVisible = false },
onRename = {
viewModel.rename(it)
wallet.label = it
renameVisible = false
}
)
}
if (wallet != null) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(50.dp))
Text(
text = wallet.label,
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
actionsVisible = true
}
)
}
)
Spacer(modifier = Modifier.height(4.dp))
Card {
Row(
modifier = Modifier.padding(16.dp, 8.dp),
verticalAlignment = Alignment.Bottom,
) {
Text(
text = "%.2f".format(wallet.balance / 100.0),
fontSize = 32.sp
)
Text(
text = BuildConfig.CURRENCY_UNIT,
fontSize = 14.sp
)
}
}
Spacer(modifier = Modifier.height(50.dp))
TransactionList(viewModel = viewModel)
}
}
}
@Composable
fun TransactionList(viewModel: WalletViewModel) {
val transactions by viewModel.transactions.collectAsState(initial = listOf())
LazyColumn {
items(transactions) {
Card(
modifier = Modifier
.width(250.dp)
.padding(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp, 2.dp),
horizontalArrangement = Arrangement.Center
) {
Text(it.label ?: it.id.toString())
Spacer(modifier = Modifier
.fillMaxWidth()
.weight(1f))
Text(
text = "%.2f %s".format(it.value / 100.0, BuildConfig.CURRENCY_UNIT),
color = if (it.value < 0) MaterialTheme.colorScheme.error else Color.Unspecified
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault())
.format(Date(it.timestamp))
)
}
}
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun TransactionDialog(
onDismiss: () -> Unit,
onConfirm: (String, Int) -> Unit
) {
var label by rememberSaveable { mutableStateOf("") }
var value by rememberSaveable { mutableStateOf("") }
val (firstFocus, secondFocus) = remember { FocusRequester.createRefs() }
LaunchedEffect(Unit) {
firstFocus.requestFocus()
}
Dialog(
onDismissRequest = {
label = ""
onDismiss()
}
) {
Card(
modifier = Modifier
.fillMaxWidth(),
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row {
TextField(
value = label,
onValueChange = { label = it },
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.focusRequester(firstFocus)
.focusProperties { next = secondFocus },
supportingText = { Text("Label") },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
singleLine = true
)
Spacer(modifier = Modifier.width(6.dp))
TextField(
value = value,
onValueChange = { value = it },
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.focusRequester(secondFocus),
supportingText = { Text("Value") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
singleLine = true
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = {
label = ""
onDismiss()
}
) {
Text(text = "Cancel")
}
TextButton(
onClick = {
onConfirm(label, (value.toDouble() * 100).toInt())
}
) {
Text("Create transaction")
}
}
}
}
}
}
@Composable
fun RenameDialog(
name: String,
onDismiss: () -> Unit,
onRename: (String) -> Unit
) {
var value by rememberSaveable { mutableStateOf(name) }
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Dialog(
onDismissRequest = {
onDismiss()
}
) {
Card {
Column(
modifier = Modifier.padding(16.dp)
) {
Row {
TextField(
value = value,
onValueChange = { value = it },
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.focusRequester(focusRequester),
supportingText = { Text("New name") },
singleLine = true
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = {
value = name
onDismiss()
}
) {
Text(text = "Cancel")
}
TextButton(
onClick = {
onRename(value)
}
) {
Text("Rename")
}
}
}
}
}
}
@Composable
fun ActionsDialog(
onRename: () -> Unit,
onDismiss: () -> Unit,
onDelete: () -> Unit
) {
Dialog(
onDismissRequest = {
onDismiss()
}
) {
Card(
modifier = Modifier
.fillMaxWidth(),
) {
Column(
modifier = Modifier.padding(16.dp)
) {
TextButton(onClick = onRename) {
Text("Rename wallet")
}
TextButton(onClick = onDelete) {
Text("Delete wallet")
}
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
}
}
}

View file

@ -13,14 +13,14 @@ interface TransactionDao {
fun getWalletTransactions(walletId: Long, after: Long = 0, limit: Int = 10): Flow<List<Transaction>>
@Insert
fun insertTransaction(transaction: Transaction)
suspend fun insertTransaction(transaction: Transaction)
@Insert
fun insertTransactions(transactions: List<Transaction>)
suspend fun insertTransactions(transactions: List<Transaction>)
@Delete
fun deleteTransaction(transaction: Transaction)
suspend fun deleteTransaction(transaction: Transaction)
@Insert
fun deleteTransactions(transactions: List<Transaction>)
suspend fun deleteTransactions(transactions: List<Transaction>)
}

View file

@ -23,14 +23,14 @@ interface WalletDao {
suspend fun insertWallets(wallets: List<Wallet>): LongArray
@Update
fun updateWallet(wallet: Wallet)
suspend fun updateWallet(wallet: Wallet)
@Update
fun updateWallets(wallets: List<Wallet>)
suspend fun updateWallets(wallets: List<Wallet>)
@Delete
fun deleteWallet(wallet: Wallet)
suspend fun deleteWallet(wallet: Wallet)
@Delete
fun deleteWallets(wallet: List<Wallet>)
suspend fun deleteWallets(wallet: List<Wallet>)
}

View file

@ -0,0 +1,71 @@
package eu.m724.coincounter.wallet
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.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Create
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import dagger.hilt.android.AndroidEntryPoint
import eu.m724.coincounter.ui.theme.CoinCounterTheme
import eu.m724.coincounter.wallet.compose.TransactionDialog
import eu.m724.coincounter.wallet.compose.WalletActivityView
@AndroidEntryPoint
class WalletActivity : ComponentActivity() {
private val viewModel: WalletViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val walletId = intent.getLongExtra("walletId", 0)
viewModel.init(walletId)
enableEdgeToEdge()
setContent {
CoinCounterTheme {
var transactionDialogShown by rememberSaveable { mutableStateOf(false) }
if (transactionDialogShown) {
TransactionDialog(
onDismiss = { transactionDialogShown = false },
onConfirm = { label, value, absolute ->
transactionDialogShown = false
viewModel.transact(value, absolute, label)
}
)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
floatingActionButton = {
FloatingActionButton(onClick = {
transactionDialogShown = true
}) {
Icon(Icons.Filled.Create, "New transaction")
}
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
WalletActivityView(
viewModel = viewModel,
finish = { finish() }
)
}
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package eu.m724.coincounter
package eu.m724.coincounter.wallet
import android.content.Context
import androidx.room.Room
@ -28,10 +28,10 @@ class WalletRepository @Inject constructor(
fun getWalletById(id: Long): Flow<Wallet> = walletDao.getWalletById(id)
suspend fun insertWallet(wallet: Wallet): Long = walletDao.insertWallet(wallet)
fun updateWallet(wallet: Wallet) = walletDao.updateWallet(wallet)
fun deleteWallet(wallet: Wallet) = walletDao.deleteWallet(wallet)
suspend fun updateWallet(wallet: Wallet) = walletDao.updateWallet(wallet)
suspend fun deleteWallet(wallet: Wallet) = walletDao.deleteWallet(wallet)
fun getWalletTransactions(walletId: Long) = transactionDao.getWalletTransactions(walletId)
fun insertTransaction(transaction: Transaction) = transactionDao.insertTransaction(transaction)
suspend fun insertTransaction(transaction: Transaction) = transactionDao.insertTransaction(transaction)
}

View file

@ -1,4 +1,4 @@
package eu.m724.coincounter
package eu.m724.coincounter.wallet
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -45,21 +45,24 @@ class WalletViewModel @Inject constructor(
}
}
fun transact(value: Int, label: String?) {
val transaction = Transaction(
walletId = walletId,
value = value,
label = label
)
fun transact(value: Int, absolute: Boolean, label: String?) {
viewModelScope.launch(Dispatchers.IO) {
val wallet = wallet.first()
val transaction = Transaction(
walletId = walletId,
value = if (!absolute) value else value - wallet.balance,
label = label
)
repository.insertTransaction(transaction)
wallet.let {
repository.updateWallet(
wallet.copy(balance = wallet.balance + value)
wallet.copy(balance = wallet.balance + transaction.value)
)
}
repository.insertTransaction(transaction)
}
}
}

View file

@ -0,0 +1,169 @@
package eu.m724.coincounter.wallet.compose
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
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.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
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.R
/**
* A dialog that's basically transaction creator
*
* @param onDismiss called on dialog dismiss
* @param onConfirm on confirmation, contains transaction name and value in cents, and if the value is absolute
*/
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun TransactionDialog(
onDismiss: () -> Unit,
onConfirm: (String, Int, Boolean) -> Unit
) {
var label by rememberSaveable { mutableStateOf("") }
var value by rememberSaveable { mutableStateOf("0.00") }
var absoluteChecked by rememberSaveable { mutableStateOf(false) }
val (firstFocus, secondFocus) = remember { FocusRequester.createRefs() }
val context = LocalContext.current
var valueValid by rememberSaveable { mutableStateOf(true) }
LaunchedEffect(Unit) {
firstFocus.requestFocus()
}
Dialog(
onDismissRequest = {
label = ""
onDismiss()
}
) {
Card(
modifier = Modifier
.fillMaxWidth(),
) {
Column(
modifier = Modifier
.padding(16.dp)
.height(IntrinsicSize.Min)
) {
TextField(
value = label,
onValueChange = { label = it },
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.focusRequester(firstFocus)
.focusProperties { next = secondFocus },
supportingText = { Text("Label") },
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next
),
singleLine = true
)
Row {
TextField(
value = value,
onValueChange = {
value = it
valueValid = value.toDoubleOrNull() != null
},
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.focusRequester(secondFocus),
supportingText = {
Text(
text = "Value",
color = if (!valueValid) MaterialTheme.colorScheme.error else Color.Unspecified
)
},
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Done
),
isError = !valueValid,
trailingIcon = {
if (!valueValid)
Icon(painterResource(id = R.drawable.baseline_error_24), "error", tint = MaterialTheme.colorScheme.error)
},
singleLine = true
)
Spacer(modifier = Modifier.width(10.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Switch(
checked = absoluteChecked,
onCheckedChange = { absoluteChecked = it }
)
Text(text = "Absolute")
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = {
label = ""
onDismiss()
}
) {
Text(text = "Cancel")
}
TextButton(
onClick = {
val doubleValue = value.toDoubleOrNull()
if (doubleValue != null) {
onConfirm(label, (doubleValue * 100).toInt(), absoluteChecked) // TODO handle fixed point
} else {
Toast.makeText(context, "Value is not a number", Toast.LENGTH_SHORT).show()
}
}
) {
Text("Create transaction")
}
}
}
}
}
}
@Preview
@Composable
fun TransactionDialogPreview() {
TransactionDialog(onDismiss = { }, onConfirm = { _, _, _ -> })
}

View file

@ -0,0 +1,73 @@
package eu.m724.coincounter.wallet.compose
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.wallet.WalletViewModel
import java.text.DateFormat
import java.util.Date
import java.util.Locale
/**
* A list of transactions for this wallet
*
* @param viewModel the view model
*/
@Composable
fun TransactionList(
viewModel: WalletViewModel
) {
val transactions by viewModel.transactions.collectAsState(initial = listOf())
LazyColumn {
items(transactions) { transaction ->
Card(
modifier = Modifier
.width(250.dp)
.padding(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp, 2.dp),
horizontalArrangement = Arrangement.Center
) {
Text(transaction.label ?: transaction.id.toString())
Spacer(modifier = Modifier
.fillMaxWidth()
.weight(1f))
Text(
text = "%.2f %s".format(transaction.value / 100.0, BuildConfig.CURRENCY_UNIT),
color = if (transaction.value < 0) MaterialTheme.colorScheme.error else Color.Unspecified
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault())
.format(Date(transaction.timestamp))
)
}
}
}
}
}

View file

@ -0,0 +1,51 @@
package eu.m724.coincounter.wallet.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
/**
* A dialog that's shown when you long click on wallet name
*
* @param onDismiss called on dismiss
* @param onRename called on wallet rename request
* @param onDelete called on wallet delete request
*/
@Composable
fun ActionsDialog(
onDismiss: () -> Unit,
onRename: () -> Unit,
onDelete: () -> Unit
) {
Dialog(
onDismissRequest = {
onDismiss()
}
) {
Card(
modifier = Modifier
.fillMaxWidth(),
) {
Column(
modifier = Modifier.padding(16.dp)
) {
TextButton(onClick = onRename) {
Text("Rename wallet")
}
TextButton(onClick = onDelete) {
Text("Delete wallet")
}
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
}
}
}

View file

@ -0,0 +1,106 @@
package eu.m724.coincounter.wallet.compose
import androidx.compose.foundation.gestures.detectTapGestures
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.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.m724.coincounter.BuildConfig
import eu.m724.coincounter.wallet.WalletViewModel
@Composable
fun WalletActivityView(
viewModel: WalletViewModel,
finish: () -> Unit
) {
val walletState by viewModel.wallet.collectAsState(initial = null)
val wallet = walletState
var actionsVisible by rememberSaveable { mutableStateOf(false) }
var renameVisible by rememberSaveable { mutableStateOf(false) }
if (actionsVisible) {
ActionsDialog(
onRename = {
actionsVisible = false
renameVisible = true
},
onDismiss = {
actionsVisible = false
},
onDelete = {
viewModel.delete()
finish()
}
)
}
if (renameVisible) {
RenameDialog(
name = wallet!!.label,
onDismiss = { renameVisible = false },
onRename = {
viewModel.rename(it)
wallet.label = it
renameVisible = false
}
)
}
if (wallet != null) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(50.dp))
Text(
text = wallet.label,
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
actionsVisible = true
}
)
}
)
Spacer(modifier = Modifier.height(4.dp))
Card {
Row(
modifier = Modifier.padding(16.dp, 8.dp),
verticalAlignment = Alignment.Bottom,
) {
Text(
text = "%.2f".format(wallet.balance / 100.0),
fontSize = 32.sp
)
Text(
text = BuildConfig.CURRENCY_UNIT,
fontSize = 14.sp
)
}
}
Spacer(modifier = Modifier.height(50.dp))
TransactionList(
viewModel = viewModel
)
}
}
}

View file

@ -0,0 +1,82 @@
package eu.m724.coincounter.wallet.compose
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.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@Composable
fun RenameDialog(
name: String,
onDismiss: () -> Unit,
onRename: (String) -> Unit
) {
var value by rememberSaveable { mutableStateOf(name) }
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Dialog(
onDismissRequest = {
onDismiss()
}
) {
Card {
Column(
modifier = Modifier.padding(16.dp)
) {
Row {
TextField(
value = value,
onValueChange = { value = it },
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.focusRequester(focusRequester),
supportingText = { Text("New name") },
singleLine = true
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = {
value = name
onDismiss()
}
) {
Text(text = "Cancel")
}
TextButton(
onClick = {
onRename(value)
}
) {
Text("Rename")
}
}
}
}
}
}

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>