Compare commits
19 commits
Author | SHA1 | Date | |
---|---|---|---|
a215969f5e | |||
65cd9ce170 | |||
2535e4bea6 | |||
0ee9ece012 | |||
dad71108d8 | |||
fb565c1a9d | |||
989720b69b | |||
bf4f0f6385 | |||
3d6fe3bcb2 | |||
3f09577411 | |||
97f1905179 | |||
7f9eccba34 | |||
88095d1edb | |||
9d43dcfb9b | |||
32feac499f | |||
1d8979a436 | |||
59839fe8ca | |||
b5610ae7b0 | |||
a7c9957512 |
45 changed files with 1538 additions and 310 deletions
|
@ -4,6 +4,14 @@
|
||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<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>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<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">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.0" />
|
<option name="version" value="2.0.10" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -1,4 +1,3 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||||
|
|
|
@ -25,6 +25,17 @@
|
||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2160" />
|
<option name="screenY" value="2160" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="31" />
|
<option name="api" value="31" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
|
@ -69,6 +80,28 @@
|
||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2400" />
|
<option name="screenY" value="2400" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="29" />
|
<option name="api" value="29" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
|
@ -91,6 +124,17 @@
|
||||||
<option name="screenX" value="1440" />
|
<option name="screenX" value="1440" />
|
||||||
<option name="screenY" value="3088" />
|
<option name="screenY" value="3088" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
@ -102,6 +146,17 @@
|
||||||
<option name="screenX" value="2208" />
|
<option name="screenX" value="2208" />
|
||||||
<option name="screenY" value="1840" />
|
<option name="screenY" value="1840" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
@ -146,6 +201,17 @@
|
||||||
<option name="screenX" value="720" />
|
<option name="screenX" value="720" />
|
||||||
<option name="screenY" value="1600" />
|
<option name="screenY" value="1600" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
|
@ -179,17 +245,6 @@
|
||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2400" />
|
<option name="screenY" value="2400" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
|
@ -246,6 +301,17 @@
|
||||||
<option name="screenX" value="1600" />
|
<option name="screenX" value="1600" />
|
||||||
<option name="screenY" value="2560" />
|
<option name="screenY" value="2560" />
|
||||||
</PersistentDeviceSelectionData>
|
</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>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="29" />
|
<option name="api" value="29" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
|
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/build
|
/build
|
||||||
|
/release
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.jetbrains.kotlin.android)
|
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 {
|
android {
|
||||||
namespace = "eu.m724.coincounter"
|
namespace = "eu.m724.coincounter"
|
||||||
compileSdk = 34
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.m724.coincounter"
|
applicationId = "eu.m724.coincounter"
|
||||||
minSdk = 34
|
minSdk = 30
|
||||||
targetSdk = 34
|
targetSdk = 35
|
||||||
versionCode = 3
|
versionCode = 5
|
||||||
versionName = "1.1.1"
|
versionName = "2.0.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
@ -38,19 +43,28 @@ android {
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.5.1"
|
kotlinCompilerExtensionVersion = "1.5.15"
|
||||||
}
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.hilt.android)
|
||||||
|
kapt(libs.hilt.android.compiler)
|
||||||
|
|
||||||
|
|
||||||
|
ksp(libs.androidx.room.compiler)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
@ -59,7 +73,8 @@ dependencies {
|
||||||
implementation(libs.androidx.ui.graphics)
|
implementation(libs.androidx.ui.graphics)
|
||||||
implementation(libs.androidx.ui.tooling.preview)
|
implementation(libs.androidx.ui.tooling.preview)
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.room.runtime)
|
||||||
|
implementation(libs.androidx.room.ktx)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
@ -68,3 +83,8 @@ dependencies {
|
||||||
debugImplementation(libs.androidx.ui.tooling)
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
debugImplementation(libs.androidx.ui.test.manifest)
|
debugImplementation(libs.androidx.ui.test.manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow references to generated code
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes = true
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
||||||
}
|
|
|
@ -3,20 +3,26 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".MyApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.CoinCounter"
|
android:theme="@style/Theme.CoinCounter"
|
||||||
android:enableOnBackInvokedCallback="true"
|
tools:targetApi="35">
|
||||||
tools:targetApi="31">
|
|
||||||
<activity
|
<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:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/title_activity_home"
|
||||||
android:theme="@style/Theme.CoinCounter">
|
android:theme="@style/Theme.CoinCounter">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
54
app/src/main/java/eu/m724/coincounter/CurrencyUtils.kt
Normal file
54
app/src/main/java/eu/m724/coincounter/CurrencyUtils.kt
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = "zł",
|
|
||||||
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 "zł", fontSize = 10.sp, lineHeight = 4.sp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
11
app/src/main/java/eu/m724/coincounter/MyApplication.kt
Normal file
11
app/src/main/java/eu/m724/coincounter/MyApplication.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
14
app/src/main/java/eu/m724/coincounter/data/AppDatabase.kt
Normal file
14
app/src/main/java/eu/m724/coincounter/data/AppDatabase.kt
Normal 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
|
||||||
|
}
|
|
@ -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>)
|
||||||
|
}
|
36
app/src/main/java/eu/m724/coincounter/data/dao/WalletDao.kt
Normal file
36
app/src/main/java/eu/m724/coincounter/data/dao/WalletDao.kt
Normal 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>)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
)
|
17
app/src/main/java/eu/m724/coincounter/data/entity/Wallet.kt
Normal file
17
app/src/main/java/eu/m724/coincounter/data/entity/Wallet.kt
Normal 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
|
||||||
|
)
|
59
app/src/main/java/eu/m724/coincounter/home/HomeActivity.kt
Normal file
59
app/src/main/java/eu/m724/coincounter/home/HomeActivity.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
35
app/src/main/java/eu/m724/coincounter/home/HomeViewModel.kt
Normal file
35
app/src/main/java/eu/m724/coincounter/home/HomeViewModel.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = { _, _, _ -> })
|
||||||
|
}
|
|
@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
app/src/main/res/drawable/baseline_error_24.xml
Normal file
5
app/src/main/res/drawable/baseline_error_24.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||||
|
|
||||||
|
</vector>
|
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
|
@ -0,0 +1 @@
|
||||||
|
unqualifiedResLocale=en
|
20
app/src/main/res/values-de/strings.xml
Normal file
20
app/src/main/res/values-de/strings.xml
Normal 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>
|
20
app/src/main/res/values-eo/strings.xml
Normal file
20
app/src/main/res/values-eo/strings.xml
Normal 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>
|
20
app/src/main/res/values-es/strings.xml
Normal file
20
app/src/main/res/values-es/strings.xml
Normal 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>
|
20
app/src/main/res/values-fr/strings.xml
Normal file
20
app/src/main/res/values-fr/strings.xml
Normal 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>
|
20
app/src/main/res/values-pl/strings.xml
Normal file
20
app/src/main/res/values-pl/strings.xml
Normal 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>
|
20
app/src/main/res/values-zh/strings.xml
Normal file
20
app/src/main/res/values-zh/strings.xml
Normal 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>
|
|
@ -1,3 +1,21 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Coin Counter</string>
|
<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>
|
</resources>
|
|
@ -2,4 +2,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.jetbrains.kotlin.android) 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
|
||||||
}
|
}
|
|
@ -1,18 +1,22 @@
|
||||||
[versions]
|
[versions]
|
||||||
agp = "8.5.1"
|
agp = "8.5.2"
|
||||||
datastorePreferences = "1.1.1"
|
kotlin = "2.0.10"
|
||||||
kotlin = "1.9.0"
|
coreKtx = "1.13.1"
|
||||||
coreKtx = "1.10.1"
|
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.1.5"
|
junitVersion = "1.2.1"
|
||||||
espressoCore = "3.5.1"
|
espressoCore = "3.6.1"
|
||||||
lifecycleRuntimeKtx = "2.6.1"
|
lifecycleRuntimeKtx = "2.8.4"
|
||||||
activityCompose = "1.8.0"
|
activityCompose = "1.9.1"
|
||||||
composeBom = "2024.04.01"
|
composeBom = "2024.06.00"
|
||||||
|
room = "2.6.1"
|
||||||
|
ksp = "2.0.10-1.0.24"
|
||||||
|
hilt = "2.44"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
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" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
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-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
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]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
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"}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue