From f0b863744dbd3aead1a9d2f441299d49e07c78cc Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Thu, 19 Dec 2024 22:08:13 +0700 Subject: [PATCH 01/57] Init watch app module --- gradle.properties | 33 +++--- gradle/libs.versions.toml | 11 ++ settings.gradle | 1 + watchapp/.gitignore | 1 + watchapp/build.gradle.kts | 69 +++++++++++ watchapp/lint.xml | 8 ++ watchapp/proguard-rules.pro | 21 ++++ watchapp/src/main/AndroidManifest.xml | 39 +++++++ .../watchapp/presentation/MainActivity.kt | 73 ++++++++++++ .../presentation/quickpairs/QuickPairItem.kt | 107 ++++++++++++++++++ .../quickpairs/QuickPairsScreen.kt | 8 ++ .../rate/watchapp/presentation/theme/Theme.kt | 17 +++ .../src/main/res/drawable/splash_icon.xml | 27 +++++ .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../src/main/res/values-round/strings.xml | 3 + watchapp/src/main/res/values/strings.xml | 8 ++ watchapp/src/main/res/values/styles.xml | 8 ++ 21 files changed, 414 insertions(+), 20 deletions(-) create mode 100644 watchapp/.gitignore create mode 100644 watchapp/build.gradle.kts create mode 100644 watchapp/lint.xml create mode 100644 watchapp/proguard-rules.pro create mode 100644 watchapp/src/main/AndroidManifest.xml create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/Theme.kt create mode 100644 watchapp/src/main/res/drawable/splash_icon.xml create mode 100644 watchapp/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 watchapp/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 watchapp/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 watchapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 watchapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 watchapp/src/main/res/values-round/strings.xml create mode 100644 watchapp/src/main/res/values/strings.xml create mode 100644 watchapp/src/main/res/values/styles.xml diff --git a/gradle.properties b/gradle.properties index a00c9439b..ef1e9b7ea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,27 +1,20 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html +# # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m -Dfile.encoding=UTF-8 +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# # When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app"s APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true +#Mon Dec 02 22:06:10 ICT 2024 android.defaults.buildfeatures.buildconfig=true +android.enableJetifier=true android.nonFinalResIds=false +android.nonTransitiveRClass=true +android.useAndroidX=true +kotlin.code.style=official +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d936546e6..aef266a5d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,11 @@ workRuntimeKtx = "2.8.1" appcompat = "1.7.0" material = "1.12.0" gson = "2.11.0" +playServicesWearable = "18.2.0" +composeBom = "2023.08.00" +composeMaterial = "1.2.1" +composeFoundation = "1.2.1" +coreSplashscreen = "1.0.1" [libraries] ark-about = { module = "dev.arkbuilders.components:about", version.ref = "arkAbout" } @@ -85,7 +90,13 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } +play-services-wearable = { group = "com.google.android.gms", name = "play-services-wearable", version.ref = "playServicesWearable" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" } +androidx-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "composeFoundation" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } [plugins] android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"} +androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/settings.gradle b/settings.gradle index faefe68e9..899a2aee0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,3 +39,4 @@ include ':feature:pairalert' include ':feature:quickwidget' include ':feature:search' include ':feature:settings' +include ':watchapp' diff --git a/watchapp/.gitignore b/watchapp/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/watchapp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts new file mode 100644 index 000000000..57a01bdb9 --- /dev/null +++ b/watchapp/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + id("com.android.application") version "8.1.3" + id("org.jetbrains.kotlin.android") +} +android { + namespace = "dev.arkbuilders.rate.watchapp" + compileSdk = 34 + + defaultConfig { + applicationId = "dev.arkbuilders.rate.watchapp" + minSdk = 29 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + vectorDrawables { + useSupportLibrary = true + } + + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.8" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(project(":cryptoicons")) + implementation(project(":fiaticons")) + implementation(project(":feature:quick")) + implementation(project(":core:domain")) + implementation(project(":core:presentation")) + + implementation(libs.play.services.wearable) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.compose.material) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.core.splashscreen) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} diff --git a/watchapp/lint.xml b/watchapp/lint.xml new file mode 100644 index 000000000..44fac75b8 --- /dev/null +++ b/watchapp/lint.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/watchapp/proguard-rules.pro b/watchapp/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/watchapp/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/watchapp/src/main/AndroidManifest.xml b/watchapp/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8547e121e --- /dev/null +++ b/watchapp/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt new file mode 100644 index 000000000..02bc6090a --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -0,0 +1,73 @@ +/* While this template provides a good starting point for using Wear Compose, you can always + * take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter and + * https://github.com/android/wear-os-samples/tree/main/ComposeAdvanced to find the most up to date + * changes to the libraries and their usages. + */ + +package dev.arkbuilders.rate.watchapp.presentation + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.compose.material.MaterialTheme +import androidx.wear.compose.material.Text +import androidx.wear.compose.material.TimeText +import dev.arkbuilders.rate.watchapp.R +import dev.arkbuilders.rate.watchapp.presentation.theme.ArkrateTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + + super.onCreate(savedInstanceState) + + setTheme(android.R.style.Theme_DeviceDefault) + + setContent { + WearApp("Android") + } + } +} + +@Composable +fun WearApp(greetingName: String) { + ArkrateTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colors.background), + contentAlignment = Alignment.Center + ) { + TimeText() + Greeting(greetingName = greetingName) + } + } +} + +@Composable +fun Greeting(greetingName: String) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary, + text = stringResource(R.string.hello_world, greetingName) + ) +} + +@Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true) +@Composable +fun DefaultPreview() { + WearApp("Preview Android") +} \ No newline at end of file diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt new file mode 100644 index 000000000..6361b095b --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt @@ -0,0 +1,107 @@ +package dev.arkbuilders.rate.watchapp.presentation.quickpairs + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.domain.CurrUtils +import dev.arkbuilders.rate.core.domain.model.Amount +import dev.arkbuilders.rate.core.domain.model.CurrencyCode +import dev.arkbuilders.rate.core.presentation.utils.IconUtils +import dev.arkbuilders.rate.feature.quick.domain.model.PinnedQuickPair +import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair +import java.math.BigDecimal +import java.time.OffsetDateTime + +@Composable +fun QuickPairItem( + modifier: Modifier = Modifier, + quick: PinnedQuickPair, +) { + Column( + modifier = modifier + .padding(12.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row(modifier = modifier.align(Alignment.Start)) { + CurrIcon( + modifier = modifier.size(16.dp), + code = quick.pair.from + ) + CurrIcon( + modifier = modifier.size(16.dp), + code = quick.pair.to.first().code + ) + } + Text( + modifier = modifier.fillMaxWidth(), + text = "${quick.pair.from} to ${ + quick.pair.to.joinToString( + separator = ", ", + ) { it.code } + }", + ) + Text( + modifier = modifier.fillMaxWidth(), + text = + "${CurrUtils.prepareToDisplay(quick.pair.amount)} ${quick.pair.from} = " + + "${CurrUtils.prepareToDisplay(quick.actualTo.first().value)} ${quick.actualTo.first().code}", + ) + Text( + modifier = modifier.fillMaxWidth(), + text = "${CurrUtils.prepareToDisplay(quick.pair.amount)} ${quick.actualTo.first().code}", + ) + } +} + +@Composable +fun CurrIcon( + modifier: Modifier = Modifier, + code: CurrencyCode, +) { + val ctx = LocalContext.current + Icon( + modifier = modifier, + painter = painterResource(id = IconUtils.iconForCurrCode(ctx, code)), + contentDescription = code, + tint = Color.Unspecified, + ) +} + + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun QuickPairItemPreview() { + QuickPairItem( + quick = PinnedQuickPair( + pair = QuickPair( + id = 1, + from = "BTC", + amount = BigDecimal.valueOf(1.2), + to = listOf(Amount("USD", BigDecimal.valueOf(12.0))), + calculatedDate = OffsetDateTime.now(), + pinnedDate = null, + group = null + ), + actualTo = listOf(Amount("USD", BigDecimal.valueOf(12.0))), + refreshDate = OffsetDateTime.now(), + ) + ) +} + diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt new file mode 100644 index 000000000..cbb571f80 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -0,0 +1,8 @@ +package dev.arkbuilders.rate.watchapp.presentation.quickpairs + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun QuickPairsScreen(modifier: Modifier = Modifier ) { +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/Theme.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/Theme.kt new file mode 100644 index 000000000..370c5c7ff --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/Theme.kt @@ -0,0 +1,17 @@ +package dev.arkbuilders.rate.watchapp.presentation.theme + +import androidx.compose.runtime.Composable +import androidx.wear.compose.material.MaterialTheme + +@Composable +fun ArkrateTheme( + content: @Composable () -> Unit +) { + /** + * Empty theme to customize for your app. + * See: https://developer.android.com/jetpack/compose/designsystems/custom + */ + MaterialTheme( + content = content + ) +} \ No newline at end of file diff --git a/watchapp/src/main/res/drawable/splash_icon.xml b/watchapp/src/main/res/drawable/splash_icon.xml new file mode 100644 index 000000000..7874e83f0 --- /dev/null +++ b/watchapp/src/main/res/drawable/splash_icon.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/watchapp/src/main/res/mipmap-hdpi/ic_launcher.webp b/watchapp/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/watchapp/src/main/res/mipmap-mdpi/ic_launcher.webp b/watchapp/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!To6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/watchapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/watchapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..28d4b77f9f036a47549d47db79c16788749dca10 GIT binary patch literal 2884 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 + From the Round world,\nHello, %1$s! + \ No newline at end of file diff --git a/watchapp/src/main/res/values/strings.xml b/watchapp/src/main/res/values/strings.xml new file mode 100644 index 000000000..7da1efdcb --- /dev/null +++ b/watchapp/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + watchapp + + From the Square world,\nHello, %1$s! + \ No newline at end of file diff --git a/watchapp/src/main/res/values/styles.xml b/watchapp/src/main/res/values/styles.xml new file mode 100644 index 000000000..85dec6d67 --- /dev/null +++ b/watchapp/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + From 85f55d7ca797a277d5346defe1baefa5f09f3150 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 21 Dec 2024 22:11:00 +0700 Subject: [PATCH 02/57] Implement quick pair item --- .../presentation/quickpairs/QuickPairItem.kt | 107 ++++++++++++++---- 1 file changed, 84 insertions(+), 23 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt index 6361b095b..644a24913 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt @@ -1,26 +1,37 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs +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.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable +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.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.wear.compose.material.Icon import androidx.wear.compose.material.Text import dev.arkbuilders.rate.core.domain.CurrUtils import dev.arkbuilders.rate.core.domain.model.Amount import dev.arkbuilders.rate.core.domain.model.CurrencyCode +import dev.arkbuilders.rate.core.presentation.theme.ArkColor import dev.arkbuilders.rate.core.presentation.utils.IconUtils import dev.arkbuilders.rate.feature.quick.domain.model.PinnedQuickPair import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair @@ -32,6 +43,9 @@ fun QuickPairItem( modifier: Modifier = Modifier, quick: PinnedQuickPair, ) { + var isExpanded by remember { + mutableStateOf(true) + } Column( modifier = modifier .padding(12.dp) @@ -44,29 +58,73 @@ fun QuickPairItem( modifier = modifier.size(16.dp), code = quick.pair.from ) - CurrIcon( - modifier = modifier.size(16.dp), - code = quick.pair.to.first().code + if (quick.pair.to.size > 1) { + Box( + modifier = + modifier + .size(16.dp) + .background(ArkColor.BGTertiary, CircleShape), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = "+ ${quick.pair.to.size}", + fontWeight = FontWeight.SemiBold, + fontSize = 8.sp, + color = ArkColor.TextTertiary, + ) + } + } else { + CurrIcon( + modifier = modifier.size(16.dp), + code = quick.pair.to.first().code + ) + } + + + Text( + text = "2 mins ago", + modifier = modifier.fillMaxWidth(), + textAlign = TextAlign.End + ) + } + if (isExpanded) { + Text( + modifier = modifier.fillMaxWidth(), + text = "${CurrUtils.prepareToDisplay(quick.pair.amount)} ${quick.pair.from} = ", + ) + quick.pair.to.forEach { + Row( + modifier = Modifier.padding(top = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CurrIcon( + modifier = Modifier.size(20.dp), + code = it.code + ) + Text( + modifier = modifier + .fillMaxWidth() + .padding(start = 8.dp), + text = "${CurrUtils.prepareToDisplay(it.value)} ${it.code}", + ) + } + } + } else { + Text( + modifier = modifier.fillMaxWidth(), + text = "${quick.pair.from} to ${ + quick.pair.to.joinToString( + separator = ", ", + ) { it.code } + }", + ) + Text( + modifier = modifier.fillMaxWidth(), + text = + "${CurrUtils.prepareToDisplay(quick.pair.amount)} ${quick.pair.from} = " + + "${CurrUtils.prepareToDisplay(quick.pair.to.first().value)} ${quick.pair.to.first().code}", ) } - Text( - modifier = modifier.fillMaxWidth(), - text = "${quick.pair.from} to ${ - quick.pair.to.joinToString( - separator = ", ", - ) { it.code } - }", - ) - Text( - modifier = modifier.fillMaxWidth(), - text = - "${CurrUtils.prepareToDisplay(quick.pair.amount)} ${quick.pair.from} = " + - "${CurrUtils.prepareToDisplay(quick.actualTo.first().value)} ${quick.actualTo.first().code}", - ) - Text( - modifier = modifier.fillMaxWidth(), - text = "${CurrUtils.prepareToDisplay(quick.pair.amount)} ${quick.actualTo.first().code}", - ) } } @@ -94,12 +152,15 @@ fun QuickPairItemPreview() { id = 1, from = "BTC", amount = BigDecimal.valueOf(1.2), - to = listOf(Amount("USD", BigDecimal.valueOf(12.0))), + to = listOf( + Amount("USD", BigDecimal.valueOf(12.0)), + Amount("EUR", BigDecimal.valueOf(12.0)) + ), calculatedDate = OffsetDateTime.now(), pinnedDate = null, group = null ), - actualTo = listOf(Amount("USD", BigDecimal.valueOf(12.0))), + actualTo = listOf(Amount("USD", BigDecimal.valueOf(12.0))), refreshDate = OffsetDateTime.now(), ) ) From 604e0e2f757db3984c7f3967b837150226eb5f0e Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 22 Dec 2024 22:07:24 +0700 Subject: [PATCH 03/57] Update quick pair class --- .../presentation/quickpairs/QuickPairItem.kt | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt index 644a24913..df723531b 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt @@ -41,7 +41,7 @@ import java.time.OffsetDateTime @Composable fun QuickPairItem( modifier: Modifier = Modifier, - quick: PinnedQuickPair, + quick: QuickPair, ) { var isExpanded by remember { mutableStateOf(true) @@ -56,9 +56,9 @@ fun QuickPairItem( Row(modifier = modifier.align(Alignment.Start)) { CurrIcon( modifier = modifier.size(16.dp), - code = quick.pair.from + code = quick.from ) - if (quick.pair.to.size > 1) { + if (quick.to.size > 1) { Box( modifier = modifier @@ -67,7 +67,7 @@ fun QuickPairItem( ) { Text( modifier = Modifier.align(Alignment.Center), - text = "+ ${quick.pair.to.size}", + text = "+ ${quick.to.size}", fontWeight = FontWeight.SemiBold, fontSize = 8.sp, color = ArkColor.TextTertiary, @@ -76,7 +76,7 @@ fun QuickPairItem( } else { CurrIcon( modifier = modifier.size(16.dp), - code = quick.pair.to.first().code + code = quick.to.first().code ) } @@ -90,9 +90,9 @@ fun QuickPairItem( if (isExpanded) { Text( modifier = modifier.fillMaxWidth(), - text = "${CurrUtils.prepareToDisplay(quick.pair.amount)} ${quick.pair.from} = ", + text = "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = ", ) - quick.pair.to.forEach { + quick.to.forEach { Row( modifier = Modifier.padding(top = 8.dp), verticalAlignment = Alignment.CenterVertically, @@ -112,8 +112,8 @@ fun QuickPairItem( } else { Text( modifier = modifier.fillMaxWidth(), - text = "${quick.pair.from} to ${ - quick.pair.to.joinToString( + text = "${quick.from} to ${ + quick.to.joinToString( separator = ", ", ) { it.code } }", @@ -121,8 +121,8 @@ fun QuickPairItem( Text( modifier = modifier.fillMaxWidth(), text = - "${CurrUtils.prepareToDisplay(quick.pair.amount)} ${quick.pair.from} = " + - "${CurrUtils.prepareToDisplay(quick.pair.to.first().value)} ${quick.pair.to.first().code}", + "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = " + + "${CurrUtils.prepareToDisplay(quick.to.first().value)} ${quick.to.first().code}", ) } } @@ -147,21 +147,17 @@ fun CurrIcon( @Composable fun QuickPairItemPreview() { QuickPairItem( - quick = PinnedQuickPair( - pair = QuickPair( - id = 1, - from = "BTC", - amount = BigDecimal.valueOf(1.2), - to = listOf( - Amount("USD", BigDecimal.valueOf(12.0)), - Amount("EUR", BigDecimal.valueOf(12.0)) - ), - calculatedDate = OffsetDateTime.now(), - pinnedDate = null, - group = null + quick = QuickPair( + id = 1, + from = "BTC", + amount = BigDecimal.valueOf(1.2), + to = listOf( + Amount("USD", BigDecimal.valueOf(12.0)), + Amount("EUR", BigDecimal.valueOf(12.0)) ), - actualTo = listOf(Amount("USD", BigDecimal.valueOf(12.0))), - refreshDate = OffsetDateTime.now(), + calculatedDate = OffsetDateTime.now(), + pinnedDate = null, + group = null ) ) } From 9566b52d21e15ac6136b3f6da08760a86eb8aff3 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 22 Dec 2024 22:27:35 +0700 Subject: [PATCH 04/57] Update composables --- .../quickpairs/composables/QuickPairsEmpty.kt | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt new file mode 100644 index 000000000..35e3cf7d1 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt @@ -0,0 +1,59 @@ +package dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.CoreRDrawable +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +@Composable +fun QuickPairsEmpty( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = CoreRDrawable.ic_empty_quick), + contentDescription = "", + tint = Color.Unspecified, + ) + Text( + modifier = Modifier.padding(top = 16.dp), + text = "Empty Here, But Full of Possibilities!", + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + ) + Text( + modifier = Modifier.padding(top = 6.dp, start = 24.dp, end = 24.dp), + text = "Calculate currency from Rate App", + fontSize = 14.sp, + lineHeight = 20.sp, + color = ArkColor.TextTertiary, + textAlign = TextAlign.Center, + ) + + } +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun QuickPairItemPreview() { + QuickPairsEmpty() +} From 21c8768833ab8cae9dd0e9c1ab609afe16cc56b8 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 22 Dec 2024 22:49:11 +0700 Subject: [PATCH 05/57] Tweak UI --- watchapp/build.gradle.kts | 5 ++--- .../quickpairs/composables/QuickPairsEmpty.kt | 13 ++++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts index 57a01bdb9..5bc638c8c 100644 --- a/watchapp/build.gradle.kts +++ b/watchapp/build.gradle.kts @@ -4,12 +4,11 @@ plugins { } android { namespace = "dev.arkbuilders.rate.watchapp" - compileSdk = 34 + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { applicationId = "dev.arkbuilders.rate.watchapp" - minSdk = 29 - targetSdk = 34 + minSdk = libs.versions.minSdk.get().toInt() versionCode = 1 versionName = "1.0" vectorDrawables { diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt index 35e3cf7d1..db2cca03a 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt @@ -3,6 +3,7 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -35,16 +36,18 @@ fun QuickPairsEmpty( tint = Color.Unspecified, ) Text( - modifier = Modifier.padding(top = 16.dp), + modifier = modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), text = "Empty Here, But Full of Possibilities!", fontWeight = FontWeight.SemiBold, - fontSize = 20.sp, + fontSize = 16.sp, + textAlign = TextAlign.Center, ) Text( - modifier = Modifier.padding(top = 6.dp, start = 24.dp, end = 24.dp), + modifier = modifier.padding(start = 24.dp, end = 24.dp), text = "Calculate currency from Rate App", - fontSize = 14.sp, - lineHeight = 20.sp, + fontSize = 12.sp, color = ArkColor.TextTertiary, textAlign = TextAlign.Center, ) From f7f79796960a5fdbfee330bc45751e296a8e14cd Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 25 Dec 2024 22:43:47 +0700 Subject: [PATCH 06/57] Tweak spacing --- .../presentation/quickpairs/QuickPairItem.kt | 164 ----------------- .../quickpairs/composables/QuickPairItem.kt | 166 ++++++++++++++++++ 2 files changed, 166 insertions(+), 164 deletions(-) delete mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt deleted file mode 100644 index df723531b..000000000 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairItem.kt +++ /dev/null @@ -1,164 +0,0 @@ -package dev.arkbuilders.rate.watchapp.presentation.quickpairs - -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.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -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.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.wear.compose.material.Icon -import androidx.wear.compose.material.Text -import dev.arkbuilders.rate.core.domain.CurrUtils -import dev.arkbuilders.rate.core.domain.model.Amount -import dev.arkbuilders.rate.core.domain.model.CurrencyCode -import dev.arkbuilders.rate.core.presentation.theme.ArkColor -import dev.arkbuilders.rate.core.presentation.utils.IconUtils -import dev.arkbuilders.rate.feature.quick.domain.model.PinnedQuickPair -import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair -import java.math.BigDecimal -import java.time.OffsetDateTime - -@Composable -fun QuickPairItem( - modifier: Modifier = Modifier, - quick: QuickPair, -) { - var isExpanded by remember { - mutableStateOf(true) - } - Column( - modifier = modifier - .padding(12.dp) - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row(modifier = modifier.align(Alignment.Start)) { - CurrIcon( - modifier = modifier.size(16.dp), - code = quick.from - ) - if (quick.to.size > 1) { - Box( - modifier = - modifier - .size(16.dp) - .background(ArkColor.BGTertiary, CircleShape), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = "+ ${quick.to.size}", - fontWeight = FontWeight.SemiBold, - fontSize = 8.sp, - color = ArkColor.TextTertiary, - ) - } - } else { - CurrIcon( - modifier = modifier.size(16.dp), - code = quick.to.first().code - ) - } - - - Text( - text = "2 mins ago", - modifier = modifier.fillMaxWidth(), - textAlign = TextAlign.End - ) - } - if (isExpanded) { - Text( - modifier = modifier.fillMaxWidth(), - text = "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = ", - ) - quick.to.forEach { - Row( - modifier = Modifier.padding(top = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - CurrIcon( - modifier = Modifier.size(20.dp), - code = it.code - ) - Text( - modifier = modifier - .fillMaxWidth() - .padding(start = 8.dp), - text = "${CurrUtils.prepareToDisplay(it.value)} ${it.code}", - ) - } - } - } else { - Text( - modifier = modifier.fillMaxWidth(), - text = "${quick.from} to ${ - quick.to.joinToString( - separator = ", ", - ) { it.code } - }", - ) - Text( - modifier = modifier.fillMaxWidth(), - text = - "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = " + - "${CurrUtils.prepareToDisplay(quick.to.first().value)} ${quick.to.first().code}", - ) - } - } -} - -@Composable -fun CurrIcon( - modifier: Modifier = Modifier, - code: CurrencyCode, -) { - val ctx = LocalContext.current - Icon( - modifier = modifier, - painter = painterResource(id = IconUtils.iconForCurrCode(ctx, code)), - contentDescription = code, - tint = Color.Unspecified, - ) -} - - -@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) -@Composable -fun QuickPairItemPreview() { - QuickPairItem( - quick = QuickPair( - id = 1, - from = "BTC", - amount = BigDecimal.valueOf(1.2), - to = listOf( - Amount("USD", BigDecimal.valueOf(12.0)), - Amount("EUR", BigDecimal.valueOf(12.0)) - ), - calculatedDate = OffsetDateTime.now(), - pinnedDate = null, - group = null - ) - ) -} - diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt new file mode 100644 index 000000000..e1c4162bd --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt @@ -0,0 +1,166 @@ +package dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables + +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.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +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.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material.Card +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.domain.CurrUtils +import dev.arkbuilders.rate.core.domain.model.Amount +import dev.arkbuilders.rate.core.domain.model.CurrencyCode +import dev.arkbuilders.rate.core.presentation.theme.ArkColor +import dev.arkbuilders.rate.core.presentation.utils.IconUtils +import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair +import java.math.BigDecimal +import java.time.OffsetDateTime + +@Composable +fun QuickPairItem( + modifier: Modifier = Modifier, + quick: QuickPair, +) { + var isExpanded by remember { + mutableStateOf(true) + } + Card( + onClick = { /*TODO*/ }, + modifier = modifier + .padding(horizontal = 12.dp, vertical = 4.dp) + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row(modifier = modifier.align(Alignment.Start)) { + CurrIcon( + modifier = modifier.size(16.dp), + code = quick.from + ) + if (quick.to.size > 1) { + Box( + modifier = + modifier + .size(16.dp) + .background(ArkColor.BGTertiary, CircleShape), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = "+ ${quick.to.size}", + fontWeight = FontWeight.SemiBold, + fontSize = 8.sp, + color = ArkColor.TextTertiary, + ) + } + } else { + CurrIcon( + modifier = modifier.size(16.dp), + code = quick.to.first().code + ) + } + + + Text( + text = "2 mins ago", + modifier = modifier.fillMaxWidth(), + textAlign = TextAlign.End + ) + } + if (isExpanded) { + Text( + modifier = modifier.fillMaxWidth(), + text = "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = ", + ) + quick.to.forEach { + Row( + modifier = Modifier.padding(top = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CurrIcon( + modifier = Modifier.size(20.dp), + code = it.code + ) + Text( + modifier = modifier + .fillMaxWidth() + .padding(start = 8.dp), + text = "${CurrUtils.prepareToDisplay(it.value)} ${it.code}", + ) + } + } + } else { + Text( + modifier = modifier.fillMaxWidth(), + text = "${quick.from} to ${ + quick.to.joinToString( + separator = ", ", + ) { it.code } + }", + ) + Text( + modifier = modifier.fillMaxWidth(), + text = + "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = " + + "${CurrUtils.prepareToDisplay(quick.to.first().value)} ${quick.to.first().code}", + ) + } + } + } +} + +@Composable +fun CurrIcon( + modifier: Modifier = Modifier, + code: CurrencyCode, +) { + val ctx = LocalContext.current + Icon( + modifier = modifier, + painter = painterResource(id = IconUtils.iconForCurrCode(ctx, code)), + contentDescription = code, + tint = Color.Unspecified, + ) +} + + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun QuickPairItemPreview() { + QuickPairItem( + quick = QuickPair( + id = 1, + from = "BTC", + amount = BigDecimal.valueOf(1.2), + to = listOf( + Amount("USD", BigDecimal.valueOf(12.0)), + Amount("EUR", BigDecimal.valueOf(12.0)) + ), + calculatedDate = OffsetDateTime.now(), + pinnedDate = null, + group = null + ) + ) +} + From 67d32154a47274ac2213382edeffbc6c37a41c03 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 25 Dec 2024 22:44:54 +0700 Subject: [PATCH 07/57] Display empty and quick pairs list --- .../quickpairs/QuickPairsScreen.kt | 26 ++++++++++++++++++- .../quickpairs/composables/QuickPairsEmpty.kt | 10 +++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt index cbb571f80..43759171c 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -1,8 +1,32 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairItem +import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairsEmpty @Composable -fun QuickPairsScreen(modifier: Modifier = Modifier ) { +fun QuickPairsScreen( + modifier: Modifier = Modifier, + viewModel: QuickPairsViewModel = QuickPairsViewModel() +) { + val quickPairsList = viewModel.quickPairs.collectAsState().value + + if (quickPairsList.isEmpty()) { + QuickPairsEmpty(modifier = modifier.fillMaxSize()) + } else { + LazyColumn(modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(4.dp) + ) { + items(quickPairsList) { quickPair -> + QuickPairItem(quick = quickPair) + } + } + } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt index db2cca03a..941b829e3 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt @@ -1,6 +1,5 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -19,16 +18,13 @@ import androidx.compose.ui.unit.sp import androidx.wear.compose.material.Icon import androidx.wear.compose.material.Text import dev.arkbuilders.rate.core.presentation.CoreRDrawable -import dev.arkbuilders.rate.core.presentation.theme.ArkColor @Composable fun QuickPairsEmpty( modifier: Modifier = Modifier ) { Column( - modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center ) { Icon( painter = painterResource(id = CoreRDrawable.ic_empty_quick), @@ -45,10 +41,10 @@ fun QuickPairsEmpty( textAlign = TextAlign.Center, ) Text( - modifier = modifier.padding(start = 24.dp, end = 24.dp), + modifier = modifier.fillMaxWidth() + .padding(horizontal = 8.dp), text = "Calculate currency from Rate App", fontSize = 12.sp, - color = ArkColor.TextTertiary, textAlign = TextAlign.Center, ) @@ -57,6 +53,6 @@ fun QuickPairsEmpty( @Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) @Composable -fun QuickPairItemPreview() { +fun QuickPairEmptyPreview() { QuickPairsEmpty() } From 7726f4eb54accf9d34685fc17c1e65a8c3634d1d Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 25 Dec 2024 22:45:07 +0700 Subject: [PATCH 08/57] Display empty and quick pairs list --- .../watchapp/presentation/MainActivity.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index 02bc6090a..11bf93fa4 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -1,9 +1,3 @@ -/* While this template provides a good starting point for using Wear Compose, you can always - * take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter and - * https://github.com/android/wear-os-samples/tree/main/ComposeAdvanced to find the most up to date - * changes to the libraries and their usages. - */ - package dev.arkbuilders.rate.watchapp.presentation import android.os.Bundle @@ -22,9 +16,13 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.wear.compose.material.MaterialTheme +import androidx.wear.compose.material.Scaffold import androidx.wear.compose.material.Text import androidx.wear.compose.material.TimeText +import androidx.wear.compose.material.Vignette +import androidx.wear.compose.material.VignettePosition import dev.arkbuilders.rate.watchapp.R +import dev.arkbuilders.rate.watchapp.presentation.quickpairs.QuickPairsScreen import dev.arkbuilders.rate.watchapp.presentation.theme.ArkrateTheme class MainActivity : ComponentActivity() { @@ -36,7 +34,14 @@ class MainActivity : ComponentActivity() { setTheme(android.R.style.Theme_DeviceDefault) setContent { - WearApp("Android") + Scaffold( + vignette = { + Vignette(vignettePosition = VignettePosition.TopAndBottom) + } + ) { + QuickPairsScreen() + } + } } } @@ -70,4 +75,4 @@ fun Greeting(greetingName: String) { @Composable fun DefaultPreview() { WearApp("Preview Android") -} \ No newline at end of file +} From 1bbef9acf6e7008df10a130bc58aa2aa46bbb60c Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 25 Dec 2024 22:45:20 +0700 Subject: [PATCH 09/57] Create viewmodel with fake data --- .../quickpairs/QuickPairsViewModel.kt | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt new file mode 100644 index 000000000..2dd8f0152 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt @@ -0,0 +1,97 @@ +package dev.arkbuilders.rate.watchapp.presentation.quickpairs + +import androidx.lifecycle.ViewModel +import dev.arkbuilders.rate.core.domain.model.Amount +import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import java.math.BigDecimal +import java.time.OffsetDateTime + +class QuickPairsViewModel : ViewModel() { + + private val _quickPairs: MutableStateFlow> = MutableStateFlow(listOf()) + val quickPairs: StateFlow> = _quickPairs + + init { + val a = listOf( + QuickPair( + id = 1, + from = "BTC", + amount = BigDecimal.valueOf(1.2), + to = listOf( + Amount("USD", BigDecimal.valueOf(12.0)), + Amount("EUR", BigDecimal.valueOf(12.0)) + ), + calculatedDate = OffsetDateTime.now(), + pinnedDate = null, + group = null + ), + QuickPair( + id = 1, + from = "BTC", + amount = BigDecimal.valueOf(1.2), + to = listOf( + Amount("USD", BigDecimal.valueOf(12.0)), + Amount("EUR", BigDecimal.valueOf(12.0)) + ), + calculatedDate = OffsetDateTime.now(), + pinnedDate = null, + group = null + ), + + QuickPair( + id = 1, + from = "BTC", + amount = BigDecimal.valueOf(1.2), + to = listOf( + Amount("USD", BigDecimal.valueOf(12.0)), + Amount("EUR", BigDecimal.valueOf(12.0)) + ), + calculatedDate = OffsetDateTime.now(), + pinnedDate = null, + group = null + ), + + QuickPair( + id = 1, + from = "BTC", + amount = BigDecimal.valueOf(1.2), + to = listOf( + Amount("USD", BigDecimal.valueOf(12.0)), + Amount("EUR", BigDecimal.valueOf(12.0)) + ), + calculatedDate = OffsetDateTime.now(), + pinnedDate = null, + group = null + ), + QuickPair( + id = 1, + from = "BTC", + amount = BigDecimal.valueOf(1.2), + to = listOf( + Amount("USD", BigDecimal.valueOf(12.0)), + Amount("EUR", BigDecimal.valueOf(12.0)) + ), + calculatedDate = OffsetDateTime.now(), + pinnedDate = null, + group = null + ), + + QuickPair( + id = 1, + from = "BTC", + amount = BigDecimal.valueOf(1.2), + to = listOf( + Amount("USD", BigDecimal.valueOf(12.0)), + Amount("EUR", BigDecimal.valueOf(12.0)) + ), + calculatedDate = OffsetDateTime.now(), + pinnedDate = null, + group = null + ) + ) + _quickPairs.value = a + } + +} From 52a9aafd6414a6d549384498e0ade81e6e1e911d Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Thu, 26 Dec 2024 23:13:49 +0700 Subject: [PATCH 10/57] Create option item --- .../addquickpairs/composables/OptionItem.kt | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt new file mode 100644 index 000000000..28ef67c80 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt @@ -0,0 +1,76 @@ +package dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables + +import androidx.compose.foundation.BorderStroke +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material.ButtonDefaults +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.OutlinedButton +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.CoreRDrawable +import dev.arkbuilders.rate.core.presentation.CoreRString +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +@Composable +fun OptionItem( + modifier: Modifier = Modifier, + icon: Painter, + text: String, + onClick: () -> Unit, +) { + OutlinedButton( + modifier = modifier + .fillMaxWidth() + .padding(8.dp), + onClick = onClick, + shape = RoundedCornerShape(20), + border = ButtonDefaults.buttonBorder(BorderStroke(1.dp, ArkColor.Border)), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White), + ) { + Row( + modifier = Modifier.fillMaxWidth().align(Alignment.Center), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = icon, + contentDescription = "", + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "text", + fontWeight = FontWeight.SemiBold, + ) + } + } +} + + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun OptionItemPreview(modifier: Modifier = Modifier) { + OptionItem( + modifier = modifier.fillMaxWidth(), + icon = painterResource(id = CoreRDrawable.ic_download), + text = stringResource(id = CoreRString.edit), + onClick = {}, + ) +} From c63aa8e14b7fe71531a45f9b1900b8b2b0bc8d07 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Thu, 26 Dec 2024 23:14:02 +0700 Subject: [PATCH 11/57] Implement add quick pair screen --- .../addquickpairs/AddQuickPairs.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt new file mode 100644 index 000000000..67ecdbdc7 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt @@ -0,0 +1,48 @@ +package dev.arkbuilders.rate.watchapp.presentation.addquickpairs + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.CoreRDrawable +import dev.arkbuilders.rate.core.presentation.CoreRString +import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables.OptionItem + +@Composable +fun AddQuickPairs(modifier: Modifier = Modifier) { + Scaffold(modifier = modifier.fillMaxSize()) { + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center + ) { + Text( + modifier = modifier.fillMaxWidth(), + text = "Options", + textAlign = TextAlign.Center + ) + OptionItem( + modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally), + icon = painterResource(id = CoreRDrawable.ic_download), + text = stringResource(id = CoreRString.edit), + onClick = {}, + ) + } + + } +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun AddQuickPairsPreview() { + AddQuickPairs() +} From 63fbd61850195e597b5470f8abcf68e990ce803b Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Thu, 26 Dec 2024 23:14:09 +0700 Subject: [PATCH 12/57] Create viewmodel --- .../presentation/addquickpairs/AddQuickPairsViewModel.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt new file mode 100644 index 000000000..1b95d3563 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt @@ -0,0 +1,7 @@ +package dev.arkbuilders.rate.watchapp.presentation.addquickpairs + +import androidx.lifecycle.ViewModel + +class AddQuickPairsViewModel : ViewModel() { + +} From eb9dd008233e14bff16eac8f6e842f15d3dd1b0e Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 28 Dec 2024 22:28:50 +0700 Subject: [PATCH 13/57] Implement options screen --- .../src/main/res/drawable/ic_update.xml | 13 ++++ .../src/main/res/values/strings.xml | 2 + .../watchapp/presentation/MainActivity.kt | 4 +- .../addquickpairs/AddQuickPairs.kt | 62 ++++++++++++++++--- .../addquickpairs/composables/OptionItem.kt | 16 +++-- 5 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 core/presentation/src/main/res/drawable/ic_update.xml diff --git a/core/presentation/src/main/res/drawable/ic_update.xml b/core/presentation/src/main/res/drawable/ic_update.xml new file mode 100644 index 000000000..b8df7f914 --- /dev/null +++ b/core/presentation/src/main/res/drawable/ic_update.xml @@ -0,0 +1,13 @@ + + + diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 36cb9383f..8e09573f8 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -146,6 +146,8 @@ Delete Re-use Edit + Update + Options Oops, request time out! Please check your connection and refresh the page to try again. diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index 11bf93fa4..a92422201 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -22,6 +22,7 @@ import androidx.wear.compose.material.TimeText import androidx.wear.compose.material.Vignette import androidx.wear.compose.material.VignettePosition import dev.arkbuilders.rate.watchapp.R +import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.AddQuickPairs import dev.arkbuilders.rate.watchapp.presentation.quickpairs.QuickPairsScreen import dev.arkbuilders.rate.watchapp.presentation.theme.ArkrateTheme @@ -39,7 +40,8 @@ class MainActivity : ComponentActivity() { Vignette(vignettePosition = VignettePosition.TopAndBottom) } ) { - QuickPairsScreen() + AddQuickPairs() +// QuickPairsScreen() } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt index 67ecdbdc7..b6a26353a 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -20,24 +21,71 @@ import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables.Opti @Composable fun AddQuickPairs(modifier: Modifier = Modifier) { - Scaffold(modifier = modifier.fillMaxSize()) { - Column( - modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center - ) { + LazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + + Text( modifier = modifier.fillMaxWidth(), text = "Options", textAlign = TextAlign.Center ) + } + item { + + OptionItem( + modifier = Modifier + .fillMaxWidth(), + icon = painterResource(id = CoreRDrawable.ic_update), + text = stringResource(id = CoreRString.update), + onClick = {}, + ) + } + item { + OptionItem( - modifier = Modifier.fillMaxWidth().align(Alignment.CenterHorizontally), - icon = painterResource(id = CoreRDrawable.ic_download), + modifier = Modifier + .fillMaxWidth(), + icon = painterResource(id = CoreRDrawable.ic_pin), + text = stringResource(id = CoreRString.pin), + onClick = {}, + ) + } + + item { + + OptionItem( + modifier = Modifier + .fillMaxWidth(), + icon = painterResource(id = CoreRDrawable.ic_edit), text = stringResource(id = CoreRString.edit), onClick = {}, ) } + item { + OptionItem( + modifier = Modifier + .fillMaxWidth(), + icon = painterResource(id = CoreRDrawable.ic_reuse), + text = stringResource(id = CoreRString.re_use), + onClick = {}, + ) + } + item { + OptionItem( + modifier = Modifier + .fillMaxWidth(), + icon = painterResource(id = CoreRDrawable.ic_delete), + text = stringResource(id = CoreRString.delete), + onClick = {}, + isDeleteButton = true + ) + } } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt index 28ef67c80..714df88e7 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt @@ -35,6 +35,7 @@ fun OptionItem( icon: Painter, text: String, onClick: () -> Unit, + isDeleteButton: Boolean = false, ) { OutlinedButton( modifier = modifier @@ -42,11 +43,18 @@ fun OptionItem( .padding(8.dp), onClick = onClick, shape = RoundedCornerShape(20), - border = ButtonDefaults.buttonBorder(BorderStroke(1.dp, ArkColor.Border)), - colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.White), + border = ButtonDefaults.buttonBorder( + borderStroke = BorderStroke( + width = 1.dp, + color = if (isDeleteButton) Color.Red else ArkColor.Border + ), + ), + colors = ButtonDefaults.outlinedButtonColors(contentColor = if (isDeleteButton) Color.Red else Color.White), ) { Row( - modifier = Modifier.fillMaxWidth().align(Alignment.Center), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { @@ -56,7 +64,7 @@ fun OptionItem( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "text", + text = text, fontWeight = FontWeight.SemiBold, ) } From e0996f4e11ac9f2c81867a64b8c0cfc8a24fe38d Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Tue, 7 Jan 2025 21:48:19 +0700 Subject: [PATCH 14/57] Improve list effect --- gradle/libs.versions.toml | 2 ++ watchapp/build.gradle.kts | 1 + .../presentation/addquickpairs/AddQuickPairs.kt | 11 ++--------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aef266a5d..55c5dc007 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +composeMaterialVersion = "1.4.0" kotlin = "1.9.22" agp = "8.1.3" coreKtx = "1.13.1" @@ -44,6 +45,7 @@ composeFoundation = "1.2.1" coreSplashscreen = "1.0.1" [libraries] +androidx-compose-material-v140 = { module = "androidx.wear.compose:compose-material", version.ref = "composeMaterialVersion" } ark-about = { module = "dev.arkbuilders.components:about", version.ref = "arkAbout" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts index 5bc638c8c..5552ee37f 100644 --- a/watchapp/build.gradle.kts +++ b/watchapp/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(project(":feature:quick")) implementation(project(":core:domain")) implementation(project(":core:presentation")) + implementation(libs.androidx.compose.material.v140) implementation(libs.play.services.wearable) implementation(platform(libs.androidx.compose.bom)) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt index b6a26353a..a68e3e203 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt @@ -1,10 +1,8 @@ package dev.arkbuilders.rate.watchapp.presentation.addquickpairs import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -13,7 +11,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview -import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import androidx.wear.compose.material.Text import dev.arkbuilders.rate.core.presentation.CoreRDrawable import dev.arkbuilders.rate.core.presentation.CoreRString @@ -21,14 +19,12 @@ import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables.Opti @Composable fun AddQuickPairs(modifier: Modifier = Modifier) { - LazyColumn( + ScalingLazyColumn( modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { item { - - Text( modifier = modifier.fillMaxWidth(), text = "Options", @@ -36,7 +32,6 @@ fun AddQuickPairs(modifier: Modifier = Modifier) { ) } item { - OptionItem( modifier = Modifier .fillMaxWidth(), @@ -46,7 +41,6 @@ fun AddQuickPairs(modifier: Modifier = Modifier) { ) } item { - OptionItem( modifier = Modifier .fillMaxWidth(), @@ -57,7 +51,6 @@ fun AddQuickPairs(modifier: Modifier = Modifier) { } item { - OptionItem( modifier = Modifier .fillMaxWidth(), From ae36e647bab2cb4e22595a94ed429adc725cab33 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Tue, 7 Jan 2025 21:49:42 +0700 Subject: [PATCH 15/57] Extract composable and improve imports --- .../arkbuilders/rate/watchapp/presentation/MainActivity.kt | 5 ++--- .../presentation/addquickpairs/composables/OptionItem.kt | 2 -- .../{AddQuickPairs.kt => composables/OptionsMenu.kt} | 7 +++---- 3 files changed, 5 insertions(+), 9 deletions(-) rename watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/{AddQuickPairs.kt => composables/OptionsMenu.kt} (94%) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index a92422201..81c882eb7 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -22,8 +22,7 @@ import androidx.wear.compose.material.TimeText import androidx.wear.compose.material.Vignette import androidx.wear.compose.material.VignettePosition import dev.arkbuilders.rate.watchapp.R -import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.AddQuickPairs -import dev.arkbuilders.rate.watchapp.presentation.quickpairs.QuickPairsScreen +import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables.OptionsMenu import dev.arkbuilders.rate.watchapp.presentation.theme.ArkrateTheme class MainActivity : ComponentActivity() { @@ -40,7 +39,7 @@ class MainActivity : ComponentActivity() { Vignette(vignettePosition = VignettePosition.TopAndBottom) } ) { - AddQuickPairs() + OptionsMenu() // QuickPairsScreen() } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt index 714df88e7..93997adac 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt @@ -6,7 +6,6 @@ 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.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -20,7 +19,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.wear.compose.material.ButtonDefaults import androidx.wear.compose.material.Icon import androidx.wear.compose.material.OutlinedButton diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionsMenu.kt similarity index 94% rename from watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt rename to watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionsMenu.kt index a68e3e203..4c20abdd9 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairs.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionsMenu.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.rate.watchapp.presentation.addquickpairs +package dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxSize @@ -15,10 +15,9 @@ import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import androidx.wear.compose.material.Text import dev.arkbuilders.rate.core.presentation.CoreRDrawable import dev.arkbuilders.rate.core.presentation.CoreRString -import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables.OptionItem @Composable -fun AddQuickPairs(modifier: Modifier = Modifier) { +fun OptionsMenu(modifier: Modifier = Modifier) { ScalingLazyColumn( modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, @@ -85,5 +84,5 @@ fun AddQuickPairs(modifier: Modifier = Modifier) { @Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) @Composable fun AddQuickPairsPreview() { - AddQuickPairs() + OptionsMenu() } From 0c5a42a72ad72748b7f52e4d2c622619414bd261 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Tue, 7 Jan 2025 21:52:12 +0700 Subject: [PATCH 16/57] Improve list effect --- .../presentation/quickpairs/QuickPairsScreen.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt index 43759171c..9d4c59030 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -2,12 +2,11 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairItem import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairsEmpty @@ -21,11 +20,12 @@ fun QuickPairsScreen( if (quickPairsList.isEmpty()) { QuickPairsEmpty(modifier = modifier.fillMaxSize()) } else { - LazyColumn(modifier = modifier.fillMaxSize(), + ScalingLazyColumn( + modifier = modifier.fillMaxSize(), contentPadding = PaddingValues(4.dp) ) { - items(quickPairsList) { quickPair -> - QuickPairItem(quick = quickPair) + items(quickPairsList.size, key = null) { idx -> + QuickPairItem(quick = quickPairsList[idx]) } } } From 3f29554d9ad91b42b53b93b2533a185d97734eca Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Tue, 7 Jan 2025 22:22:03 +0700 Subject: [PATCH 17/57] Set up navigation --- gradle/libs.versions.toml | 4 + watchapp/build.gradle.kts | 2 + .../watchapp/presentation/MainActivity.kt | 82 +++++++----------- .../addquickpairs/AddQuickPairsScreen.kt | 84 +++++++++++++++++++ .../quickpairs/QuickPairsScreen.kt | 5 +- .../quickpairs/composables/QuickPairItem.kt | 6 +- 6 files changed, 126 insertions(+), 57 deletions(-) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55c5dc007..49a3bc24b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,6 +43,8 @@ composeBom = "2023.08.00" composeMaterial = "1.2.1" composeFoundation = "1.2.1" coreSplashscreen = "1.0.1" +material3Android = "1.3.1" +composeNavigation = "1.4.0" [libraries] androidx-compose-material-v140 = { module = "androidx.wear.compose:compose-material", version.ref = "composeMaterialVersion" } @@ -97,6 +99,8 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", versi androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" } androidx-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "composeFoundation" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } +androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" } +androidx-compose-navigation = { group = "androidx.wear.compose", name = "compose-navigation", version.ref = "composeNavigation" } [plugins] android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts index 5552ee37f..cecaf38f9 100644 --- a/watchapp/build.gradle.kts +++ b/watchapp/build.gradle.kts @@ -62,6 +62,8 @@ dependencies { implementation(libs.androidx.compose.foundation) implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.material3.android) + implementation(libs.androidx.compose.navigation) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index 81c882eb7..096c34b65 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -4,25 +4,14 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.Scaffold -import androidx.wear.compose.material.Text -import androidx.wear.compose.material.TimeText import androidx.wear.compose.material.Vignette import androidx.wear.compose.material.VignettePosition -import dev.arkbuilders.rate.watchapp.R -import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables.OptionsMenu +import androidx.wear.compose.navigation.SwipeDismissableNavHost +import androidx.wear.compose.navigation.composable +import androidx.wear.compose.navigation.rememberSwipeDismissableNavController +import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.AddQuickPairsScreen +import dev.arkbuilders.rate.watchapp.presentation.quickpairs.QuickPairsScreen import dev.arkbuilders.rate.watchapp.presentation.theme.ArkrateTheme class MainActivity : ComponentActivity() { @@ -34,46 +23,33 @@ class MainActivity : ComponentActivity() { setTheme(android.R.style.Theme_DeviceDefault) setContent { - Scaffold( - vignette = { - Vignette(vignettePosition = VignettePosition.TopAndBottom) + ArkrateTheme { + val navController = rememberSwipeDismissableNavController() + Scaffold( + vignette = { + Vignette(vignettePosition = VignettePosition.TopAndBottom) + } + ) { + AddQuickPairsScreen() + SwipeDismissableNavHost( + navController = navController, + startDestination = "list" + ) { + composable("list") { + QuickPairsScreen( + onNavigateToAdd = { + navController.navigate("addquickpairs") + } + ) + } + composable("addquickpairs") { + AddQuickPairsScreen( + ) + } + } } - ) { - OptionsMenu() -// QuickPairsScreen() } } } } - -@Composable -fun WearApp(greetingName: String) { - ArkrateTheme { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colors.background), - contentAlignment = Alignment.Center - ) { - TimeText() - Greeting(greetingName = greetingName) - } - } -} - -@Composable -fun Greeting(greetingName: String) { - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - color = MaterialTheme.colors.primary, - text = stringResource(R.string.hello_world, greetingName) - ) -} - -@Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true) -@Composable -fun DefaultPreview() { - WearApp("Preview Android") -} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt new file mode 100644 index 000000000..e5aec70da --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt @@ -0,0 +1,84 @@ +package dev.arkbuilders.rate.watchapp.presentation.addquickpairs + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MenuItemColors +import androidx.compose.runtime.Composable +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.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.material.Text + +@Composable +fun AddQuickPairsScreen(modifier: Modifier = Modifier) { + var expanded by remember { mutableStateOf(false) } + val scrollState = rememberScrollState() + + ScalingLazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + Text( + modifier = modifier.fillMaxWidth(), + text = "Add", + textAlign = TextAlign.Center + ) + } + + item { + Text( + modifier = modifier.fillMaxWidth(), + text = "From", + ) + } + + item { + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + scrollState = scrollState + ) { + repeat(8) { + DropdownMenuItem( + text = { Text("Item ${it + 1}") }, + onClick = { /* TODO */ }, + leadingIcon = { Icon(Icons.Outlined.Edit, contentDescription = null) } + ) + } + } + } + item { + BasicTextField( + modifier = modifier.fillMaxWidth(), + value = "", + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { text -> + }, + ) + } + + } +} + +@Composable +fun AddQuickPairsScreenPreview() { + AddQuickPairsScreen() +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt index 9d4c59030..b03aa98d7 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -13,7 +13,8 @@ import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPa @Composable fun QuickPairsScreen( modifier: Modifier = Modifier, - viewModel: QuickPairsViewModel = QuickPairsViewModel() + viewModel: QuickPairsViewModel = QuickPairsViewModel(), + onNavigateToAdd: () -> Unit ) { val quickPairsList = viewModel.quickPairs.collectAsState().value @@ -25,7 +26,7 @@ fun QuickPairsScreen( contentPadding = PaddingValues(4.dp) ) { items(quickPairsList.size, key = null) { idx -> - QuickPairItem(quick = quickPairsList[idx]) + QuickPairItem(quick = quickPairsList[idx], onClick = onNavigateToAdd) } } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt index e1c4162bd..45302f60b 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt @@ -41,12 +41,13 @@ import java.time.OffsetDateTime fun QuickPairItem( modifier: Modifier = Modifier, quick: QuickPair, + onClick: () -> Unit, ) { var isExpanded by remember { mutableStateOf(true) } Card( - onClick = { /*TODO*/ }, + onClick = onClick, modifier = modifier .padding(horizontal = 12.dp, vertical = 4.dp) ) { @@ -160,7 +161,8 @@ fun QuickPairItemPreview() { calculatedDate = OffsetDateTime.now(), pinnedDate = null, group = null - ) + ), + onClick = {} ) } From 03e816fa2109634637b26be72b52cb695397b451 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Tue, 7 Jan 2025 22:22:59 +0700 Subject: [PATCH 18/57] Tweak UI --- .../presentation/quickpairs/QuickPairsScreen.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt index b03aa98d7..bacc6374d 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -2,11 +2,14 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.material.Text import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairItem import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairsEmpty @@ -25,6 +28,13 @@ fun QuickPairsScreen( modifier = modifier.fillMaxSize(), contentPadding = PaddingValues(4.dp) ) { + item { + Text( + modifier = modifier.fillMaxWidth(), + text = "Options", + textAlign = TextAlign.Center + ) + } items(quickPairsList.size, key = null) { idx -> QuickPairItem(quick = quickPairsList[idx], onClick = onNavigateToAdd) } From 2531ec5cb8a604fd01f5671ab11452fc683d0a8f Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 29 Mar 2025 22:31:58 +0700 Subject: [PATCH 19/57] Create UI --- .../addquickpairs/AddQuickPairsScreen.kt | 56 ++++++++++++++----- .../quickpairs/QuickPairsScreen.kt | 2 +- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt index e5aec70da..0994de524 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt @@ -1,16 +1,25 @@ package dev.arkbuilders.rate.watchapp.presentation.addquickpairs import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.Email +import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MenuDefaults import androidx.compose.material3.MenuItemColors import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -22,6 +31,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import androidx.wear.compose.material.Text @@ -51,21 +62,7 @@ fun AddQuickPairsScreen(modifier: Modifier = Modifier) { } item { - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - scrollState = scrollState - ) { - repeat(8) { - DropdownMenuItem( - text = { Text("Item ${it + 1}") }, - onClick = { /* TODO */ }, - leadingIcon = { Icon(Icons.Outlined.Edit, contentDescription = null) } - ) - } - } - } - item { + BasicTextField( modifier = modifier.fillMaxWidth(), value = "", @@ -75,10 +72,39 @@ fun AddQuickPairsScreen(modifier: Modifier = Modifier) { ) } + item { + Box( + modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart) + ) { + IconButton(onClick = { expanded = true }) { + Icon(Icons.Default.MoreVert, contentDescription = "Localized description") + } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + DropdownMenuItem( + text = { Text("Edit") }, + onClick = { /* Handle edit! */ }, + leadingIcon = { Icon(Icons.Outlined.Edit, contentDescription = null) } + ) + DropdownMenuItem( + text = { Text("Settings") }, + onClick = { /* Handle settings! */ }, + leadingIcon = { Icon(Icons.Outlined.Settings, contentDescription = null) } + ) + DropdownMenuItem( + text = { Text("Send Feedback") }, + onClick = { /* Handle send feedback! */ }, + leadingIcon = { Icon(Icons.Outlined.Email, contentDescription = null) }, + trailingIcon = { Text("F11", textAlign = TextAlign.Center) } + ) + } + } + } + } } @Composable +@Preview fun AddQuickPairsScreenPreview() { AddQuickPairsScreen() } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt index bacc6374d..8002c451e 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -31,7 +31,7 @@ fun QuickPairsScreen( item { Text( modifier = modifier.fillMaxWidth(), - text = "Options", + text = "Quick", textAlign = TextAlign.Center ) } From 58290f381889a8c2ac1c8d89a217653d1a90dd03 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 09:58:36 +0700 Subject: [PATCH 20/57] Create options screen --- build.gradle.kts | 2 +- core/db/build.gradle.kts | 2 +- gradle/libs.versions.toml | 23 +++- settings.gradle | 0 settings.gradle.kts | 4 +- watchapp/build.gradle.kts | 12 +- .../watchapp/presentation/MainActivity.kt | 14 +- .../presentation/options/OptionsScreen.kt | 110 +++++++++++++++ .../presentation/options/WearOptionButton.kt | 129 ++++++++++++++++++ .../options/WearOptionsHomeScreen.kt | 124 +++++++++++++++++ .../presentation/options/WearPageIndicator.kt | 64 +++++++++ .../quickpairs/QuickPairsScreen.kt | 3 +- .../quickpairs/QuickPairsViewModel.kt | 13 +- .../quickpairs/composables/QuickPairItem.kt | 3 +- 14 files changed, 478 insertions(+), 25 deletions(-) delete mode 100644 settings.gradle create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionButton.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionsHomeScreen.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearPageIndicator.kt diff --git a/build.gradle.kts b/build.gradle.kts index 68c93594d..ca411f2f5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.9.1" apply false + alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.compose.compiler) apply false diff --git a/core/db/build.gradle.kts b/core/db/build.gradle.kts index c54cf41b4..30ef0a54e 100644 --- a/core/db/build.gradle.kts +++ b/core/db/build.gradle.kts @@ -52,7 +52,7 @@ dependencies { ksp(libs.androidx.room.compiler) testImplementation(libs.junit) - androidTestImplementation(libs.androidx.room.testing) +// androidTestImplementation(libs.androidx.room.testing) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6ceed63c0..c63a9a2d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,13 @@ [versions] +composeNavigation = "1.3.0" kotlin = "2.1.20" agp = "8.1.4" +playServicesWearable = "18.2.0" coreKtx = "1.15.0" minSdk = "26" compileSdk = "35" ksp = "2.1.20-1.0.32" - +androidApp = "8.9.1" arkAbout = "0.2.0" activityCompose = "1.10.1" composeDestinationsVersion = "2.1.0" @@ -35,8 +37,15 @@ workRuntimeKtx = "2.8.1" appcompat = "1.7.0" material = "1.12.0" gson = "2.11.0" +reorderable = "2.4.3" +playReview = "2.0.2" +composeBom = "2023.08.00" +composeMaterial = "1.2.1" +composeFoundation = "1.2.1" +coreSplashscreen = "1.0.1" [libraries] +androidx-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "composeNavigation" } ark-about = { module = "dev.arkbuilders.components:about", version.ref = "arkAbout" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } @@ -55,7 +64,7 @@ androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "composeUi" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" } -compose-destinations-animations = { module = "io.github.raamcosta.compose-destinations:animations-core", version.ref = "composeDestinationsVersion" } +compose-destinations-core = { module = "io.github.raamcosta.compose-destinations:core", version.ref = "composeDestinationsVersion" } compose-destinations-compiler = { module = "io.github.raamcosta.compose-destinations:ksp", version.ref = "composeDestinationsVersion" } arrow-fx-coroutines = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrowFxCoroutines" } @@ -66,6 +75,9 @@ dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +play-review = { group = "com.google.android.play", name = "review", version.ref = "playReview" } +play-review-ktx = { group = "com.google.android.play", name = "review-ktx", version.ref = "playReview" } + firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx", version = "" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx", version = "" } @@ -84,8 +96,15 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" } +play-services-wearable = { group = "com.google.android.gms", name = "play-services-wearable", version.ref = "playServicesWearable" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" } +androidx-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "composeFoundation" } +androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } + [plugins] android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"} +android-application = { id = "com.android.application", version.ref = "androidApp"} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index e69de29bb..000000000 diff --git a/settings.gradle.kts b/settings.gradle.kts index 5090e0e26..975c7bc98 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,9 +2,9 @@ import java.util.Properties pluginManagement { repositories { - gradlePluginPortal() google() mavenCentral() + gradlePluginPortal() maven { setUrl("https://jitpack.io") } @@ -54,6 +54,8 @@ include(":feature:quickwidget") include(":feature:search") include(":feature:settings") include(":feature:onboarding") +include(":watchapp") + fun getLocalProps(): Properties { val props = Properties() diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts index cecaf38f9..da43078b8 100644 --- a/watchapp/build.gradle.kts +++ b/watchapp/build.gradle.kts @@ -1,6 +1,7 @@ plugins { - id("com.android.application") version "8.1.3" + id("com.android.application") id("org.jetbrains.kotlin.android") + alias(libs.plugins.compose.compiler) } android { namespace = "dev.arkbuilders.rate.watchapp" @@ -52,18 +53,19 @@ dependencies { implementation(project(":feature:quick")) implementation(project(":core:domain")) implementation(project(":core:presentation")) - implementation(libs.androidx.compose.material.v140) implementation(libs.play.services.wearable) - implementation(platform(libs.androidx.compose.bom)) +// implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) + implementation (libs.androidx.compose.navigation )// Or the latest version + implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.compose.material) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.splashscreen) - implementation(libs.androidx.material3.android) - implementation(libs.androidx.compose.navigation) + implementation(libs.material3) + implementation(libs.navigation.compose) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index 096c34b65..80c4a5d04 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -11,7 +11,7 @@ import androidx.wear.compose.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.AddQuickPairsScreen -import dev.arkbuilders.rate.watchapp.presentation.quickpairs.QuickPairsScreen +import dev.arkbuilders.rate.watchapp.presentation.options.OptionsScreen import dev.arkbuilders.rate.watchapp.presentation.theme.ArkrateTheme class MainActivity : ComponentActivity() { @@ -30,17 +30,17 @@ class MainActivity : ComponentActivity() { Vignette(vignettePosition = VignettePosition.TopAndBottom) } ) { - AddQuickPairsScreen() SwipeDismissableNavHost( navController = navController, startDestination = "list" ) { composable("list") { - QuickPairsScreen( - onNavigateToAdd = { - navController.navigate("addquickpairs") - } - ) + OptionsScreen() +// QuickPairsScreen( +// onNavigateToAdd = { +//// navController.navigate("addquickpairs") +// } +// ) } composable("addquickpairs") { AddQuickPairsScreen( diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt new file mode 100644 index 000000000..e287e3d63 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt @@ -0,0 +1,110 @@ +package dev.arkbuilders.rate.watchapp.presentation.options + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.material.PositionIndicator +import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.material.Text +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +@Composable +fun OptionsScreen( + modifier: Modifier = Modifier, + onUpdateClick: () -> Unit = {}, + onPinClick: () -> Unit = {}, + onEditClick: () -> Unit = {}, + onReuseClick: () -> Unit = {}, + onDeleteClick: () -> Unit = {} +) { + val listState = rememberScalingLazyListState() + + Scaffold( + positionIndicator = { + PositionIndicator(scalingLazyListState = listState) + } + ) { + ScalingLazyColumn( + modifier = modifier.fillMaxSize() + .background(ArkColor.BGSecondaryAlt), + state = listState, + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + text = "Options", + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = ArkColor.TextPrimary + ) + } + + item { + WearOptionButton( + text = "Update", + icon = WearOptionButtonIcon.Refresh, + onClick = onUpdateClick + ) + } + + item { + WearOptionButton( + text = "Pin", + icon = WearOptionButtonIcon.Pin, + onClick = onPinClick + ) + } + + item { + WearOptionButton( + text = "Edit", + icon = WearOptionButtonIcon.Edit, + onClick = onEditClick + ) + } + + item { + WearOptionButton( + text = "Re-Use", + icon = WearOptionButtonIcon.Reuse, + onClick = onReuseClick + ) + } + + item { + WearOptionButton( + text = "Delete", + icon = WearOptionButtonIcon.Delete, + buttonType = WearOptionButtonType.Destructive, + onClick = onDeleteClick + ) + } + } + } +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun OptionsScreenPreview() { + OptionsScreen() +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionButton.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionButton.kt new file mode 100644 index 000000000..efd37852d --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionButton.kt @@ -0,0 +1,129 @@ +package dev.arkbuilders.rate.watchapp.presentation.options + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material.icons.outlined.Star +import androidx.compose.material.icons.outlined.Share +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material.Button +import androidx.wear.compose.material.ButtonDefaults +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +enum class WearOptionButtonType { + Default, + Destructive +} + +enum class WearOptionButtonIcon(val imageVector: ImageVector) { + Refresh(Icons.Outlined.Refresh), + Pin(Icons.Outlined.Star), + Edit(Icons.Outlined.Edit), + Reuse(Icons.Outlined.Share), + Delete(Icons.Outlined.Delete) +} + +@Composable +fun WearOptionButton( + text: String, + icon: WearOptionButtonIcon, + onClick: () -> Unit, + modifier: Modifier = Modifier, + buttonType: WearOptionButtonType = WearOptionButtonType.Default +) { + val colors = when (buttonType) { + WearOptionButtonType.Default -> ButtonDefaults.buttonColors( + backgroundColor = Color.White, + contentColor = ArkColor.TextSecondary + ) + + WearOptionButtonType.Destructive -> ButtonDefaults.buttonColors( + backgroundColor = Color.White, + contentColor = ArkColor.FGErrorPrimary + ) + } + + val borderStroke = when (buttonType) { + WearOptionButtonType.Default -> ButtonDefaults.buttonBorder( + borderStroke = androidx.compose.foundation.BorderStroke(1.dp, ArkColor.BorderSecondary) + ) + WearOptionButtonType.Destructive -> ButtonDefaults.buttonBorder( + borderStroke = androidx.compose.foundation.BorderStroke(1.dp, ArkColor.BorderError) + ) + } + + Button( + onClick = onClick, + modifier = modifier.fillMaxWidth(), + colors = colors, + border = borderStroke + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 6.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon.imageVector, + contentDescription = text, + modifier = Modifier.size(20.dp), + tint = when (buttonType) { + WearOptionButtonType.Default -> ArkColor.TextSecondary + WearOptionButtonType.Destructive -> ArkColor.FGErrorPrimary + } + ) + Text( + text = text, + modifier = Modifier.padding(start = 6.dp), + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + textAlign = TextAlign.Start, + color = when (buttonType) { + WearOptionButtonType.Default -> ArkColor.TextSecondary + WearOptionButtonType.Destructive -> ArkColor.FGErrorPrimary + } + ) + } + } +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun WearOptionButtonPreview() { + WearOptionButton( + text = "Update", + icon = WearOptionButtonIcon.Refresh, + onClick = {} + ) +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun WearOptionButtonDestructivePreview() { + WearOptionButton( + text = "Delete", + icon = WearOptionButtonIcon.Delete, + buttonType = WearOptionButtonType.Destructive, + onClick = {} + ) +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionsHomeScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionsHomeScreen.kt new file mode 100644 index 000000000..b433a0f02 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionsHomeScreen.kt @@ -0,0 +1,124 @@ +package dev.arkbuilders.rate.watchapp.presentation.options + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.material.PositionIndicator +import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +@Composable +fun WearOptionsHomeScreen( + modifier: Modifier = Modifier, + currentPage: Int = 1, + totalPages: Int = 3, + onUpdateClick: () -> Unit = {}, + onPinClick: () -> Unit = {}, + onEditClick: () -> Unit = {}, + onReuseClick: () -> Unit = {}, + onDeleteClick: () -> Unit = {} +) { + val listState = rememberScalingLazyListState() + + Scaffold( + modifier = modifier + .fillMaxSize() + .clip(CircleShape), + positionIndicator = { + PositionIndicator(scalingLazyListState = listState) + } + ) { + ScalingLazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Title section + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + text = "Options", + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = ArkColor.TextPrimary + ) + } + + // Page indicator + item { + WearPageIndicator( + totalPages = totalPages, + currentPage = currentPage, + modifier = Modifier.padding(vertical = 6.dp) + ) + } + + // Main slot - Option buttons + item { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + WearOptionButton( + text = "Update", + icon = WearOptionButtonIcon.Refresh, + onClick = onUpdateClick + ) + + WearOptionButton( + text = "Pin", + icon = WearOptionButtonIcon.Pin, + onClick = onPinClick + ) + + WearOptionButton( + text = "Edit", + icon = WearOptionButtonIcon.Edit, + onClick = onEditClick + ) + + WearOptionButton( + text = "Re-Use", + icon = WearOptionButtonIcon.Reuse, + onClick = onReuseClick + ) + + WearOptionButton( + text = "Delete", + icon = WearOptionButtonIcon.Delete, + buttonType = WearOptionButtonType.Destructive, + onClick = onDeleteClick + ) + } + } + } + } +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun WearOptionsHomeScreenPreview() { + WearOptionsHomeScreen() +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearPageIndicator.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearPageIndicator.kt new file mode 100644 index 000000000..e257c4446 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearPageIndicator.kt @@ -0,0 +1,64 @@ +package dev.arkbuilders.rate.watchapp.presentation.options + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +@Composable +fun WearPageIndicator( + totalPages: Int, + currentPage: Int, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier.padding(6.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + repeat(totalPages) { index -> + PageIndicatorDot( + isSelected = index == currentPage, + modifier = Modifier.size(6.dp) + ) + } + } +} + +@Composable +private fun PageIndicatorDot( + isSelected: Boolean, + modifier: Modifier = Modifier +) { + val backgroundColor = if (isSelected) { + ArkColor.TextSecondary + } else { + ArkColor.TextSecondary.copy(alpha = 0.3f) + } + + Box( + modifier = modifier + .clip(CircleShape) + .background(backgroundColor) + ) +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun WearPageIndicatorPreview() { + WearPageIndicator( + totalPages = 3, + currentPage = 1 + ) +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt index 8002c451e..20e9b4b50 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import androidx.wear.compose.material.Text import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairItem @@ -19,7 +20,7 @@ fun QuickPairsScreen( viewModel: QuickPairsViewModel = QuickPairsViewModel(), onNavigateToAdd: () -> Unit ) { - val quickPairsList = viewModel.quickPairs.collectAsState().value + val quickPairsList = viewModel.quickPairs.collectAsStateWithLifecycle().value if (quickPairsList.isEmpty()) { QuickPairsEmpty(modifier = modifier.fillMaxSize()) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt index 2dd8f0152..00a91c3a7 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt @@ -2,6 +2,7 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs import androidx.lifecycle.ViewModel import dev.arkbuilders.rate.core.domain.model.Amount +import dev.arkbuilders.rate.core.domain.model.Group import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -25,7 +26,7 @@ class QuickPairsViewModel : ViewModel() { ), calculatedDate = OffsetDateTime.now(), pinnedDate = null, - group = null + group = Group.empty() ), QuickPair( id = 1, @@ -37,7 +38,7 @@ class QuickPairsViewModel : ViewModel() { ), calculatedDate = OffsetDateTime.now(), pinnedDate = null, - group = null + group = Group.empty() ), QuickPair( @@ -50,7 +51,7 @@ class QuickPairsViewModel : ViewModel() { ), calculatedDate = OffsetDateTime.now(), pinnedDate = null, - group = null + group = Group.empty() ), QuickPair( @@ -63,7 +64,7 @@ class QuickPairsViewModel : ViewModel() { ), calculatedDate = OffsetDateTime.now(), pinnedDate = null, - group = null + group = Group.empty() ), QuickPair( id = 1, @@ -75,7 +76,7 @@ class QuickPairsViewModel : ViewModel() { ), calculatedDate = OffsetDateTime.now(), pinnedDate = null, - group = null + group = Group.empty() ), QuickPair( @@ -88,7 +89,7 @@ class QuickPairsViewModel : ViewModel() { ), calculatedDate = OffsetDateTime.now(), pinnedDate = null, - group = null + group = Group.empty() ) ) _quickPairs.value = a diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt index 45302f60b..84e6a8d5a 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt @@ -31,6 +31,7 @@ import androidx.wear.compose.material.Text import dev.arkbuilders.rate.core.domain.CurrUtils import dev.arkbuilders.rate.core.domain.model.Amount import dev.arkbuilders.rate.core.domain.model.CurrencyCode +import dev.arkbuilders.rate.core.domain.model.Group import dev.arkbuilders.rate.core.presentation.theme.ArkColor import dev.arkbuilders.rate.core.presentation.utils.IconUtils import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair @@ -160,7 +161,7 @@ fun QuickPairItemPreview() { ), calculatedDate = OffsetDateTime.now(), pinnedDate = null, - group = null + group = Group.empty() ), onClick = {} ) From eacf30961539ee6c1abd6febd65db5d4195bf1c8 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 14:06:11 +0700 Subject: [PATCH 21/57] Add new pair screens --- .../watchapp/presentation/MainActivity.kt | 13 +- .../addquickpairs/AddQuickPairsScreen.kt | 38 ++-- .../composables/CurrencyInputField.kt | 200 ++++++++++++++++++ .../addquickpairs/composables/SwapButton.kt | 84 ++++++++ 4 files changed, 317 insertions(+), 18 deletions(-) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/CurrencyInputField.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/SwapButton.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index 80c4a5d04..fcbbef159 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -12,6 +12,7 @@ import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.AddQuickPairsScreen import dev.arkbuilders.rate.watchapp.presentation.options.OptionsScreen +import dev.arkbuilders.rate.watchapp.presentation.quickpairs.QuickPairsScreen import dev.arkbuilders.rate.watchapp.presentation.theme.ArkrateTheme class MainActivity : ComponentActivity() { @@ -35,12 +36,12 @@ class MainActivity : ComponentActivity() { startDestination = "list" ) { composable("list") { - OptionsScreen() -// QuickPairsScreen( -// onNavigateToAdd = { -//// navController.navigate("addquickpairs") -// } -// ) +// OptionsScreen() + QuickPairsScreen( + onNavigateToAdd = { + navController.navigate("addquickpairs") + } + ) } composable("addquickpairs") { AddQuickPairsScreen( diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt index 0994de524..cb30dd7ef 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt @@ -1,5 +1,6 @@ package dev.arkbuilders.rate.watchapp.presentation.addquickpairs +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -35,6 +36,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.theme.ArkColor +import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables.CurrencyInputField @Composable fun AddQuickPairsScreen(modifier: Modifier = Modifier) { @@ -42,7 +45,7 @@ fun AddQuickPairsScreen(modifier: Modifier = Modifier) { val scrollState = rememberScrollState() ScalingLazyColumn( - modifier = modifier.fillMaxSize(), + modifier = modifier.fillMaxSize().background(ArkColor.BGSecondaryAlt), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -50,25 +53,36 @@ fun AddQuickPairsScreen(modifier: Modifier = Modifier) { Text( modifier = modifier.fillMaxWidth(), text = "Add", - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + color = ArkColor.TextPrimary ) } item { - Text( - modifier = modifier.fillMaxWidth(), - text = "From", + CurrencyInputField( + label = "To", + currencyCode = "EUR", + value = "0.92", + onValueChange = { }, + onCurrencyClick = { + + }, + showLabel = true, + showDeleteButton = false, + onDeleteClick = {} ) } item { - - BasicTextField( - modifier = modifier.fillMaxWidth(), - value = "", - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - onValueChange = { text -> - }, + CurrencyInputField( + label = "To", + currencyCode = "EUR", + value = "0.92", + onValueChange = { }, + onCurrencyClick = {}, + showLabel = false, + showDeleteButton = false, + onDeleteClick = {} ) } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/CurrencyInputField.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/CurrencyInputField.kt new file mode 100644 index 000000000..7dc6eb924 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/CurrencyInputField.kt @@ -0,0 +1,200 @@ +package dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.KeyboardArrowDown +import androidx.compose.runtime.Composable +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.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material.Button +import androidx.wear.compose.material.ButtonDefaults +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +@Composable +fun CurrencyInputField( + label: String, + currencyCode: String, + value: String, + onValueChange: (String) -> Unit, + onCurrencyClick: () -> Unit, + modifier: Modifier = Modifier, + showDeleteButton: Boolean = false, + onDeleteClick: () -> Unit = {}, + showLabel: Boolean = true, + hintText: String = "This is a hint text to help user." +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.Bottom + ) { + // Main input field + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + if (showLabel) { + Text( + text = label, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = ArkColor.TextSecondary + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = Color.White, + shape = RoundedCornerShape(8.dp) + ) + .border( + width = 1.dp, + color = ArkColor.BorderSecondary, + shape = RoundedCornerShape(8.dp) + ) + ) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + // Currency dropdown + Box( + modifier = Modifier + .width(80.dp) + .clickable { onCurrencyClick() } + .padding(horizontal = 14.dp, vertical = 10.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = currencyCode, + fontSize = 14.sp, + color = ArkColor.TextSecondary + ) + Icon( + imageVector = Icons.Outlined.KeyboardArrowDown, + contentDescription = "Select currency", + modifier = Modifier.size(16.dp), + tint = ArkColor.FGQuinary + ) + } + } + + // Value input + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier + .weight(1f) + .padding(horizontal = 12.dp, vertical = 10.dp), + textStyle = TextStyle( + fontSize = 14.sp, + color = ArkColor.TextPrimary, + textAlign = TextAlign.Start + ), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true + ) + } + } + + if (showLabel) { + Text( + text = hintText, + fontSize = 14.sp, + color = ArkColor.TextTertiary + ) + } + } + + // Delete button + if (showDeleteButton) { + Button( + onClick = onDeleteClick, + modifier = Modifier.size(44.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color.White, + contentColor = ArkColor.FGErrorPrimary + ), + border = ButtonDefaults.buttonBorder( + borderStroke = BorderStroke(1.dp, ArkColor.BorderError) + ), + shape = RoundedCornerShape(8.dp) + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Delete", + modifier = Modifier.size(20.dp), + tint = ArkColor.FGErrorPrimary + ) + } + } + } +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun CurrencyInputFieldPreview() { + var value by remember { mutableStateOf("1") } + + CurrencyInputField( + label = "From", + currencyCode = "USD", + value = value, + onValueChange = { value = it }, + onCurrencyClick = {}, + showDeleteButton = true, + onDeleteClick = {} + ) +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun CurrencyInputFieldNoLabelPreview() { + var value by remember { mutableStateOf("0.92") } + + CurrencyInputField( + label = "To", + currencyCode = "EUR", + value = value, + onValueChange = { value = it }, + onCurrencyClick = {}, + showLabel = false, + showDeleteButton = true, + onDeleteClick = {} + ) +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/SwapButton.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/SwapButton.kt new file mode 100644 index 000000000..e74406a7f --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/SwapButton.kt @@ -0,0 +1,84 @@ +package dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AccountBox +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material.Button +import androidx.wear.compose.material.ButtonDefaults +import androidx.wear.compose.material.Icon +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +@Composable +fun SwapButton( + onSwapClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Left divider + Box( + modifier = Modifier + .weight(1f) + .height(1.dp) + .background(ArkColor.BorderSecondary) + ) + + // Swap button + Button( + onClick = onSwapClick, + modifier = Modifier.size(40.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color.White, + contentColor = ArkColor.TextPrimary + ), + border = ButtonDefaults.buttonBorder( + borderStroke = BorderStroke(1.dp, ArkColor.BorderSecondary) + ), + shape = CircleShape + ) { + Icon( + imageVector = Icons.Outlined.AccountBox, + contentDescription = "Swap currencies", + modifier = Modifier.size(20.dp), + tint = ArkColor.TextPrimary + ) + } + + // Right divider + Box( + modifier = Modifier + .weight(1f) + .height(1.dp) + .background(ArkColor.BorderSecondary) + ) + } +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun SwapButtonPreview() { + SwapButton( + onSwapClick = {} + ) +} From dc5aaaa29ca6b1f3a15b2537e600e5516ca03aaa Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 14:10:14 +0700 Subject: [PATCH 22/57] Add composables --- .../quickpairs/QuickPairsScreen.kt | 12 ++ .../theme/WearComponentExamples.kt | 165 ++++++++++++++++ .../presentation/theme/WearComponents.kt | 178 ++++++++++++++++++ .../watchapp/presentation/theme/WearDialog.kt | 162 ++++++++++++++++ 4 files changed, 517 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponentExamples.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt index 20e9b4b50..f641614bd 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -3,6 +3,8 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier @@ -13,6 +15,8 @@ import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import androidx.wear.compose.material.Text import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairItem import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairsEmpty +import dev.arkbuilders.rate.watchapp.presentation.theme.WearButton +import dev.arkbuilders.rate.watchapp.presentation.theme.WearButtonStyle @Composable fun QuickPairsScreen( @@ -36,6 +40,14 @@ fun QuickPairsScreen( textAlign = TextAlign.Center ) } + item { + WearButton( + text = "Add", + onClick = onNavigateToAdd, + style = WearButtonStyle.Primary, + leadingIcon = Icons.Outlined.Add + ) + } items(quickPairsList.size, key = null) { idx -> QuickPairItem(quick = quickPairsList[idx], onClick = onNavigateToAdd) } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponentExamples.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponentExamples.kt new file mode 100644 index 000000000..4bd0ebd71 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponentExamples.kt @@ -0,0 +1,165 @@ +package dev.arkbuilders.rate.watchapp.presentation.theme + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.runtime.Composable +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.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.material.PositionIndicator +import androidx.wear.compose.material.Scaffold +import dev.arkbuilders.rate.watchapp.presentation.options.WearOptionsHomeScreen +import dev.arkbuilders.rate.watchapp.presentation.options.WearPageIndicator + +/** + * Examples of how to use the WearOS components created for the ARK Rate app. + * These components follow the Figma design system and WearOS best practices. + */ + +@Composable +fun WearButtonExamples(modifier: Modifier = Modifier) { + val listState = rememberScalingLazyListState() + + Scaffold( + positionIndicator = { + PositionIndicator(scalingLazyListState = listState) + } + ) { + ScalingLazyColumn( + modifier = modifier.fillMaxSize(), + state = listState, + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + WearButton( + text = "Primary Button", + onClick = {}, + style = WearButtonStyle.Primary, + leadingIcon = Icons.Outlined.Add + ) + } + + item { + WearButton( + text = "Secondary Button", + onClick = {}, + style = WearButtonStyle.Secondary, + leadingIcon = Icons.Outlined.Edit + ) + } + + item { + WearButton( + text = "Outlined Button", + onClick = {}, + style = WearButtonStyle.Outlined, + leadingIcon = Icons.Outlined.Refresh + ) + } + + item { + WearButton( + text = "Destructive Button", + onClick = {}, + style = WearButtonStyle.Destructive + ) + } + + item { + WearPageIndicator( + totalPages = 5, + currentPage = 2 + ) + } + } + } +} + +@Composable +fun WearDialogExamples(modifier: Modifier = Modifier) { + var showConfirmDialog by remember { mutableStateOf(false) } + var showInfoDialog by remember { mutableStateOf(false) } + + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + WearButton( + text = "Show Confirmation Dialog", + onClick = { showConfirmDialog = true } + ) + + WearButton( + text = "Show Info Dialog", + onClick = { showInfoDialog = true } + ) + + if (showConfirmDialog) { + WearConfirmationDialog( + title = "Delete Item", + message = "Are you sure you want to delete this item?", + onConfirm = { + showConfirmDialog = false + // Handle confirmation + }, + onDismiss = { showConfirmDialog = false }, + isDestructive = true + ) + } + + if (showInfoDialog) { + WearInfoDialog( + title = "Success", + message = "Operation completed successfully!", + onDismiss = { showInfoDialog = false } + ) + } + } +} + +// Preview showing the main Options screen from Figma +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun WearOptionsHomeScreenExample() { + WearOptionsHomeScreen( + currentPage = 1, + totalPages = 3, + onUpdateClick = { /* Handle update */ }, + onPinClick = { /* Handle pin */ }, + onEditClick = { /* Handle edit */ }, + onReuseClick = { /* Handle reuse */ }, + onDeleteClick = { /* Handle delete */ } + ) +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun WearButtonExamplesPreview() { + WearButtonExamples() +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun WearDialogExamplesPreview() { + WearDialogExamples() +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt new file mode 100644 index 000000000..dcceb57d5 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt @@ -0,0 +1,178 @@ +package dev.arkbuilders.rate.watchapp.presentation.theme + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material.Button +import androidx.wear.compose.material.ButtonDefaults +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.OutlinedButton +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +enum class WearButtonStyle { + Primary, + Secondary, + Outlined, + Destructive +} + +@Composable +fun WearButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: WearButtonStyle = WearButtonStyle.Primary, + leadingIcon: ImageVector? = null, + enabled: Boolean = true +) { + when (style) { + WearButtonStyle.Primary -> { + Button( + onClick = onClick, + modifier = modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + backgroundColor = ArkColor.Primary, + contentColor = Color.White + ), + enabled = enabled + ) { + ButtonContent(text = text, leadingIcon = leadingIcon) + } + } + + WearButtonStyle.Secondary -> { + Button( + onClick = onClick, + modifier = modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + backgroundColor = ArkColor.BGSecondaryAlt, + contentColor = ArkColor.TextSecondary + ), + enabled = enabled + ) { + ButtonContent(text = text, leadingIcon = leadingIcon) + } + } + + WearButtonStyle.Outlined -> { + OutlinedButton( + onClick = onClick, + modifier = modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = ArkColor.TextSecondary + ), + border = ButtonDefaults.buttonBorder( + borderStroke = BorderStroke(1.dp, ArkColor.BorderSecondary) + ), + enabled = enabled + ) { + ButtonContent(text = text, leadingIcon = leadingIcon) + } + } + + WearButtonStyle.Destructive -> { + Button( + onClick = onClick, + modifier = modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + backgroundColor = ArkColor.UtilityError500, + contentColor = Color.White + ), + enabled = enabled + ) { + ButtonContent(text = text, leadingIcon = leadingIcon) + } + } + } +} + +@Composable +private fun ButtonContent( + text: String, + leadingIcon: ImageVector?, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = if (leadingIcon != null) Arrangement.Start else Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + leadingIcon?.let { icon -> + Icon( + imageVector = icon, + contentDescription = text, + modifier = Modifier.size(18.dp) + ) + } + Text( + text = text, + modifier = Modifier.padding( + start = if (leadingIcon != null) 8.dp else 0.dp + ), + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + textAlign = if (leadingIcon != null) TextAlign.Start else TextAlign.Center + ) + } +} + +@Composable +fun WearCompactButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + style: WearButtonStyle = WearButtonStyle.Outlined +) { + val colors = when (style) { + WearButtonStyle.Primary -> ButtonDefaults.buttonColors( + backgroundColor = ArkColor.Primary, + contentColor = Color.White + ) + + WearButtonStyle.Destructive -> ButtonDefaults.buttonColors( + backgroundColor = ArkColor.UtilityError500, + contentColor = Color.White + ) + + else -> ButtonDefaults.buttonColors( + backgroundColor = Color.White, + contentColor = ArkColor.TextSecondary + ) + } + + val border = if (style == WearButtonStyle.Outlined) { + ButtonDefaults.buttonBorder( + borderStroke = BorderStroke(1.dp, ArkColor.BorderSecondary) + ) + } else { + ButtonDefaults.buttonBorder() + } + + Button( + onClick = onClick, + modifier = modifier, + colors = colors, + border = border, + shape = RoundedCornerShape(20.dp) + ) { + Text( + text = text, + fontWeight = FontWeight.Medium, + fontSize = 14.sp + ) + } +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt new file mode 100644 index 000000000..58714fd9d --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt @@ -0,0 +1,162 @@ +package dev.arkbuilders.rate.watchapp.presentation.theme + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.Close +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +@Composable +fun WearConfirmationDialog( + title: String, + message: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + confirmText: String = "Yes", + dismissText: String = "No", + confirmIcon: ImageVector = Icons.Outlined.Check, + dismissIcon: ImageVector = Icons.Outlined.Close, + isDestructive: Boolean = false +) { + Dialog(onDismissRequest = onDismiss) { + Column( + modifier = modifier + .fillMaxWidth() + .background( + color = Color.White, + shape = RoundedCornerShape(16.dp) + ) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = title, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = ArkColor.TextPrimary + ) + + Text( + text = message, + fontSize = 14.sp, + textAlign = TextAlign.Center, + color = ArkColor.TextSecondary + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + WearCompactButton( + text = dismissText, + onClick = onDismiss, + modifier = Modifier.weight(1f), + style = WearButtonStyle.Outlined + ) + + WearCompactButton( + text = confirmText, + onClick = onConfirm, + modifier = Modifier.weight(1f), + style = if (isDestructive) WearButtonStyle.Destructive else WearButtonStyle.Primary + ) + } + } + } +} + +@Composable +fun WearInfoDialog( + title: String, + message: String, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + dismissText: String = "OK" +) { + Dialog(onDismissRequest = onDismiss) { + Column( + modifier = modifier + .fillMaxWidth() + .background( + color = Color.White, + shape = RoundedCornerShape(16.dp) + ) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = title, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = ArkColor.TextPrimary + ) + + Text( + text = message, + fontSize = 14.sp, + textAlign = TextAlign.Center, + color = ArkColor.TextSecondary + ) + + Spacer(modifier = Modifier.height(8.dp)) + + WearCompactButton( + text = dismissText, + onClick = onDismiss, + modifier = Modifier.fillMaxWidth(), + style = WearButtonStyle.Primary + ) + } + } +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun WearConfirmationDialogPreview() { + WearConfirmationDialog( + title = "Delete Item", + message = "Are you sure you want to delete this item? This action cannot be undone.", + onConfirm = {}, + onDismiss = {}, + isDestructive = true + ) +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun WearInfoDialogPreview() { + WearInfoDialog( + title = "Success", + message = "Your changes have been saved successfully.", + onDismiss = {} + ) +} From 29bddf601bc812c7bd079c4f04eac6d045ac7527 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 21:01:51 +0700 Subject: [PATCH 23/57] Refactor so that class be free from di code --- .../core/data/network/OkHttpClientBuilder.kt | 2 -- .../rate/core/data/repo/TimestampRepoImpl.kt | 2 -- .../repo/currency/CryptoCurrencyDataSource.kt | 2 -- .../repo/currency/CurrencyInfoDataSource.kt | 2 -- .../data/repo/currency/CurrencyRepoImpl.kt | 2 -- .../repo/currency/FallbackRatesProvider.kt | 2 -- .../repo/currency/FiatCurrencyDataSource.kt | 2 -- .../rate/core/di/modules/RepoModule.kt | 30 +++++++++++++++++++ 8 files changed, 30 insertions(+), 14 deletions(-) diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/OkHttpClientBuilder.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/OkHttpClientBuilder.kt index 54e654e89..419ef1fad 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/OkHttpClientBuilder.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/network/OkHttpClientBuilder.kt @@ -4,9 +4,7 @@ import android.content.Context import android.webkit.WebSettings import okhttp3.OkHttpClient import javax.inject.Inject -import javax.inject.Singleton -@Singleton class OkHttpClientBuilder @Inject constructor(val context: Context) { fun build(): OkHttpClient { val agent = WebSettings.getDefaultUserAgent(context) diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/TimestampRepoImpl.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/TimestampRepoImpl.kt index 943137507..7eb7b15bc 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/TimestampRepoImpl.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/TimestampRepoImpl.kt @@ -8,9 +8,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import java.time.OffsetDateTime import javax.inject.Inject -import javax.inject.Singleton -@Singleton class TimestampRepoImpl @Inject constructor(private val dao: TimestampDao) : TimestampRepo { override suspend fun rememberTimestamp(type: TimestampType) = dao.insert( diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CryptoCurrencyDataSource.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CryptoCurrencyDataSource.kt index ceb9856a9..ba0179db5 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CryptoCurrencyDataSource.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CryptoCurrencyDataSource.kt @@ -8,9 +8,7 @@ import dev.arkbuilders.rate.core.data.network.api.CryptoAPI import dev.arkbuilders.rate.core.domain.model.CurrencyRate import dev.arkbuilders.rate.core.domain.model.CurrencyType import javax.inject.Inject -import javax.inject.Singleton -@Singleton class CryptoCurrencyDataSource @Inject constructor( private val cryptoAPI: CryptoAPI, private val cryptoRateResponseMapper: CryptoRateResponseMapper, diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyInfoDataSource.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyInfoDataSource.kt index ca8ef3948..9c995d064 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyInfoDataSource.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyInfoDataSource.kt @@ -11,9 +11,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext import javax.inject.Inject -import javax.inject.Singleton -@Singleton class CurrencyInfoDataSource @Inject constructor( private val ctx: Context, ) { diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt index 2f79c7e89..f25fafdee 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/CurrencyRepoImpl.kt @@ -20,9 +20,7 @@ import kotlinx.coroutines.withContext import java.time.Duration import java.time.OffsetDateTime import javax.inject.Inject -import javax.inject.Singleton -@Singleton class CurrencyRepoImpl @Inject constructor( private val fiatDataSource: FiatCurrencyDataSource, private val cryptoDataSource: CryptoCurrencyDataSource, diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FallbackRatesProvider.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FallbackRatesProvider.kt index d3638c0fc..5e3a84fdc 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FallbackRatesProvider.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FallbackRatesProvider.kt @@ -12,9 +12,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.time.OffsetDateTime import javax.inject.Inject -import javax.inject.Singleton -@Singleton class FallbackRatesProvider @Inject constructor( private val ctx: Context, private val fiatRateResponseMapper: FiatRateResponseMapper, diff --git a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FiatCurrencyDataSource.kt b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FiatCurrencyDataSource.kt index 742e1f464..132874c71 100644 --- a/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FiatCurrencyDataSource.kt +++ b/core/data/src/main/java/dev/arkbuilders/rate/core/data/repo/currency/FiatCurrencyDataSource.kt @@ -8,9 +8,7 @@ import dev.arkbuilders.rate.core.data.network.api.FiatAPI import dev.arkbuilders.rate.core.domain.model.CurrencyRate import dev.arkbuilders.rate.core.domain.model.CurrencyType import javax.inject.Inject -import javax.inject.Singleton -@Singleton class FiatCurrencyDataSource @Inject constructor( private val fiatAPI: FiatAPI, private val fiatRateResponseMapper: FiatRateResponseMapper, diff --git a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt index c14a324db..75f7277f6 100644 --- a/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt +++ b/core/di/src/main/java/dev/arkbuilders/rate/core/di/modules/RepoModule.kt @@ -3,6 +3,8 @@ package dev.arkbuilders.rate.core.di.modules import android.content.Context import dagger.Module import dagger.Provides +import dev.arkbuilders.rate.core.data.mapper.CryptoRateResponseMapper +import dev.arkbuilders.rate.core.data.mapper.FiatRateResponseMapper import dev.arkbuilders.rate.core.data.network.NetworkStatusImpl import dev.arkbuilders.rate.core.data.preferences.PrefsImpl import dev.arkbuilders.rate.core.data.repo.AnalyticsManagerImpl @@ -35,6 +37,34 @@ import javax.inject.Singleton @Module class RepoModule { + @Singleton + @Provides + fun provideFCryptoRateResponseMapper(): CryptoRateResponseMapper { + return CryptoRateResponseMapper() + } + + @Singleton + @Provides + fun provideFiatRateResponseMapper(): FiatRateResponseMapper { + return FiatRateResponseMapper() + } + + @Singleton + @Provides + fun provideFallbackRatesProvider( + context: Context, + fiatRateResponseMapper: FiatRateResponseMapper, + cryptoRateResponseMapper: CryptoRateResponseMapper, + buildConfigFieldsProvider: BuildConfigFieldsProvider, + ): FallbackRatesProvider { + return FallbackRatesProvider( + context, + fiatRateResponseMapper, + cryptoRateResponseMapper, + buildConfigFieldsProvider, + ) + } + @Singleton @Provides fun currencyRepo( From c7020b193bf4d2e1a40be1ef6aac32709b658f7f Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 21:02:13 +0700 Subject: [PATCH 24/57] Setup hilt for di --- build.gradle.kts | 1 + gradle/libs.versions.toml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index ca411f2f5..507df665f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,7 @@ plugins { id("com.google.gms.google-services") version "4.4.2" apply false id("com.google.firebase.crashlytics") version "3.0.1" apply false id("org.jlleitschuh.gradle.ktlint") version "12.1.1" + id("com.google.dagger.hilt.android") version "2.56.2" apply false } allprojects { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c63a9a2d4..4b3c6b5cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] composeNavigation = "1.3.0" +hilt = "2.56.2" kotlin = "2.1.20" agp = "8.1.4" playServicesWearable = "18.2.0" @@ -75,6 +76,8 @@ dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } play-review = { group = "com.google.android.play", name = "review", version.ref = "playReview" } play-review-ktx = { group = "com.google.android.play", name = "review-ktx", version.ref = "playReview" } From 9fbff91371832f13dc19218d0dad5f670bf7e002 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 21:02:55 +0700 Subject: [PATCH 25/57] Set up di and db --- watchapp/build.gradle.kts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts index da43078b8..d1d11ffa4 100644 --- a/watchapp/build.gradle.kts +++ b/watchapp/build.gradle.kts @@ -2,6 +2,8 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") alias(libs.plugins.compose.compiler) + alias(libs.plugins.ksp) + id("com.google.dagger.hilt.android") } android { namespace = "dev.arkbuilders.rate.watchapp" @@ -48,12 +50,26 @@ android { } dependencies { + implementation(project(":core:db")) + implementation(project(":core:data")) + implementation(project(":cryptoicons")) implementation(project(":fiaticons")) implementation(project(":feature:quick")) implementation(project(":core:domain")) implementation(project(":core:presentation")) + implementation("androidx.hilt:hilt-navigation-compose:1.2.0") + + + implementation(libs.retrofit) + implementation(libs.converter.gson) + implementation(libs.logging.interceptor) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) + implementation(libs.hilt.android) + ksp(libs.hilt.android.compiler) implementation(libs.play.services.wearable) // implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) From 5f6cde75cc2c649c15645612f55a4ac07eff3393 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 21:02:59 +0700 Subject: [PATCH 26/57] Set up di and db --- watchapp/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/watchapp/src/main/AndroidManifest.xml b/watchapp/src/main/AndroidManifest.xml index 8547e121e..eff222470 100644 --- a/watchapp/src/main/AndroidManifest.xml +++ b/watchapp/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ Date: Sat, 6 Sep 2025 21:03:08 +0700 Subject: [PATCH 27/57] Clone di graph --- .../arkbuilders/rate/watchapp/di/ApiModule.kt | 67 +++++++ .../arkbuilders/rate/watchapp/di/DBModule.kt | 40 +++++ .../rate/watchapp/di/RepoModule.kt | 165 ++++++++++++++++++ .../rate/watchapp/di/UseCaseModule.kt | 66 +++++++ 4 files changed, 338 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/DBModule.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/UseCaseModule.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt new file mode 100644 index 000000000..dba841a91 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/ApiModule.kt @@ -0,0 +1,67 @@ +package dev.arkbuilders.rate.watchapp.di + +import android.content.Context +import android.webkit.WebSettings +import com.google.gson.GsonBuilder +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dev.arkbuilders.rate.core.data.network.OkHttpClientBuilder +import dev.arkbuilders.rate.core.data.network.api.CryptoAPI +import dev.arkbuilders.rate.core.data.network.api.FiatAPI +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class ApiModule { + + @Singleton + @Provides + fun clientBuilder(@ApplicationContext context: Context): OkHttpClient { + val client = + OkHttpClient.Builder() + .addNetworkInterceptor { chain -> + chain.proceed( + chain.request() + .newBuilder() + .build(), + ) + } + .build() + + return client + } + + @Singleton + @Provides + fun cryptoAPI(clientBuilder: OkHttpClient): CryptoAPI { + val httpClient = clientBuilder + val gson = GsonBuilder().create() + + return Retrofit.Builder() + .baseUrl("https://raw.githubusercontent.com") + .addConverterFactory(GsonConverterFactory.create(gson)) + .client(httpClient) + .build() + .create(CryptoAPI::class.java) + } + + @Singleton + @Provides + fun fiatAPI(clientBuilder: OkHttpClient): FiatAPI { + val httpClient = clientBuilder + val gson = GsonBuilder().create() + + return Retrofit.Builder() + .baseUrl("https://raw.githubusercontent.com") + .addConverterFactory(GsonConverterFactory.create(gson)) + .client(httpClient) + .build() + .create(FiatAPI::class.java) + } +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/DBModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/DBModule.kt new file mode 100644 index 000000000..57d1743aa --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/DBModule.kt @@ -0,0 +1,40 @@ +package dev.arkbuilders.rate.watchapp.di + +import android.app.Application +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dev.arkbuilders.rate.core.db.Database +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class DBModule { + @Singleton + @Provides + fun database(app: Application): Database { + return Database.build(app) + } + + @Provides + fun assetsDao(db: Database) = db.assetsDao() + + @Provides + fun quickDao(db: Database) = db.quickDao() + + @Provides + fun rateDao(db: Database) = db.rateDao() + + @Provides + fun pairAlertDao(db: Database) = db.pairAlertDao() + + @Provides + fun fetchTimestampDao(db: Database) = db.fetchTimestampDao() + + @Provides + fun codeUseStatDao(db: Database) = db.codeUseStatDao() + + @Provides + fun groupDao(db: Database) = db.groupDao() +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt new file mode 100644 index 000000000..d906ea87f --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt @@ -0,0 +1,165 @@ +package dev.arkbuilders.rate.watchapp.di + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dev.arkbuilders.rate.core.data.mapper.CryptoRateResponseMapper +import dev.arkbuilders.rate.core.data.mapper.FiatRateResponseMapper +import dev.arkbuilders.rate.core.data.network.NetworkStatusImpl +import dev.arkbuilders.rate.core.data.network.OkHttpClientBuilder +import dev.arkbuilders.rate.core.data.network.api.CryptoAPI +import dev.arkbuilders.rate.core.data.preferences.PrefsImpl +import dev.arkbuilders.rate.core.data.repo.AnalyticsManagerImpl +import dev.arkbuilders.rate.core.data.repo.BuildConfigFieldsProviderImpl +import dev.arkbuilders.rate.core.data.repo.CodeUseStatRepoImpl +import dev.arkbuilders.rate.core.data.repo.GooglePlayInAppReviewManagerImpl +import dev.arkbuilders.rate.core.data.repo.GroupRepoImpl +import dev.arkbuilders.rate.core.data.repo.TimestampRepoImpl +import dev.arkbuilders.rate.core.data.repo.currency.CryptoCurrencyDataSource +import dev.arkbuilders.rate.core.data.repo.currency.CurrencyInfoDataSource +import dev.arkbuilders.rate.core.data.repo.currency.CurrencyRepoImpl +import dev.arkbuilders.rate.core.data.repo.currency.FallbackRatesProvider +import dev.arkbuilders.rate.core.data.repo.currency.FiatCurrencyDataSource +import dev.arkbuilders.rate.core.data.repo.currency.LocalCurrencyDataSource +import dev.arkbuilders.rate.core.db.dao.CodeUseStatDao +import dev.arkbuilders.rate.core.db.dao.CurrencyRateDao +import dev.arkbuilders.rate.core.db.dao.GroupDao +import dev.arkbuilders.rate.core.db.dao.TimestampDao +import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.repo.AnalyticsManager +import dev.arkbuilders.rate.core.domain.repo.CodeUseStatRepo +import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo +import dev.arkbuilders.rate.core.domain.repo.GroupRepo +import dev.arkbuilders.rate.core.domain.repo.InAppReviewManager +import dev.arkbuilders.rate.core.domain.repo.NetworkStatus +import dev.arkbuilders.rate.core.domain.repo.Prefs +import dev.arkbuilders.rate.core.domain.repo.TimestampRepo +import dev.arkbuilders.rate.core.domain.usecase.DefaultGroupNameProvider +import dev.arkbuilders.rate.core.presentation.utils.DefaultGroupNameProviderImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class RepoModule { + + @Singleton + @Provides + fun buildConfigFieldsProvider(): BuildConfigFieldsProvider = BuildConfigFieldsProviderImpl() + + @Singleton + @Provides + fun provideFCryptoRateResponseMapper(): CryptoRateResponseMapper{ + return CryptoRateResponseMapper() + } + + @Singleton + @Provides + fun provideFiatRateResponseMapper(): FiatRateResponseMapper{ + return FiatRateResponseMapper() + } + + @Singleton + @Provides + fun provideFallbackRatesProvider( + @ApplicationContext context: Context, + fiatRateResponseMapper: FiatRateResponseMapper, + cryptoRateResponseMapper: CryptoRateResponseMapper, + buildConfigFieldsProvider: BuildConfigFieldsProvider, + ): FallbackRatesProvider { + return FallbackRatesProvider( + context, + fiatRateResponseMapper, + cryptoRateResponseMapper, + buildConfigFieldsProvider, + ) + } + + @Singleton + @Provides + fun provideCurrencyInfoDataSource( + @ApplicationContext context: Context, + ): CurrencyInfoDataSource { + return CurrencyInfoDataSource(context) + } + + @Singleton + @Provides + fun provideCryptoCurrencyDataSource( + cryptoAPI: CryptoAPI, + cryptoRateResponseMapper: CryptoRateResponseMapper, + ): CryptoCurrencyDataSource { + return CryptoCurrencyDataSource(cryptoAPI, cryptoRateResponseMapper) + } + + @Singleton + @Provides + fun provideLocalCurrencyDataSource(dao: CurrencyRateDao):LocalCurrencyDataSource { + return LocalCurrencyDataSource(dao) + } + + @Singleton + @Provides + fun currencyRepo( + fiatCurrencyDataSource: FiatCurrencyDataSource, + cryptoCurrencyDataSource: CryptoCurrencyDataSource, + localCurrencyDataSource: LocalCurrencyDataSource, + currencyInfoDataSource: CurrencyInfoDataSource, + timestampRepo: TimestampRepo, + networkStatus: NetworkStatus, + fallbackRatesProvider: FallbackRatesProvider, + ): CurrencyRepo = + CurrencyRepoImpl( + fiatCurrencyDataSource, + cryptoCurrencyDataSource, + localCurrencyDataSource, + fallbackRatesProvider, + currencyInfoDataSource, + timestampRepo, + networkStatus, + ) + + @Singleton + @Provides + fun groupRepo(groupDao: GroupDao): GroupRepo = GroupRepoImpl(groupDao) + + @Singleton + @Provides + fun prefs(@ApplicationContext context: Context): Prefs = PrefsImpl(context) + + @Singleton + @Provides + fun codeUseStatRepo(codeUseStatDao: CodeUseStatDao): CodeUseStatRepo = + CodeUseStatRepoImpl(codeUseStatDao) + + @Singleton + @Provides + fun analyticsManager(prefs: Prefs): AnalyticsManager = AnalyticsManagerImpl(prefs) + + @Singleton + @Provides + fun timestampRepo(timestampDao: TimestampDao): TimestampRepo = TimestampRepoImpl(timestampDao) + + @Singleton + @Provides + fun networkStatus(@ApplicationContext context: Context): NetworkStatus = + NetworkStatusImpl(context) + + @Singleton + @Provides + fun defaultGroupNameProvider(@ApplicationContext context: Context): DefaultGroupNameProvider = + DefaultGroupNameProviderImpl(context) + + @Singleton + @Provides + fun inAppReviewManager( + analyticsManager: AnalyticsManager, + buildConfigFieldsProvider: BuildConfigFieldsProvider, + ): InAppReviewManager = + GooglePlayInAppReviewManagerImpl( + analyticsManager, + buildConfigFieldsProvider.provide(), + ) +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/UseCaseModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/UseCaseModule.kt new file mode 100644 index 000000000..cec60593c --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/UseCaseModule.kt @@ -0,0 +1,66 @@ +package dev.arkbuilders.rate.watchapp.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.core.domain.repo.CodeUseStatRepo +import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo +import dev.arkbuilders.rate.core.domain.repo.GroupRepo +import dev.arkbuilders.rate.core.domain.repo.InAppReviewManager +import dev.arkbuilders.rate.core.domain.repo.Prefs +import dev.arkbuilders.rate.core.domain.usecase.CalcFrequentCurrUseCase +import dev.arkbuilders.rate.core.domain.usecase.ConvertWithRateUseCase +import dev.arkbuilders.rate.core.domain.usecase.DefaultGroupNameProvider +import dev.arkbuilders.rate.core.domain.usecase.GetGroupByIdOrCreateDefaultUseCase +import dev.arkbuilders.rate.core.domain.usecase.GroupReorderSwapUseCase +import dev.arkbuilders.rate.core.domain.usecase.SearchUseCase +import dev.arkbuilders.rate.core.domain.usecase.ValidateGroupNameUseCase +import dev.arkbuilders.rate.feature.quick.domain.usecase.LaunchInAppReviewUseCase +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class UseCaseModule { + @Singleton + @Provides + fun calcFrequentCurrUseCase(codeUseStatRepo: CodeUseStatRepo) = + CalcFrequentCurrUseCase(codeUseStatRepo) + + @Singleton + @Provides + fun convertWithRateUseCase(currencyRepo: CurrencyRepo) = ConvertWithRateUseCase(currencyRepo) + + @Singleton + @Provides + fun prepopulateDefaultGroupUseCase( + groupRepo: GroupRepo, + defaultGroupNameProvider: DefaultGroupNameProvider, + ) = GetGroupByIdOrCreateDefaultUseCase(groupRepo, defaultGroupNameProvider) + + @Singleton + @Provides + fun groupReorderSwapUseCase(groupRepo: GroupRepo) = GroupReorderSwapUseCase(groupRepo) + + @Singleton + @Provides + fun validateGroupNameUseCase() = ValidateGroupNameUseCase() + + @Singleton + @Provides + fun searchUseCase(buildConfigFieldsProvider: BuildConfigFieldsProvider) = + SearchUseCase(buildConfigFieldsProvider.provide()) + + + @Singleton + @Provides + fun provideLaunchInAppReviewUseCase( + + inAppReviewManager: InAppReviewManager, + prefs: Prefs, + buildConfigFieldsProvider: BuildConfigFieldsProvider + + ) = + LaunchInAppReviewUseCase(inAppReviewManager, prefs, buildConfigFieldsProvider) +} From e8255bd583e08bce1f9c0ec1802f5fce4d8fb92d Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 21:03:30 +0700 Subject: [PATCH 28/57] Set up hilt --- .../rate/watchapp/presentation/RateWatchApplication.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt new file mode 100644 index 000000000..b3da758e1 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt @@ -0,0 +1,9 @@ +package dev.arkbuilders.rate.watchapp.presentation + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class RateWatchApplication: Application() { + +} From d595fb6ab633ce5ae25fe8efeb879d6ade92538e Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 21:03:41 +0700 Subject: [PATCH 29/57] Init data --- .../watchapp/presentation/MainActivity.kt | 4 +++- .../presentation/main/MainViewModel.kt | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/main/MainViewModel.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index fcbbef159..12d35348c 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -10,12 +10,14 @@ import androidx.wear.compose.material.VignettePosition import androidx.wear.compose.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavController +import dagger.hilt.android.AndroidEntryPoint import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.AddQuickPairsScreen -import dev.arkbuilders.rate.watchapp.presentation.options.OptionsScreen import dev.arkbuilders.rate.watchapp.presentation.quickpairs.QuickPairsScreen import dev.arkbuilders.rate.watchapp.presentation.theme.ArkrateTheme +@AndroidEntryPoint class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/main/MainViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/main/MainViewModel.kt new file mode 100644 index 000000000..0b1c34e81 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/main/MainViewModel.kt @@ -0,0 +1,23 @@ +package dev.arkbuilders.rate.watchapp.presentation.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo +import jakarta.inject.Inject +import kotlinx.coroutines.launch + +@HiltViewModel +class MainViewModel @Inject constructor( + private val currencyRepo: CurrencyRepo, + ): ViewModel() { + + init { + viewModelScope.launch { + currencyRepo.initialize() + launch { + currencyRepo.getCurrencyRates() + } + } + } +} From 80b9a52ec572fe4bcd5177007c9805fe427c70a6 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 21:03:57 +0700 Subject: [PATCH 30/57] Set up compose viewmodel --- .../watchapp/presentation/quickpairs/QuickPairsScreen.kt | 4 ++-- .../presentation/quickpairs/QuickPairsViewModel.kt | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt index f641614bd..67029153e 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -6,10 +6,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import androidx.wear.compose.material.Text @@ -21,7 +21,7 @@ import dev.arkbuilders.rate.watchapp.presentation.theme.WearButtonStyle @Composable fun QuickPairsScreen( modifier: Modifier = Modifier, - viewModel: QuickPairsViewModel = QuickPairsViewModel(), + viewModel: QuickPairsViewModel = hiltViewModel(), onNavigateToAdd: () -> Unit ) { val quickPairsList = viewModel.quickPairs.collectAsStateWithLifecycle().value diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt index 00a91c3a7..c7e4cc1ca 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt @@ -1,15 +1,22 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel import dev.arkbuilders.rate.core.domain.model.Amount import dev.arkbuilders.rate.core.domain.model.Group +import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair +import jakarta.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import java.math.BigDecimal import java.time.OffsetDateTime -class QuickPairsViewModel : ViewModel() { +@HiltViewModel +class QuickPairsViewModel @Inject constructor( + private val currencyRepo: CurrencyRepo, +) : ViewModel() { + private val _quickPairs: MutableStateFlow> = MutableStateFlow(listOf()) val quickPairs: StateFlow> = _quickPairs From 778d74e0f71c30f3c3e13cdb04da8f0e380291f2 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 21:21:43 +0700 Subject: [PATCH 31/57] Implement search screen --- .../presentation/search/SearchScreen.kt | 31 +++++++++++++++++++ .../presentation/search/SearchViewModel.kt | 17 ++++++++++ 2 files changed, 48 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchScreen.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchViewModel.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchScreen.kt new file mode 100644 index 000000000..442246777 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchScreen.kt @@ -0,0 +1,31 @@ +package dev.arkbuilders.rate.watchapp.presentation.search + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import dev.arkbuilders.rate.core.presentation.theme.ArkColor +import dev.arkbuilders.rate.core.presentation.ui.SearchTextField + +@Composable +fun SearchScreen( + modifier: Modifier = Modifier, + viewModel: SearchViewModel = hiltViewModel(), +) { + ScalingLazyColumn( + modifier = modifier.fillMaxSize().background(ArkColor.BGSecondaryAlt), + contentPadding = PaddingValues(4.dp) + ) { + item { + SearchTextField(modifier = Modifier + .fillMaxWidth() + .padding(16.dp)) + } + } +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchViewModel.kt new file mode 100644 index 000000000..67b7de3bc --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchViewModel.kt @@ -0,0 +1,17 @@ +package dev.arkbuilders.rate.watchapp.presentation.search + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.arkbuilders.rate.core.domain.usecase.SearchUseCase +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( +// private val searchUseCase: SearchUseCase, +): ViewModel() { + + init { + + } + +} From 57be820e9409cb7abab47dafb301794131fc09f2 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 6 Sep 2025 21:21:56 +0700 Subject: [PATCH 32/57] Fix wrong annotation --- .../dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt | 1 + .../rate/watchapp/presentation/main/MainViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index 12d35348c..35b6253c9 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -39,6 +39,7 @@ class MainActivity : ComponentActivity() { ) { composable("list") { // OptionsScreen() +// SearchScreen() QuickPairsScreen( onNavigateToAdd = { navController.navigate("addquickpairs") diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/main/MainViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/main/MainViewModel.kt index 0b1c34e81..744915d44 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/main/MainViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/main/MainViewModel.kt @@ -4,8 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo -import jakarta.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( From cb9d96b0de836e35de06597fc5a22904c859a735 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 9 May 2026 16:15:26 +0700 Subject: [PATCH 33/57] Configure build --- watchapp/build.gradle.kts | 55 ++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts index d1d11ffa4..9846d9e2e 100644 --- a/watchapp/build.gradle.kts +++ b/watchapp/build.gradle.kts @@ -9,6 +9,11 @@ android { namespace = "dev.arkbuilders.rate.watchapp" compileSdk = libs.versions.compileSdk.get().toInt() + buildFeatures { + compose = true + buildConfig = true + } + defaultConfig { applicationId = "dev.arkbuilders.rate.watchapp" minSdk = libs.versions.minSdk.get().toInt() @@ -18,6 +23,37 @@ android { useSupportLibrary = true } + val cryptoRatesLastModified = + rootProject.file("core/data/src/main/assets/crypto-rates.json").lastModified() + val fiatRatesLastModified = + rootProject.file("core/data/src/main/assets/fiat-rates.json").lastModified() + + buildConfigField( + "long", + "CRYPTO_LAST_MODIFIED", + cryptoRatesLastModified.toString(), + ) + buildConfigField( + "long", + "FIAT_LAST_MODIFIED", + fiatRatesLastModified.toString(), + ) + + val cryptoIcons = collectCurrencyIcons(project.rootDir.resolve("cryptoicons")) + val fiatIcons = collectCurrencyIcons(project.rootDir.resolve("fiaticons")) + val allIcons = (cryptoIcons + fiatIcons).distinct() + + buildConfigField( + "String[]", + "ICON_CODES", + allIcons.joinToString( + prefix = "new String[] {", + postfix = "}", + separator = ", ", + ) { + "\"$it\"" + }, + ) } buildTypes { @@ -30,17 +66,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } kotlinOptions { - jvmTarget = "17" - } - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.8" + jvmTarget = "21" } packaging { resources { @@ -49,6 +79,12 @@ android { } } +fun collectCurrencyIcons(moduleDir: File): List { + val drawableDir = moduleDir.resolve("src/main/res/drawable") + return drawableDir.listFiles()?.map { it.nameWithoutExtension.uppercase() } + ?.map { if (it == "curr_try") "try" else it } ?: emptyList() +} + dependencies { implementation(project(":core:db")) implementation(project(":core:data")) @@ -74,6 +110,7 @@ dependencies { // implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation (libs.androidx.compose.navigation )// Or the latest version + implementation("androidx.wear.compose:compose-material3:1.6.1") // Or current version implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.compose.material) From 64ae30e1b186d8a0523a7a00c9da69dd6ac4cd8d Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 9 May 2026 16:16:13 +0700 Subject: [PATCH 34/57] Set up navigation and build config for json file --- .../watchapp/presentation/MainActivity.kt | 57 ++++++++++++++++++- .../presentation/RateWatchApplication.kt | 32 +++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index 35b6253c9..b1ed511ce 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -12,8 +12,13 @@ import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import dagger.hilt.android.AndroidEntryPoint import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.AddQuickPairsScreen +import dev.arkbuilders.rate.watchapp.presentation.options.OptionsScreen +import dev.arkbuilders.rate.watchapp.presentation.options.SuccessScreen import dev.arkbuilders.rate.watchapp.presentation.quickpairs.QuickPairsScreen +import dev.arkbuilders.rate.watchapp.presentation.search.SearchScreen import dev.arkbuilders.rate.watchapp.presentation.theme.ArkrateTheme +import androidx.navigation.NavType +import androidx.navigation.navArgument @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -38,16 +43,64 @@ class MainActivity : ComponentActivity() { startDestination = "list" ) { composable("list") { -// OptionsScreen() -// SearchScreen() QuickPairsScreen( onNavigateToAdd = { navController.navigate("addquickpairs") + }, + onNavigateToSearch = { + navController.navigate("search") + }, + onNavigateToOptions = { id -> + navController.navigate("options/$id") } ) } composable("addquickpairs") { AddQuickPairsScreen( + navController = navController, + onNavigateToSearch = { field -> + navController.navigate("search/$field") + }, + onNavigateBack = { navController.popBackStack() } + ) + } + composable( + route = "options/{id}", + arguments = listOf(navArgument("id") { type = NavType.LongType }) + ) { + OptionsScreen( + onSearchClick = { navController.navigate("search") }, + onDeleteSuccess = { navController.navigate("success") }, + onUpdateClick = { navController.popBackStack() }, + onPinClick = { navController.popBackStack() }, + onReuseClick = { navController.popBackStack() } + ) + } + composable( + route = "search/{field}", + arguments = listOf(navArgument("field") { type = NavType.StringType }) + ) { backStackEntry -> + val field = backStackEntry.arguments?.getString("field") + SearchScreen( + onCurrencyClick = { code -> + navController.previousBackStackEntry?.savedStateHandle?.set("selected_currency", code) + navController.previousBackStackEntry?.savedStateHandle?.set("target_field", field) + navController.popBackStack() + } + ) + } + composable("search") { + SearchScreen( + onCurrencyClick = { code -> + navController.popBackStack() + } + ) + } + composable("success") { + SuccessScreen( + onTimeout = { + navController.popBackStack("list", inclusive = false) + } ) } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt index b3da758e1..cfcbf018e 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/RateWatchApplication.kt @@ -3,7 +3,39 @@ package dev.arkbuilders.rate.watchapp.presentation import android.app.Application import dagger.hilt.android.HiltAndroidApp +import dev.arkbuilders.rate.core.domain.BuildConfigFields +import dev.arkbuilders.rate.core.domain.BuildConfigFieldsProvider +import dev.arkbuilders.rate.watchapp.BuildConfig +import java.time.Instant +import java.time.ZoneOffset +import javax.inject.Inject + @HiltAndroidApp class RateWatchApplication: Application() { + @Inject + lateinit var buildConfigFieldsProvider: BuildConfigFieldsProvider + + override fun onCreate() { + super.onCreate() + initBuildConfigFields() + } + + private fun initBuildConfigFields() { + val fallbackCryptoRatesFetchDate = + Instant.ofEpochMilli(BuildConfig.CRYPTO_LAST_MODIFIED).atOffset(ZoneOffset.UTC) + val fallbackFiatRatesFetchDate = + Instant.ofEpochMilli(BuildConfig.FIAT_LAST_MODIFIED).atOffset(ZoneOffset.UTC) + buildConfigFieldsProvider.init( + BuildConfigFields( + buildType = BuildConfig.BUILD_TYPE, + versionCode = BuildConfig.VERSION_CODE, + versionName = BuildConfig.VERSION_NAME, + isGooglePlayBuild = false, // Default to false for watch app for now + fallbackCryptoRatesFetchDate = fallbackCryptoRatesFetchDate, + fallbackFiatRatesFetchDate = fallbackFiatRatesFetchDate, + availableIconCodes = BuildConfig.ICON_CODES.toSet(), + ), + ) + } } From 631cbff055533cd6d7766371fcf566b5654b28ee Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 9 May 2026 16:16:35 +0700 Subject: [PATCH 35/57] Theming and shared components --- .../rate/watchapp/presentation/theme/Theme.kt | 25 +++++++++++++++--- .../presentation/theme/WearComponents.kt | 26 +++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/Theme.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/Theme.kt index 370c5c7ff..d8672db26 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/Theme.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/Theme.kt @@ -3,15 +3,32 @@ package dev.arkbuilders.rate.watchapp.presentation.theme import androidx.compose.runtime.Composable import androidx.wear.compose.material.MaterialTheme +import androidx.compose.ui.graphics.Color +import androidx.wear.compose.material.Colors +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +val LightWearColors = Colors( + primary = ArkColor.Primary, + primaryVariant = ArkColor.BrandSecondary, + secondary = ArkColor.Secondary, + secondaryVariant = ArkColor.Teal700, + background = Color.White, + surface = Color.White, + error = ArkColor.UtilityError500, + onPrimary = Color.White, + onSecondary = Color.White, + onBackground = ArkColor.TextPrimary, + onSurface = ArkColor.TextPrimary, + onSurfaceVariant = ArkColor.TextSecondary, + onError = Color.White +) + @Composable fun ArkrateTheme( content: @Composable () -> Unit ) { - /** - * Empty theme to customize for your app. - * See: https://developer.android.com/jetpack/compose/designsystems/custom - */ MaterialTheme( + colors = LightWearColors, content = content ) } \ No newline at end of file diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt index dcceb57d5..df8bf2e8b 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -176,3 +177,28 @@ fun WearCompactButton( ) } } + +@Composable +fun WearFab( + onClick: () -> Unit, + icon: ImageVector, + modifier: Modifier = Modifier, + backgroundColor: Color = ArkColor.Primary, + contentColor: Color = Color.White +) { + Button( + onClick = onClick, + modifier = modifier.size(52.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = backgroundColor, + contentColor = contentColor + ), + shape = CircleShape + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } +} From 29c3a0bcbc2c6f1a4ad598a919eec81e11b739a7 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 9 May 2026 16:17:29 +0700 Subject: [PATCH 36/57] Refacor and implement add quick pairs screen --- .../addquickpairs/AddQuickPairsScreen.kt | 297 ++++++++++++++---- .../addquickpairs/AddQuickPairsViewModel.kt | 116 ++++++- .../addquickpairs/composables/WearNumpad.kt | 128 ++++++++ 3 files changed, 471 insertions(+), 70 deletions(-) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/WearNumpad.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt index cb30dd7ef..8de9fb20c 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt @@ -1,124 +1,283 @@ package dev.arkbuilders.rate.watchapp.presentation.addquickpairs import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.outlined.Edit -import androidx.compose.material.icons.outlined.Email -import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MenuDefaults -import androidx.compose.material3.MenuItemColors +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.Refresh 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.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.material.Button +import androidx.wear.compose.material.ButtonDefaults +import androidx.wear.compose.material.Icon import androidx.wear.compose.material.Text import dev.arkbuilders.rate.core.presentation.theme.ArkColor -import dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables.CurrencyInputField +import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.CurrIcon @Composable -fun AddQuickPairsScreen(modifier: Modifier = Modifier) { - var expanded by remember { mutableStateOf(false) } - val scrollState = rememberScrollState() +fun AddQuickPairsScreen( + modifier: Modifier = Modifier, + viewModel: AddQuickPairsViewModel = hiltViewModel(), + navController: NavHostController, + onNavigateToSearch: (String) -> Unit, + onNavigateBack: () -> Unit +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + val selectedCurrency = navController.currentBackStackEntry + ?.savedStateHandle + ?.getStateFlow("selected_currency", null) + ?.collectAsStateWithLifecycle() + + val targetField = navController.currentBackStackEntry + ?.savedStateHandle + ?.getStateFlow("target_field", null) + ?.collectAsStateWithLifecycle() + + LaunchedEffect(selectedCurrency?.value) { + val code = selectedCurrency?.value ?: return@LaunchedEffect + val field = targetField?.value ?: return@LaunchedEffect + + if (field == "from") { + viewModel.onBaseCurrencyChanged(code) + } else if (field == "to") { + viewModel.onTargetCurrencyChanged(code) + } + + // Clear the result after processing + navController.currentBackStackEntry?.savedStateHandle?.remove("selected_currency") + navController.currentBackStackEntry?.savedStateHandle?.remove("target_field") + } + + if (state.isSaved) { + onNavigateBack() + } ScalingLazyColumn( - modifier = modifier.fillMaxSize().background(ArkColor.BGSecondaryAlt), - verticalArrangement = Arrangement.Center, + modifier = modifier + .fillMaxSize() + .background(Color.White), + verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.CenterHorizontally ) { item { Text( - modifier = modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), text = "Add", textAlign = TextAlign.Center, - color = ArkColor.TextPrimary + fontWeight = FontWeight.Bold, + color = ArkColor.TextPrimary, + fontSize = 16.sp ) } item { - CurrencyInputField( - label = "To", - currencyCode = "EUR", - value = "0.92", - onValueChange = { }, - onCurrencyClick = { - - }, - showLabel = true, - showDeleteButton = false, - onDeleteClick = {} + Text( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + text = "From", + textAlign = TextAlign.Start, + color = ArkColor.TextSecondary, + fontSize = 12.sp ) } item { - CurrencyInputField( - label = "To", - currencyCode = "EUR", - value = "0.92", - onValueChange = { }, - onCurrencyClick = {}, - showLabel = false, - showDeleteButton = false, - onDeleteClick = {} - ) + Row( + modifier = Modifier + .fillMaxWidth(0.9f) + .clip(RoundedCornerShape(8.dp)) + .background(ArkColor.UtilitySuccess50) // Light greenish background + .border(1.dp, ArkColor.UtilitySuccess200, RoundedCornerShape(8.dp)) + .padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + // Removed Edit icon as per request + + // Currency Selector + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable { onNavigateToSearch("from") } + ) { + CurrIcon(modifier = Modifier.size(20.dp), code = state.baseCurrency) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = state.baseCurrency, + color = ArkColor.TextPrimary, + fontWeight = FontWeight.Medium, + fontSize = 14.sp + ) + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = "Select Currency", + tint = ArkColor.Primary, + modifier = Modifier.size(16.dp) + ) + } + + // Amount + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.CenterEnd + ) { + if (state.baseAmount.isEmpty()) { + Text( + text = "0", + color = ArkColor.TextPlaceHolder, + fontSize = 14.sp, + textAlign = TextAlign.End + ) + } + BasicTextField( + value = state.baseAmount, + onValueChange = { viewModel.onAmountInput(it) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + textStyle = TextStyle( + color = ArkColor.TextPrimary, + fontSize = 14.sp, + textAlign = TextAlign.End + ), + modifier = Modifier.fillMaxWidth() + ) + } + } } item { Box( - modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart) + modifier = Modifier + .padding(vertical = 4.dp) + .size(24.dp) + .clip(CircleShape) + .background(Color.White) + .border(1.dp, ArkColor.BorderSecondary, CircleShape) + .clickable { viewModel.onSwap() }, + contentAlignment = Alignment.Center ) { - IconButton(onClick = { expanded = true }) { - Icon(Icons.Default.MoreVert, contentDescription = "Localized description") - } - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - DropdownMenuItem( - text = { Text("Edit") }, - onClick = { /* Handle edit! */ }, - leadingIcon = { Icon(Icons.Outlined.Edit, contentDescription = null) } + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Swap", + tint = ArkColor.Primary, + modifier = Modifier.size(14.dp) + ) + } + } + + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + text = "To", + textAlign = TextAlign.Start, + color = ArkColor.TextSecondary, + fontSize = 12.sp + ) + } + + item { + Row( + modifier = Modifier + .fillMaxWidth(0.9f) + .clip(RoundedCornerShape(8.dp)) + .background(Color.White) + .border(1.dp, ArkColor.BorderSecondary, RoundedCornerShape(8.dp)) + .padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable { onNavigateToSearch("to") } + ) { + CurrIcon(modifier = Modifier.size(20.dp), code = state.targetCurrency) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = state.targetCurrency, + color = ArkColor.TextPrimary, + fontWeight = FontWeight.Medium, + fontSize = 14.sp ) - DropdownMenuItem( - text = { Text("Settings") }, - onClick = { /* Handle settings! */ }, - leadingIcon = { Icon(Icons.Outlined.Settings, contentDescription = null) } + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = "Select Currency", + tint = ArkColor.Primary, + modifier = Modifier.size(16.dp) ) - DropdownMenuItem( - text = { Text("Send Feedback") }, - onClick = { /* Handle send feedback! */ }, - leadingIcon = { Icon(Icons.Outlined.Email, contentDescription = null) }, - trailingIcon = { Text("F11", textAlign = TextAlign.Center) } + } + + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.CenterEnd + ) { + if (state.targetAmount.isEmpty()) { + Text( + text = "0", + color = ArkColor.TextPlaceHolder, + fontSize = 14.sp, + textAlign = TextAlign.End + ) + } + BasicTextField( + value = state.targetAmount, + onValueChange = { viewModel.onTargetAmountInput(it) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + textStyle = TextStyle( + color = ArkColor.TextPrimary, + fontSize = 14.sp, + textAlign = TextAlign.End + ), + modifier = Modifier.fillMaxWidth() ) } } } + item { + Button( + onClick = { viewModel.savePair() }, + modifier = Modifier + .padding(top = 16.dp, bottom = 8.dp) + .fillMaxWidth(0.8f) + .height(36.dp), + colors = ButtonDefaults.primaryButtonColors(backgroundColor = ArkColor.Primary) + ) { + Text("Save Pair", fontSize = 14.sp, fontWeight = FontWeight.Bold) + } + } } } - -@Composable -@Preview -fun AddQuickPairsScreenPreview() { - AddQuickPairsScreen() -} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt index 1b95d3563..f2a40d770 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt @@ -1,7 +1,121 @@ package dev.arkbuilders.rate.watchapp.presentation.addquickpairs import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.arkbuilders.rate.core.domain.CurrUtils +import dev.arkbuilders.rate.core.domain.model.Amount +import dev.arkbuilders.rate.core.domain.usecase.ConvertWithRateUseCase +import dev.arkbuilders.rate.core.domain.usecase.GetGroupByIdOrCreateDefaultUseCase +import dev.arkbuilders.rate.core.domain.model.GroupFeatureType +import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair +import dev.arkbuilders.rate.feature.quick.domain.repo.QuickRepo +import dev.arkbuilders.rate.core.domain.toBigDecimalArk +import dev.arkbuilders.rate.core.domain.toDoubleArk +import jakarta.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.time.OffsetDateTime -class AddQuickPairsViewModel : ViewModel() { +data class AddQuickState( + val baseCurrency: String = "USD", + val targetCurrency: String = "EUR", + val baseAmount: String = "", + val targetAmount: String = "", + val isSaved: Boolean = false +) +@HiltViewModel +class AddQuickPairsViewModel @Inject constructor( + private val quickRepo: QuickRepo, + private val convertUseCase: ConvertWithRateUseCase, + private val getGroupByIdOrCreateDefaultUseCase: GetGroupByIdOrCreateDefaultUseCase +) : ViewModel() { + + private val _state = MutableStateFlow(AddQuickState()) + val state: StateFlow = _state.asStateFlow() + + init { + calculate() + } + + fun onBaseCurrencyChanged(code: String) { + _state.value = _state.value.copy(baseCurrency = code) + calculate() + } + + fun onTargetCurrencyChanged(code: String) { + _state.value = _state.value.copy(targetCurrency = code) + calculate() + } + + fun onAmountInput(input: String) { + val newAmount = CurrUtils.validateInput(_state.value.baseAmount, input) + _state.value = _state.value.copy(baseAmount = newAmount) + calculate(fromBase = true) + } + + fun onTargetAmountInput(input: String) { + val newAmount = CurrUtils.validateInput(_state.value.targetAmount, input) + _state.value = _state.value.copy(targetAmount = newAmount) + calculate(fromBase = false) + } + + fun onSwap() { + val currentState = _state.value + _state.value = currentState.copy( + baseCurrency = currentState.targetCurrency, + targetCurrency = currentState.baseCurrency + ) + calculate(fromBase = true) + } + + private fun calculate(fromBase: Boolean = true) { + viewModelScope.launch { + val s = _state.value + val sourceAmount = if (fromBase) s.baseAmount else s.targetAmount + val sourceCurrency = if (fromBase) s.baseCurrency else s.targetCurrency + val destCurrency = if (fromBase) s.targetCurrency else s.baseCurrency + + if (sourceAmount.isEmpty() || sourceAmount.toDoubleArk() == 0.0) { + if (fromBase) { + _state.value = s.copy(targetAmount = "") + } else { + _state.value = s.copy(baseAmount = "") + } + return@launch + } + + val (amount, _) = convertUseCase.invoke( + Amount(sourceCurrency, sourceAmount.toBigDecimalArk()), + destCurrency + ) + val roundValue = CurrUtils.roundOff(amount.value) + if (fromBase) { + _state.value = _state.value.copy(targetAmount = roundValue) + } else { + _state.value = _state.value.copy(baseAmount = roundValue) + } + } + } + + fun savePair() { + viewModelScope.launch { + val s = _state.value + val group = getGroupByIdOrCreateDefaultUseCase(null, GroupFeatureType.Quick) + val quick = QuickPair( + id = 0, + from = s.baseCurrency, + amount = s.baseAmount.toBigDecimalArk(), + to = listOf(Amount(s.targetCurrency, s.targetAmount.toBigDecimalArk())), + calculatedDate = OffsetDateTime.now(), + pinnedDate = null, + group = group + ) + quickRepo.insert(quick) + _state.value = s.copy(isSaved = true) + } + } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/WearNumpad.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/WearNumpad.kt new file mode 100644 index 000000000..9b56ab812 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/WearNumpad.kt @@ -0,0 +1,128 @@ +package dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.theme.ArkColor + +@Composable +fun WearNumpad( + modifier: Modifier = Modifier, + amount: String, + onNumberClick: (Int) -> Unit, + onDotClick: () -> Unit, + onDeleteClick: () -> Unit, + onConfirmClick: () -> Unit +) { + Column( + modifier = modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Display + Text( + text = amount.ifEmpty { "0" }, + fontWeight = FontWeight.Bold, + color = ArkColor.TextPrimary, + modifier = Modifier.padding(bottom = 8.dp) + ) + + // Numpad 1-3 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + NumpadButton("1") { onNumberClick(1) } + NumpadButton("2") { onNumberClick(2) } + NumpadButton("3") { onNumberClick(3) } + } + + // Numpad 4-6 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + NumpadButton("4") { onNumberClick(4) } + NumpadButton("5") { onNumberClick(5) } + NumpadButton("6") { onNumberClick(6) } + } + + // Numpad 7-9 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + NumpadButton("7") { onNumberClick(7) } + NumpadButton("8") { onNumberClick(8) } + NumpadButton("9") { onNumberClick(9) } + } + + // Bottom row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + NumpadButton(".") { onDotClick() } + NumpadButton("0") { onNumberClick(0) } + + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(ArkColor.BGTertiary) + .clickable { onDeleteClick() }, + contentAlignment = Alignment.Center + ) { + Icon(Icons.Default.ArrowBack, contentDescription = "Delete", tint = ArkColor.TextPrimary) + } + } + + // Confirm Button + Box( + modifier = Modifier + .padding(top = 8.dp) + .size(40.dp) + .clip(CircleShape) + .background(ArkColor.Primary) + .clickable { onConfirmClick() }, + contentAlignment = Alignment.Center + ) { + Icon(Icons.Default.Check, contentDescription = "Confirm", tint = Color.White) + } + } +} + +@Composable +fun NumpadButton(text: String, onClick: () -> Unit) { + Box( + modifier = Modifier + .size(40.dp) + .padding(2.dp) + .clip(CircleShape) + .background(ArkColor.BGSecondaryAlt) + .clickable { onClick() }, + contentAlignment = Alignment.Center + ) { + Text(text = text, fontWeight = FontWeight.SemiBold, color = ArkColor.TextPrimary) + } +} From 088452e8cd9278886006f6608878e31836c2b9cb Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 9 May 2026 16:18:04 +0700 Subject: [PATCH 37/57] Implement quick pairs screen --- .../quickpairs/QuickPairsScreen.kt | 132 ++++++++++++--- .../quickpairs/QuickPairsViewModel.kt | 120 ++++--------- .../quickpairs/composables/QuickPairItem.kt | 157 +++++++++++------- .../quickpairs/composables/QuickPairsEmpty.kt | 71 +++++--- 4 files changed, 273 insertions(+), 207 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt index 67029153e..88fe234a7 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -1,55 +1,135 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.wear.compose.foundation.lazy.ScalingLazyColumn -import androidx.wear.compose.material.Text +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.material3.Text +import dev.arkbuilders.rate.core.presentation.theme.ArkColor import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairItem import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.QuickPairsEmpty -import dev.arkbuilders.rate.watchapp.presentation.theme.WearButton -import dev.arkbuilders.rate.watchapp.presentation.theme.WearButtonStyle +import dev.arkbuilders.rate.watchapp.presentation.theme.WearFab @Composable fun QuickPairsScreen( modifier: Modifier = Modifier, viewModel: QuickPairsViewModel = hiltViewModel(), - onNavigateToAdd: () -> Unit + onNavigateToAdd: () -> Unit, + onNavigateToSearch: () -> Unit, + onNavigateToOptions: (Long) -> Unit ) { val quickPairsList = viewModel.quickPairs.collectAsStateWithLifecycle().value + val listState = rememberScalingLazyListState() - if (quickPairsList.isEmpty()) { - QuickPairsEmpty(modifier = modifier.fillMaxSize()) - } else { - ScalingLazyColumn( - modifier = modifier.fillMaxSize(), - contentPadding = PaddingValues(4.dp) - ) { - item { - Text( - modifier = modifier.fillMaxWidth(), - text = "Quick", - textAlign = TextAlign.Center - ) + var isFabVisible by remember { mutableStateOf(true) } + var lastScrollIndex by remember { mutableIntStateOf(0) } + var lastScrollOffset by remember { mutableIntStateOf(0) } + + LaunchedEffect(listState) { + snapshotFlow { listState.centerItemIndex to listState.centerItemScrollOffset } + .collect { (index, offset) -> + if (index > lastScrollIndex || (index == lastScrollIndex && offset > lastScrollOffset)) { + isFabVisible = false + } else if (index < lastScrollIndex || (index == lastScrollIndex && offset < lastScrollOffset)) { + isFabVisible = true + } + lastScrollIndex = index + lastScrollOffset = offset } - item { - WearButton( - text = "Add", - onClick = onNavigateToAdd, - style = WearButtonStyle.Primary, - leadingIcon = Icons.Outlined.Add + } + + val fabProgress by animateFloatAsState( + targetValue = if (isFabVisible) 1f else 0f, + animationSpec = tween(durationMillis = 800), + label = "fabProgress" + ) + + Scaffold( + modifier = modifier.fillMaxSize(), + containerColor = Color.Transparent, + floatingActionButtonPosition = FabPosition.Center, + floatingActionButton = { + WearFab( + onClick = { if (isFabVisible) onNavigateToAdd() }, + icon = Icons.Outlined.Add, + modifier = Modifier + .padding(bottom = 8.dp) + .graphicsLayer { + alpha = fabProgress + scaleX = fabProgress + scaleY = fabProgress + } + ) + } + ) { padding -> + Box(modifier = Modifier.padding(padding).fillMaxSize()) { + + if (quickPairsList.isEmpty()) { + QuickPairsEmpty( + modifier = Modifier.fillMaxSize(), + onAddClick = onNavigateToAdd ) - } - items(quickPairsList.size, key = null) { idx -> - QuickPairItem(quick = quickPairsList[idx], onClick = onNavigateToAdd) + } else { + ScalingLazyColumn( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + state = listState, + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues( + top = 4.dp, + start = 4.dp, + end = 4.dp, + bottom = 40.dp + ) + ) { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + text = "Quick", + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + textAlign = TextAlign.Center, + color = ArkColor.TextPrimary + ) + } + items(quickPairsList.size, key = { quickPairsList[it].id }) { idx -> + QuickPairItem( + quick = quickPairsList[idx], + onClick = { onNavigateToSearch() } + ) + } + } } } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt index c7e4cc1ca..b7b7314e2 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt @@ -1,105 +1,43 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dev.arkbuilders.rate.core.domain.model.Amount -import dev.arkbuilders.rate.core.domain.model.Group -import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo +import dev.arkbuilders.rate.core.domain.usecase.ConvertWithRateUseCase import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair -import jakarta.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow +import dev.arkbuilders.rate.feature.quick.domain.repo.QuickRepo +import javax.inject.Inject +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import java.math.BigDecimal -import java.time.OffsetDateTime +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch @HiltViewModel class QuickPairsViewModel @Inject constructor( - private val currencyRepo: CurrencyRepo, + private val quickRepo: QuickRepo, + private val convertUseCase: ConvertWithRateUseCase, ) : ViewModel() { - - private val _quickPairs: MutableStateFlow> = MutableStateFlow(listOf()) - val quickPairs: StateFlow> = _quickPairs - - init { - val a = listOf( - QuickPair( - id = 1, - from = "BTC", - amount = BigDecimal.valueOf(1.2), - to = listOf( - Amount("USD", BigDecimal.valueOf(12.0)), - Amount("EUR", BigDecimal.valueOf(12.0)) - ), - calculatedDate = OffsetDateTime.now(), - pinnedDate = null, - group = Group.empty() - ), - QuickPair( - id = 1, - from = "BTC", - amount = BigDecimal.valueOf(1.2), - to = listOf( - Amount("USD", BigDecimal.valueOf(12.0)), - Amount("EUR", BigDecimal.valueOf(12.0)) - ), - calculatedDate = OffsetDateTime.now(), - pinnedDate = null, - group = Group.empty() - ), - - QuickPair( - id = 1, - from = "BTC", - amount = BigDecimal.valueOf(1.2), - to = listOf( - Amount("USD", BigDecimal.valueOf(12.0)), - Amount("EUR", BigDecimal.valueOf(12.0)) - ), - calculatedDate = OffsetDateTime.now(), - pinnedDate = null, - group = Group.empty() - ), - - QuickPair( - id = 1, - from = "BTC", - amount = BigDecimal.valueOf(1.2), - to = listOf( - Amount("USD", BigDecimal.valueOf(12.0)), - Amount("EUR", BigDecimal.valueOf(12.0)) - ), - calculatedDate = OffsetDateTime.now(), - pinnedDate = null, - group = Group.empty() - ), - QuickPair( - id = 1, - from = "BTC", - amount = BigDecimal.valueOf(1.2), - to = listOf( - Amount("USD", BigDecimal.valueOf(12.0)), - Amount("EUR", BigDecimal.valueOf(12.0)) - ), - calculatedDate = OffsetDateTime.now(), - pinnedDate = null, - group = Group.empty() - ), - - QuickPair( - id = 1, - from = "BTC", - amount = BigDecimal.valueOf(1.2), - to = listOf( - Amount("USD", BigDecimal.valueOf(12.0)), - Amount("EUR", BigDecimal.valueOf(12.0)) - ), - calculatedDate = OffsetDateTime.now(), - pinnedDate = null, - group = Group.empty() - ) + val quickPairs: StateFlow> = quickRepo.allFlow() + .map { pairs -> + pairs.map { pair -> + val actualTo = pair.to.map { toAmount -> + val (convertedAmt, _) = convertUseCase.invoke(pair.from, pair.amount, toAmount.code) + convertedAmt + } + pair.copy(to = actualTo) + }.sortedByDescending { it.calculatedDate } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() ) - _quickPairs.value = a - } + fun deletePair(pair: QuickPair) { + viewModelScope.launch { + quickRepo.delete(pair.id) + } + } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt index 84e6a8d5a..fb47a5d47 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt @@ -1,13 +1,18 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box 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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -20,18 +25,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.wear.compose.material.Card -import androidx.wear.compose.material.Icon -import androidx.wear.compose.material.Text +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.Text import dev.arkbuilders.rate.core.domain.CurrUtils import dev.arkbuilders.rate.core.domain.model.Amount import dev.arkbuilders.rate.core.domain.model.CurrencyCode import dev.arkbuilders.rate.core.domain.model.Group +import dev.arkbuilders.rate.core.presentation.CoreRDrawable import dev.arkbuilders.rate.core.presentation.theme.ArkColor import dev.arkbuilders.rate.core.presentation.utils.IconUtils import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair @@ -44,89 +48,116 @@ fun QuickPairItem( quick: QuickPair, onClick: () -> Unit, ) { - var isExpanded by remember { - mutableStateOf(true) - } - Card( - onClick = onClick, + var isExpanded by remember { mutableStateOf(false) } + + Column( modifier = modifier - .padding(horizontal = 12.dp, vertical = 4.dp) + .fillMaxWidth() + .clickable { onClick() } + .padding(horizontal = 12.dp, vertical = 8.dp) + .background(Color.White) ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + // Top row: Flags and "2 mins ago" + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Row(modifier = modifier.align(Alignment.Start)) { - CurrIcon( - modifier = modifier.size(16.dp), - code = quick.from - ) + Row(verticalAlignment = Alignment.CenterVertically) { + CurrIcon(modifier = Modifier.size(24.dp), code = quick.from) if (quick.to.size > 1) { Box( - modifier = - modifier - .size(16.dp) + modifier = Modifier + .size(24.dp) + .offset(x = (-8).dp) + .border(1.dp, Color.White, CircleShape) .background(ArkColor.BGTertiary, CircleShape), + contentAlignment = Alignment.Center ) { Text( - modifier = Modifier.align(Alignment.Center), - text = "+ ${quick.to.size}", - fontWeight = FontWeight.SemiBold, - fontSize = 8.sp, + text = "+${quick.to.size}", + fontWeight = FontWeight.Bold, + fontSize = 10.sp, color = ArkColor.TextTertiary, ) } - } else { + } else if (quick.to.isNotEmpty()) { CurrIcon( - modifier = modifier.size(16.dp), + modifier = Modifier + .size(24.dp) + .offset(x = (-8).dp) + .border(1.dp, Color.White, CircleShape), code = quick.to.first().code ) } + } + Text( + text = "2 mins ago", + color = ArkColor.TextTertiary, + fontSize = 10.sp + ) + } - - Text( - text = "2 mins ago", - modifier = modifier.fillMaxWidth(), - textAlign = TextAlign.End - ) + // Title Row: "EUR to USD" and Chevron + Row( + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + val titleText = if (quick.to.size > 1) { + "${quick.from} to ${quick.to.first().code}, and ${quick.to.size - 1}+" + } else { + "${quick.from} to ${quick.to.firstOrNull()?.code ?: ""}" } - if (isExpanded) { - Text( - modifier = modifier.fillMaxWidth(), - text = "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = ", + Text( + text = titleText, + color = ArkColor.TextPrimary, + fontWeight = FontWeight.Bold, + fontSize = 14.sp + ) + if (quick.to.size > 1) { + Icon( + painter = painterResource(if (isExpanded) CoreRDrawable.ic_chevron_up else CoreRDrawable.ic_chevron), + contentDescription = "Expand", + tint = ArkColor.FGSecondary, + modifier = Modifier + .size(16.dp) + .clickable { isExpanded = !isExpanded } ) - quick.to.forEach { + } + } + + // Subtitle: Base amount + val baseAmountText = if (quick.to.size == 1) { + "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = ${CurrUtils.prepareToDisplay(quick.to.first().value)} ${quick.to.first().code}" + } else { + "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = ${CurrUtils.prepareToDisplay(quick.to.first().value)} ${quick.to.first().code}" + } + Text( + text = baseAmountText, + color = ArkColor.TextTertiary, + fontSize = 12.sp, + modifier = Modifier.padding(top = 2.dp) + ) + + // Expanded view + if (isExpanded && quick.to.size > 1) { + Column(modifier = Modifier.padding(top = 8.dp)) { + quick.to.forEach { target -> Row( - modifier = Modifier.padding(top = 8.dp), - verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically ) { - CurrIcon( - modifier = Modifier.size(20.dp), - code = it.code - ) + CurrIcon(modifier = Modifier.size(16.dp), code = target.code) + Spacer(modifier = Modifier.width(8.dp)) Text( - modifier = modifier - .fillMaxWidth() - .padding(start = 8.dp), - text = "${CurrUtils.prepareToDisplay(it.value)} ${it.code}", + text = "${CurrUtils.prepareToDisplay(target.value)} ${target.code}", + color = ArkColor.TextPrimary, + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp ) } } - } else { - Text( - modifier = modifier.fillMaxWidth(), - text = "${quick.from} to ${ - quick.to.joinToString( - separator = ", ", - ) { it.code } - }", - ) - Text( - modifier = modifier.fillMaxWidth(), - text = - "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = " + - "${CurrUtils.prepareToDisplay(quick.to.first().value)} ${quick.to.first().code}", - ) } } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt index 941b829e3..a1edf69b9 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt @@ -15,44 +15,61 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.wear.compose.material.Icon -import androidx.wear.compose.material.Text +import androidx.wear.compose.material3.Icon +import androidx.wear.compose.material3.Text import dev.arkbuilders.rate.core.presentation.CoreRDrawable +import androidx.compose.ui.res.stringResource +import dev.arkbuilders.rate.core.presentation.CoreRString +import dev.arkbuilders.rate.core.presentation.theme.ArkColor +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height + @Composable fun QuickPairsEmpty( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onAddClick: () -> Unit ) { - Column( + androidx.wear.compose.foundation.lazy.ScalingLazyColumn( + modifier = modifier.fillMaxSize().background(Color.White), horizontalAlignment = Alignment.CenterHorizontally, + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 24.dp) ) { - Icon( - painter = painterResource(id = CoreRDrawable.ic_empty_quick), - contentDescription = "", - tint = Color.Unspecified, - ) - Text( - modifier = modifier - .padding(horizontal = 8.dp) - .fillMaxWidth(), - text = "Empty Here, But Full of Possibilities!", - fontWeight = FontWeight.SemiBold, - fontSize = 16.sp, - textAlign = TextAlign.Center, - ) - Text( - modifier = modifier.fillMaxWidth() - .padding(horizontal = 8.dp), - text = "Calculate currency from Rate App", - fontSize = 12.sp, - textAlign = TextAlign.Center, - ) - + item { + Icon( + painter = painterResource(id = CoreRDrawable.ic_empty_quick), + contentDescription = null, + tint = Color.Unspecified, + ) + } + item { + Text( + modifier = Modifier.padding(top = 8.dp), + text = stringResource(CoreRString.quick_empty_title), + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + color = ArkColor.TextPrimary, + textAlign = TextAlign.Center, + ) + } + item { + Text( + modifier = Modifier.padding(top = 4.dp), + text = stringResource(CoreRString.quick_empty_desc), + fontSize = 12.sp, + color = ArkColor.TextTertiary, + textAlign = TextAlign.Center, + ) + } + item { + Spacer(modifier = Modifier.height(12.dp)) + } } } @Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) @Composable fun QuickPairEmptyPreview() { - QuickPairsEmpty() + QuickPairsEmpty(onAddClick = {}) } From b9a15e0fbcccad9af35b24723f393c891fbc4a52 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 9 May 2026 16:18:14 +0700 Subject: [PATCH 38/57] Update option button --- .../presentation/options/OptionsScreen.kt | 20 +++++++++++++------ .../presentation/options/WearOptionButton.kt | 13 +++++++++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt index e287e3d63..fabd1460e 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -21,16 +22,20 @@ import androidx.wear.compose.material.Scaffold import androidx.wear.compose.material.Text import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState import dev.arkbuilders.rate.core.presentation.theme.ArkColor +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle @Composable fun OptionsScreen( modifier: Modifier = Modifier, + viewModel: OptionsViewModel = hiltViewModel(), onUpdateClick: () -> Unit = {}, onPinClick: () -> Unit = {}, - onEditClick: () -> Unit = {}, + onSearchClick: () -> Unit = {}, onReuseClick: () -> Unit = {}, - onDeleteClick: () -> Unit = {} + onDeleteSuccess: () -> Unit = {} ) { + val quickPair by viewModel.quickPair.collectAsStateWithLifecycle() val listState = rememberScalingLazyListState() Scaffold( @@ -63,6 +68,7 @@ fun OptionsScreen( WearOptionButton( text = "Update", icon = WearOptionButtonIcon.Refresh, + buttonType = WearOptionButtonType.Success, onClick = onUpdateClick ) } @@ -77,9 +83,9 @@ fun OptionsScreen( item { WearOptionButton( - text = "Edit", - icon = WearOptionButtonIcon.Edit, - onClick = onEditClick + text = "Search", + icon = WearOptionButtonIcon.Search, + onClick = onSearchClick ) } @@ -96,7 +102,9 @@ fun OptionsScreen( text = "Delete", icon = WearOptionButtonIcon.Delete, buttonType = WearOptionButtonType.Destructive, - onClick = onDeleteClick + onClick = { + viewModel.deletePair(onDeleted = onDeleteSuccess) + } ) } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionButton.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionButton.kt index efd37852d..3651e076b 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionButton.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/WearOptionButton.kt @@ -11,6 +11,7 @@ import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Star import androidx.compose.material.icons.outlined.Share +import androidx.compose.material.icons.outlined.Search import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -30,6 +31,7 @@ import dev.arkbuilders.rate.core.presentation.theme.ArkColor enum class WearOptionButtonType { Default, + Success, Destructive } @@ -38,6 +40,7 @@ enum class WearOptionButtonIcon(val imageVector: ImageVector) { Pin(Icons.Outlined.Star), Edit(Icons.Outlined.Edit), Reuse(Icons.Outlined.Share), + Search(Icons.Outlined.Search), Delete(Icons.Outlined.Delete) } @@ -54,7 +57,10 @@ fun WearOptionButton( backgroundColor = Color.White, contentColor = ArkColor.TextSecondary ) - + WearOptionButtonType.Success -> ButtonDefaults.buttonColors( + backgroundColor = ArkColor.UtilitySuccess50, + contentColor = ArkColor.Primary + ) WearOptionButtonType.Destructive -> ButtonDefaults.buttonColors( backgroundColor = Color.White, contentColor = ArkColor.FGErrorPrimary @@ -65,6 +71,9 @@ fun WearOptionButton( WearOptionButtonType.Default -> ButtonDefaults.buttonBorder( borderStroke = androidx.compose.foundation.BorderStroke(1.dp, ArkColor.BorderSecondary) ) + WearOptionButtonType.Success -> ButtonDefaults.buttonBorder( + borderStroke = androidx.compose.foundation.BorderStroke(1.dp, ArkColor.UtilitySuccess200) + ) WearOptionButtonType.Destructive -> ButtonDefaults.buttonBorder( borderStroke = androidx.compose.foundation.BorderStroke(1.dp, ArkColor.BorderError) ) @@ -89,6 +98,7 @@ fun WearOptionButton( modifier = Modifier.size(20.dp), tint = when (buttonType) { WearOptionButtonType.Default -> ArkColor.TextSecondary + WearOptionButtonType.Success -> ArkColor.Primary WearOptionButtonType.Destructive -> ArkColor.FGErrorPrimary } ) @@ -100,6 +110,7 @@ fun WearOptionButton( textAlign = TextAlign.Start, color = when (buttonType) { WearOptionButtonType.Default -> ArkColor.TextSecondary + WearOptionButtonType.Success -> ArkColor.TextSecondary WearOptionButtonType.Destructive -> ArkColor.FGErrorPrimary } ) From b885b13c989e37a46722a686757850c88941efd6 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 9 May 2026 16:18:32 +0700 Subject: [PATCH 39/57] Search screen --- .../presentation/search/SearchScreen.kt | 58 +++++++++++++++++-- .../presentation/search/SearchViewModel.kt | 42 +++++++++++++- 2 files changed, 94 insertions(+), 6 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchScreen.kt index 442246777..33a4fbfbb 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchScreen.kt @@ -1,31 +1,79 @@ package dev.arkbuilders.rate.watchapp.presentation.search import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.material.Text import dev.arkbuilders.rate.core.presentation.theme.ArkColor import dev.arkbuilders.rate.core.presentation.ui.SearchTextField +import dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables.CurrIcon @Composable fun SearchScreen( modifier: Modifier = Modifier, viewModel: SearchViewModel = hiltViewModel(), + onCurrencyClick: (String) -> Unit = {} ) { + val state by viewModel.state.collectAsState() + ScalingLazyColumn( - modifier = modifier.fillMaxSize().background(ArkColor.BGSecondaryAlt), - contentPadding = PaddingValues(4.dp) + modifier = modifier.fillMaxSize().background(Color.White), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { item { - SearchTextField(modifier = Modifier - .fillMaxWidth() - .padding(16.dp)) + SearchTextField( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + text = state.query, + onValueChange = { viewModel.onQueryChange(it) } + ) + } + + items(state.filteredCurrencies.size) { index -> + val currency = state.filteredCurrencies[index] + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onCurrencyClick(currency.code) } + .padding(vertical = 8.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + CurrIcon(modifier = Modifier.size(24.dp), code = currency.code) + Column(modifier = Modifier.padding(start = 8.dp)) { + Text( + text = currency.code, + color = ArkColor.TextPrimary, + fontWeight = FontWeight.Bold, + fontSize = 14.sp + ) + Text( + text = currency.name, + color = ArkColor.TextTertiary, + fontSize = 10.sp + ) + } + } } } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchViewModel.kt index 67b7de3bc..ba2de89b9 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/search/SearchViewModel.kt @@ -1,17 +1,57 @@ package dev.arkbuilders.rate.watchapp.presentation.search import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dev.arkbuilders.rate.core.domain.model.CurrencyInfo +import dev.arkbuilders.rate.core.domain.repo.CurrencyRepo import dev.arkbuilders.rate.core.domain.usecase.SearchUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject +data class SearchState( + val query: String = "", + val allCurrencies: List = emptyList(), + val filteredCurrencies: List = emptyList(), + val isLoading: Boolean = true +) + @HiltViewModel class SearchViewModel @Inject constructor( -// private val searchUseCase: SearchUseCase, + private val currencyRepo: CurrencyRepo, + private val searchUseCase: SearchUseCase ): ViewModel() { + private val _state = MutableStateFlow(SearchState()) + val state: StateFlow = _state.asStateFlow() + init { + viewModelScope.launch { + val all = currencyRepo.getCurrencyInfo() + _state.update { it.copy( + allCurrencies = all, + filteredCurrencies = all, + isLoading = false + ) } + } + } + fun onQueryChange(query: String) { + _state.update { it.copy(query = query) } + filter() } + private fun filter() { + val currentState = _state.value + val filtered = searchUseCase( + currentState.allCurrencies, + emptyList(), + currentState.query + ) + _state.update { it.copy(filteredCurrencies = filtered) } + } } From 8a6be743fcd09ac868275cb2c5fbfa8c622f6011 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 9 May 2026 16:18:42 +0700 Subject: [PATCH 40/57] Inject dao --- .../dev/arkbuilders/rate/watchapp/di/RepoModule.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt index d906ea87f..b3c532818 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/di/RepoModule.kt @@ -39,6 +39,9 @@ import dev.arkbuilders.rate.core.domain.repo.Prefs import dev.arkbuilders.rate.core.domain.repo.TimestampRepo import dev.arkbuilders.rate.core.domain.usecase.DefaultGroupNameProvider import dev.arkbuilders.rate.core.presentation.utils.DefaultGroupNameProviderImpl +import dev.arkbuilders.rate.feature.quick.data.QuickRepoImpl +import dev.arkbuilders.rate.feature.quick.domain.repo.QuickRepo +import dev.arkbuilders.rate.core.db.dao.QuickPairDao import javax.inject.Singleton @Module @@ -162,4 +165,11 @@ class RepoModule { analyticsManager, buildConfigFieldsProvider.provide(), ) + + @Singleton + @Provides + fun provideQuickRepo( + quickPairDao: QuickPairDao, + groupRepo: GroupRepo + ): QuickRepo = QuickRepoImpl(quickPairDao, groupRepo) } From 0da93926377ad0f75341e1d9f2a1351e9af16b1d Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 10 May 2026 17:36:37 +0700 Subject: [PATCH 41/57] Add options screen --- .../presentation/options/OptionsViewModel.kt | 45 ++++++++++++++ .../presentation/options/SuccessScreen.kt | 59 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/SuccessScreen.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt new file mode 100644 index 000000000..cdb89d1e3 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt @@ -0,0 +1,45 @@ +package dev.arkbuilders.rate.watchapp.presentation.options + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair +import dev.arkbuilders.rate.feature.quick.domain.repo.QuickRepo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class OptionsViewModel @Inject constructor( + private val quickRepo: QuickRepo, + savedStateHandle: SavedStateHandle +) : ViewModel() { + + private val pairId: Long = checkNotNull(savedStateHandle["id"]) + + private val _quickPair = MutableStateFlow(null) + val quickPair: StateFlow = _quickPair.asStateFlow() + + init { + viewModelScope.launch { + // Find the pair from the repo + // QuickRepo doesn't have a getPair(id) that returns a flow of one item in the current interface + // We can collect the list and find it. + quickRepo.allFlow().collect { pairs -> + _quickPair.value = pairs.find { it.id == pairId } + } + } + } + + fun deletePair(onDeleted: () -> Unit) { + viewModelScope.launch { + _quickPair.value?.let { p -> + quickRepo.delete(p.id) + onDeleted() + } + } + } +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/SuccessScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/SuccessScreen.kt new file mode 100644 index 000000000..e6f841e20 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/SuccessScreen.kt @@ -0,0 +1,59 @@ +package dev.arkbuilders.rate.watchapp.presentation.options + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.theme.ArkColor +import kotlinx.coroutines.delay + +@Composable +fun SuccessScreen( + modifier: Modifier = Modifier, + message: String = "Delete Successful", + onTimeout: () -> Unit +) { + LaunchedEffect(Unit) { + delay(1500L) + onTimeout() + } + + Column( + modifier = modifier + .fillMaxSize() + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = "Success", + tint = ArkColor.BrandSecondary, + modifier = Modifier + .size(48.dp) + .padding(bottom = 8.dp) + ) + Text( + text = message, + color = ArkColor.TextPrimary, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + textAlign = TextAlign.Center + ) + } +} From a0836a72208905b651784961ffef3447d598c64d Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 10 May 2026 17:36:58 +0700 Subject: [PATCH 42/57] Implement watch face to load quick pairs --- .../watchface/QuickPairComplicationService.kt | 76 +++++++++ .../watchapp/watchface/QuickPairsRenderer.kt | 145 ++++++++++++++++++ .../watchface/QuickPairsWatchFaceService.kt | 80 ++++++++++ watchapp/src/main/res/xml/watch_face.xml | 2 + 4 files changed, 303 insertions(+) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairComplicationService.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsRenderer.kt create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsWatchFaceService.kt create mode 100644 watchapp/src/main/res/xml/watch_face.xml diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairComplicationService.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairComplicationService.kt new file mode 100644 index 000000000..f465068d5 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairComplicationService.kt @@ -0,0 +1,76 @@ +package dev.arkbuilders.rate.watchapp.watchface + +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.LongTextComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.ShortTextComplicationData +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService +import dagger.hilt.android.AndroidEntryPoint +import dev.arkbuilders.rate.core.domain.usecase.ConvertWithRateUseCase +import dev.arkbuilders.rate.feature.quick.domain.repo.QuickRepo +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +@AndroidEntryPoint +class QuickPairComplicationService : SuspendingComplicationDataSourceService() { + + @Inject + lateinit var quickRepo: QuickRepo + + @Inject + lateinit var convertUseCase: ConvertWithRateUseCase + + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + val pairs = quickRepo.allFlow().first() + val pair = pairs.firstOrNull() ?: return null + + val toAmount = pair.to.firstOrNull() ?: return null + val (convertedAmt, _) = convertUseCase.invoke(pair.from, pair.amount, toAmount.code) + + val resultStr = String.format("%.2f", convertedAmt) + val text = "$resultStr ${toAmount.code}" + val title = "${pair.amount} ${pair.from}" + + return when (request.complicationType) { + ComplicationType.SHORT_TEXT -> { + ShortTextComplicationData.Builder( + text = PlainComplicationText.Builder(text).build(), + contentDescription = PlainComplicationText.Builder("Currency Rate").build() + ).setTitle(PlainComplicationText.Builder(title).build()) + .build() + } + ComplicationType.LONG_TEXT -> { + LongTextComplicationData.Builder( + text = PlainComplicationText.Builder("$title = $text").build(), + contentDescription = PlainComplicationText.Builder("Currency Rate").build() + ).setTitle(PlainComplicationText.Builder("Quick Pair Rate").build()) + .build() + } + else -> null + } + } + + override fun getPreviewData(type: ComplicationType): ComplicationData? { + val text = "1.00 EUR" + val title = "1 USD" + return when (type) { + ComplicationType.SHORT_TEXT -> { + ShortTextComplicationData.Builder( + text = PlainComplicationText.Builder(text).build(), + contentDescription = PlainComplicationText.Builder("Currency Rate Preview").build() + ).setTitle(PlainComplicationText.Builder(title).build()) + .build() + } + ComplicationType.LONG_TEXT -> { + LongTextComplicationData.Builder( + text = PlainComplicationText.Builder("$title = $text").build(), + contentDescription = PlainComplicationText.Builder("Currency Rate Preview").build() + ).setTitle(PlainComplicationText.Builder("Quick Pair Rate").build()) + .build() + } + else -> null + } + } +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsRenderer.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsRenderer.kt new file mode 100644 index 000000000..556a7ec0e --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsRenderer.kt @@ -0,0 +1,145 @@ +package dev.arkbuilders.rate.watchapp.watchface + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RadialGradient +import android.graphics.Rect +import android.graphics.Shader +import android.graphics.Typeface +import android.view.SurfaceHolder +import androidx.wear.watchface.CanvasType +import androidx.wear.watchface.DrawMode +import androidx.wear.watchface.Renderer +import androidx.wear.watchface.WatchState +import androidx.wear.watchface.style.CurrentUserStyleRepository +import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair +import dev.arkbuilders.rate.core.presentation.utils.IconUtils +import dev.arkbuilders.rate.core.domain.CurrUtils +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +class QuickPairsRenderer( + private val context: Context, + surfaceHolder: SurfaceHolder, + private val watchState: WatchState, + currentUserStyleRepository: CurrentUserStyleRepository, +) : Renderer.CanvasRenderer( + surfaceHolder, + currentUserStyleRepository, + watchState, + CanvasType.SOFTWARE, + 1000L // 1 second refresh is enough for HH:mm +) { + private var quickPairs: List = emptyList() + private val backgroundPaint = Paint().apply { + color = Color.WHITE + isAntiAlias = true + } + + private val timePaint = Paint().apply { + color = Color.parseColor("#7F56D9") // ArkColor.Primary + textSize = 48f + textAlign = Paint.Align.CENTER + typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) + isAntiAlias = true + } + + private val titlePaint = Paint().apply { + color = Color.parseColor("#101828") // ArkColor.TextPrimary + textSize = 20f + textAlign = Paint.Align.CENTER + typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) + isAntiAlias = true + } + + private val itemTitlePaint = Paint().apply { + color = Color.parseColor("#101828") // ArkColor.TextPrimary + textSize = 18f + typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) + isAntiAlias = true + } + + private val itemSubtitlePaint = Paint().apply { + color = Color.parseColor("#475467") // ArkColor.TextTertiary + textSize = 14f + isAntiAlias = true + } + + private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm") + + fun updateQuickPairs(pairs: List) { + quickPairs = pairs + invalidate() + } + + override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) { + val isAmbient = renderParameters.drawMode == DrawMode.AMBIENT + + val centerX = bounds.centerX().toFloat() + val centerY = bounds.centerY().toFloat() + + if (isAmbient) { + canvas.drawColor(Color.BLACK) + timePaint.color = Color.WHITE + val timeText = zonedDateTime.format(timeFormatter) + canvas.drawText(timeText, centerX, centerY, timePaint) + return + } + + // Draw Background (White like the app) + canvas.drawColor(Color.WHITE) + + // Draw Time at the top + val timeText = zonedDateTime.format(timeFormatter) + canvas.drawText(timeText, centerX, 60f, timePaint) + + // Draw "Quick" title + canvas.drawText("Quick", centerX, 100f, titlePaint) + + // Draw Quick Pairs + var currentY = 140f + val pairsToShow = quickPairs.take(2) + + if (pairsToShow.isNotEmpty()) { + pairsToShow.forEach { pair -> + // Draw Icons + val fromIconId = IconUtils.iconForCurrCode(context, pair.from) + val fromIcon = context.getDrawable(fromIconId) + fromIcon?.let { + it.setBounds(30, currentY.toInt() - 20, 70, currentY.toInt() + 20) + it.draw(canvas) + } + + val toAmount = pair.to.firstOrNull() + if (toAmount != null) { + val toIconId = IconUtils.iconForCurrCode(context, toAmount.code) + val toIcon = context.getDrawable(toIconId) + toIcon?.let { + it.setBounds(50, currentY.toInt() - 20, 90, currentY.toInt() + 20) + it.draw(canvas) + } + + // Draw Title: "EUR to USD" + val titleText = "${pair.from} to ${toAmount.code}" + canvas.drawText(titleText, 100f, currentY, itemTitlePaint) + + // Draw Subtitle: "1 USD = 0.92 EUR" + currentY += 25f + val baseAmountStr = CurrUtils.prepareToDisplay(pair.amount) + val targetAmountStr = CurrUtils.prepareToDisplay(toAmount.value) + val subtitleText = "$baseAmountStr ${pair.from} = $targetAmountStr ${toAmount.code}" + canvas.drawText(subtitleText, 100f, currentY, itemSubtitlePaint) + + currentY += 45f + } + } + } else { + canvas.drawText("No pairs added", centerX, centerY + 20f, itemSubtitlePaint) + } + } + + override fun renderHighlightLayer(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) { + } +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsWatchFaceService.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsWatchFaceService.kt new file mode 100644 index 000000000..4ea0150da --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsWatchFaceService.kt @@ -0,0 +1,80 @@ +package dev.arkbuilders.rate.watchapp.watchface + +import android.util.Log +import android.view.SurfaceHolder +import androidx.wear.watchface.ComplicationSlotsManager +import androidx.wear.watchface.WatchFace +import androidx.wear.watchface.WatchFaceService +import androidx.wear.watchface.WatchFaceType +import androidx.wear.watchface.WatchState +import androidx.wear.watchface.style.CurrentUserStyleRepository +import dagger.hilt.android.AndroidEntryPoint +import dev.arkbuilders.rate.core.domain.model.Amount +import dev.arkbuilders.rate.core.domain.usecase.ConvertWithRateUseCase +import dev.arkbuilders.rate.feature.quick.domain.repo.QuickRepo +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@AndroidEntryPoint +class QuickPairsWatchFaceService : WatchFaceService() { + + @Inject + lateinit var quickRepo: QuickRepo + + @Inject + lateinit var convertUseCase: ConvertWithRateUseCase + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ): WatchFace { + Log.d("QuickPairsWatchFace", "Creating watch face") + val renderer = QuickPairsRenderer( + context = this, + surfaceHolder = surfaceHolder, + watchState = watchState, + currentUserStyleRepository = currentUserStyleRepository + ) + + scope.launch { + Log.d("QuickPairsWatchFace", "Starting data collection") + quickRepo.allFlow() + .flowOn(Dispatchers.IO) + .collectLatest { pairs -> + Log.d("QuickPairsWatchFace", "Received ${pairs.size} pairs") + val processedPairs = withContext(Dispatchers.IO) { + pairs.map { pair -> + val actualTo = pair.to.map { toAmount -> + val (convertedAmt, _) = convertUseCase.invoke(pair.from, pair.amount, toAmount.code) + Amount(convertedAmt.code, convertedAmt.value) + } + pair.copy(to = actualTo) + }.sortedByDescending { it.calculatedDate } + } + Log.d("QuickPairsWatchFace", "Updating renderer with ${processedPairs.size} processed pairs") + renderer.updateQuickPairs(processedPairs) + } + } + + return WatchFace( + watchFaceType = WatchFaceType.DIGITAL, + renderer = renderer + ) + } + + override fun onDestroy() { + scope.cancel() + super.onDestroy() + } +} diff --git a/watchapp/src/main/res/xml/watch_face.xml b/watchapp/src/main/res/xml/watch_face.xml new file mode 100644 index 000000000..060be98bb --- /dev/null +++ b/watchapp/src/main/res/xml/watch_face.xml @@ -0,0 +1,2 @@ + + From 371ed5502e4a63f1824caca29180c9f8823d2ddb Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 10 May 2026 17:38:15 +0700 Subject: [PATCH 43/57] Implement watch face to load quick pairs --- gradle/libs.versions.toml | 8 +++++ watchapp/src/main/AndroidManifest.xml | 37 ++++++++++++++++++++++++ watchapp/src/main/res/values/strings.xml | 3 +- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4b3c6b5cf..73795b699 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ composeBom = "2023.08.00" composeMaterial = "1.2.1" composeFoundation = "1.2.1" coreSplashscreen = "1.0.1" +watchface = "1.2.1" [libraries] androidx-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "composeNavigation" } @@ -104,6 +105,13 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", versi androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" } androidx-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "composeFoundation" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" } +androidx-wear-watchface = { module = "androidx.wear.watchface:watchface", version.ref = "watchface" } +androidx-wear-watchface-complications-data = { module = "androidx.wear.watchface:watchface-complications-data", version.ref = "watchface" } +androidx-wear-watchface-complications-data-source = { module = "androidx.wear.watchface:watchface-complications-data-source", version.ref = "watchface" } +androidx-wear-watchface-complications-data-source-ktx = { module = "androidx.wear.watchface:watchface-complications-data-source-ktx", version.ref = "watchface" } +androidx-wear-watchface-complications-rendering = { module = "androidx.wear.watchface:watchface-complications-rendering", version.ref = "watchface" } +androidx-wear-watchface-editor = { module = "androidx.wear.watchface:watchface-editor", version.ref = "watchface" } +androidx-wear-watchface-style = { module = "androidx.wear.watchface:watchface-style", version.ref = "watchface" } [plugins] android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/watchapp/src/main/AndroidManifest.xml b/watchapp/src/main/AndroidManifest.xml index eff222470..15e07c24c 100644 --- a/watchapp/src/main/AndroidManifest.xml +++ b/watchapp/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + @@ -35,6 +36,42 @@ + + + + + + + + + + + + + + + + + + diff --git a/watchapp/src/main/res/values/strings.xml b/watchapp/src/main/res/values/strings.xml index 7da1efdcb..b0e76bca7 100644 --- a/watchapp/src/main/res/values/strings.xml +++ b/watchapp/src/main/res/values/strings.xml @@ -1,8 +1,9 @@ watchapp + ARK Rates From the Square world,\nHello, %1$s! - \ No newline at end of file + From 6ea3b587e6f0e8c1f521f869408110c89c6a4ca8 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 10 May 2026 20:38:04 +0700 Subject: [PATCH 44/57] Implement watch face --- .../watchapp/watchface/QuickPairsRenderer.kt | 70 ++++++++----------- .../watchface/QuickPairsWatchFaceService.kt | 16 +++-- .../watchapp/watchface/WatchRefreshManager.kt | 19 +++++ 3 files changed, 60 insertions(+), 45 deletions(-) create mode 100644 watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/WatchRefreshManager.kt diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsRenderer.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsRenderer.kt index 556a7ec0e..1d41aa0e8 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsRenderer.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsRenderer.kt @@ -4,9 +4,7 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint -import android.graphics.RadialGradient import android.graphics.Rect -import android.graphics.Shader import android.graphics.Typeface import android.view.SurfaceHolder import androidx.wear.watchface.CanvasType @@ -14,9 +12,9 @@ import androidx.wear.watchface.DrawMode import androidx.wear.watchface.Renderer import androidx.wear.watchface.WatchState import androidx.wear.watchface.style.CurrentUserStyleRepository -import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair -import dev.arkbuilders.rate.core.presentation.utils.IconUtils import dev.arkbuilders.rate.core.domain.CurrUtils +import dev.arkbuilders.rate.core.presentation.utils.IconUtils +import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -46,13 +44,6 @@ class QuickPairsRenderer( isAntiAlias = true } - private val titlePaint = Paint().apply { - color = Color.parseColor("#101828") // ArkColor.TextPrimary - textSize = 20f - textAlign = Paint.Align.CENTER - typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) - isAntiAlias = true - } private val itemTitlePaint = Paint().apply { color = Color.parseColor("#101828") // ArkColor.TextPrimary @@ -69,6 +60,7 @@ class QuickPairsRenderer( private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm") + fun updateQuickPairs(pairs: List) { quickPairs = pairs invalidate() @@ -76,7 +68,7 @@ class QuickPairsRenderer( override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) { val isAmbient = renderParameters.drawMode == DrawMode.AMBIENT - + val centerX = bounds.centerX().toFloat() val centerY = bounds.centerY().toFloat() @@ -95,15 +87,14 @@ class QuickPairsRenderer( val timeText = zonedDateTime.format(timeFormatter) canvas.drawText(timeText, centerX, 60f, timePaint) - // Draw "Quick" title - canvas.drawText("Quick", centerX, 100f, titlePaint) - // Draw Quick Pairs - var currentY = 140f - val pairsToShow = quickPairs.take(2) - + var currentY = 110f + val pairsToShow = quickPairs + if (pairsToShow.isNotEmpty()) { - pairsToShow.forEach { pair -> + for (pair in pairsToShow) { + val toAmount = pair.to.firstOrNull() ?: continue + // Draw Icons val fromIconId = IconUtils.iconForCurrCode(context, pair.from) val fromIcon = context.getDrawable(fromIconId) @@ -112,31 +103,28 @@ class QuickPairsRenderer( it.draw(canvas) } - val toAmount = pair.to.firstOrNull() - if (toAmount != null) { - val toIconId = IconUtils.iconForCurrCode(context, toAmount.code) - val toIcon = context.getDrawable(toIconId) - toIcon?.let { - it.setBounds(50, currentY.toInt() - 20, 90, currentY.toInt() + 20) - it.draw(canvas) - } - - // Draw Title: "EUR to USD" - val titleText = "${pair.from} to ${toAmount.code}" - canvas.drawText(titleText, 100f, currentY, itemTitlePaint) - - // Draw Subtitle: "1 USD = 0.92 EUR" - currentY += 25f - val baseAmountStr = CurrUtils.prepareToDisplay(pair.amount) - val targetAmountStr = CurrUtils.prepareToDisplay(toAmount.value) - val subtitleText = "$baseAmountStr ${pair.from} = $targetAmountStr ${toAmount.code}" - canvas.drawText(subtitleText, 100f, currentY, itemSubtitlePaint) - - currentY += 45f + val toIconId = IconUtils.iconForCurrCode(context, toAmount.code) + val toIcon = context.getDrawable(toIconId) + toIcon?.let { + it.setBounds(50, currentY.toInt() - 20, 90, currentY.toInt() + 20) + it.draw(canvas) } + + // Draw Title: "EUR to USD" + val titleText = "${pair.from} to ${toAmount.code}" + canvas.drawText(titleText, 100f, currentY, itemTitlePaint) + + // Draw Subtitle: "1 USD = 0.92 EUR" + currentY += 25f + val baseAmountStr = CurrUtils.prepareToDisplay(pair.amount) + val targetAmountStr = CurrUtils.prepareToDisplay(toAmount.value) + val subtitleText = "$baseAmountStr ${pair.from} = $targetAmountStr ${toAmount.code}" + canvas.drawText(subtitleText, 100f, currentY, itemSubtitlePaint) + + currentY += 45f } } else { - canvas.drawText("No pairs added", centerX, centerY + 20f, itemSubtitlePaint) + canvas.drawText("No pinned pairs", centerX, centerY + 20f, itemSubtitlePaint) } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsWatchFaceService.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsWatchFaceService.kt index 4ea0150da..fa3c29cf8 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsWatchFaceService.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/QuickPairsWatchFaceService.kt @@ -17,7 +17,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -50,17 +52,23 @@ class QuickPairsWatchFaceService : WatchFaceService() { scope.launch { Log.d("QuickPairsWatchFace", "Starting data collection") quickRepo.allFlow() + .map { pairs -> pairs.filter { it.isPinned() } } + .distinctUntilChanged { old, new -> + old.size == new.size && old.all { oldPair -> + new.any { it.id == oldPair.id && it.amount == oldPair.amount && it.from == oldPair.from && it.to == oldPair.to } + } + } .flowOn(Dispatchers.IO) - .collectLatest { pairs -> - Log.d("QuickPairsWatchFace", "Received ${pairs.size} pairs") + .collectLatest { pinnedPairs -> + Log.d("QuickPairsWatchFace", "Received ${pinnedPairs.size} pinned pairs") val processedPairs = withContext(Dispatchers.IO) { - pairs.map { pair -> + pinnedPairs.map { pair -> val actualTo = pair.to.map { toAmount -> val (convertedAmt, _) = convertUseCase.invoke(pair.from, pair.amount, toAmount.code) Amount(convertedAmt.code, convertedAmt.value) } pair.copy(to = actualTo) - }.sortedByDescending { it.calculatedDate } + }.sortedByDescending { it.pinnedDate } } Log.d("QuickPairsWatchFace", "Updating renderer with ${processedPairs.size} processed pairs") renderer.updateQuickPairs(processedPairs) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/WatchRefreshManager.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/WatchRefreshManager.kt new file mode 100644 index 000000000..8244809e2 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/watchface/WatchRefreshManager.kt @@ -0,0 +1,19 @@ +package dev.arkbuilders.rate.watchapp.watchface + +import android.content.ComponentName +import android.content.Context +import androidx.wear.watchface.complications.datasource.ComplicationDataSourceUpdateRequester + +object WatchRefreshManager { + fun refreshComplications(context: Context) { + try { + val requester = ComplicationDataSourceUpdateRequester.create( + context, + ComponentName(context, QuickPairComplicationService::class.java) + ) + requester.requestUpdateAll() + } catch (e: Exception) { + e.printStackTrace() + } + } +} From 7c98eb7053fcf0ec49229c636adfc241c9255f03 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 10 May 2026 20:38:24 +0700 Subject: [PATCH 45/57] Implement update and delete --- .../watchapp/presentation/MainActivity.kt | 22 ++++++++---- .../presentation/theme/WearComponents.kt | 4 +-- .../watchapp/presentation/theme/WearDialog.kt | 35 +++++++++---------- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index b1ed511ce..69f62fb84 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -47,21 +47,27 @@ class MainActivity : ComponentActivity() { onNavigateToAdd = { navController.navigate("addquickpairs") }, - onNavigateToSearch = { - navController.navigate("search") - }, onNavigateToOptions = { id -> navController.navigate("options/$id") } ) } - composable("addquickpairs") { + composable( + route = "addquickpairs?id={id}", + arguments = listOf(navArgument("id") { + type = NavType.StringType + nullable = true + defaultValue = null + }) + ) { AddQuickPairsScreen( navController = navController, onNavigateToSearch = { field -> navController.navigate("search/$field") }, - onNavigateBack = { navController.popBackStack() } + onNavigateBack = { + navController.popBackStack("list", inclusive = false) + } ) } composable( @@ -71,9 +77,11 @@ class MainActivity : ComponentActivity() { OptionsScreen( onSearchClick = { navController.navigate("search") }, onDeleteSuccess = { navController.navigate("success") }, - onUpdateClick = { navController.popBackStack() }, + onUpdateClick = { id -> + navController.navigate("addquickpairs?id=$id") + }, onPinClick = { navController.popBackStack() }, - onReuseClick = { navController.popBackStack() } + onDismiss = { navController.popBackStack() } ) } composable( diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt index df8bf2e8b..e2f7fb49e 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearComponents.kt @@ -188,7 +188,7 @@ fun WearFab( ) { Button( onClick = onClick, - modifier = modifier.size(52.dp), + modifier = modifier.size(44.dp), colors = ButtonDefaults.buttonColors( backgroundColor = backgroundColor, contentColor = contentColor @@ -198,7 +198,7 @@ fun WearFab( Icon( imageVector = icon, contentDescription = null, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(20.dp) ) } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt index 58714fd9d..8eef38679 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt @@ -5,11 +5,10 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.Close @@ -44,14 +43,11 @@ fun WearConfirmationDialog( Dialog(onDismissRequest = onDismiss) { Column( modifier = modifier - .fillMaxWidth() - .background( - color = Color.White, - shape = RoundedCornerShape(16.dp) - ) - .padding(16.dp), + .fillMaxSize() + .background(color = Color.White) + .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.Center ) { Text( text = title, @@ -61,6 +57,8 @@ fun WearConfirmationDialog( color = ArkColor.TextPrimary ) + Spacer(modifier = Modifier.height(12.dp)) + Text( text = message, fontSize = 14.sp, @@ -68,7 +66,7 @@ fun WearConfirmationDialog( color = ArkColor.TextSecondary ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(20.dp)) Row( modifier = Modifier.fillMaxWidth(), @@ -103,14 +101,11 @@ fun WearInfoDialog( Dialog(onDismissRequest = onDismiss) { Column( modifier = modifier - .fillMaxWidth() - .background( - color = Color.White, - shape = RoundedCornerShape(16.dp) - ) - .padding(16.dp), + .fillMaxSize() + .background(color = Color.White) + .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.Center ) { Text( text = title, @@ -120,6 +115,8 @@ fun WearInfoDialog( color = ArkColor.TextPrimary ) + Spacer(modifier = Modifier.height(12.dp)) + Text( text = message, fontSize = 14.sp, @@ -127,12 +124,12 @@ fun WearInfoDialog( color = ArkColor.TextSecondary ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(20.dp)) WearCompactButton( text = dismissText, onClick = onDismiss, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(0.8f), style = WearButtonStyle.Primary ) } From 0feb3e10e9cd33fd3b8f08a4a06da631c6d0594a Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 10 May 2026 20:38:39 +0700 Subject: [PATCH 46/57] Implement update and delete --- .../addquickpairs/AddQuickPairsScreen.kt | 12 ++- .../addquickpairs/AddQuickPairsViewModel.kt | 85 ++++++++++++------- 2 files changed, 61 insertions(+), 36 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt index 8de9fb20c..4286d7ab5 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt @@ -95,7 +95,7 @@ fun AddQuickPairsScreen( modifier = Modifier .fillMaxWidth() .padding(top = 8.dp), - text = "Add", + text = if (state.editId != null) "Update" else "Add", textAlign = TextAlign.Center, fontWeight = FontWeight.Bold, color = ArkColor.TextPrimary, @@ -118,7 +118,7 @@ fun AddQuickPairsScreen( item { Row( modifier = Modifier - .fillMaxWidth(0.9f) + .fillMaxWidth(0.95f) .clip(RoundedCornerShape(8.dp)) .background(ArkColor.UtilitySuccess50) // Light greenish background .border(1.dp, ArkColor.UtilitySuccess200, RoundedCornerShape(8.dp)) @@ -126,8 +126,6 @@ fun AddQuickPairsScreen( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - // Removed Edit icon as per request - // Currency Selector Row( verticalAlignment = Alignment.CenterVertically, @@ -212,7 +210,7 @@ fun AddQuickPairsScreen( item { Row( modifier = Modifier - .fillMaxWidth(0.9f) + .fillMaxWidth(0.95f) .clip(RoundedCornerShape(8.dp)) .background(Color.White) .border(1.dp, ArkColor.BorderSecondary, RoundedCornerShape(8.dp)) @@ -271,12 +269,12 @@ fun AddQuickPairsScreen( Button( onClick = { viewModel.savePair() }, modifier = Modifier - .padding(top = 16.dp, bottom = 8.dp) + .padding(top = 16.dp, bottom = 12.dp) .fillMaxWidth(0.8f) .height(36.dp), colors = ButtonDefaults.primaryButtonColors(backgroundColor = ArkColor.Primary) ) { - Text("Save Pair", fontSize = 14.sp, fontWeight = FontWeight.Bold) + Text(if (state.editId != null) "Update Pair" else "Save Pair", fontSize = 14.sp, fontWeight = FontWeight.Bold) } } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt index f2a40d770..df1218c05 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt @@ -1,5 +1,6 @@ package dev.arkbuilders.rate.watchapp.presentation.addquickpairs +import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -7,48 +8,71 @@ import dev.arkbuilders.rate.core.domain.CurrUtils import dev.arkbuilders.rate.core.domain.model.Amount import dev.arkbuilders.rate.core.domain.usecase.ConvertWithRateUseCase import dev.arkbuilders.rate.core.domain.usecase.GetGroupByIdOrCreateDefaultUseCase +import java.time.OffsetDateTime +import androidx.lifecycle.SavedStateHandle import dev.arkbuilders.rate.core.domain.model.GroupFeatureType import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair import dev.arkbuilders.rate.feature.quick.domain.repo.QuickRepo import dev.arkbuilders.rate.core.domain.toBigDecimalArk import dev.arkbuilders.rate.core.domain.toDoubleArk +import dev.arkbuilders.rate.watchapp.watchface.WatchRefreshManager import jakarta.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import java.time.OffsetDateTime data class AddQuickState( val baseCurrency: String = "USD", - val targetCurrency: String = "EUR", val baseAmount: String = "", + val targetCurrency: String = "EUR", val targetAmount: String = "", - val isSaved: Boolean = false + val isSaved: Boolean = false, + val editId: Long? = null ) @HiltViewModel class AddQuickPairsViewModel @Inject constructor( + private val application: Application, private val quickRepo: QuickRepo, private val convertUseCase: ConvertWithRateUseCase, - private val getGroupByIdOrCreateDefaultUseCase: GetGroupByIdOrCreateDefaultUseCase + private val getGroupByIdOrCreateDefaultUseCase: GetGroupByIdOrCreateDefaultUseCase, + savedStateHandle: SavedStateHandle ) : ViewModel() { private val _state = MutableStateFlow(AddQuickState()) val state: StateFlow = _state.asStateFlow() init { - calculate() + val idStr: String? = savedStateHandle["id"] + val id = idStr?.toLongOrNull() + if (id != null) { + viewModelScope.launch { + quickRepo.allFlow().collect { pairs -> + pairs.find { it.id == id }?.let { pair -> + _state.value = _state.value.copy( + baseCurrency = pair.from, + baseAmount = CurrUtils.roundOff(pair.amount), + targetCurrency = pair.to.firstOrNull()?.code ?: "EUR", + targetAmount = CurrUtils.roundOff(pair.to.firstOrNull()?.value ?: java.math.BigDecimal.ZERO), + editId = id + ) + } + } + } + } else { + calculate(fromBase = true) + } } fun onBaseCurrencyChanged(code: String) { _state.value = _state.value.copy(baseCurrency = code) - calculate() + calculate(fromBase = true) } fun onTargetCurrencyChanged(code: String) { _state.value = _state.value.copy(targetCurrency = code) - calculate() + calculate(fromBase = true) } fun onAmountInput(input: String) { @@ -67,36 +91,36 @@ class AddQuickPairsViewModel @Inject constructor( val currentState = _state.value _state.value = currentState.copy( baseCurrency = currentState.targetCurrency, - targetCurrency = currentState.baseCurrency + targetCurrency = currentState.baseCurrency, + baseAmount = currentState.targetAmount, + targetAmount = currentState.baseAmount ) calculate(fromBase = true) } - private fun calculate(fromBase: Boolean = true) { + private fun calculate(fromBase: Boolean) { viewModelScope.launch { val s = _state.value - val sourceAmount = if (fromBase) s.baseAmount else s.targetAmount - val sourceCurrency = if (fromBase) s.baseCurrency else s.targetCurrency - val destCurrency = if (fromBase) s.targetCurrency else s.baseCurrency - - if (sourceAmount.isEmpty() || sourceAmount.toDoubleArk() == 0.0) { - if (fromBase) { + if (fromBase) { + if (s.baseAmount.isEmpty() || s.baseAmount.toDoubleArk() == 0.0) { _state.value = s.copy(targetAmount = "") - } else { - _state.value = s.copy(baseAmount = "") + return@launch } - return@launch - } - - val (amount, _) = convertUseCase.invoke( - Amount(sourceCurrency, sourceAmount.toBigDecimalArk()), - destCurrency - ) - val roundValue = CurrUtils.roundOff(amount.value) - if (fromBase) { - _state.value = _state.value.copy(targetAmount = roundValue) + val (amount, _) = convertUseCase.invoke( + Amount(s.baseCurrency, s.baseAmount.toBigDecimalArk()), + s.targetCurrency + ) + _state.value = _state.value.copy(targetAmount = CurrUtils.roundOff(amount.value)) } else { - _state.value = _state.value.copy(baseAmount = roundValue) + if (s.targetAmount.isEmpty() || s.targetAmount.toDoubleArk() == 0.0) { + _state.value = s.copy(baseAmount = "") + return@launch + } + val (amount, _) = convertUseCase.invoke( + Amount(s.targetCurrency, s.targetAmount.toBigDecimalArk()), + s.baseCurrency + ) + _state.value = _state.value.copy(baseAmount = CurrUtils.roundOff(amount.value)) } } } @@ -106,7 +130,7 @@ class AddQuickPairsViewModel @Inject constructor( val s = _state.value val group = getGroupByIdOrCreateDefaultUseCase(null, GroupFeatureType.Quick) val quick = QuickPair( - id = 0, + id = s.editId ?: 0, from = s.baseCurrency, amount = s.baseAmount.toBigDecimalArk(), to = listOf(Amount(s.targetCurrency, s.targetAmount.toBigDecimalArk())), @@ -115,6 +139,9 @@ class AddQuickPairsViewModel @Inject constructor( group = group ) quickRepo.insert(quick) + if (quick.isPinned()) { + WatchRefreshManager.refreshComplications(application) + } _state.value = s.copy(isSaved = true) } } From e7930774ac565d91cff90d1aa8f83263c981a0b9 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 10 May 2026 20:38:47 +0700 Subject: [PATCH 47/57] Update options --- .../presentation/options/OptionsScreen.kt | 58 ++++++++++++++----- .../presentation/options/OptionsViewModel.kt | 41 +++++++++++-- 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt index fabd1460e..4b736fc9a 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt @@ -8,6 +8,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable 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.text.font.FontWeight @@ -22,6 +25,8 @@ import androidx.wear.compose.material.Scaffold import androidx.wear.compose.material.Text import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState import dev.arkbuilders.rate.core.presentation.theme.ArkColor +import dev.arkbuilders.rate.watchapp.presentation.theme.WearConfirmationDialog +import dev.arkbuilders.rate.watchapp.presentation.theme.WearInfoDialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -29,14 +34,41 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle fun OptionsScreen( modifier: Modifier = Modifier, viewModel: OptionsViewModel = hiltViewModel(), - onUpdateClick: () -> Unit = {}, + onUpdateClick: (Long) -> Unit = {}, onPinClick: () -> Unit = {}, onSearchClick: () -> Unit = {}, - onReuseClick: () -> Unit = {}, - onDeleteSuccess: () -> Unit = {} + onDeleteSuccess: () -> Unit = {}, + onDismiss: () -> Unit = {} ) { val quickPair by viewModel.quickPair.collectAsStateWithLifecycle() + val showPinLimitDialog by viewModel.showPinLimitDialog.collectAsStateWithLifecycle() val listState = rememberScalingLazyListState() + var showDeleteDialog by remember { mutableStateOf(false) } + + if (showDeleteDialog) { + WearConfirmationDialog( + title = "Delete Pair", + message = "Are you sure you want to delete this pair?", + onConfirm = { + showDeleteDialog = false + viewModel.deletePair(onDeleted = onDeleteSuccess) + }, + onDismiss = { + showDeleteDialog = false + onDismiss() + }, + isDestructive = true + ) + } + + if (showPinLimitDialog) { + WearInfoDialog( + title = "Pin Limit Reached", + message = "You can only pin up to 4 pairs. Please unpin another pair first.", + onDismiss = { viewModel.dismissPinLimitDialog() }, + dismissText = "Ok got it" + ) + } Scaffold( positionIndicator = { @@ -68,16 +100,18 @@ fun OptionsScreen( WearOptionButton( text = "Update", icon = WearOptionButtonIcon.Refresh, - buttonType = WearOptionButtonType.Success, - onClick = onUpdateClick + onClick = { onUpdateClick(viewModel.pairId) } ) } item { + val isPinned = quickPair?.isPinned() == true WearOptionButton( - text = "Pin", + text = if (isPinned) "Unpin" else "Pin", icon = WearOptionButtonIcon.Pin, - onClick = onPinClick + onClick = { + viewModel.togglePin(onSuccess = onPinClick) + } ) } @@ -89,21 +123,13 @@ fun OptionsScreen( ) } - item { - WearOptionButton( - text = "Re-Use", - icon = WearOptionButtonIcon.Reuse, - onClick = onReuseClick - ) - } - item { WearOptionButton( text = "Delete", icon = WearOptionButtonIcon.Delete, buttonType = WearOptionButtonType.Destructive, onClick = { - viewModel.deletePair(onDeleted = onDeleteSuccess) + showDeleteDialog = true } ) } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt index cdb89d1e3..9f9783c0c 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt @@ -10,30 +10,63 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import dev.arkbuilders.rate.watchapp.watchface.WatchRefreshManager import javax.inject.Inject +import android.app.Application + @HiltViewModel class OptionsViewModel @Inject constructor( + private val application: Application, private val quickRepo: QuickRepo, savedStateHandle: SavedStateHandle ) : ViewModel() { - private val pairId: Long = checkNotNull(savedStateHandle["id"]) + val pairId: Long = checkNotNull(savedStateHandle["id"]) private val _quickPair = MutableStateFlow(null) val quickPair: StateFlow = _quickPair.asStateFlow() + private val _showPinLimitDialog = MutableStateFlow(false) + val showPinLimitDialog: StateFlow = _showPinLimitDialog.asStateFlow() + init { viewModelScope.launch { - // Find the pair from the repo - // QuickRepo doesn't have a getPair(id) that returns a flow of one item in the current interface - // We can collect the list and find it. quickRepo.allFlow().collect { pairs -> _quickPair.value = pairs.find { it.id == pairId } } } } + fun togglePin(onSuccess: () -> Unit) { + viewModelScope.launch { + val pair = _quickPair.value ?: return@launch + if (pair.isPinned()) { + // Unpin + val updated = pair.copy(pinnedDate = null) + quickRepo.insert(updated) + WatchRefreshManager.refreshComplications(application) + onSuccess() + } else { + // Check limit + val pinnedCount = quickRepo.getAll().count { it.isPinned() } + if (pinnedCount >= 4) { + _showPinLimitDialog.value = true + } else { + // Pin + val updated = pair.copy(pinnedDate = java.time.OffsetDateTime.now()) + quickRepo.insert(updated) + WatchRefreshManager.refreshComplications(application) + onSuccess() + } + } + } + } + + fun dismissPinLimitDialog() { + _showPinLimitDialog.value = false + } + fun deletePair(onDeleted: () -> Unit) { viewModelScope.launch { _quickPair.value?.let { p -> From ccc7e7270c876875a08faac0239c951ebb6c97f8 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 10 May 2026 20:39:25 +0700 Subject: [PATCH 48/57] Update UI --- watchapp/build.gradle.kts | 9 ++ .../quickpairs/QuickPairsScreen.kt | 58 +------- .../quickpairs/QuickPairsViewModel.kt | 15 +- .../quickpairs/composables/QuickPairItem.kt | 128 ++++++------------ .../quickpairs/composables/QuickPairsEmpty.kt | 15 +- 5 files changed, 72 insertions(+), 153 deletions(-) diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts index 9846d9e2e..2316bb8da 100644 --- a/watchapp/build.gradle.kts +++ b/watchapp/build.gradle.kts @@ -119,6 +119,15 @@ dependencies { implementation(libs.androidx.core.splashscreen) implementation(libs.material3) implementation(libs.navigation.compose) + + implementation(libs.androidx.wear.watchface) + implementation(libs.androidx.wear.watchface.complications.data) + implementation(libs.androidx.wear.watchface.complications.data.source) + implementation(libs.androidx.wear.watchface.complications.data.source.ktx) + implementation(libs.androidx.wear.watchface.complications.rendering) + implementation(libs.androidx.wear.watchface.editor) + implementation(libs.androidx.wear.watchface.style) + androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt index 88fe234a7..f29e68a06 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -1,7 +1,5 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -14,16 +12,8 @@ import androidx.compose.material.icons.outlined.Add import androidx.compose.material3.FabPosition import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -43,55 +33,24 @@ fun QuickPairsScreen( modifier: Modifier = Modifier, viewModel: QuickPairsViewModel = hiltViewModel(), onNavigateToAdd: () -> Unit, - onNavigateToSearch: () -> Unit, onNavigateToOptions: (Long) -> Unit ) { val quickPairsList = viewModel.quickPairs.collectAsStateWithLifecycle().value val listState = rememberScalingLazyListState() - var isFabVisible by remember { mutableStateOf(true) } - var lastScrollIndex by remember { mutableIntStateOf(0) } - var lastScrollOffset by remember { mutableIntStateOf(0) } - - LaunchedEffect(listState) { - snapshotFlow { listState.centerItemIndex to listState.centerItemScrollOffset } - .collect { (index, offset) -> - if (index > lastScrollIndex || (index == lastScrollIndex && offset > lastScrollOffset)) { - isFabVisible = false - } else if (index < lastScrollIndex || (index == lastScrollIndex && offset < lastScrollOffset)) { - isFabVisible = true - } - lastScrollIndex = index - lastScrollOffset = offset - } - } - - val fabProgress by animateFloatAsState( - targetValue = if (isFabVisible) 1f else 0f, - animationSpec = tween(durationMillis = 800), - label = "fabProgress" - ) - Scaffold( modifier = modifier.fillMaxSize(), containerColor = Color.Transparent, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { WearFab( - onClick = { if (isFabVisible) onNavigateToAdd() }, + onClick = onNavigateToAdd, icon = Icons.Outlined.Add, - modifier = Modifier - .padding(bottom = 8.dp) - .graphicsLayer { - alpha = fabProgress - scaleX = fabProgress - scaleY = fabProgress - } + modifier = Modifier.padding(bottom = 8.dp) ) } ) { padding -> Box(modifier = Modifier.padding(padding).fillMaxSize()) { - if (quickPairsList.isEmpty()) { QuickPairsEmpty( modifier = Modifier.fillMaxSize(), @@ -103,19 +62,12 @@ fun QuickPairsScreen( .fillMaxSize() .background(Color.White), state = listState, - verticalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues( - top = 4.dp, - start = 4.dp, - end = 4.dp, - bottom = 40.dp - ) + verticalArrangement = Arrangement.spacedBy(6.dp), ) { item { Text( modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), + .fillMaxWidth(), text = "Quick", fontWeight = FontWeight.SemiBold, fontSize = 16.sp, @@ -126,7 +78,7 @@ fun QuickPairsScreen( items(quickPairsList.size, key = { quickPairsList[it].id }) { idx -> QuickPairItem( quick = quickPairsList[idx], - onClick = { onNavigateToSearch() } + onClick = { onNavigateToOptions(quickPairsList[idx].id) } ) } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt index b7b7314e2..d791fd1fe 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt @@ -1,11 +1,13 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs +import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.arkbuilders.rate.core.domain.usecase.ConvertWithRateUseCase import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair import dev.arkbuilders.rate.feature.quick.domain.repo.QuickRepo +import dev.arkbuilders.rate.watchapp.watchface.WatchRefreshManager import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -15,6 +17,7 @@ import kotlinx.coroutines.launch @HiltViewModel class QuickPairsViewModel @Inject constructor( + private val application: Application, private val quickRepo: QuickRepo, private val convertUseCase: ConvertWithRateUseCase, ) : ViewModel() { @@ -27,7 +30,10 @@ class QuickPairsViewModel @Inject constructor( convertedAmt } pair.copy(to = actualTo) - }.sortedByDescending { it.calculatedDate } + }.sortedWith( + compareByDescending { it.isPinned() } + .thenByDescending { it.calculatedDate } + ) } .stateIn( scope = viewModelScope, @@ -37,7 +43,12 @@ class QuickPairsViewModel @Inject constructor( fun deletePair(pair: QuickPair) { viewModelScope.launch { - quickRepo.delete(pair.id) + if (pair.isPinned()) { + quickRepo.delete(pair.id) + WatchRefreshManager.refreshComplications(application) + } else { + quickRepo.delete(pair.id) + } } } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt index fb47a5d47..26b117362 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt @@ -1,24 +1,14 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box 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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable -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.graphics.Color @@ -29,27 +19,31 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.filled.Star import androidx.wear.compose.material3.Icon import androidx.wear.compose.material3.Text import dev.arkbuilders.rate.core.domain.CurrUtils import dev.arkbuilders.rate.core.domain.model.Amount import dev.arkbuilders.rate.core.domain.model.CurrencyCode import dev.arkbuilders.rate.core.domain.model.Group -import dev.arkbuilders.rate.core.presentation.CoreRDrawable import dev.arkbuilders.rate.core.presentation.theme.ArkColor import dev.arkbuilders.rate.core.presentation.utils.IconUtils import dev.arkbuilders.rate.feature.quick.domain.model.QuickPair import java.math.BigDecimal import java.time.OffsetDateTime +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.shape.CircleShape + @Composable fun QuickPairItem( modifier: Modifier = Modifier, quick: QuickPair, onClick: () -> Unit, ) { - var isExpanded by remember { mutableStateOf(false) } - Column( modifier = modifier .fillMaxWidth() @@ -65,29 +59,14 @@ fun QuickPairItem( ) { Row(verticalAlignment = Alignment.CenterVertically) { CurrIcon(modifier = Modifier.size(24.dp), code = quick.from) - if (quick.to.size > 1) { - Box( - modifier = Modifier - .size(24.dp) - .offset(x = (-8).dp) - .border(1.dp, Color.White, CircleShape) - .background(ArkColor.BGTertiary, CircleShape), - contentAlignment = Alignment.Center - ) { - Text( - text = "+${quick.to.size}", - fontWeight = FontWeight.Bold, - fontSize = 10.sp, - color = ArkColor.TextTertiary, - ) - } - } else if (quick.to.isNotEmpty()) { + val target = quick.to.firstOrNull() + if (target != null) { CurrIcon( modifier = Modifier .size(24.dp) .offset(x = (-8).dp) .border(1.dp, Color.White, CircleShape), - code = quick.to.first().code + code = target.code ) } } @@ -98,67 +77,43 @@ fun QuickPairItem( ) } - // Title Row: "EUR to USD" and Chevron + // Title Row: "EUR to USD" Row( modifier = Modifier.fillMaxWidth().padding(top = 4.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - val titleText = if (quick.to.size > 1) { - "${quick.from} to ${quick.to.first().code}, and ${quick.to.size - 1}+" - } else { - "${quick.from} to ${quick.to.firstOrNull()?.code ?: ""}" - } - Text( - text = titleText, - color = ArkColor.TextPrimary, - fontWeight = FontWeight.Bold, - fontSize = 14.sp - ) - if (quick.to.size > 1) { - Icon( - painter = painterResource(if (isExpanded) CoreRDrawable.ic_chevron_up else CoreRDrawable.ic_chevron), - contentDescription = "Expand", - tint = ArkColor.FGSecondary, - modifier = Modifier - .size(16.dp) - .clickable { isExpanded = !isExpanded } + val targetCode = quick.to.firstOrNull()?.code ?: "" + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "${quick.from} to $targetCode", + color = ArkColor.TextPrimary, + fontWeight = FontWeight.Bold, + fontSize = 14.sp ) + if (quick.isPinned()) { + Spacer(modifier = Modifier.width(4.dp)) + Icon( + imageVector = androidx.compose.material.icons.Icons.Default.Star, + contentDescription = "Pinned", + tint = ArkColor.Primary, + modifier = Modifier.size(12.dp) + ) + } } } - // Subtitle: Base amount - val baseAmountText = if (quick.to.size == 1) { - "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = ${CurrUtils.prepareToDisplay(quick.to.first().value)} ${quick.to.first().code}" - } else { - "${CurrUtils.prepareToDisplay(quick.amount)} ${quick.from} = ${CurrUtils.prepareToDisplay(quick.to.first().value)} ${quick.to.first().code}" - } - Text( - text = baseAmountText, - color = ArkColor.TextTertiary, - fontSize = 12.sp, - modifier = Modifier.padding(top = 2.dp) - ) - - // Expanded view - if (isExpanded && quick.to.size > 1) { - Column(modifier = Modifier.padding(top = 8.dp)) { - quick.to.forEach { target -> - Row( - modifier = Modifier.padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - CurrIcon(modifier = Modifier.size(16.dp), code = target.code) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "${CurrUtils.prepareToDisplay(target.value)} ${target.code}", - color = ArkColor.TextPrimary, - fontWeight = FontWeight.SemiBold, - fontSize = 12.sp - ) - } - } - } + // Subtitle: Conversion Rate + val target = quick.to.firstOrNull() + if (target != null) { + val baseAmountStr = CurrUtils.prepareToDisplay(quick.amount) + val targetAmountStr = CurrUtils.prepareToDisplay(target.value) + Text( + text = "$baseAmountStr ${quick.from} = $targetAmountStr ${target.code}", + color = ArkColor.TextTertiary, + fontSize = 12.sp, + modifier = Modifier.padding(top = 2.dp) + ) } } } @@ -177,7 +132,6 @@ fun CurrIcon( ) } - @Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) @Composable fun QuickPairItemPreview() { @@ -186,10 +140,7 @@ fun QuickPairItemPreview() { id = 1, from = "BTC", amount = BigDecimal.valueOf(1.2), - to = listOf( - Amount("USD", BigDecimal.valueOf(12.0)), - Amount("EUR", BigDecimal.valueOf(12.0)) - ), + to = listOf(Amount("USD", BigDecimal.valueOf(12.0))), calculatedDate = OffsetDateTime.now(), pinnedDate = null, group = Group.empty() @@ -197,4 +148,3 @@ fun QuickPairItemPreview() { onClick = {} ) } - diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt index a1edf69b9..f37e0e2ca 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt @@ -1,14 +1,16 @@ package dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices @@ -18,20 +20,15 @@ import androidx.compose.ui.unit.sp import androidx.wear.compose.material3.Icon import androidx.wear.compose.material3.Text import dev.arkbuilders.rate.core.presentation.CoreRDrawable - -import androidx.compose.ui.res.stringResource import dev.arkbuilders.rate.core.presentation.CoreRString import dev.arkbuilders.rate.core.presentation.theme.ArkColor -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height - +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn @Composable fun QuickPairsEmpty( modifier: Modifier = Modifier, onAddClick: () -> Unit ) { - androidx.wear.compose.foundation.lazy.ScalingLazyColumn( + ScalingLazyColumn( modifier = modifier.fillMaxSize().background(Color.White), horizontalAlignment = Alignment.CenterHorizontally, contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 24.dp) From 3f6da883f3a9574733d226ea96c2d586564dca0b Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 10 May 2026 21:05:34 +0700 Subject: [PATCH 49/57] Improve message and pin --- .../watchapp/presentation/MainActivity.kt | 23 +++++++-- .../addquickpairs/AddQuickPairsScreen.kt | 13 ++++- .../addquickpairs/AddQuickPairsViewModel.kt | 8 +-- .../presentation/options/OptionsScreen.kt | 40 ++++++++++----- .../presentation/options/OptionsViewModel.kt | 6 +-- .../presentation/options/SuccessScreen.kt | 2 +- .../watchapp/presentation/theme/WearDialog.kt | 51 ++++++++++++------- 7 files changed, 99 insertions(+), 44 deletions(-) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt index 69f62fb84..c71504766 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -67,6 +67,9 @@ class MainActivity : ComponentActivity() { }, onNavigateBack = { navController.popBackStack("list", inclusive = false) + }, + onNavigateToSuccess = { message -> + navController.navigate("success?message=$message") } ) } @@ -75,12 +78,15 @@ class MainActivity : ComponentActivity() { arguments = listOf(navArgument("id") { type = NavType.LongType }) ) { OptionsScreen( - onSearchClick = { navController.navigate("search") }, - onDeleteSuccess = { navController.navigate("success") }, + onDeleteSuccess = { message -> + navController.navigate("success?message=$message") + }, onUpdateClick = { id -> navController.navigate("addquickpairs?id=$id") }, - onPinClick = { navController.popBackStack() }, + onPinClick = { message -> + navController.navigate("success?message=$message") + }, onDismiss = { navController.popBackStack() } ) } @@ -104,8 +110,17 @@ class MainActivity : ComponentActivity() { } ) } - composable("success") { + composable( + route = "success?message={message}", + arguments = listOf(navArgument("message") { + type = NavType.StringType + nullable = true + defaultValue = "Success" + }) + ) { backStackEntry -> + val message = backStackEntry.arguments?.getString("message") ?: "Success" SuccessScreen( + message = message, onTimeout = { navController.popBackStack("list", inclusive = false) } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt index 4286d7ab5..c5b36467a 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt @@ -33,6 +33,11 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.getValue +import dev.arkbuilders.rate.watchapp.presentation.theme.WearSnackbar import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController @@ -50,7 +55,8 @@ fun AddQuickPairsScreen( viewModel: AddQuickPairsViewModel = hiltViewModel(), navController: NavHostController, onNavigateToSearch: (String) -> Unit, - onNavigateBack: () -> Unit + onNavigateBack: () -> Unit, + onNavigateToSuccess: (String) -> Unit ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -80,7 +86,10 @@ fun AddQuickPairsScreen( } if (state.isSaved) { - onNavigateBack() + val message = if (state.editId != null) "Updated Successfully" else "Added Successfully" + LaunchedEffect(Unit) { + onNavigateToSuccess(message) + } } ScalingLazyColumn( diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt index df1218c05..77e5c90d8 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsViewModel.kt @@ -28,7 +28,8 @@ data class AddQuickState( val targetCurrency: String = "EUR", val targetAmount: String = "", val isSaved: Boolean = false, - val editId: Long? = null + val editId: Long? = null, + val pinnedDate: OffsetDateTime? = null ) @HiltViewModel @@ -55,7 +56,8 @@ class AddQuickPairsViewModel @Inject constructor( baseAmount = CurrUtils.roundOff(pair.amount), targetCurrency = pair.to.firstOrNull()?.code ?: "EUR", targetAmount = CurrUtils.roundOff(pair.to.firstOrNull()?.value ?: java.math.BigDecimal.ZERO), - editId = id + editId = id, + pinnedDate = pair.pinnedDate ) } } @@ -135,7 +137,7 @@ class AddQuickPairsViewModel @Inject constructor( amount = s.baseAmount.toBigDecimalArk(), to = listOf(Amount(s.targetCurrency, s.targetAmount.toBigDecimalArk())), calculatedDate = OffsetDateTime.now(), - pinnedDate = null, + pinnedDate = s.pinnedDate, group = group ) quickRepo.insert(quick) diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt index 4b736fc9a..d986f4be3 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsScreen.kt @@ -30,20 +30,22 @@ import dev.arkbuilders.rate.watchapp.presentation.theme.WearInfoDialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.arkbuilders.rate.watchapp.presentation.theme.WearSnackbar + @Composable fun OptionsScreen( modifier: Modifier = Modifier, viewModel: OptionsViewModel = hiltViewModel(), onUpdateClick: (Long) -> Unit = {}, - onPinClick: () -> Unit = {}, - onSearchClick: () -> Unit = {}, - onDeleteSuccess: () -> Unit = {}, + onPinClick: (String) -> Unit = {}, + onDeleteSuccess: (String) -> Unit = {}, onDismiss: () -> Unit = {} ) { val quickPair by viewModel.quickPair.collectAsStateWithLifecycle() val showPinLimitDialog by viewModel.showPinLimitDialog.collectAsStateWithLifecycle() val listState = rememberScalingLazyListState() var showDeleteDialog by remember { mutableStateOf(false) } + var snackbarMessage by remember { mutableStateOf(null) } if (showDeleteDialog) { WearConfirmationDialog( @@ -51,7 +53,9 @@ fun OptionsScreen( message = "Are you sure you want to delete this pair?", onConfirm = { showDeleteDialog = false - viewModel.deletePair(onDeleted = onDeleteSuccess) + viewModel.deletePair(onDeleted = { + onDeleteSuccess("Deleted Successfully") + }) }, onDismiss = { showDeleteDialog = false @@ -70,6 +74,18 @@ fun OptionsScreen( ) } + snackbarMessage?.let { msg -> + WearSnackbar( + message = msg, + onDismiss = { + snackbarMessage = null + if (msg == "Unpinned") { + onDismiss() + } + } + ) + } + Scaffold( positionIndicator = { PositionIndicator(scalingLazyListState = listState) @@ -110,19 +126,17 @@ fun OptionsScreen( text = if (isPinned) "Unpin" else "Pin", icon = WearOptionButtonIcon.Pin, onClick = { - viewModel.togglePin(onSuccess = onPinClick) + viewModel.togglePin(onSuccess = { pinned -> + if (pinned) { + onPinClick("Pinned Successfully") + } else { + snackbarMessage = "Unpinned" + } + }) } ) } - item { - WearOptionButton( - text = "Search", - icon = WearOptionButtonIcon.Search, - onClick = onSearchClick - ) - } - item { WearOptionButton( text = "Delete", diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt index 9f9783c0c..c75492e52 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/OptionsViewModel.kt @@ -38,7 +38,7 @@ class OptionsViewModel @Inject constructor( } } - fun togglePin(onSuccess: () -> Unit) { + fun togglePin(onSuccess: (Boolean) -> Unit) { viewModelScope.launch { val pair = _quickPair.value ?: return@launch if (pair.isPinned()) { @@ -46,7 +46,7 @@ class OptionsViewModel @Inject constructor( val updated = pair.copy(pinnedDate = null) quickRepo.insert(updated) WatchRefreshManager.refreshComplications(application) - onSuccess() + onSuccess(false) } else { // Check limit val pinnedCount = quickRepo.getAll().count { it.isPinned() } @@ -57,7 +57,7 @@ class OptionsViewModel @Inject constructor( val updated = pair.copy(pinnedDate = java.time.OffsetDateTime.now()) quickRepo.insert(updated) WatchRefreshManager.refreshComplications(application) - onSuccess() + onSuccess(true) } } } diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/SuccessScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/SuccessScreen.kt index e6f841e20..80d2828a8 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/SuccessScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/options/SuccessScreen.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.delay @Composable fun SuccessScreen( modifier: Modifier = Modifier, - message: String = "Delete Successful", + message: String = "Success", onTimeout: () -> Unit ) { LaunchedEffect(Unit) { diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt index 8eef38679..a1685d387 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/theme/WearDialog.kt @@ -12,7 +12,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.Close +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -24,6 +28,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog +import kotlinx.coroutines.delay import androidx.wear.compose.material.Text import dev.arkbuilders.rate.core.presentation.theme.ArkColor @@ -136,24 +141,34 @@ fun WearInfoDialog( } } -@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) @Composable -fun WearConfirmationDialogPreview() { - WearConfirmationDialog( - title = "Delete Item", - message = "Are you sure you want to delete this item? This action cannot be undone.", - onConfirm = {}, - onDismiss = {}, - isDestructive = true - ) -} +fun WearSnackbar( + message: String, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + LaunchedEffect(message) { + delay(2000L) + onDismiss() + } -@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) -@Composable -fun WearInfoDialogPreview() { - WearInfoDialog( - title = "Success", - message = "Your changes have been saved successfully.", - onDismiss = {} - ) + Box( + modifier = modifier + .fillMaxSize() + .padding(bottom = 20.dp), + contentAlignment = Alignment.BottomCenter + ) { + Text( + text = message, + modifier = Modifier + .background( + color = Color.DarkGray.copy(alpha = 0.9f), + shape = RoundedCornerShape(20.dp) + ) + .padding(horizontal = 12.dp, vertical = 6.dp), + color = Color.White, + fontSize = 12.sp, + textAlign = TextAlign.Center + ) + } } From 8a10d74f70ce5d2e7077499a4812a3913d377dbb Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 10 May 2026 21:23:20 +0700 Subject: [PATCH 50/57] Update app name and app logo --- watchapp/build.gradle.kts | 12 ++++++++ .../addquickpairs/AddQuickPairsScreen.kt | 5 ---- .../src/main/res/drawable/splash_icon.xml | 27 ------------------ .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 1884 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 1120 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 2366 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 3688 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 4812 bytes watchapp/src/main/res/values/strings.xml | 1 - watchapp/src/main/res/values/styles.xml | 4 +-- 10 files changed, 14 insertions(+), 35 deletions(-) delete mode 100644 watchapp/src/main/res/drawable/splash_icon.xml diff --git a/watchapp/build.gradle.kts b/watchapp/build.gradle.kts index 2316bb8da..03f4a6a46 100644 --- a/watchapp/build.gradle.kts +++ b/watchapp/build.gradle.kts @@ -57,12 +57,24 @@ android { } buildTypes { + debug { + addManifestPlaceholders( + mapOf( + "appLabel" to "@string/app_name_debug", + ), + ) + } release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + addManifestPlaceholders( + mapOf( + "appLabel" to "@string/app_name", + ), + ) } } compileOptions { diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt index c5b36467a..60c580bd7 100644 --- a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt @@ -33,11 +33,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.getValue -import dev.arkbuilders.rate.watchapp.presentation.theme.WearSnackbar import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController diff --git a/watchapp/src/main/res/drawable/splash_icon.xml b/watchapp/src/main/res/drawable/splash_icon.xml deleted file mode 100644 index 7874e83f0..000000000 --- a/watchapp/src/main/res/drawable/splash_icon.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - diff --git a/watchapp/src/main/res/mipmap-hdpi/ic_launcher.webp b/watchapp/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78ecd372343283f4157dcfd918ec5165bb3..377149d72d4f6267557122e689e69d6a1795adce 100644 GIT binary patch literal 1884 zcmV-i2c!5>Nk&Fg2LJ$9MM6+kP&iCT2LJ#sN5ByfRR@B$ZR7BVz3rzE5fk7w75c^Y zZIH?jTRnB$2~=8!wsGSt9A^ai@F&!lUn17j&1qoV1+ScY>%rnOZF)K%%?-AU={^0(6>~O5*G2dT;+m9=Q;~P=z>6G}g5)_KAy#JhIrOG8O zB2Qaxx%*sWeH7}I564B96mGD-D(exmGjWR_Ev;(?J5%>8ZA`XiTC*eM$XeRv94md+ zi(PH)VXSJQ23TP)>sX6ltx9{T@76Inqb&eHEQ?#`76hQtl_-~=MEn1u{Qp|^*1P3I z^$sl)JkQRS#B2z~LCB!a5c~Ur)(!yV^`jXvPC74Nh)4bZq(Ofo6VhN#pC<3MP^YHk zj@EQAfK=r;zTg}R0QvmLBvBVj9Kup(V@84-$e9LdjdMd-S93IcJi(3szSzv2uE{we zqQE@}#(TpW)dYf_yq*P#pZgS)D$#Z#Lt1U)nBRA~qdF@_D{1p?UI5MShip)eKQ(>c zD!upaX$UE|pFkM>`yE&EB+`itW7L)y-_M`5V*EdbJ~~&rw9^6!p2ygdKbjJEJw;~1 z*k%S1xN@;<>)tzeKb_sQY{s;y0ycAdTkd1m5WiV4W(ce6ueGKuCypFD*fNtbnn@0_ zce13+Y*jj5QWndEoMmrp8OWJZ=L1Kv-Y-W(#l0N#MCVI0}S z<#_;fV*sjdR3(->UQ=1pvI+pie7}`WmgGd7m8@J8Gf(S|E4Z867Hoi1QSurQj?i~M z1*a$Y`2d7y!3Ja+U5d_&_Wc4thik^DLZcl>TCWw3a*MektjjstT*3aMOHm|=+q;-! z(f?lndHlGCG4L>Fz;cHg0KL~j07#nWhOn0azyR(ylh|V2H z@XiCa@FOChFm{_nvX(>?IR`|HU=(rQ&zCrqgfcrx)J)HO-`YfjSmXJMd8t(j_<@y( zW=0r2`}mwAkq*qKnTR$&v`X2d7TIg>xp*Za8rrDA?#fdYFCwy5I4WIDQJRd=yPZN- zBA-%cqw>Ldb|MP06NM1TBWk&V#Lvxp(f4@!GD*Ef(EYWHI>pynchAMfw@()5HNi}=*uKmPL^Lka`xww@KCYkpjhCv^m^I5>2b z3~BS{lA|c;qPU~#l0LNGhNaGi0>?5(-Fkc5TVc-z;o}NkocF^~AHD0hmjS@{VhF=z zJ%mcoPXIn`)SR0(ZcQJrQRBN+pi!Ibao#$gA98bM_{w7_(MVc$c+;q z)t8&Z_4Zxs24&9riQ6SFXNQ=El8i>9=gu2*qubf<-l+-s{V6GB9fxRl%d4u|W-KWf z#2L9b3V>spviJA;blv5=foT7)RzCqi#8V>v61BA#AuN3N0{iM5+dui`&3i_8GX)=Q zZ)Yp0s#=jLJaHBuhrNKMWk`k3W~BBh$dn^!i3sB>Dcs%eIxt1|n0y z39i~U8lQ7qS^bTkYcJdYG&`_0)#KTZ7B?n4Gq)6?<*mu)FmQ=qotT-7)r$Tt+>oyv zqq|<4VRbyBDY9{oD!vOV(d>)j&z=qs&}4MvmtF_BLS%jQ5Yz9L(r+ij`bmMJlTdfdC?a0Du4@fIt8dKp-Rj|KIE9Q?tm1VUt8*N>Gwg)_=cKC?SU? W%uQ_4XmS#s^}cph&ZPn{si2ww literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG diff --git a/watchapp/src/main/res/mipmap-mdpi/ic_launcher.webp b/watchapp/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..271fe6b878535cf4ad42742766631780e107fc1c 100644 GIT binary patch literal 1120 zcmV-m1fTm-Nk&Fk1ONb6MM6+kP&iCW1ONapFTe{BXD2JNZP!-D2``a#_@fN^t8|pZjwH#klC$@;&zn62vC|{{sS` zB1BMzFpy{1L<9lPWP?ZssK79U2@I8I2%-{dOh5!tfeoUvK?FhI87d+SK@br@fC2zS z1OZSH08kMCB2+|;M8NOokHN3+x9quBuk|u&EdXw z;aKxmR-axj-TT_D_jq0W{majf&&#pKxmPH$V@>2bTPQzexcT3HdAt1`YyQu@{QLb` zmKyo<^FBNG`u})7caAmT$Jfj8{pmD+y&o8)UE5wrQjJ~HyOXhPn`_&)ZQHhO+Za#1 zYuh&d#UwqI#rLNo`ac26|I)=-x{G-#ls`~~8L9$Ddk4veqXJc+Do!7(RhMpja8gv! zdlUN;-&ycn@Ya)gxVcM)hw062#if!n9(gl&NHw74s^;-X}6hV?5n-* z>GEcK>D02Me%p%a;^tvpHk_4$QH>%SPbpfM@5d0?>lk7FXsRp$XUCnz!f?iIdzLPr zkp|#YKJyO=h#VT~Gm(V)UO-gsbEf_hLv43kYIS8!jI|qDd-Q zF~q>Zwmu9%)YN4IS~r+mBBf$bn@N+~&pkV_ZeU#34JG5_hYW5pQIl|SPVUCNg%{2d z3TwNZMmNSl-HV9>B)@go%(7LwxzZ1-RF>5Sp(6dd7)nUgwrE) z<}bZqR#O6uL%>R1GR_mU4xLvU-KU9WX3j9i>;=nh1@0<@mJ356wWx; zMR&bPTHF2j{Y&!i?{?OzB36Ie*hB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 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 literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? diff --git a/watchapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/watchapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77f9f036a47549d47db79c16788749dca10..f2b876c2c75f2b5897cd1e914aab0a82abeaedd8 100644 GIT binary patch literal 3688 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 GuM_~jHW0D^ literal 2884 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 - watchapp ARK Rates