Compare commits

...

19 commits

Author SHA1 Message Date
a215969f5e
Add LLM translations 2024-10-03 15:54:48 +02:00
65cd9ce170
fix translation & add credits 2024-10-02 18:04:55 +02:00
2535e4bea6
...and this 2024-10-02 18:03:27 +02:00
0ee9ece012
this one too 2024-10-02 17:48:17 +02:00
dad71108d8
refactoring 2024-10-02 17:46:59 +02:00
fb565c1a9d
Delete release 2024-10-01 15:55:16 +02:00
989720b69b
Version up
Because why not
2024-10-01 15:54:12 +02:00
bf4f0f6385
Polish translation 2024-10-01 15:52:51 +02:00
3d6fe3bcb2
Localization 2024-10-01 15:52:45 +02:00
3f09577411
.gitignore releases 2024-10-01 15:52:12 +02:00
97f1905179
update
lost motivation :(
2024-09-30 19:10:36 +02:00
7f9eccba34
make it possible to rename a wallet
sorry for being lazy for some days
2024-08-15 16:25:10 +02:00
88095d1edb
make units configurable in buildconfig 2024-08-15 16:10:00 +02:00
9d43dcfb9b
bump version 2024-08-14 13:36:19 +02:00
32feac499f
fix app name 2024-08-14 13:35:47 +02:00
1d8979a436
fix float error 2024-08-14 13:34:20 +02:00
59839fe8ca
auto focus 2024-08-14 13:29:06 +02:00
b5610ae7b0
newer version 2024-08-10 17:08:24 +02:00
a7c9957512
new version 2024-08-08 16:59:33 +02:00
45 changed files with 1538 additions and 310 deletions

View file

@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-08-09T09:07:46.136113340Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/user/.android/avd/Pixel_Fold_API_34.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

View file

@ -1,6 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.0" />
<option name="languageVersion" value="2.0" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
<option name="version" value="2.0.10" />
</component>
</project>

View file

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

View file

@ -25,6 +25,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="id" value="TB370FU" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
@ -69,6 +80,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 +124,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 +146,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 +201,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" />
@ -179,17 +245,6 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="q2q" />
<option name="id" value="q2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold3" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1768" />
<option name="screenY" value="2208" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
@ -246,6 +301,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" />

3
app/.gitignore vendored
View file

@ -1 +1,2 @@
/build
/build
/release

View file

@ -1,18 +1,23 @@
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 {
namespace = "eu.m724.coincounter"
compileSdk = 34
compileSdk = 35
defaultConfig {
applicationId = "eu.m724.coincounter"
minSdk = 34
targetSdk = 34
versionCode = 3
versionName = "1.1.1"
minSdk = 30
targetSdk = 35
versionCode = 5
versionName = "2.0.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -38,19 +43,28 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
kotlinCompilerExtensionVersion = "1.5.15"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
androidResources {
generateLocaleConfig = true
}
}
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 +73,8 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@ -67,4 +82,9 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
}
// Allow references to generated code
kapt {
correctErrorTypes = true
}

Binary file not shown.

View file

@ -1,37 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "eu.m724.coincounter",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 3,
"versionName": "1.1.1",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 34
}

View file

@ -3,20 +3,26 @@
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".MyApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CoinCounter"
android:enableOnBackInvokedCallback="true"
tools:targetApi="31">
tools:targetApi="35">
<activity
android:name=".MainActivity"
android:name=".wallet.WalletActivity"
android:exported="false"
android:label="@string/title_activity_wallet"
android:theme="@style/Theme.CoinCounter" />
<activity
android:name=".home.HomeActivity"
android:exported="true"
android:label="@string/app_name"
android:label="@string/title_activity_home"
android:theme="@style/Theme.CoinCounter">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

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

View file

