diff --git a/build.gradle.kts b/build.gradle.kts index 68c93594d..507df665f 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 @@ -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/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/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/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( 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 bd94ec9d6..2e7a19c23 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -144,6 +144,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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 36b017002..4b3c6b5cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,14 @@ [versions] +composeNavigation = "1.3.0" +hilt = "2.56.2" 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" @@ -37,8 +40,13 @@ 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" } @@ -49,7 +57,6 @@ androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtim androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } -androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomRuntime" } androidx-ui = { module = "androidx.compose.ui:ui", version.ref = "composeUi" } androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "composeUi" } @@ -69,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" } @@ -90,8 +99,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.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/.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..d1d11ffa4 --- /dev/null +++ b/watchapp/build.gradle.kts @@ -0,0 +1,89 @@ +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" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId = "dev.arkbuilders.rate.watchapp" + minSdk = libs.versions.minSdk.get().toInt() + 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(":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) + 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.material3) + implementation(libs.navigation.compose) + 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..eff222470 --- /dev/null +++ b/watchapp/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + 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) +} 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..35b6253c9 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/MainActivity.kt @@ -0,0 +1,59 @@ +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.wear.compose.material.Scaffold +import androidx.wear.compose.material.Vignette +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.quickpairs.QuickPairsScreen +import dev.arkbuilders.rate.watchapp.presentation.theme.ArkrateTheme + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + + super.onCreate(savedInstanceState) + + setTheme(android.R.style.Theme_DeviceDefault) + + setContent { + ArkrateTheme { + val navController = rememberSwipeDismissableNavController() + Scaffold( + vignette = { + Vignette(vignettePosition = VignettePosition.TopAndBottom) + } + ) { + SwipeDismissableNavHost( + navController = navController, + startDestination = "list" + ) { + composable("list") { +// OptionsScreen() +// SearchScreen() + QuickPairsScreen( + onNavigateToAdd = { + navController.navigate("addquickpairs") + } + ) + } + composable("addquickpairs") { + AddQuickPairsScreen( + ) + } + } + } + } + + } + } +} 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() { + +} 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..cb30dd7ef --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/AddQuickPairsScreen.kt @@ -0,0 +1,124 @@ +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 +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 +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.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) { + var expanded by remember { mutableStateOf(false) } + val scrollState = rememberScrollState() + + ScalingLazyColumn( + modifier = modifier.fillMaxSize().background(ArkColor.BGSecondaryAlt), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + Text( + modifier = modifier.fillMaxWidth(), + text = "Add", + textAlign = TextAlign.Center, + color = ArkColor.TextPrimary + ) + } + + item { + CurrencyInputField( + label = "To", + currencyCode = "EUR", + value = "0.92", + onValueChange = { }, + onCurrencyClick = { + + }, + showLabel = true, + showDeleteButton = false, + onDeleteClick = {} + ) + } + + item { + CurrencyInputField( + label = "To", + currencyCode = "EUR", + value = "0.92", + onValueChange = { }, + onCurrencyClick = {}, + showLabel = false, + showDeleteButton = false, + onDeleteClick = {} + ) + } + + 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/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() { + +} 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/OptionItem.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt new file mode 100644 index 000000000..93997adac --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionItem.kt @@ -0,0 +1,82 @@ +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.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.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, + isDeleteButton: Boolean = false, +) { + OutlinedButton( + modifier = modifier + .fillMaxWidth() + .padding(8.dp), + onClick = onClick, + shape = RoundedCornerShape(20), + 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), + 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 = {}, + ) +} diff --git a/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionsMenu.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionsMenu.kt new file mode 100644 index 000000000..4c20abdd9 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/addquickpairs/composables/OptionsMenu.kt @@ -0,0 +1,88 @@ +package dev.arkbuilders.rate.watchapp.presentation.addquickpairs.composables + +import androidx.compose.foundation.layout.Arrangement +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.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.material.Text +import dev.arkbuilders.rate.core.presentation.CoreRDrawable +import dev.arkbuilders.rate.core.presentation.CoreRString + +@Composable +fun OptionsMenu(modifier: Modifier = Modifier) { + ScalingLazyColumn( + 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(), + 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 + ) + } + } +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun AddQuickPairsPreview() { + OptionsMenu() +} 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 = {} + ) +} 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..744915d44 --- /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 kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor( + private val currencyRepo: CurrencyRepo, + ): ViewModel() { + + init { + viewModelScope.launch { + currencyRepo.initialize() + launch { + currencyRepo.getCurrencyRates() + } + } + } +} 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 new file mode 100644 index 000000000..67029153e --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsScreen.kt @@ -0,0 +1,56 @@ +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.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 +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( + modifier: Modifier = Modifier, + viewModel: QuickPairsViewModel = hiltViewModel(), + onNavigateToAdd: () -> Unit +) { + val quickPairsList = viewModel.quickPairs.collectAsStateWithLifecycle().value + + 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 + ) + } + 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/quickpairs/QuickPairsViewModel.kt b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt new file mode 100644 index 000000000..c7e4cc1ca --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/QuickPairsViewModel.kt @@ -0,0 +1,105 @@ +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 + +@HiltViewModel +class QuickPairsViewModel @Inject constructor( + private val currencyRepo: CurrencyRepo, +) : 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() + ) + ) + _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 new file mode 100644 index 000000000..84e6a8d5a --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairItem.kt @@ -0,0 +1,169 @@ +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.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 +import java.math.BigDecimal +import java.time.OffsetDateTime + +@Composable +fun QuickPairItem( + modifier: Modifier = Modifier, + quick: QuickPair, + onClick: () -> Unit, +) { + var isExpanded by remember { + mutableStateOf(true) + } + Card( + onClick = onClick, + 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 = Group.empty() + ), + 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 new file mode 100644 index 000000000..941b829e3 --- /dev/null +++ b/watchapp/src/main/java/dev/arkbuilders/rate/watchapp/presentation/quickpairs/composables/QuickPairsEmpty.kt @@ -0,0 +1,58 @@ +package dev.arkbuilders.rate.watchapp.presentation.quickpairs.composables + +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 +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 + +@Composable +fun QuickPairsEmpty( + modifier: Modifier = Modifier +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + 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, + ) + + } +} + +@Preview(device = Devices.WEAR_OS_LARGE_ROUND, showSystemUi = true) +@Composable +fun QuickPairEmptyPreview() { + QuickPairsEmpty() +} 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 { + + } + +} 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/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 = {} + ) +} 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 000000000..c209e78ec Binary files /dev/null and b/watchapp/src/main/res/mipmap-hdpi/ic_launcher.webp differ 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 000000000..4f0f1d64e Binary files /dev/null and b/watchapp/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/watchapp/src/main/res/mipmap-xhdpi/ic_launcher.webp b/watchapp/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/watchapp/src/main/res/mipmap-xhdpi/ic_launcher.webp differ 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 000000000..28d4b77f9 Binary files /dev/null and b/watchapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/watchapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/watchapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/watchapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/watchapp/src/main/res/values-round/strings.xml b/watchapp/src/main/res/values-round/strings.xml new file mode 100644 index 000000000..42f12297f --- /dev/null +++ b/watchapp/src/main/res/values-round/strings.xml @@ -0,0 +1,3 @@ + + 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 @@ + + + +