diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..813f6cc 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index fdf8d99..1dbfe25 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,13 @@
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 22f5a9b..a42c2f1 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,6 +1,11 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
+ alias(libs.plugins.devtools.ksp)
+ alias(libs.plugins.compose.compiler)
+
+ id("kotlin-kapt")
+ alias(libs.plugins.hilt.android)
}
android {
@@ -40,7 +45,7 @@ android {
compose = true
}
composeOptions {
- kotlinCompilerExtensionVersion = "1.5.1"
+ kotlinCompilerExtensionVersion = "1.5.15"
}
packaging {
resources {
@@ -50,7 +55,11 @@ android {
}
dependencies {
+ implementation(libs.hilt.android)
+ kapt(libs.hilt.android.compiler)
+
+ ksp(libs.androidx.room.compiler)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
@@ -59,7 +68,8 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
- implementation(libs.androidx.datastore)
+ implementation(libs.androidx.room.runtime)
+ implementation(libs.androidx.room.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -67,4 +77,9 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
-}
\ No newline at end of file
+}
+
+// Allow references to generated code
+kapt {
+ correctErrorTypes = true
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 804086b..f0dd42a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
+ tools:targetApi="35">
+
Unit
+) {
+ val total by viewModel.totalBalance.collectAsState(initial = 0)
+ val wallets by viewModel.wallets.collectAsState(initial = listOf())
Column {
BalanceView(total)
WalletList(
wallets = wallets,
- onAdd = { viewModel.addWallet(it) }
+ onClick = {
+ onClick(it)
+ },
+ onAdd = {
+ viewModel.addWallet(it)
+ }
)
}
}
@@ -117,170 +140,129 @@ fun BalanceView(balance: Int) {
}
}
-@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"
- )
- }
- }
- }
- }
-}
-
+@OptIn(ExperimentalLayoutApi::class)
@Composable
fun WalletList(
wallets: List,
+ onClick: (Wallet) -> Unit,
onAdd: (String) -> Unit
) {
- LazyColumn(
- modifier = Modifier.width(500.dp)
+ FlowRow(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ maxItemsInEachRow = 3
) {
- items(wallets) { wallet ->
- WalletCard(wallet)
- }
- item {
- AddWalletBox(onAdd)
+ wallets.forEach { wallet ->
+ WalletCard(
+ wallet = wallet,
+ onClick = {
+ onClick(wallet)
+ }
+ )
}
+ AddWalletButton(
+ onAdd = onAdd
+ )
}
}
@Composable
-fun WalletCard(wallet: Wallet) {
- var expanded by rememberSaveable { mutableStateOf(false) }
- val balance by wallet.balance.collectAsState()
-
+fun WalletCard(
+ wallet: Wallet,
+ onClick: () -> Unit
+) {
Button(
- onClick = {
- expanded = !expanded
- },
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp, 2.dp),
+ modifier = Modifier.padding(8.dp),
+ onClick = onClick,
colors = ButtonDefaults.buttonColors().copy(
containerColor = CardDefaults.cardColors().containerColor,
contentColor = CardDefaults.cardColors().contentColor
),
- contentPadding = PaddingValues(8.dp, 2.dp)
+ shape = RoundedCornerShape(30),
+ contentPadding = PaddingValues(16.dp, 4.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)
- }
- )
- }
- }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(wallet.label)
+ Text(formatCurrency(wallet.balance))
}
}
}
-@OptIn(ExperimentalLayoutApi::class)
@Composable
-fun UnitButtons(
- onClick: (Int) -> Unit
+fun AddWalletButton(
+ onAdd: (String) -> Unit
) {
- val increments = listOf(1, 2, 5, 10, 20, 50, 100, 200, 500)
- var add by rememberSaveable { mutableStateOf(true) }
+ 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() }
- Row {
- TextButton(
- onClick = { add = false },
- modifier = Modifier
- .size(48.dp)
- .aspectRatio(1f),
- border = if (!add) BorderStroke(1.dp, Color.Red) else null,
+ Card(
+ modifier = Modifier
+ .padding(8.dp)
+ .height(48.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
) {
- 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,
+ AnimatedVisibility(
+ visible = expanded
) {
- Text(
- text = "+",
- fontSize = 24.sp
- )
- }
- }
- FlowRow {
- increments.forEach {
- TextButton(onClick = {
- onClick(if (add) it else -it)
- }) {
- Text(formatCurrency(it))
+ 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 = "Add wallet",
+ modifier = Modifier.rotate(angle)
+ )
}
}
}
diff --git a/app/src/main/java/eu/m724/coincounter/MainViewModel.kt b/app/src/main/java/eu/m724/coincounter/MainViewModel.kt
index 5884569..eeb11ec 100644
--- a/app/src/main/java/eu/m724/coincounter/MainViewModel.kt
+++ b/app/src/main/java/eu/m724/coincounter/MainViewModel.kt
@@ -1,66 +1,34 @@
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
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import eu.m724.coincounter.data.entity.Wallet
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import javax.inject.Inject
-class MainViewModel(
- application: Application
-) : AndroidViewModel(application) {
- private val _totalBalance: MutableStateFlow = MutableStateFlow(0)
- val totalBalance: StateFlow = _totalBalance.asStateFlow()
+@HiltViewModel
+class MainViewModel @Inject constructor(
+ private val repository: WalletRepository
+) : ViewModel() {
+ val wallets: Flow> = repository.getAllWallets()
- private val _wallets: MutableStateFlow> = MutableStateFlow(listOf())
- val wallets: StateFlow> = _wallets.asStateFlow()
+ private val _openEvent = MutableSharedFlow()
+ val openEvent = _openEvent.asSharedFlow()
- private val dataPreferences = application.getSharedPreferences("data", Application.MODE_PRIVATE)
- private val walletsPreferences = application.getSharedPreferences("wallets", Application.MODE_PRIVATE)
-
- private var highestId = 0
-
- fun init() {
- val savedWallets = dataPreferences.getStringSet("walletIds", null)
-
- if (savedWallets != null) {
- val savedWalletIds = savedWallets.map { it.toInt() }.sorted()
- val w = arrayListOf()
-
- savedWalletIds.forEach { id ->
- val wallet = Wallet.fromPreferences(id, walletsPreferences) { wa, mo -> onWalletUpdate(wa, mo) }
- w.add(wallet)
- _totalBalance.value += wallet.balance.value
- if (wallet.id > highestId)
- highestId = wallet.id
- }
-
- _wallets.value = w
- } else {
- _wallets.value = listOf(Wallet.default { wa, mo -> onWalletUpdate(wa, mo) })
- _totalBalance.value = 2000
- }
- }
-
- fun saveAll() {
- dataPreferences.edit(true) {
- putStringSet("walletIds", wallets.value.map { it.id.toString() }.toSet())
- }
-
-
- walletsPreferences.edit(true) {
- wallets.value.forEach {
- it.toPreferences(this)
- }
- }
+ val totalBalance: Flow = wallets.map { wallets ->
+ wallets.sumOf { it.balance }
}
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
+ val wallet = Wallet(label = name)
+ viewModelScope.launch {
+ val id = repository.insertWallet(wallet)
+ _openEvent.emit(id)
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/MyApplication.kt b/app/src/main/java/eu/m724/coincounter/MyApplication.kt
new file mode 100644
index 0000000..e2080de
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/MyApplication.kt
@@ -0,0 +1,12 @@
+package eu.m724.coincounter
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class MyApplication : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/Wallet.kt b/app/src/main/java/eu/m724/coincounter/Wallet.kt
deleted file mode 100644
index e548b2b..0000000
--- a/app/src/main/java/eu/m724/coincounter/Wallet.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package eu.m724.coincounter
-
-import android.content.SharedPreferences
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-class Wallet(
- val id: Int,
- val name: String,
- balance: Int,
- val onUpdate: (Wallet, Int) -> Unit
-) {
- companion object {
- fun default(onUpdate: (Wallet, Int) -> Unit): Wallet {
- return Wallet(0, "Primary", 2000, onUpdate)
- }
-
- fun fromPreferences(id: Int, sharedPreferences: SharedPreferences, onUpdate: (Wallet, Int) -> Unit): Wallet {
- return Wallet(
- id,
- sharedPreferences.getString("${id}_name", "Wallet")!!,
- sharedPreferences.getInt("${id}_balance", 0),
- onUpdate
- )
- }
- }
-
- fun toPreferences(editor: SharedPreferences.Editor) {
- editor.putString("${id}_name", name)
- editor.putInt("${id}_balance", balance.value)
- }
-
- private val _balance: MutableStateFlow = MutableStateFlow(balance)
- val balance: StateFlow = _balance.asStateFlow()
-
- fun fund(money: Int) {
- _balance.value += money
- onUpdate(this, money)
- }
-}
diff --git a/app/src/main/java/eu/m724/coincounter/WalletActivity.kt b/app/src/main/java/eu/m724/coincounter/WalletActivity.kt
new file mode 100644
index 0000000..4dfaeda
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/WalletActivity.kt
@@ -0,0 +1,291 @@
+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.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.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+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) }
+
+ if (actionsVisible) {
+ ActionsDialog(
+ onDismiss = { actionsVisible = false },
+ onDelete = {
+ viewModel.delete()
+ finish()
+ }
+ )
+ }
+
+ 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 = "zł",
+ 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(200.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".format(it.value / 100.0),
+ 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))
+ )
+ }
+ }
+ }
+ }
+}
+
+
+@Composable
+fun TransactionDialog(
+ onDismiss: () -> Unit,
+ onConfirm: (String, Int) -> Unit
+) {
+ var label by rememberSaveable { mutableStateOf("") }
+ var value by rememberSaveable { mutableStateOf("0") }
+
+ 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),
+ supportingText = { Text("Label") },
+ singleLine = true
+ )
+ Spacer(modifier = Modifier.width(6.dp))
+ TextField(
+ value = value,
+ onValueChange = { value = it },
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ supportingText = { Text("Value") },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ singleLine = true
+ )
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = {
+ label = ""
+ onDismiss()
+ }) {
+ Text(text = "Cancel")
+ }
+ TextButton(onClick = {
+ onConfirm(label, (value.toFloat() * 100).toInt())
+ }) {
+ Text("Create transaction")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ActionsDialog(
+ onDismiss: () -> Unit,
+ onDelete: () -> Unit
+) {
+ Dialog(
+ onDismissRequest = {
+ onDismiss()
+ }
+ ) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth(),
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ TextButton(onClick = onDelete) {
+ Text("Delete wallet")
+ }
+ TextButton(onClick = onDismiss) {
+ Text("Cancel")
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/WalletRepository.kt b/app/src/main/java/eu/m724/coincounter/WalletRepository.kt
new file mode 100644
index 0000000..aaf8ec2
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/WalletRepository.kt
@@ -0,0 +1,37 @@
+package eu.m724.coincounter
+
+import android.content.Context
+import androidx.room.Room
+import dagger.hilt.android.qualifiers.ApplicationContext
+import eu.m724.coincounter.data.AppDatabase
+import eu.m724.coincounter.data.dao.TransactionDao
+import eu.m724.coincounter.data.dao.WalletDao
+import eu.m724.coincounter.data.entity.Transaction
+import eu.m724.coincounter.data.entity.Wallet
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class WalletRepository @Inject constructor(
+ @ApplicationContext private val applicationContext: Context
+) {
+ private val database: AppDatabase = Room.databaseBuilder(
+ applicationContext,
+ AppDatabase::class.java,
+ "database-v1"
+ ).build()
+ private val walletDao: WalletDao = database.walletDao()
+ private val transactionDao: TransactionDao = database.transactionDao()
+
+ fun getAllWallets(): Flow> = walletDao.getAllWallets()
+ fun getWalletById(id: Long): Flow = 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)
+
+ fun getWalletTransactions(walletId: Long) = transactionDao.getWalletTransactions(walletId)
+
+ fun insertTransaction(transaction: Transaction) = transactionDao.insertTransaction(transaction)
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/WalletViewModel.kt b/app/src/main/java/eu/m724/coincounter/WalletViewModel.kt
new file mode 100644
index 0000000..9e17164
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/WalletViewModel.kt
@@ -0,0 +1,53 @@
+package eu.m724.coincounter
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import eu.m724.coincounter.data.entity.Transaction
+import eu.m724.coincounter.data.entity.Wallet
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+import kotlin.properties.Delegates
+
+@HiltViewModel
+class WalletViewModel @Inject constructor(
+ private val repository: WalletRepository
+) : ViewModel() {
+ private var walletId by Delegates.notNull()
+
+ lateinit var wallet: Flow
+ lateinit var transactions: Flow>
+
+ fun init(walletId: Long) {
+ this.walletId = walletId
+ wallet = repository.getWalletById(walletId)
+ transactions = repository.getWalletTransactions(walletId)
+ }
+
+ fun delete() {
+ viewModelScope.launch(Dispatchers.IO) {
+ repository.deleteWallet(wallet.first())
+ }
+ }
+
+ fun transact(value: Int, label: String?) {
+ val transaction = Transaction(
+ walletId = walletId,
+ value = value,
+ label = label
+ )
+
+ viewModelScope.launch(Dispatchers.IO) {
+ val wallet = wallet.first()
+ wallet.let {
+ repository.updateWallet(
+ wallet.copy(balance = wallet.balance + value)
+ )
+ }
+ repository.insertTransaction(transaction)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/data/AppDatabase.kt b/app/src/main/java/eu/m724/coincounter/data/AppDatabase.kt
new file mode 100644
index 0000000..896329e
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/data/AppDatabase.kt
@@ -0,0 +1,14 @@
+package eu.m724.coincounter.data
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import eu.m724.coincounter.data.dao.TransactionDao
+import eu.m724.coincounter.data.dao.WalletDao
+import eu.m724.coincounter.data.entity.Transaction
+import eu.m724.coincounter.data.entity.Wallet
+
+@Database(entities = [Wallet::class, Transaction::class], version = 1)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun walletDao(): WalletDao
+ abstract fun transactionDao(): TransactionDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/data/dao/TransactionDao.kt b/app/src/main/java/eu/m724/coincounter/data/dao/TransactionDao.kt
new file mode 100644
index 0000000..2855d3d
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/data/dao/TransactionDao.kt
@@ -0,0 +1,26 @@
+package eu.m724.coincounter.data.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import eu.m724.coincounter.data.entity.Transaction
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface TransactionDao {
+ @Query("SELECT * FROM transactions WHERE walletId = :walletId AND timestamp > :after ORDER BY timestamp DESC LIMIT :limit")
+ fun getWalletTransactions(walletId: Long, after: Long = 0, limit: Int = 10): Flow>
+
+ @Insert
+ fun insertTransaction(transaction: Transaction)
+
+ @Insert
+ fun insertTransactions(transactions: List)
+
+ @Delete
+ fun deleteTransaction(transaction: Transaction)
+
+ @Insert
+ fun deleteTransactions(transactions: List)
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/data/dao/WalletDao.kt b/app/src/main/java/eu/m724/coincounter/data/dao/WalletDao.kt
new file mode 100644
index 0000000..5218838
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/data/dao/WalletDao.kt
@@ -0,0 +1,36 @@
+package eu.m724.coincounter.data.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+import eu.m724.coincounter.data.entity.Wallet
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface WalletDao {
+ @Query("SELECT * FROM wallets")
+ fun getAllWallets(): Flow>
+
+ @Query("SELECT * FROM wallets WHERE id = (:id)")
+ fun getWalletById(id: Long): Flow
+
+ @Insert
+ suspend fun insertWallet(wallet: Wallet): Long
+
+ @Insert
+ suspend fun insertWallets(wallets: List): LongArray
+
+ @Update
+ fun updateWallet(wallet: Wallet)
+
+ @Update
+ fun updateWallets(wallets: List)
+
+ @Delete
+ fun deleteWallet(wallet: Wallet)
+
+ @Delete
+ fun deleteWallets(wallet: List)
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/data/entity/Transaction.kt b/app/src/main/java/eu/m724/coincounter/data/entity/Transaction.kt
new file mode 100644
index 0000000..e2a3c35
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/data/entity/Transaction.kt
@@ -0,0 +1,40 @@
+package eu.m724.coincounter.data.entity
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.PrimaryKey
+
+/**
+ * represents adding or removing money from a wallet,
+ * with optional metadata
+ */
+@Entity(
+ tableName = "transactions",
+ foreignKeys = [
+ ForeignKey(
+ entity = Wallet::class,
+ parentColumns = ["id"],
+ childColumns = ["walletId"],
+ onDelete = ForeignKey.CASCADE
+ )
+ ]
+)
+data class Transaction(
+ /** used for internal reference */
+ @PrimaryKey(autoGenerate = true)
+ val id: Long = 0,
+
+ /**
+ * the wallet id that money was used from
+ */
+ val walletId: Long,
+
+ /** how much cents added (positive) or removed (negative) */
+ val value: Int,
+
+ /** label of the transaction, set by user */
+ val label: String? = null,
+
+ /** timestamp in unix milllis when the transaction was performed */
+ val timestamp: Long = System.currentTimeMillis()
+)
\ No newline at end of file
diff --git a/app/src/main/java/eu/m724/coincounter/data/entity/Wallet.kt b/app/src/main/java/eu/m724/coincounter/data/entity/Wallet.kt
new file mode 100644
index 0000000..7770b33
--- /dev/null
+++ b/app/src/main/java/eu/m724/coincounter/data/entity/Wallet.kt
@@ -0,0 +1,17 @@
+package eu.m724.coincounter.data.entity
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "wallets")
+data class Wallet(
+ @PrimaryKey(autoGenerate = true)
+ val id: Long = 0,
+
+ /** the wallet label set by user */
+ var label: String = "Wallet",
+
+ /** balance in cents
+ * for example 123 cents = 1.23 euro or any other currency */
+ var balance: Int = 0
+)
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a915b7b..d5fa150 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,4 +1,5 @@
Coin Counter
MainActivity
+ WalletActivity
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index f74b04b..fbdde38 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,4 +2,7 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
+ alias(libs.plugins.devtools.ksp) apply false
+ alias(libs.plugins.compose.compiler) apply false
+ alias(libs.plugins.hilt.android) apply false
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1d6d8b3..1faad84 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,18 +1,22 @@
[versions]
-agp = "8.5.1"
-datastore = "1.1.1"
-kotlin = "1.9.0"
-coreKtx = "1.10.1"
+agp = "8.5.2"
+kotlin = "2.0.10"
+coreKtx = "1.13.1"
junit = "4.13.2"
-junitVersion = "1.1.5"
-espressoCore = "3.5.1"
-lifecycleRuntimeKtx = "2.6.1"
-activityCompose = "1.8.0"
-composeBom = "2024.04.01"
+junitVersion = "1.2.1"
+espressoCore = "3.6.1"
+lifecycleRuntimeKtx = "2.8.4"
+activityCompose = "1.9.1"
+composeBom = "2024.06.00"
+room = "2.6.1"
+ksp = "2.0.10-1.0.24"
+hilt = "2.44"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
-androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
+androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
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" }
@@ -26,8 +30,13 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt"}
+hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt"}
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt"}