@ -1,237 +0,0 @@
package eu.m724.coincounter
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.asIntState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.FontScaling
import androidx.compose.ui.unit.FontScalingLinear
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import eu.m724.coincounter.ui.theme.CoinCounterTheme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
val Context.dataStore by preferencesDataStore(name = "preferences")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val increments = listOf(1, 2, 5, 10, 20, 50, 100, 200, 500)
enableEdgeToEdge()
setContent {
AppView(increments, dataStore)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppView(increments: List<Int>, dataStore: DataStore<Preferences>) {
val BALANCE_KEY = intPreferencesKey("balance")
val balanceFlow: Flow<Int> =
dataStore.data.map { preferences ->
preferences[BALANCE_KEY] ?: 0
}
val balance = balanceFlow.collectAsState(initial = 0).value
println("Loading balance: $balance")
val balanceState = rememberSaveable { mutableIntStateOf(0) }
balanceState.intValue = balance // this is required for some reason
CoinCounterTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(modifier = Modifier
.fillMaxSize()
.padding(innerPadding)) {
Box(modifier = Modifier
.fillMaxWidth()
.weight(.25f),
contentAlignment = Alignment.Center) {
StatusPart(balanceState)
}
Column(modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalArrangement = Arrangement.Center) {
ControlPart(increments, balanceState, dataStore)
}
}
}
}
}
@Composable
fun StatusPart(balanceState: MutableState<Int>) {
val value by balanceState.asIntState()
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = "%.2f".format(value / 100.0),
fontSize = 48.sp
)
Text(
text = "",
fontSize = 32.sp,
modifier = Modifier.alpha(.7f))
}
}
@Composable
fun ControlPart(increments: List<Int>, balanceState: MutableIntState, dataStore: DataStore<Preferences>) {
val multiplier = remember { mutableIntStateOf(1) }
val scope = rememberCoroutineScope()
val BALANCE_KEY = intPreferencesKey("balance")
suspend fun setBalance(value: Int) {
dataStore.edit { preferences ->
preferences[BALANCE_KEY] = value
}
}
Row {
Column(
modifier = Modifier
.fillMaxSize()
.weight(0.5f),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier.clip(CircleShape).background(if (multiplier.intValue == 1) Color.Green else Color.Red)
) {
Button(
onClick = { multiplier.intValue = -1 },
modifier = Modifier
.size(64.dp)
.aspectRatio(1f)
.alpha(if (multiplier.intValue == -1) 1f else 0.7f),
contentPadding = PaddingValues(0.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Red,
contentColor = Color.White
)
) {
Text(text = "-", fontSize = 24.sp)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { multiplier.intValue = 1 },
modifier = Modifier
.size(64.dp)
.aspectRatio(1f)
.alpha(if (multiplier.intValue == 1) 1f else 0.7f),
contentPadding = PaddingValues(0.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Green,
contentColor = Color.Black
)
) {
Text(text = "+", fontSize = 24.sp)
}
}
}
Column(
modifier = Modifier
.fillMaxHeight()
.weight(1f),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 64.dp),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(increments) { increment ->
Button(
onClick = {
balanceState.intValue += increment * multiplier.intValue
scope.launch {
println("Saved")
setBalance(balanceState.intValue)
}
},
modifier = Modifier
.aspectRatio(1f),
contentPadding = PaddingValues(0.dp),
colors = ButtonDefaults.buttonColors(containerColor = currencyColor(increment), contentColor = Color.Black),
) {
CurrencyLabel(value = increment)
}
}
}
}
}
}
fun currencyColor(value: Int): Color {
return if (value < 10) {
Color(244,164,96)
} else if (value < 200) {
Color(226, 226, 255)
} else
Color(240, 230, 140)
}
@Composable
fun CurrencyLabel(value: Int) {
Row(verticalAlignment = Alignment.Bottom) {
Text(text = (if (value >= 100) value / 100 else value).toString(), fontSize = 18.sp)
Text(text = if (value < 100) "gr" else "", fontSize = 10.sp, lineHeight = 4.sp)
}
}

View file

@ -0,0 +1,11 @@
package eu.m724.coincounter
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}

View file

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

View file

@ -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<List<Transaction>>
@Insert
suspend fun insertTransaction(transaction: Transaction)
@Insert
suspend fun insertTransactions(transactions: List<Transaction>)
@Delete
suspend fun deleteTransaction(transaction: Transaction)
@Insert
suspend fun deleteTransactions(transactions: List<Transaction>)
}

View file

@ -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<List<Wallet>>
@Query("SELECT * FROM wallets WHERE id = (:id)")
fun getWalletById(id: Long): Flow<Wallet>
@Insert
suspend fun insertWallet(wallet: Wallet): Long
@Insert
suspend fun insertWallets(wallets: List<Wallet>): LongArray
@Update
suspend fun updateWallet(wallet: Wallet)
@Update
suspend fun updateWallets(wallets: List<Wallet>)
@Delete
suspend fun deleteWallet(wallet: Wallet)
@Delete
suspend fun deleteWallets(wallet: List<Wallet>)
}

View file

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

View file

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

View file

