update
lost motivation :(
This commit is contained in:
parent
7f9eccba34
commit
97f1905179
17 changed files with 651 additions and 422 deletions
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,7 +5,6 @@ import dagger.hilt.android.HiltAndroidApp
|
|||
|
||||
@HiltAndroidApp
|
||||
class MyApplication : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>)
|
||||
}
|
|
@ -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>)
|
||||
}
|
|
@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = { _, _, _ -> })
|
||||
}
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
app/src/main/res/drawable/baseline_error_24.xml
Normal file
5
app/src/main/res/drawable/baseline_error_24.xml
Normal 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>
|
Loading…
Reference in a new issue