@ -0,0 +1,59 @@
package eu.m724.coincounter.home
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import eu.m724.coincounter.home.compose.HomeActivityView
import eu.m724.coincounter.ui.theme.CoinCounterTheme
import eu.m724.coincounter.wallet.WalletActivity
import kotlinx.coroutines.launch
// TODO modularize
@AndroidEntryPoint
class HomeActivity : ComponentActivity() {
private val viewModel: HomeViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
viewModel.openEvent.collect {
openWallet(it)
}
}
enableEdgeToEdge()
setContent {
CoinCounterTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box(
modifier = Modifier.padding(innerPadding)
) {
HomeActivityView(
viewModel = viewModel,
onClick = {
openWallet(it.id)
}
)
}
}
}
}
}
private fun openWallet(walletId: Long) {
val intent = Intent(application.applicationContext, WalletActivity::class.java)
intent.putExtra("walletId", walletId)
startActivity(intent)
}
}

View file

@ -0,0 +1,35 @@
package eu.m724.coincounter.home
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
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: WalletRepository
) : ViewModel() {
val wallets: Flow<List<Wallet>> = repository.getAllWallets()
private val _openEvent = MutableSharedFlow<Long>()
val openEvent = _openEvent.asSharedFlow()
val totalBalance: Flow<Int> = wallets.map { wallets ->
wallets.sumOf { it.balance }
}
fun addWallet(name: String) {
val wallet = Wallet(label = name)
viewModelScope.launch {
val id = repository.insertWallet(wallet)
_openEvent.emit(id)
}
}
}

View file

@ -0,0 +1,55 @@
package eu.m724.coincounter.home.compose
import android.widget.Toast
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.m724.coincounter.CurrencyUtils
import eu.m724.coincounter.R
@Composable
fun BalanceView(balance: Int) {
val context = LocalContext.current
Column(
modifier = Modifier
.height(150.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
Toast.makeText(
context,
context.getString(R.string.translation_credits),
Toast.LENGTH_SHORT
).show()
}
)
},
verticalAlignment = Alignment.Bottom
) {
Text(
text = CurrencyUtils.formatNoCurrency(balance),
fontSize = 32.sp
)
Text(
text = CurrencyUtils.currencySymbol,
fontSize = 14.sp
)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,76 @@
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 androidx.compose.ui.res.stringResource
import dagger.hilt.android.AndroidEntryPoint
import eu.m724.coincounter.R
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(
imageVector = Icons.Filled.Create,
contentDescription = stringResource(R.string.wallet_new_transaction)
)
}
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
WalletActivityView(
viewModel = viewModel,
finish = { finish() }
)
}
}
}
}
}
}

View file

@ -0,0 +1,37 @@
package eu.m724.coincounter.wallet
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<List<Wallet>> = walletDao.getAllWallets()
fun getWalletById(id: Long): Flow<Wallet> = walletDao.getWalletById(id)
suspend fun insertWallet(wallet: Wallet): Long = walletDao.insertWallet(wallet)
suspend fun updateWallet(wallet: Wallet) = walletDao.updateWallet(wallet)
suspend fun deleteWallet(wallet: Wallet) = walletDao.deleteWallet(wallet)
fun getWalletTransactions(walletId: Long) = transactionDao.getWalletTransactions(walletId)
suspend fun insertTransaction(transaction: Transaction) = transactionDao.insertTransaction(transaction)
}

View file

@ -0,0 +1,68 @@
package eu.m724.coincounter.wallet
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.flow.map
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<Long>()
lateinit var wallet: Flow<Wallet>
lateinit var transactions: Flow<List<Transaction>>
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 rename(label: String) {
wallet = wallet.map {
it.copy(label = label)
}
viewModelScope.launch(Dispatchers.IO) {
val wallet = wallet.first()
repository.updateWallet(wallet)
}
}
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 + transaction.value)
)
}
}
}
}

View file

@ -0,0 +1,176 @@
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.res.stringResource
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.CurrencyUtils
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(stringResource(R.string.create_transaction_title)) },
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 = stringResource(R.string.create_transaction_value, CurrencyUtils.currencyName),
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(
painter = painterResource(id = R.drawable.baseline_error_24),
contentDescription = stringResource(R.string.create_transaction_value_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 = stringResource(R.string.create_transaction_absolute))
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = {
label = ""
onDismiss()
}
) {
Text(text = stringResource(R.string.create_transaction_cancel))
}
TextButton(
onClick = {
val doubleValue = value.toDoubleOrNull()
if (doubleValue != null) {
onConfirm(label, (doubleValue * 100).toInt(), absoluteChecked) // TODO handle fixed point
} else {
Toast.makeText(context,
context.getString(R.string.create_transaction_nan), Toast.LENGTH_SHORT).show()
}
}
) {
Text(stringResource(R.string.create_transaction_confirm))
}
}
}
}
}
}
@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.CurrencyUtils
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 = CurrencyUtils.formatCurrency(transaction.value),
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,53 @@
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import eu.m724.coincounter.R
/**
* 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(stringResource(R.string.wallet_actions_rename))
}
TextButton(onClick = onDelete) {
Text(stringResource(R.string.wallet_actions_delete))
}
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.wallet_actions_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.CurrencyUtils
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 = CurrencyUtils.formatNoCurrency(wallet.balance),
fontSize = 32.sp
)
Text(
text = CurrencyUtils.currencySymbol,
fontSize = 14.sp
)
}
}
Spacer(modifier = Modifier.height(50.dp))
TransactionList(
viewModel = viewModel
)
}
}
}

View file

@ -0,0 +1,84 @@
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import eu.m724.coincounter.R
@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(stringResource(R.string.wallet_rename_name)) },
singleLine = true
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = {
value = name
onDismiss()
}
) {
Text(text = stringResource(R.string.wallet_rename_cancel))
}
TextButton(
onClick = {
onRename(value)
}
) {
Text(stringResource(R.string.wallet_rename_confirm))
}
}
}
}
}
}

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>

View file

@ -0,0 +1 @@
unqualifiedResLocale=en

View file

@ -0,0 +1,20 @@
<resources>
<string name="app_name">Münzzähler</string>
<string name="title_activity_home">Münzzähler</string>
<string name="create_transaction_confirm">Transaktion erstellen</string>
<string name="create_transaction_nan">Wert muss eine Zahl sein</string>
<string name="create_transaction_cancel">Abbrechen</string>
<string name="create_transaction_absolute">Absolut</string>
<string name="create_transaction_value_error">Fehler</string>
<string name="create_transaction_value">Wert (%1$s)</string>
<string name="create_transaction_title">Titel</string>
<string name="wallet_actions_rename">Wallet umbenennen</string>
<string name="wallet_actions_delete">Wallet löschen</string>
<string name="wallet_actions_cancel">Abbrechen</string>
<string name="wallet_rename_name">Neuer Name</string>
<string name="wallet_rename_cancel">Abbrechen</string>
<string name="wallet_rename_confirm">Umbenennen</string>
<string name="wallet_new_transaction">Neue Transaktion</string>
<string name="home_add_wallet">Wallet erstellen</string>
<string name="translation_credits">Deutsche Übersetzung von Mistral Large 2</string>
</resources>

View file

@ -0,0 +1,20 @@
<resources>
<string name="app_name">MoneroKalkulilo</string>
<string name="title_activity_home">MoneroKalkulilo</string>
<string name="create_transaction_confirm">Krei transakcion</string>
<string name="create_transaction_nan">Valoro devas esti nombro</string>
<string name="create_transaction_cancel">Nuligi</string>
<string name="create_transaction_absolute">Absoluta</string>
<string name="create_transaction_value_error">Eraro</string>
<string name="create_transaction_value">Valoro (%1$s)</string>
<string name="create_transaction_title">Titolo</string>
<string name="wallet_actions_rename">Renomi monujon</string>
<string name="wallet_actions_delete">Forigi monujon</string>
<string name="wallet_actions_cancel">Nuligi</string>
<string name="wallet_rename_name">Nova nomo</string>
<string name="wallet_rename_cancel">Nuligi</string>
<string name="wallet_rename_confirm">Renomi</string>
<string name="wallet_new_transaction">Nova transakcio</string>
<string name="home_add_wallet">Krei monujon</string>
<string name="translation_credits">Esperanta traduko farita de Llama 3.1</string>
</resources>

View file

@ -0,0 +1,20 @@
<resources>
<string name="app_name">Contador de Monedas</string>
<string name="title_activity_home">Contador de Monedas</string>
<string name="create_transaction_confirm">Crear transacción</string>
<string name="create_transaction_nan">El valor debe ser un número</string>
<string name="create_transaction_cancel">Cancelar</string>
<string name="create_transaction_absolute">Absoluto</string>
<string name="create_transaction_value_error">Error</string>
<string name="create_transaction_value">Valor (%1$s)</string>
<string name="create_transaction_title">Título</string>
<string name="wallet_actions_rename">Renombrar cartera</string>
<string name="wallet_actions_delete">Eliminar cartera</string>
<string name="wallet_actions_cancel">Cancelar</string>
<string name="wallet_rename_name">Nuevo nombre</string>
<string name="wallet_rename_cancel">Cancelar</string>
<string name="wallet_rename_confirm">Renombrar</string>
<string name="wallet_new_transaction">Nueva transacción</string>
<string name="home_add_wallet">Crear cartera</string>
<string name="translation_credits">Traducción al español por Llama 3.1</string>
</resources>

View file

@ -0,0 +1,20 @@
<resources>
<string name="app_name">Compteur de Pièces</string>
<string name="title_activity_home">Compteur de Pièces</string>
<string name="create_transaction_confirm">Créer une transaction</string>
<string name="create_transaction_nan">La valeur doit être un nombre</string>
<string name="create_transaction_cancel">Annuler</string>
<string name="create_transaction_absolute">Absolu</string>
<string name="create_transaction_value_error">Erreur</string>
<string name="create_transaction_value">Valeur (%1$s)</string>
<string name="create_transaction_title">Titre</string>
<string name="wallet_actions_rename">Renommer le portefeuille</string>
<string name="wallet_actions_delete">Supprimer le portefeuille</string>
<string name="wallet_actions_cancel">Annuler</string>
<string name="wallet_rename_name">Nouveau nom</string>
<string name="wallet_rename_cancel">Annuler</string>
<string name="wallet_rename_confirm">Renommer</string>
<string name="wallet_new_transaction">Nouvelle transaction</string>
<string name="home_add_wallet">Créer un portefeuille</string>
<string name="translation_credits">Traduction en français par Mistral Large 2</string>
</resources>

View file

@ -0,0 +1,20 @@
<resources>
<string name="app_name">Coin Counter</string>
<string name="title_activity_home">Coin Counter</string>
<string name="create_transaction_confirm">Utwórz transakcję</string>
<string name="create_transaction_nan">Wartość musi być liczbą</string>
<string name="create_transaction_cancel">Anuluj</string>
<string name="create_transaction_absolute">Bezwzględna</string>
<string name="create_transaction_value_error">Błąd</string>
<string name="create_transaction_value">Wartość (%1$s)</string>
<string name="create_transaction_title">Tytuł</string>
<string name="wallet_actions_rename">Zmień nazwę portfela</string>
<string name="wallet_actions_delete">Usuń portfel</string>
<string name="wallet_actions_cancel">Anuluj</string>
<string name="wallet_rename_name">Nowa nazwa</string>
<string name="wallet_rename_cancel">Anuluj</string>
<string name="wallet_rename_confirm">Zmień nazwę</string>
<string name="wallet_new_transaction">Nowa transakcja</string>
<string name="home_add_wallet">Utwórz portfel</string>
<string name="translation_credits">Polskie tłumaczenie autorstwa m724</string>
</resources>

View file

@ -0,0 +1,20 @@
<resources>
<string name="app_name">硬币计数器</string>
<string name="title_activity_home">硬币计数器</string>
<string name="create_transaction_confirm">创建交易</string>
<string name="create_transaction_nan">值必须是数字</string>
<string name="create_transaction_cancel">取消</string>
<string name="create_transaction_absolute">绝对值</string>
<string name="create_transaction_value_error">错误</string>
<string name="create_transaction_value">值 (%1$s)</string>
<string name="create_transaction_title">标题</string>
<string name="wallet_actions_rename">重命名钱包</string>
<string name="wallet_actions_delete">删除钱包</string>
<string name="wallet_actions_cancel">取消</string>
<string name="wallet_rename_name">新名称</string>
<string name="wallet_rename_cancel">取消</string>
<string name="wallet_rename_confirm">重命名</string>
<string name="wallet_new_transaction">新交易</string>
<string name="home_add_wallet">创建钱包</string>
<string name="translation_credits">中文翻译由 Qwen 2.5 提供</string>
</resources>

View file

@ -1,3 +1,21 @@
<resources>
<string name="app_name">Coin Counter</string>
<string name="title_activity_home">Coin Counter</string>
<string name="title_activity_wallet" translatable="false">WalletActivity</string>
<string name="create_transaction_confirm">Create transaction</string>
<string name="create_transaction_nan">Value must be a number</string>
<string name="create_transaction_cancel">Cancel</string>
<string name="create_transaction_absolute">Absolute</string>
<string name="create_transaction_value_error">Error</string>
<string name="create_transaction_value">Value (%1$s)</string>
<string name="create_transaction_title">Title</string>
<string name="wallet_actions_rename">Rename wallet</string>
<string name="wallet_actions_delete">Delete wallet</string>
<string name="wallet_actions_cancel">Cancel</string>
<string name="wallet_rename_name">New name</string>
<string name="wallet_rename_cancel">Cancel</string>
<string name="wallet_rename_confirm">Rename</string>
<string name="wallet_new_transaction">New transaction</string>
<string name="home_add_wallet">Create wallet</string>
<string name="translation_credits">English language by the CoinCounter Team</string>
</resources>

View file

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

View file

@ -1,18 +1,22 @@
[versions]
agp = "8.5.1"
datastorePreferences = "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-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
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"}