diff --git a/build.gradle.kts b/build.gradle.kts index ed7cadb..a1c5691 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) } android { @@ -12,6 +13,11 @@ android { targetSdk = 37 versionCode = 1 versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + compose = true } } @@ -21,8 +27,29 @@ kotlin { dependencies { implementation(project(":purplequartz")) + implementation(platform(libs.compose.bom)) + implementation(libs.activity.compose) + implementation(libs.compose.foundation) + implementation(libs.compose.material3) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.lifecycle.runtime.compose) + implementation(libs.lifecycle.viewmodel.compose) + implementation(libs.navigation.compose) implementation(libs.coroutines.android) - implementation(libs.coil) + implementation(libs.coil.compose) implementation(libs.coil.network.okhttp) implementation(libs.okhttp) + + testImplementation(libs.junit) + testImplementation(libs.coroutines.test) + + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.compose.ui.test.junit4) + androidTestImplementation(libs.navigation.testing) + + debugImplementation(libs.compose.ui.tooling) + debugImplementation(libs.compose.ui.test.manifest) } diff --git a/gradle.lockfile b/gradle.lockfile index abfe56e..07ba590 100644 --- a/gradle.lockfile +++ b/gradle.lockfile @@ -1,38 +1,148 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -androidx.annotation:annotation-experimental:1.4.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.annotation:annotation-jvm:1.10.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.activity:activity-compose:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.activity:activity-ktx:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.activity:activity:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-experimental:1.4.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-jvm:1.10.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.annotation:annotation-jvm:1.9.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath -androidx.annotation:annotation:1.10.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation:1.10.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.annotation:annotation:1.9.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath androidx.appcompat:appcompat-resources:1.7.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.arch.core:core-runtime:2.2.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.collection:collection-jvm:1.6.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.collection:collection:1.6.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-annotation-android:1.11.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-annotation:1.11.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.core:core-ktx:1.19.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.core:core-viewtree:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.core:core:1.19.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.arch.core:core-runtime:2.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.autofill:autofill:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection-jvm:1.5.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.collection:collection-jvm:1.6.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection-ktx:1.6.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.collection:collection:1.5.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.collection:collection:1.6.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-core-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-core:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-layout-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-layout:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material3:material3-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material3:material3:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-ripple-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-ripple:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-annotation-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-annotation:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-retain-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-retain:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-saveable-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-saveable:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-geometry-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-geometry:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-graphics-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-graphics:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-test-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.compose.ui:ui-test-junit4-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.compose.ui:ui-test-junit4:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.compose.ui:ui-test-manifest:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.compose.ui:ui-text-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-text:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-tooling-data-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-tooling-data:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-tooling-preview-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-preview:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-unit-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-unit:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-util-android:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-util:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui:1.11.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose:compose-bom:2026.05.01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.concurrent:concurrent-futures-ktx:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.concurrent:concurrent-futures:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath +androidx.core:core-ktx:1.18.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.core:core-ktx:1.19.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.core:core:1.18.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +androidx.core:core:1.19.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.customview:customview-poolingcontainer:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.documentfile:documentfile:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.dynamicanimation:dynamicanimation:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.emoji2:emoji2:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.exifinterface:exifinterface:1.4.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.interpolator:interpolator:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-common-jvm:2.9.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-common:2.9.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-runtime-android:2.9.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.lifecycle:lifecycle-runtime:2.9.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.profileinstaller:profileinstaller:1.4.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.graphics:graphics-path:1.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.interpolator:interpolator:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.legacy:legacy-support-core-utils:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common-java8:2.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common-jvm:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core-ktx:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-livedata:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-process:2.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-compose:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-ktx-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-testing-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime-testing:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-compose-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.loader:loader:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-common-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-common:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-compose-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-compose:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-runtime-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-runtime:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigation:navigation-testing-android:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.navigation:navigation-testing:2.9.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.navigationevent:navigationevent-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-compose-android:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent-compose:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.navigationevent:navigationevent:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.print:print:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.profileinstaller:profileinstaller:1.4.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-compose-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-compose:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate-ktx:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.savedstate:savedstate:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.sqlite:sqlite-android:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.sqlite:sqlite-bundled-android:2.6.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.sqlite:sqlite-bundled:2.6.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.sqlite:sqlite:2.6.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.startup:startup-runtime:1.2.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.tracing:tracing:1.2.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.startup:startup-runtime:1.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.test.espresso:espresso-core:3.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test.ext:junit:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test.services:storage:1.6.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test:annotation:1.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test:core:1.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test:monitor:1.8.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.test:runner:1.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.tracing:tracing:1.1.0=debugAndroidTestCompileClasspath +androidx.tracing:tracing:1.2.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.transition:transition:1.6.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.vectordrawable:vectordrawable-animated:1.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.vectordrawable:vectordrawable:1.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.window:window-core-android:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.window:window-core:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.window:window:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.android.tools.analytics-library:protos:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools.analytics-library:shared:32.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools.analytics-library:tracker:32.2.1=androidLintTool @@ -81,12 +191,14 @@ com.fasterxml.jackson.core:jackson-core:2.22.0=debugAndroidTestCompileClasspath, com.fasterxml.jackson.core:jackson-databind:2.22.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.fasterxml.jackson.module:jackson-module-kotlin:2.22.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.22.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.accompanist:accompanist-drawablepainter:0.37.3=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.android:annotations:4.1.1.4=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-core com.google.api.grpc:proto-google-common-protos:2.17.0=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core com.google.api.grpc:proto-google-common-protos:2.48.0=unified-test-platform-android-test-plugin-host-emulator-control com.google.auto.service:auto-service-annotations:1.1.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle com.google.auto.service:auto-service:1.1.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle com.google.auto:auto-common:1.2.1=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.code.findbugs:jsr305:2.0.2=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath com.google.code.findbugs:jsr305:3.0.2=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher com.google.code.gson:gson:2.10.1=unified-test-platform-core,unified-test-platform-gradle-work-action com.google.code.gson:gson:2.11.0=androidLintTool,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-result-listener-gradle @@ -100,7 +212,7 @@ com.google.guava:failureaccess:1.0.1=unified-test-platform-android-driver-instru com.google.guava:failureaccess:1.0.2=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action com.google.guava:guava:32.0.1-jre=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.guava:guava:33.3.1-jre=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action -com.google.guava:listenablefuture:1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher com.google.j2objc:j2objc-annotations:2.8=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action @@ -121,6 +233,7 @@ com.squareup.okhttp3:okhttp-coroutines:5.4.0=debugAndroidTestLintChecksClasspath com.squareup.okhttp3:okhttp:5.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.squareup.okio:okio-jvm:3.17.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.squareup.okio:okio:3.17.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup:javawriter:2.1.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath com.sun.istack:istack-commons-runtime:3.0.8=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.sun.xml.fastinfoset:FastInfoset:1.2.16=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.vitorpamplona.negentropy:kmp-negentropy-android:v1.0.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath @@ -135,6 +248,10 @@ fr.acinq.secp256k1:secp256k1-kmp-jni-common:0.23.0=debugAndroidTestCompileClassp fr.acinq.secp256k1:secp256k1-kmp-jvm:0.23.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath fr.acinq.secp256k1:secp256k1-kmp:0.23.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath io.coil-kt.coil3:coil-android:3.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.coil-kt.coil3:coil-compose-android:3.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.coil-kt.coil3:coil-compose-core-android:3.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.coil-kt.coil3:coil-compose-core:3.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.coil-kt.coil3:coil-compose:3.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath io.coil-kt.coil3:coil-core-android:3.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath io.coil-kt.coil3:coil-core:3.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath io.coil-kt.coil3:coil-network-core-android:3.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath @@ -195,7 +312,8 @@ io.perfmark:perfmark-api:0.27.0=unified-test-platform-android-test-plugin-host-e jakarta.activation:jakarta.activation-api:1.2.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle javax.annotation:javax.annotation-api:1.3.2=unified-test-platform-android-test-plugin-host-emulator-control -javax.inject:javax.inject:1=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action +javax.inject:javax.inject:1=androidLintTool,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action +junit:junit:4.13.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath net.java.dev.jna:jna-platform:5.6.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action net.java.dev.jna:jna:5.6.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action net.sf.kxml:kxml2:2.3.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action @@ -214,6 +332,29 @@ org.codehaus.mojo:animal-sniffer-annotations:1.23=unified-test-platform-core org.codehaus.mojo:animal-sniffer-annotations:1.24=unified-test-platform-android-test-plugin-host-emulator-control org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.glassfish.jaxb:txw2:2.3.2=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.hamcrest:hamcrest-integration:1.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +org.hamcrest:hamcrest-library:1.3=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.androidx.savedstate:savedstate-compose:1.3.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.androidx.savedstate:savedstate:1.3.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.animation:animation-core:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.animation:animation:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.foundation:foundation-layout:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.foundation:foundation:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.runtime:runtime-saveable:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.runtime:runtime:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui-geometry:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui-graphics:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui-text:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui-unit:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui-util:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:compose-group-mapping:2.4.0=composeMappingProducerClasspath org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-build-tools-api:2.4.20-Beta1=kotlinAbiValidationCompatClasspath org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath @@ -225,6 +366,7 @@ org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinBuildToolsApiClasspa org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.20-Beta1=kotlinAbiValidationCompatClasspath org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-compiler-runner:2.4.20-Beta1=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:2.4.0=kotlin-extension,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-daemon-client:2.4.20-Beta1=kotlinAbiValidationCompatClasspath org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath @@ -245,7 +387,8 @@ org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=androidLintTool,unified-test-plat org.jetbrains.kotlin:kotlin-stdlib:1.8.21=unified-test-platform-android-test-plugin,unified-test-platform-core org.jetbrains.kotlin:kotlin-stdlib:1.9.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action -org.jetbrains.kotlin:kotlin-stdlib:2.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.2.21=composeMappingProducerClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlin:kotlin-stdlib:2.4.20-Beta1=kotlinAbiValidationCompatClasspath org.jetbrains.kotlin:kotlin-tooling-core:2.4.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-tooling-core:2.4.20-Beta1=kotlinAbiValidationCompatClasspath @@ -253,36 +396,43 @@ org.jetbrains.kotlinx:atomicfu-jvm:0.22.0=unified-test-platform-android-driver-i org.jetbrains.kotlinx:atomicfu:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.5.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-collections-immutable:0.5.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-datetime:0.7.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-io-core:0.8.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-bom:1.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath org.jetbrains.kotlinx:kotlinx-serialization-cbor-jvm:1.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains:annotations:13.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath -org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher -org.jspecify:jspecify:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains:annotations:13.0=composeMappingProducerClasspath,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jspecify:jspecify:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jvnet.staxex:stax-ex:1.8.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.ow2.asm:asm-analysis:9.9=androidLintTool org.ow2.asm:asm-commons:9.9=androidLintTool org.ow2.asm:asm-tree:9.9=androidLintTool +org.ow2.asm:asm-tree:9.9.1=composeMappingProducerClasspath org.ow2.asm:asm:9.9=androidLintTool -empty=androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAndroidTestRuntimeClasspath,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues +org.ow2.asm:asm:9.9.1=composeMappingProducerClasspath +empty=androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugReverseMetadataValues,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,kotlinCompilerPluginClasspath,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata,releaseReverseMetadataValues diff --git a/src/androidTest/kotlin/dev/zapstore/app/MainActivityTest.kt b/src/androidTest/kotlin/dev/zapstore/app/MainActivityTest.kt new file mode 100644 index 0000000..60b4c45 --- /dev/null +++ b/src/androidTest/kotlin/dev/zapstore/app/MainActivityTest.kt @@ -0,0 +1,17 @@ +package dev.zapstore.app + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import org.junit.Rule +import org.junit.Test + +class MainActivityTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun navigationHostLaunchesAtHome() { + composeRule.onNodeWithTag("searchField").assertIsDisplayed() + } +} diff --git a/src/androidTest/kotlin/dev/zapstore/app/ScreensTest.kt b/src/androidTest/kotlin/dev/zapstore/app/ScreensTest.kt new file mode 100644 index 0000000..0caa969 --- /dev/null +++ b/src/androidTest/kotlin/dev/zapstore/app/ScreensTest.kt @@ -0,0 +1,107 @@ +package dev.zapstore.app + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performImeAction +import com.vitorpamplona.quartz.nip01Core.core.Event +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +class ScreensTest { + @get:Rule + val composeRule = createComposeRule() + + @Test + fun homeSearchAndStackCallbacksAreWired() { + var submitted = false + var cleared = false + var openedStack: String? = null + val stack = StackInfo( + event( + id = "stack-id", + kind = Catalog.appStackKind, + tags = arrayOf(arrayOf("name", "Privacy")), + ), + ) + + composeRule.setContent { + ZapstoreTheme { + HomeScreen( + state = HomeUiState( + searchQuery = "zap", + stacks = listOf(stack), + stacksLoading = false, + ), + onSearchQueryChanged = {}, + onSearchSubmitted = { submitted = true }, + onSearchCleared = { cleared = true }, + onStackClick = { openedStack = it }, + onAppClick = { _, _ -> }, + ) + } + } + + composeRule.onNodeWithTag("searchField").performImeAction() + composeRule.onNodeWithText("×").performClick() + composeRule.onNodeWithTag("stack:stack-id").performClick() + + assertTrue(submitted) + assertTrue(cleared) + assertEquals("stack-id", openedStack) + } + + @Test + fun appDetailBackAndExternalLinkCallbacksAreWired() { + var backed = false + var openedUrl: String? = null + val app = AppInfo( + event( + kind = Catalog.appKind, + tags = arrayOf( + arrayOf("d", "zap"), + arrayOf("name", "Zap"), + arrayOf("repository", "https://example.com/repo"), + ), + ), + ) + + composeRule.setContent { + ZapstoreTheme { + AppDetailScreen( + state = AppDetailUiState( + app = app, + appLoading = false, + releaseLoading = false, + ), + onBack = { backed = true }, + onOpenUrl = { openedUrl = it }, + ) + } + } + + composeRule.onNodeWithTag("backButton").assertIsDisplayed().performClick() + composeRule.onNodeWithText("https://example.com/repo").performClick() + + assertTrue(backed) + assertEquals("https://example.com/repo", openedUrl) + } +} + +private fun event( + id: String = "a".repeat(64), + kind: Int, + tags: Array> = emptyArray(), +): Event = Event( + id = id, + pubKey = "1".repeat(64), + createdAt = 1_750_000_000, + kind = kind, + tags = tags, + content = "", + sig = "2".repeat(128), +) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index cf0b648..e68e52f 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -2,11 +2,10 @@ - - - diff --git a/src/main/assets/licenses/inter.txt b/src/main/assets/licenses/inter.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/src/main/assets/licenses/inter.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/main/kotlin/dev/zapstore/app/AppDetailActivity.kt b/src/main/kotlin/dev/zapstore/app/AppDetailActivity.kt deleted file mode 100644 index ae9b60e..0000000 --- a/src/main/kotlin/dev/zapstore/app/AppDetailActivity.kt +++ /dev/null @@ -1,264 +0,0 @@ -package dev.zapstore.app - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.view.Gravity -import android.view.ViewGroup -import android.text.method.LinkMovementMethod -import android.widget.HorizontalScrollView -import android.widget.LinearLayout -import android.widget.TextView -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import java.text.DateFormat -import java.util.Date - -class AppDetailActivity : Activity() { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) - private lateinit var content: LinearLayout - private var app: AppInfo? = null - private var release: ReleaseInfo? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val identifier = intent.getStringExtra(EXTRA_IDENTIFIER) ?: run { - finish() - return - } - val author = intent.getStringExtra(EXTRA_AUTHOR) - content = LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - setPadding(20.dp, 0, 20.dp, 36.dp) - } - setContentView(android.widget.ScrollView(this).apply { - setBackgroundColor(ZapUi.background) - addView(content) - }) - render() - observeApp(identifier, author) - observeRelease(identifier) - } - - private fun observeApp(identifier: String, author: String?) { - scope.launch { - Catalog.query( - this@AppDetailActivity, - Filter( - authors = author?.let(::listOf), - kinds = listOf(Catalog.appKind), - tags = mapOf("d" to listOf(identifier)), - limit = 3, - ), - ).collect { state -> - state.items.firstOrNull()?.let { app = AppInfo(it) } - render(state.error?.message) - } - } - } - - private fun observeRelease(identifier: String) { - scope.launch { - Catalog.query( - this@AppDetailActivity, - Filter(kinds = listOf(Catalog.releaseKind), tags = mapOf("i" to listOf(identifier)), limit = 10), - ).collect { state -> - release = state.items.map(::ReleaseInfo).maxByOrNull { it.event.createdAt } - render(state.error?.message) - } - } - } - - private fun render(error: String? = null) { - content.removeAllViews() - - val currentApp = app - if (currentApp == null) { - content.addView(TextView(this).apply { - background = ZapUi.rounded(ZapUi.surfaceVariant, 16.dp) - }, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 100.dp).apply { topMargin = 20.dp }) - error?.let { content.addView(ZapUi.label(this, it)) } - return - } - content.addView(LinearLayout(this).apply { - gravity = Gravity.TOP - addView(ZapUi.appIcon(this@AppDetailActivity, currentApp.name, currentApp.identifier, currentApp.iconUrl), LinearLayout.LayoutParams(84.dp, 84.dp)) - addView(LinearLayout(this@AppDetailActivity).apply { - orientation = LinearLayout.VERTICAL - addView(ZapUi.title(this@AppDetailActivity, currentApp.name, 27f)) - addView(ZapUi.label(this@AppDetailActivity, currentApp.identifier, 13f)) - release?.let { - addView(TextView(this@AppDetailActivity).apply { - text = buildString { - append(it.version) - it.channel?.let { channel -> append(" · ").append(channel) } - } - textSize = 13f - setTextColor(ZapUi.textColor) - background = ZapUi.rounded(ZapUi.primary, 12.dp) - setPadding(10.dp, 4.dp, 10.dp, 4.dp) - }, LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 10.dp - }) - } - }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { marginStart = 16.dp }) - }, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { topMargin = 14.dp }) - - content.addView(ZapUi.label(this, "Published by · ${currentApp.event.pubKey.take(16)}…", 13f), LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 14.dp - }) - if (currentApp.screenshots.isNotEmpty()) { - val row = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL } - currentApp.screenshots.take(5).forEach { url -> - row.addView(ZapUi.appIcon(this, currentApp.name, url, url), LinearLayout.LayoutParams(176.dp, 176.dp).apply { marginEnd = 10.dp }) - } - content.addView(HorizontalScrollView(this).apply { addView(row) }, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 20.dp - }) - } - if (currentApp.event.content.isNotBlank()) { - content.addView(body(currentApp.event.content), LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 20.dp - }) - } else if (currentApp.summary.isNotBlank()) { - content.addView(body(currentApp.summary), LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 20.dp - }) - } - - latestReleaseDivider() - release?.let { - content.addView(versionRow(it)) - if (it.notes.isNotBlank()) content.addView(body(it.notes), LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 18.dp - }) - } ?: content.addView(TextView(this).apply { - background = ZapUi.rounded(ZapUi.surfaceVariant, 8.dp) - }, LinearLayout.LayoutParams(220.dp, 32.dp)) - - content.addView(infoCard(currentApp), LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 28.dp - }) - } - - private fun section(value: String) { - content.addView(ZapUi.title(this, value, 19f), LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 26.dp - bottomMargin = 8.dp - }) - } - - private fun latestReleaseDivider() { - val divider = LinearLayout(this).apply { - gravity = Gravity.CENTER_VERTICAL - orientation = LinearLayout.HORIZONTAL - } - divider.addView(TextView(this).apply { setBackgroundColor(ZapUi.outline) }, LinearLayout.LayoutParams(0, 1.dp, 1f)) - divider.addView(TextView(this).apply { - text = "LATEST RELEASE" - textSize = 12f - typeface = resources.getFont(R.font.inter_bold) - letterSpacing = .12f - setTextColor(ZapUi.textColor) - setPadding(16.dp, 0, 16.dp, 0) - }) - divider.addView(TextView(this).apply { setBackgroundColor(ZapUi.outline) }, LinearLayout.LayoutParams(0, 1.dp, 1f)) - content.addView(divider, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 30.dp - bottomMargin = 16.dp - }) - } - - private fun versionRow(value: ReleaseInfo): LinearLayout = LinearLayout(this).apply { - gravity = Gravity.CENTER_VERTICAL - background = ZapUi.rounded(ZapUi.surfaceVariant, 8.dp) - setPadding(12.dp, 8.dp, 12.dp, 8.dp) - addView(ZapUi.label(this@AppDetailActivity, "Version:", 14f)) - addView(TextView(this@AppDetailActivity).apply { - text = value.version - textSize = 14f - typeface = resources.getFont(R.font.inter_bold) - setTextColor(ZapUi.textColor) - }, LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { marginStart = 6.dp }) - addView(ZapUi.label(this@AppDetailActivity, "(${DateFormat.getDateInstance(DateFormat.MEDIUM).format(Date(value.event.createdAt * 1_000))})", 13f), LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - marginStart = 6.dp - }) - } - - private fun infoCard(currentApp: AppInfo): LinearLayout = LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - background = ZapUi.rounded(ZapUi.surface, 12.dp) - setPadding(16.dp, 8.dp, 16.dp, 8.dp) - infoRow("Source", currentApp.repository ?: "Not available", currentApp.repository) - currentApp.license?.let { infoRow("License", it) } - infoRow("App ID", currentApp.identifier) - infoRow("Author", "${currentApp.event.pubKey.take(16)}…") - release?.let { infoRow("Release date", DateFormat.getDateInstance(DateFormat.MEDIUM).format(Date(it.event.createdAt * 1_000))) } - } - - private fun LinearLayout.infoRow(label: String, value: String, link: String? = null) { - addView(LinearLayout(this@AppDetailActivity).apply { - gravity = Gravity.CENTER_VERTICAL - setPadding(0, 10.dp, 0, 10.dp) - addView(ZapUi.label(this@AppDetailActivity, label, 14f), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)) - addView(TextView(this@AppDetailActivity).apply { - text = value - textSize = 14f - setTextColor(if (link != null) ZapUi.primary else ZapUi.textColor) - maxLines = 1 - ellipsize = android.text.TextUtils.TruncateAt.MIDDLE - if (link != null && (link.startsWith("https://") || link.startsWith("http://"))) { - setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link))) } - } - }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)) - }) - } - - private fun body(value: String) = TextView(this).apply { - text = ZapMarkdown.parse(value) - textSize = 15f - typeface = resources.getFont(R.font.inter_regular) - setTextColor(ZapUi.muted) - setLineSpacing(5f, 1f) - movementMethod = LinkMovementMethod.getInstance() - } - - private fun addExternalLink(label: String, rawUrl: String) { - if (!rawUrl.startsWith("https://") && !rawUrl.startsWith("http://")) return - content.addView(TextView(this).apply { - text = label - textSize = 15f - setTextColor(ZapUi.primary) - setPadding(0, 10.dp, 0, 0) - setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rawUrl))) } - }) - } - - override fun onResume() { - super.onResume() - Catalog.refreshConnections() - } - - override fun onDestroy() { - scope.cancel() - super.onDestroy() - } - - private val Int.dp: Int get() = (this * resources.displayMetrics.density).toInt() - - companion object { - private const val EXTRA_IDENTIFIER = "identifier" - private const val EXTRA_AUTHOR = "author" - - fun intent(context: Context, identifier: String, author: String?): Intent = - Intent(context, AppDetailActivity::class.java) - .putExtra(EXTRA_IDENTIFIER, identifier) - .putExtra(EXTRA_AUTHOR, author) - } -} diff --git a/src/main/kotlin/dev/zapstore/app/Catalog.kt b/src/main/kotlin/dev/zapstore/app/Catalog.kt index c74711d..7379746 100644 --- a/src/main/kotlin/dev/zapstore/app/Catalog.kt +++ b/src/main/kotlin/dev/zapstore/app/Catalog.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket import dev.zapstore.purplequartz.PurpleQuartz +import dev.zapstore.purplequartz.QueryState import dev.zapstore.purplequartz.QuerySource import dev.zapstore.purplequartz.RemoteMode import kotlinx.coroutines.CoroutineScope @@ -20,39 +21,47 @@ object Catalog { const val appKind = 32_267 const val appStackKind = 30_267 const val communityPubkey = "acfeaea6e51420e8068fac446ca9d17d7a9ef6a5d20d93894e50fee3d4902a84" +} +interface CatalogRepository { + fun query(filter: Filter): Flow + + fun queryRemote(filter: Filter): Flow + + fun refreshConnections() +} + +class PurpleQuartzCatalogRepository(context: Context) : CatalogRepository { + private val applicationContext = context.applicationContext private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private var client: PurpleQuartz? = null - private fun client(context: Context): PurpleQuartz = + private fun client(): PurpleQuartz = client ?: PurpleQuartz.create( - context.applicationContext, + applicationContext, BasicOkHttpWebSocket.Builder { OkHttpClient() }, scope, ).also { client = it } - fun query(context: Context, filter: Filter): Flow = - client(context).query( + override fun query(filter: Filter): Flow = + client().query( filter = filter, source = QuerySource.LocalAndRemote( - relays = setOf(relay.normalizeRelayUrl()), + relays = setOf(Catalog.relay.normalizeRelayUrl()), mode = RemoteMode.Stream, ), ) - fun queryLocal(context: Context, filter: Filter): Flow = - client(context).query(filter, QuerySource.Local) - - fun queryRemote(context: Context, filter: Filter): Flow = - client(context).query( + override fun queryRemote(filter: Filter): Flow = + client().query( filter = filter, source = QuerySource.Remote( - relays = setOf(relay.normalizeRelayUrl()), + relays = setOf(Catalog.relay.normalizeRelayUrl()), mode = RemoteMode.OneShot(), ), ) - fun refreshConnections() { + override fun refreshConnections() { client?.refreshConnections() } } diff --git a/src/main/kotlin/dev/zapstore/app/Components.kt b/src/main/kotlin/dev/zapstore/app/Components.kt new file mode 100644 index 0000000..37196a8 --- /dev/null +++ b/src/main/kotlin/dev/zapstore/app/Components.kt @@ -0,0 +1,189 @@ +package dev.zapstore.app + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage + +@Composable +fun AppIcon( + title: String, + iconUrl: String?, + modifier: Modifier = Modifier, + cornerRadius: Dp = 16.dp, +) { + val shape = RoundedCornerShape(cornerRadius) + Box( + modifier = modifier + .clip(shape) + .background(ZapIconBackground), + contentAlignment = Alignment.Center, + ) { + Text( + text = title.take(1).uppercase(), + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.titleLarge, + ) + iconUrl + ?.takeIf(::isHttpUrl) + ?.let { url -> + AsyncImage( + model = url, + contentDescription = stringResource(R.string.app_icon_description, title), + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } + } +} + +@Composable +fun AppCard( + app: AppInfo, + onClick: () -> Unit, + modifier: Modifier = Modifier, + release: ReleaseInfo? = null, +) { + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(ZapSurface) + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 14.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + AppIcon( + title = app.name, + iconUrl = app.iconUrl, + modifier = Modifier.size(52.dp), + ) + Spacer(Modifier.width(12.dp)) + Column(Modifier.weight(1f)) { + Text( + text = app.name, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = buildString { + release?.let { + append(it.version) + it.channel?.let { channel -> append(" · ").append(channel) } + append(" · ") + } + append(app.identifier) + }, + color = ZapMuted, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + } + } + if (app.summary.isNotBlank()) { + Spacer(Modifier.height(10.dp)) + MarkdownText( + value = app.summary, + color = ZapMuted, + style = MaterialTheme.typography.bodyMedium, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +fun StackCard( + stack: StackInfo, + appsByAddress: Map, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .width(176.dp) + .clip(RoundedCornerShape(18.dp)) + .background(ZapSurface) + .clickable(onClick = onClick) + .padding(14.dp), + ) { + Text( + text = stack.name, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stack.description.ifBlank { "${stack.appAddresses.size} apps" }, + color = ZapMuted, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Spacer(Modifier.height(12.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + stack.appAddresses.take(3).forEach { address -> + val app = appsByAddress[address] + AppIcon( + title = app?.name ?: "?", + iconUrl = app?.iconUrl, + modifier = Modifier.size(34.dp), + cornerRadius = 10.dp, + ) + } + } + } +} + +@Composable +fun SectionTitle( + value: String, + modifier: Modifier = Modifier, +) { + Text( + text = value, + style = MaterialTheme.typography.titleLarge, + modifier = modifier, + ) +} + +@Composable +fun StatusText( + value: String, + modifier: Modifier = Modifier, +) { + Text( + text = value, + color = ZapMuted, + style = MaterialTheme.typography.labelMedium, + modifier = modifier, + ) +} + +fun isHttpUrl(value: String): Boolean = + value.startsWith("https://") || value.startsWith("http://") diff --git a/src/main/kotlin/dev/zapstore/app/MainActivity.kt b/src/main/kotlin/dev/zapstore/app/MainActivity.kt index dde7167..d9b9fbc 100644 --- a/src/main/kotlin/dev/zapstore/app/MainActivity.kt +++ b/src/main/kotlin/dev/zapstore/app/MainActivity.kt @@ -1,286 +1,137 @@ package dev.zapstore.app -import android.app.Activity +import android.content.Intent +import android.net.Uri import android.os.Bundle -import android.view.Gravity -import android.view.ViewGroup -import android.widget.HorizontalScrollView -import android.widget.EditText -import android.widget.FrameLayout -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.TextView -import android.view.inputmethod.EditorInfo -import android.text.Editable -import android.text.TextWatcher -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.core.net.toUri +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument -class MainActivity : Activity() { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) - private lateinit var stackList: LinearLayout - private lateinit var releaseList: LinearLayout - private lateinit var searchResults: LinearLayout - private lateinit var searchStatus: TextView - private lateinit var searchField: EditText - private lateinit var status: TextView - private var appQueryJob: Job? = null - private var stackPreviewJob: Job? = null - private var searchJob: Job? = null - private var releaseApps: Map = emptyMap() - private var stackApps: Map = emptyMap() +class MainActivity : ComponentActivity() { + private val repository: CatalogRepository + get() = (application as ZapstoreApplication).catalogRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(buildScreen()) - observeStacks() - observeLatestReleases() - } + enableEdgeToEdge() + setContent { + ZapstoreTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + ) { + val navController = rememberNavController() + val context = LocalContext.current - private fun buildScreen(): ScrollView { - val content = LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - setPadding(20.dp, 0, 20.dp, 32.dp) - } - lateinit var clearSearch: TextView - searchField = EditText(this).apply { - hint = "Search apps" - textSize = 17f - setSingleLine(true) - imeOptions = EditorInfo.IME_ACTION_SEARCH - setHintTextColor(ZapUi.muted) - setTextColor(ZapUi.textColor) - setPadding(16.dp, 0, 52.dp, 0) - background = ZapUi.rounded(ZapUi.surfaceVariant, 16.dp) - setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - search(searchField.text.toString()) - true - } else false - } - addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) = Unit - override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) { - clearSearch.visibility = if (text.isNullOrEmpty()) TextView.GONE else TextView.VISIBLE - } - override fun afterTextChanged(text: Editable?) = Unit - }) - } - clearSearch = TextView(this).apply { - text = "×" - textSize = 28f - gravity = Gravity.CENTER - setTextColor(ZapUi.muted) - visibility = TextView.GONE - setOnClickListener { - searchField.text.clear() - searchStatus.visibility = TextView.GONE - searchResults.removeAllViews() - } - contentDescription = "Clear search" - } - content.addView(FrameLayout(this).apply { - addView(searchField, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 54.dp)) - addView(clearSearch, FrameLayout.LayoutParams(48.dp, 54.dp, Gravity.END)) - }) - searchStatus = ZapUi.label(this, "", 13f).apply { - visibility = TextView.GONE - } - content.addView(searchStatus, LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 14.dp - bottomMargin = 12.dp - }) - searchResults = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL } - content.addView(searchResults) + NavHost( + navController = navController, + startDestination = HOME_ROUTE, + ) { + composable(HOME_ROUTE) { + val viewModel: HomeViewModel = viewModel( + factory = homeViewModelFactory(repository), + ) + val state by viewModel.uiState.collectAsStateWithLifecycle() + HomeScreen( + state = state, + onSearchQueryChanged = viewModel::onSearchQueryChanged, + onSearchSubmitted = viewModel::submitSearch, + onSearchCleared = viewModel::clearSearch, + onStackClick = { stackId -> + navController.navigate(stackRoute(stackId)) + }, + onAppClick = { identifier, author -> + navController.navigate(appRoute(identifier, author)) + }, + ) + } - content.addView(sectionTitle("Curated stacks"), LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 28.dp - bottomMargin = 10.dp - }) - stackList = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL } - content.addView(HorizontalScrollView(this).apply { - isHorizontalScrollBarEnabled = false - addView(stackList) - }) + composable( + route = STACK_ROUTE, + arguments = listOf( + navArgument(STACK_ID_ARGUMENT) { type = NavType.StringType }, + ), + ) { + val viewModel: StackDetailViewModel = viewModel( + factory = stackDetailViewModelFactory(repository), + ) + val state by viewModel.uiState.collectAsStateWithLifecycle() + StackDetailScreen( + state = state, + onBack = { navController.popBackStack() }, + onAppClick = { identifier, author -> + navController.navigate(appRoute(identifier, author)) + }, + ) + } - content.addView(sectionTitle("Latest releases"), LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 30.dp - bottomMargin = 6.dp - }) - status = ZapUi.label(this, "Connecting to relay.zapstore.dev", 13f) - content.addView(status, LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - bottomMargin = 14.dp - }) - releaseList = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL } - content.addView(releaseList) - return ScrollView(this).apply { - setBackgroundColor(ZapUi.background) - addView(content) - } - } - - private fun observeStacks() { - scope.launch { - Catalog.query( - this@MainActivity, - Filter(authors = listOf(Catalog.communityPubkey), kinds = listOf(Catalog.appStackKind), limit = 20), - ).collect { state -> - val stacks = state.items.map(::StackInfo).sortedByDescending { it.event.createdAt } - renderStacks(stacks, state.items.isEmpty()) - fetchStackPreviewApps(stacks) - } - } - } - - private fun observeLatestReleases() { - scope.launch { - Catalog.query( - this@MainActivity, - Filter(kinds = listOf(Catalog.releaseKind), limit = 20), - ).collect { state -> - val releases = state.items.map(::ReleaseInfo) - .sortedByDescending { it.event.createdAt } - .distinctBy { it.appIdentifier ?: it.event.id } - .take(10) - status.text = state.error?.message ?: "${releases.size} recently updated apps" - renderReleases(releases) - fetchReleaseApps(releases) - } - } - } - - private fun fetchReleaseApps(releases: List) { - val identifiers = releases.mapNotNull(ReleaseInfo::appIdentifier).toSet() - appQueryJob?.cancel() - if (identifiers.isEmpty()) return - appQueryJob = scope.launch { - Catalog.query( - this@MainActivity, - Filter(kinds = listOf(Catalog.appKind), tags = mapOf("d" to identifiers.toList()), limit = identifiers.size * 3), - ).collect { state -> - releaseApps = state.items.map(::AppInfo).associateBy(AppInfo::identifier) - renderReleases(releases) - } - } - } - - private fun fetchStackPreviewApps(stacks: List) { - val coordinates = stacks.flatMap(StackInfo::appAddresses).mapNotNull(String::toAppCoordinate).distinct() - stackPreviewJob?.cancel() - if (coordinates.isEmpty()) return - stackPreviewJob = scope.launch { - Catalog.query( - this@MainActivity, - Filter( - authors = coordinates.map(AppCoordinate::author).distinct(), - kinds = listOf(Catalog.appKind), - tags = mapOf("d" to coordinates.map(AppCoordinate::identifier).distinct()), - limit = coordinates.size * 2, - ), - ).collect { state -> - stackApps = state.items.map(::AppInfo).associateBy(AppInfo::address) - renderStacks(stacks, false) - } - } - } - - private fun renderStacks(stacks: List, loading: Boolean) { - stackList.removeAllViews() - if (stacks.isEmpty()) { - stackList.addView(ZapUi.label(this, if (loading) "Loading community stacks…" else "No stacks found.")) - return - } - stacks.forEach { stack -> - val card = LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - setPadding(14.dp, 14.dp, 14.dp, 14.dp) - background = ZapUi.rounded(ZapUi.surface, 18.dp) - isClickable = true - setOnClickListener { startActivity(StackDetailActivity.intent(this@MainActivity, stack.event.id)) } - } - card.addView(ZapUi.title(this, stack.name, 16f)) - card.addView(ZapUi.label(this, if (stack.description.isBlank()) "${stack.appAddresses.size} apps" else stack.description, 12f)) - val previews = LinearLayout(this).apply { - orientation = LinearLayout.HORIZONTAL - setPadding(0, 12.dp, 0, 0) - } - stack.appAddresses.take(3).forEach { address -> - val app = stackApps[address] - previews.addView( - ZapUi.appIcon(this, app?.name ?: "?", address, app?.iconUrl), - LinearLayout.LayoutParams(34.dp, 34.dp).apply { marginEnd = 6.dp }, - ) - } - card.addView(previews) - stackList.addView(card, LinearLayout.LayoutParams(176.dp, ViewGroup.LayoutParams.WRAP_CONTENT).apply { marginEnd = 10.dp }) - } - } - - private fun renderReleases(releases: List) { - releaseList.removeAllViews() - releases.forEach { release -> - val app = release.appIdentifier?.let(releaseApps::get) ?: AppInfo(release.event) - releaseList.addView( - ZapUi.appCard(this, app, release) { - startActivity(AppDetailActivity.intent(this, app.identifier, app.event.pubKey)) - }, - LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { bottomMargin = 12.dp }, - ) - } - } - - private fun search(rawQuery: String) { - val query = rawQuery.trim() - searchResults.removeAllViews() - searchJob?.cancel() - if (query.length < 3) { - searchStatus.visibility = TextView.GONE - return - } - searchStatus.visibility = TextView.VISIBLE - searchStatus.text = "Searching relay.zapstore.dev…" - searchJob = scope.launch { - Catalog.queryRemote( - this@MainActivity, - Filter(kinds = listOf(Catalog.appKind), search = query, limit = 20), - ).collect { state -> - val apps = state.items.map(::AppInfo).distinctBy(AppInfo::address) - searchStatus.text = state.error?.message ?: when { - apps.isEmpty() -> "No apps found for “$query”." - else -> "${apps.size} results for “$query”" - } - searchResults.removeAllViews() - apps.forEach { app -> - searchResults.addView( - ZapUi.appCard(this@MainActivity, app) { - startActivity(AppDetailActivity.intent(this@MainActivity, app.identifier, app.event.pubKey)) - }, - LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - bottomMargin = 12.dp - }, - ) + composable( + route = APP_ROUTE, + arguments = listOf( + navArgument(APP_IDENTIFIER_ARGUMENT) { type = NavType.StringType }, + navArgument(APP_AUTHOR_ARGUMENT) { + type = NavType.StringType + nullable = true + defaultValue = null + }, + ), + ) { + val viewModel: AppDetailViewModel = viewModel( + factory = appDetailViewModelFactory(repository), + ) + val state by viewModel.uiState.collectAsStateWithLifecycle() + AppDetailScreen( + state = state, + onBack = { navController.popBackStack() }, + onOpenUrl = { value -> + if (isHttpUrl(value)) { + runCatching { + context.startActivity( + Intent(Intent.ACTION_VIEW, value.toUri()), + ) + } + } + }, + ) + } + } } } } } - private fun sectionTitle(value: String): TextView = ZapUi.title(this, value, 21f) - override fun onResume() { super.onResume() - Catalog.refreshConnections() + repository.refreshConnections() + } +} + +private const val HOME_ROUTE = "home" +private const val STACK_ROUTE = "stack/{$STACK_ID_ARGUMENT}" +private const val APP_ROUTE = + "app/{$APP_IDENTIFIER_ARGUMENT}?$APP_AUTHOR_ARGUMENT={$APP_AUTHOR_ARGUMENT}" + +private fun stackRoute(stackId: String): String = + "stack/${Uri.encode(stackId)}" + +private fun appRoute(identifier: String, author: String?): String = buildString { + append("app/").append(Uri.encode(identifier)) + author?.let { + append("?").append(APP_AUTHOR_ARGUMENT).append("=").append(Uri.encode(it)) } - - override fun onDestroy() { - scope.cancel() - super.onDestroy() - } - - private val Int.dp: Int get() = (this * resources.displayMetrics.density).toInt() } diff --git a/src/main/kotlin/dev/zapstore/app/Markdown.kt b/src/main/kotlin/dev/zapstore/app/Markdown.kt index 5c71da6..15e5fb3 100644 --- a/src/main/kotlin/dev/zapstore/app/Markdown.kt +++ b/src/main/kotlin/dev/zapstore/app/Markdown.kt @@ -1,86 +1,176 @@ package dev.zapstore.app -import android.text.Html -import android.text.Spanned +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.LinkInteractionListener +import androidx.compose.ui.text.ParagraphStyle +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextIndent +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp object ZapMarkdown { - fun parse(value: String): Spanned = - Html.fromHtml(toHtml(value), Html.FROM_HTML_MODE_LEGACY) - - private fun toHtml(value: String): String { - val lines = value.lines() - val html = StringBuilder() - var inUnorderedList = false - var inOrderedList = false - - fun closeLists() { - if (inUnorderedList) { - html.append("") - inUnorderedList = false - } - if (inOrderedList) { - html.append("") - inOrderedList = false - } - } - - lines.forEach { line -> - val trimmed = line.trim() - val unordered = trimmed.matches(Regex("^[-*+]\\s+.+")) - val ordered = trimmed.matches(Regex("^\\d+[.)]\\s+.+")) + private val headingPattern = Regex("^(#{1,6})\\s+(.+)$") + private val unorderedListPattern = Regex("^[-*+]\\s+(.+)$") + private val orderedListPattern = Regex("^(\\d+)[.)]\\s+(.+)$") + private val inlinePattern = Regex( + """\[([^\]]+)]\(([^)\s]+)(?:\s+"[^"]*")?\)|`([^`]+)`|(\*\*|__)(.+?)\4|(? Unit)? = null, + ): AnnotatedString = buildAnnotatedString { + value.lines().forEachIndexed { index, rawLine -> + val line = rawLine.trim() when { - trimmed.isEmpty() -> { - closeLists() - html.append("
") - } + line.isEmpty() -> Unit - unordered -> { - if (!inUnorderedList) { - closeLists() - html.append("
    ") - inUnorderedList = true - } - html.append("
  • ").append(inline(trimmed.replaceFirst(Regex("^[-*+]\\s+"), ""))).append("
  • ") - } - - ordered -> { - if (!inOrderedList) { - closeLists() - html.append("
      ") - inOrderedList = true - } - html.append("
    1. ").append(inline(trimmed.replaceFirst(Regex("^\\d+[.)]\\s+"), ""))).append("
    2. ") - } - - else -> { - closeLists() - val heading = Regex("^(#{1,6})\\s+(.+)$").matchEntire(trimmed) - if (heading != null) { - val level = heading.groupValues[1].length - html.append("") - .append(inline(heading.groupValues[2])) - .append("") - } else { - html.append(inline(trimmed)).append("
      ") + headingPattern.matches(line) -> { + val match = requireNotNull(headingPattern.matchEntire(line)) + val level = match.groupValues[1].length + withStyle( + SpanStyle( + fontFamily = InterDisplayFontFamily, + fontWeight = FontWeight.ExtraBold, + fontSize = headingSize(level), + ), + ) { + appendInline(match.groupValues[2], onOpenUrl) } } + + unorderedListPattern.matches(line) -> { + val content = requireNotNull(unorderedListPattern.matchEntire(line)).groupValues[1] + withStyle(ParagraphStyle(textIndent = TextIndent(restLine = 16.sp))) { + append("• ") + appendInline(content, onOpenUrl) + } + } + + orderedListPattern.matches(line) -> { + val match = requireNotNull(orderedListPattern.matchEntire(line)) + withStyle(ParagraphStyle(textIndent = TextIndent(restLine = 20.sp))) { + append(match.groupValues[1]) + append(". ") + appendInline(match.groupValues[2], onOpenUrl) + } + } + + else -> appendInline(line, onOpenUrl) } + + if (index != value.lines().lastIndex) append("\n") } - closeLists() - return html.toString() } - private fun inline(value: String): String { - var result = Html.escapeHtml(value) - result = result.replace(Regex("`([^`]+)`")) { "${it.groupValues[1]}" } - result = result.replace(Regex("""\[([^\]]+)]\(([^)\s]+)(?:\s+"[^"]*")?\)""")) { - val label = it.groupValues[1] - val url = it.groupValues[2] - """$label""" + private fun AnnotatedString.Builder.appendInline( + value: String, + onOpenUrl: ((String) -> Unit)?, + ) { + var cursor = 0 + inlinePattern.findAll(value).forEach { match -> + append(value.substring(cursor, match.range.first)) + when { + match.groupValues[1].isNotEmpty() -> { + val label = match.groupValues[1] + val url = match.groupValues[2] + if (isHttpUrl(url)) { + withLink( + LinkAnnotation.Url( + url = url, + styles = TextLinkStyles( + style = SpanStyle( + color = ZapPrimary, + textDecoration = TextDecoration.Underline, + ), + ), + linkInteractionListener = onOpenUrl?.let { callback -> + LinkInteractionListener { link -> + (link as? LinkAnnotation.Url) + ?.url + ?.takeIf(::isHttpUrl) + ?.let(callback) + } + }, + ), + ) { + append(label) + } + } else { + append(label) + } + } + + match.groupValues[3].isNotEmpty() -> { + withStyle( + SpanStyle( + fontFamily = FontFamily.Monospace, + background = ZapSurfaceVariant, + ), + ) { + append(match.groupValues[3]) + } + } + + match.groupValues[4].isNotEmpty() -> { + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(match.groupValues[5]) + } + } + + match.groupValues[6].isNotEmpty() -> { + withStyle(SpanStyle(fontStyle = androidx.compose.ui.text.font.FontStyle.Italic)) { + append(match.groupValues[7]) + } + } + } + cursor = match.range.last + 1 } - result = result.replace(Regex("""(\*\*|__)(.+?)\1""")) { "${it.groupValues[2]}" } - result = result.replace(Regex("""(?${it.groupValues[2]}" } - return result + append(value.substring(cursor)) + } + + private fun headingSize(level: Int): TextUnit = when (level) { + 1 -> 27.sp + 2 -> 24.sp + 3 -> 21.sp + 4 -> 18.sp + 5 -> 16.sp + else -> 14.sp } } + +@Composable +fun MarkdownText( + value: String, + color: Color = MaterialTheme.colorScheme.onSurface, + style: TextStyle = MaterialTheme.typography.bodyMedium, + maxLines: Int = Int.MAX_VALUE, + overflow: TextOverflow = TextOverflow.Clip, + onOpenUrl: ((String) -> Unit)? = null, +) { + val annotated = remember(value, onOpenUrl) { + ZapMarkdown.parse(value, onOpenUrl) + } + Text( + text = annotated, + color = color, + style = style, + maxLines = maxLines, + overflow = overflow, + ) +} diff --git a/src/main/kotlin/dev/zapstore/app/Screens.kt b/src/main/kotlin/dev/zapstore/app/Screens.kt new file mode 100644 index 0000000..d39ba4a --- /dev/null +++ b/src/main/kotlin/dev/zapstore/app/Screens.kt @@ -0,0 +1,503 @@ +package dev.zapstore.app + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import java.text.DateFormat +import java.util.Date + +@Composable +fun HomeScreen( + state: HomeUiState, + onSearchQueryChanged: (String) -> Unit, + onSearchSubmitted: () -> Unit, + onSearchCleared: () -> Unit, + onStackClick: (String) -> Unit, + onAppClick: (identifier: String, author: String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(ZapBackground) + .windowInsetsPadding(WindowInsets.safeDrawing), + contentPadding = PaddingValues(start = 20.dp, top = 8.dp, end = 20.dp, bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + OutlinedTextField( + value = state.searchQuery, + onValueChange = onSearchQueryChanged, + placeholder = { Text(stringResource(R.string.search_apps)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { onSearchSubmitted() }), + trailingIcon = { + if (state.searchQuery.isNotEmpty()) { + val clearDescription = stringResource(R.string.clear_search) + TextButton( + onClick = onSearchCleared, + modifier = Modifier.semantics { + contentDescription = clearDescription + }, + ) { + Text( + text = "×", + style = MaterialTheme.typography.titleLarge, + ) + } + } + }, + modifier = Modifier + .fillMaxWidth() + .testTag("searchField"), + shape = RoundedCornerShape(16.dp), + ) + } + + state.searchMessage?.let { message -> + item { StatusText(message, Modifier.testTag("searchStatus")) } + } + items(state.searchResults, key = { it.address }) { app -> + AppCard( + app = app, + onClick = { onAppClick(app.identifier, app.event.pubKey) }, + modifier = Modifier.testTag("searchResult:${app.address}"), + ) + } + + item { + SectionTitle( + value = stringResource(R.string.curated_stacks), + modifier = Modifier.padding(top = 16.dp), + ) + } + item { + when { + state.stacks.isNotEmpty() -> { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(end = 8.dp), + ) { + items(state.stacks, key = { it.event.id }) { stack -> + StackCard( + stack = stack, + appsByAddress = state.stackApps, + onClick = { onStackClick(stack.event.id) }, + modifier = Modifier.testTag("stack:${stack.event.id}"), + ) + } + } + } + + state.stacksLoading -> StatusText(stringResource(R.string.loading_stacks)) + state.stacksError != null -> StatusText(state.stacksError) + else -> StatusText(stringResource(R.string.no_stacks)) + } + } + + item { + SectionTitle( + value = stringResource(R.string.latest_releases), + modifier = Modifier.padding(top = 8.dp), + ) + } + item { StatusText(state.releasesMessage) } + items(state.releases, key = { it.event.id }) { release -> + val app = release.appIdentifier?.let(state.releaseApps::get) ?: AppInfo(release.event) + AppCard( + app = app, + release = release, + onClick = { onAppClick(app.identifier, app.event.pubKey) }, + modifier = Modifier.testTag("release:${release.event.id}"), + ) + } + } +} + +@Composable +fun StackDetailScreen( + state: StackDetailUiState, + onBack: () -> Unit, + onAppClick: (identifier: String, author: String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(ZapBackground) + .windowInsetsPadding(WindowInsets.safeDrawing), + contentPadding = PaddingValues(start = 20.dp, top = 4.dp, end = 20.dp, bottom = 36.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { BackButton(onBack) } + + val stack = state.stack + if (stack == null) { + item { + Text( + text = when { + state.stackLoading -> stringResource(R.string.loading_stack) + else -> stringResource(R.string.stack_not_found) + }, + style = MaterialTheme.typography.headlineSmall, + ) + } + state.error?.let { error -> item { StatusText(error) } } + return@LazyColumn + } + + item { + Text( + text = stack.name, + style = MaterialTheme.typography.displaySmall, + ) + } + item { + StatusText( + stack.description.ifBlank { + pluralStringResource( + R.plurals.curated_apps, + stack.appAddresses.size, + stack.appAddresses.size, + ) + }, + ) + } + item { + SectionTitle( + value = stringResource(R.string.apps), + modifier = Modifier.padding(top = 12.dp), + ) + } + items( + items = stack.appAddresses.mapNotNull(state.appsByAddress::get), + key = { it.address }, + ) { app -> + AppCard( + app = app, + onClick = { onAppClick(app.identifier, app.event.pubKey) }, + modifier = Modifier.testTag("stackApp:${app.address}"), + ) + } + if (state.appsByAddress.isEmpty()) { + item { + StatusText( + if (state.appsLoading) { + stringResource(R.string.loading_stack_apps) + } else { + stringResource(R.string.no_stack_apps) + }, + ) + } + } + state.error?.let { error -> item { StatusText(error) } } + } +} + +@Composable +fun AppDetailScreen( + state: AppDetailUiState, + onBack: () -> Unit, + onOpenUrl: (String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(ZapBackground) + .windowInsetsPadding(WindowInsets.safeDrawing), + contentPadding = PaddingValues(start = 20.dp, top = 4.dp, end = 20.dp, bottom = 36.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { BackButton(onBack) } + + val app = state.app + if (app == null) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + .clip(RoundedCornerShape(16.dp)) + .background(ZapSurfaceVariant), + contentAlignment = Alignment.Center, + ) { + Text( + text = if (state.appLoading) { + stringResource(R.string.loading_app) + } else { + stringResource(R.string.app_not_found) + }, + color = ZapMuted, + ) + } + } + state.error?.let { error -> item { StatusText(error) } } + return@LazyColumn + } + + item { + Row(verticalAlignment = Alignment.Top) { + AppIcon( + title = app.name, + iconUrl = app.iconUrl, + modifier = Modifier.size(84.dp), + ) + Spacer(Modifier.width(16.dp)) + Column(Modifier.weight(1f)) { + Text( + text = app.name, + style = MaterialTheme.typography.displaySmall, + ) + StatusText(app.identifier) + state.release?.let { release -> + Spacer(Modifier.height(10.dp)) + Text( + text = buildString { + append(release.version) + release.channel?.let { append(" · ").append(it) } + }, + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(ZapPrimary) + .padding(horizontal = 10.dp, vertical = 4.dp), + ) + } + } + } + } + item { + StatusText( + stringResource(R.string.published_by, app.event.pubKey.take(16)), + ) + } + + if (app.screenshots.isNotEmpty()) { + item { + LazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + items(app.screenshots.take(5), key = { it }) { url -> + AsyncImage( + model = url, + contentDescription = stringResource(R.string.screenshot_description, app.name), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(176.dp) + .clip(RoundedCornerShape(16.dp)) + .background(ZapIconBackground), + ) + } + } + } + } + + val description = app.event.content.ifBlank { app.summary } + if (description.isNotBlank()) { + item { + MarkdownText( + value = description, + color = ZapMuted, + style = MaterialTheme.typography.bodyLarge, + onOpenUrl = onOpenUrl, + ) + } + } + + item { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 8.dp), + ) { + HorizontalDivider(Modifier.weight(1f), color = ZapOutline) + Text( + text = stringResource(R.string.latest_release).uppercase(), + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 16.dp), + ) + HorizontalDivider(Modifier.weight(1f), color = ZapOutline) + } + } + + state.release?.let { release -> + item { VersionRow(release) } + if (release.notes.isNotBlank()) { + item { + MarkdownText( + value = release.notes, + color = ZapMuted, + style = MaterialTheme.typography.bodyLarge, + onOpenUrl = onOpenUrl, + ) + } + } + } ?: item { + Box( + Modifier + .width(220.dp) + .height(32.dp) + .clip(RoundedCornerShape(8.dp)) + .background(ZapSurfaceVariant), + ) + } + + item { + AppInfoCard( + app = app, + release = state.release, + onOpenUrl = onOpenUrl, + modifier = Modifier.padding(top = 8.dp), + ) + } + state.error?.let { error -> item { StatusText(error) } } + } +} + +@Composable +private fun BackButton(onClick: () -> Unit) { + TextButton( + onClick = onClick, + modifier = Modifier.testTag("backButton"), + ) { + Text("‹ ${stringResource(R.string.back)}") + } +} + +@Composable +private fun VersionRow(release: ReleaseInfo) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(ZapSurfaceVariant) + .padding(horizontal = 12.dp, vertical = 8.dp), + ) { + StatusText(stringResource(R.string.version)) + Spacer(Modifier.width(6.dp)) + Text( + text = release.version, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + ) + Spacer(Modifier.width(6.dp)) + StatusText("(${formatDate(release.event.createdAt)})") + } +} + +@Composable +private fun AppInfoCard( + app: AppInfo, + release: ReleaseInfo?, + onOpenUrl: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(ZapSurface) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + InfoRow( + label = stringResource(R.string.source), + value = app.repository ?: stringResource(R.string.not_available), + link = app.repository, + onOpenUrl = onOpenUrl, + ) + app.license?.let { + InfoRow(stringResource(R.string.license), it, onOpenUrl = onOpenUrl) + } + InfoRow(stringResource(R.string.app_id), app.identifier, onOpenUrl = onOpenUrl) + InfoRow( + stringResource(R.string.author), + "${app.event.pubKey.take(16)}…", + onOpenUrl = onOpenUrl, + ) + release?.let { + InfoRow( + stringResource(R.string.release_date), + formatDate(it.event.createdAt), + onOpenUrl = onOpenUrl, + ) + } + } +} + +@Composable +private fun InfoRow( + label: String, + value: String, + link: String? = null, + onOpenUrl: (String) -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 10.dp), + ) { + StatusText(label, Modifier.weight(1f)) + Text( + text = value, + color = if (link?.let(::isHttpUrl) == true) ZapPrimary else ZapText, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + modifier = Modifier + .weight(1f) + .then( + if (link?.let(::isHttpUrl) == true) { + Modifier.clickable { onOpenUrl(link) } + } else { + Modifier + }, + ), + ) + } +} + +private fun formatDate(createdAt: Long): String = + DateFormat.getDateInstance(DateFormat.MEDIUM).format(Date(createdAt * 1_000)) diff --git a/src/main/kotlin/dev/zapstore/app/SearchActivity.kt b/src/main/kotlin/dev/zapstore/app/SearchActivity.kt deleted file mode 100644 index b294b59..0000000 --- a/src/main/kotlin/dev/zapstore/app/SearchActivity.kt +++ /dev/null @@ -1,131 +0,0 @@ -package dev.zapstore.app - -import android.app.Activity -import android.os.Bundle -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.text.Editable -import android.text.TextWatcher -import android.widget.EditText -import android.widget.FrameLayout -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.TextView -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch - -class SearchActivity : Activity() { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) - private lateinit var searchField: EditText - private lateinit var status: TextView - private lateinit var results: LinearLayout - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(buildScreen()) - } - - private fun buildScreen(): LinearLayout = LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - setPadding(20.dp, 0, 20.dp, 28.dp) - setBackgroundColor(ZapUi.background) - addView(ZapUi.title(this@SearchActivity, "Search apps", 24f)) - lateinit var clearSearch: TextView - searchField = EditText(this@SearchActivity).apply { - hint = "Search apps" - textSize = 17f - setSingleLine(true) - imeOptions = EditorInfo.IME_ACTION_SEARCH - setHintTextColor(ZapUi.muted) - setTextColor(ZapUi.textColor) - setPadding(16.dp, 0, 52.dp, 0) - background = ZapUi.rounded(ZapUi.surfaceVariant, 16.dp) - setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - search(searchField.text.toString()) - true - } else false - } - addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) = Unit - override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) { - clearSearch.visibility = if (text.isNullOrEmpty()) TextView.GONE else TextView.VISIBLE - } - override fun afterTextChanged(text: Editable?) = Unit - }) - } - clearSearch = TextView(this@SearchActivity).apply { - text = "×" - textSize = 28f - gravity = android.view.Gravity.CENTER - setTextColor(ZapUi.muted) - visibility = TextView.GONE - setOnClickListener { - searchField.text.clear() - status.visibility = TextView.GONE - results.removeAllViews() - } - contentDescription = "Clear search" - } - addView(FrameLayout(this@SearchActivity).apply { - addView(searchField, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 54.dp)) - addView(clearSearch, FrameLayout.LayoutParams(48.dp, 54.dp, android.view.Gravity.END)) - }, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 54.dp)) - status = ZapUi.label(this@SearchActivity, "", 13f).apply { - visibility = TextView.GONE - } - addView(status, LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 14.dp - bottomMargin = 12.dp - }) - results = LinearLayout(this@SearchActivity).apply { orientation = LinearLayout.VERTICAL } - addView(ScrollView(this@SearchActivity).apply { addView(results) }, LinearLayout.LayoutParams(0, 0, 1f)) - } - - private fun search(rawQuery: String) { - val query = rawQuery.trim() - results.removeAllViews() - if (query.length < 3) { - status.visibility = TextView.GONE - return - } - status.visibility = TextView.VISIBLE - status.text = "Searching relay.zapstore.dev…" - scope.launch { - Catalog.queryRemote(this@SearchActivity, Filter(kinds = listOf(Catalog.appKind), search = query, limit = 20)).collect { state -> - val apps = state.items.map(::AppInfo).distinctBy(AppInfo::address) - status.text = state.error?.message ?: when { - apps.isEmpty() -> "No apps found for “$query”." - else -> "${apps.size} results for “$query”" - } - results.removeAllViews() - apps.forEach { app -> - results.addView( - ZapUi.appCard(this@SearchActivity, app) { - startActivity(AppDetailActivity.intent(this@SearchActivity, app.identifier, app.event.pubKey)) - }, - LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - bottomMargin = 12.dp - }, - ) - } - } - } - } - - override fun onResume() { - super.onResume() - Catalog.refreshConnections() - } - - override fun onDestroy() { - scope.cancel() - super.onDestroy() - } - - private val Int.dp: Int get() = (this * resources.displayMetrics.density).toInt() -} diff --git a/src/main/kotlin/dev/zapstore/app/StackDetailActivity.kt b/src/main/kotlin/dev/zapstore/app/StackDetailActivity.kt deleted file mode 100644 index 869c4e4..0000000 --- a/src/main/kotlin/dev/zapstore/app/StackDetailActivity.kt +++ /dev/null @@ -1,123 +0,0 @@ -package dev.zapstore.app - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.Gravity -import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.TextView -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch - -class StackDetailActivity : Activity() { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) - private lateinit var content: LinearLayout - private var stack: StackInfo? = null - private var appsByAddress: Map = emptyMap() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val stackId = intent.getStringExtra(EXTRA_STACK_ID) ?: run { - finish() - return - } - content = LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - setPadding(20.dp, 0, 20.dp, 36.dp) - } - setContentView(ScrollView(this).apply { - setBackgroundColor(ZapUi.background) - addView(content) - }) - render() - observeStack(stackId) - } - - private fun observeStack(stackId: String) { - scope.launch { - Catalog.query( - this@StackDetailActivity, - Filter(ids = listOf(stackId), kinds = listOf(Catalog.appStackKind), limit = 1), - ).collect { state -> - stack = state.items.firstOrNull()?.let(::StackInfo) - render(state.error?.message) - stack?.let(::observeApps) - } - } - } - - private fun observeApps(currentStack: StackInfo) { - val coordinates = currentStack.appAddresses.mapNotNull(String::toAppCoordinate) - if (coordinates.isEmpty()) return - scope.launch { - Catalog.query( - this@StackDetailActivity, - Filter( - authors = coordinates.map(AppCoordinate::author).distinct(), - kinds = listOf(Catalog.appKind), - tags = mapOf("d" to coordinates.map(AppCoordinate::identifier).distinct()), - limit = coordinates.size * 2, - ), - ).collect { state -> - appsByAddress = state.items.map(::AppInfo).associateBy(AppInfo::address) - render(state.error?.message) - } - } - } - - private fun render(error: String? = null) { - content.removeAllViews() - val currentStack = stack - if (currentStack == null) { - content.addView(ZapUi.title(this, "Loading stack…", 24f), LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 20.dp - }) - error?.let { content.addView(ZapUi.label(this, it)) } - return - } - content.addView(ZapUi.title(this, currentStack.name, 27f), LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 14.dp - }) - content.addView(ZapUi.label(this, currentStack.description.ifBlank { "${currentStack.appAddresses.size} curated apps" }, 14f)) - content.addView(ZapUi.title(this, "Apps", 20f), LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - topMargin = 28.dp - bottomMargin = 10.dp - }) - currentStack.appAddresses.forEach { address -> - val app = appsByAddress[address] ?: return@forEach - content.addView( - ZapUi.appCard(this, app) { - startActivity(AppDetailActivity.intent(this, app.identifier, app.event.pubKey)) - }, - LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { bottomMargin = 12.dp }, - ) - } - if (appsByAddress.isEmpty()) content.addView(ZapUi.label(this, "Loading apps in this stack…")) - } - - override fun onResume() { - super.onResume() - Catalog.refreshConnections() - } - - override fun onDestroy() { - scope.cancel() - super.onDestroy() - } - - private val Int.dp: Int get() = (this * resources.displayMetrics.density).toInt() - - companion object { - private const val EXTRA_STACK_ID = "stack_id" - - fun intent(context: Context, stackId: String): Intent = - Intent(context, StackDetailActivity::class.java).putExtra(EXTRA_STACK_ID, stackId) - } -} diff --git a/src/main/kotlin/dev/zapstore/app/Theme.kt b/src/main/kotlin/dev/zapstore/app/Theme.kt new file mode 100644 index 0000000..e9c5956 --- /dev/null +++ b/src/main/kotlin/dev/zapstore/app/Theme.kt @@ -0,0 +1,93 @@ +package dev.zapstore.app + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val ZapBackground = Color(0xFF0A0F1A) +val ZapSurface = Color(0xFF0F141B) +val ZapSurfaceVariant = Color(0xFF161C27) +val ZapOutline = Color(0xFF2D3748) +val ZapPrimary = Color(0xFF3A6FCC) +val ZapText = Color(0xFFE8EAED) +val ZapMuted = Color(0xFFB8BCC8) +val ZapIconBackground = Color(0xFF2D2D2D) + +val InterFontFamily = FontFamily( + Font(R.font.inter_regular, FontWeight.Normal), + Font(R.font.inter_bold, FontWeight.Bold), +) + +val InterDisplayFontFamily = FontFamily( + Font(R.font.inter_display_extra_bold, FontWeight.ExtraBold), +) + +private val ZapColorScheme = darkColorScheme( + primary = ZapPrimary, + onPrimary = Color.White, + background = ZapBackground, + onBackground = ZapText, + surface = ZapSurface, + onSurface = ZapText, + surfaceVariant = ZapSurfaceVariant, + onSurfaceVariant = ZapMuted, + outline = ZapOutline, +) + +private val ZapTypography = Typography( + displaySmall = Typography().displaySmall.copy( + fontFamily = InterDisplayFontFamily, + fontWeight = FontWeight.ExtraBold, + fontSize = 27.sp, + ), + headlineSmall = Typography().headlineSmall.copy( + fontFamily = InterDisplayFontFamily, + fontWeight = FontWeight.ExtraBold, + fontSize = 24.sp, + ), + titleLarge = Typography().titleLarge.copy( + fontFamily = InterDisplayFontFamily, + fontWeight = FontWeight.ExtraBold, + fontSize = 21.sp, + ), + titleMedium = Typography().titleMedium.copy( + fontFamily = InterDisplayFontFamily, + fontWeight = FontWeight.ExtraBold, + fontSize = 17.sp, + ), + bodyLarge = Typography().bodyLarge.copy( + fontFamily = InterFontFamily, + fontSize = 15.sp, + ), + bodyMedium = Typography().bodyMedium.copy( + fontFamily = InterFontFamily, + fontSize = 14.sp, + ), + bodySmall = Typography().bodySmall.copy( + fontFamily = InterFontFamily, + fontSize = 12.sp, + ), + labelLarge = Typography().labelLarge.copy( + fontFamily = InterFontFamily, + fontWeight = FontWeight.Bold, + ), + labelMedium = Typography().labelMedium.copy( + fontFamily = InterFontFamily, + fontSize = 13.sp, + ), +) + +@Composable +fun ZapstoreTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = ZapColorScheme, + typography = ZapTypography, + content = content, + ) +} diff --git a/src/main/kotlin/dev/zapstore/app/Ui.kt b/src/main/kotlin/dev/zapstore/app/Ui.kt deleted file mode 100644 index 937f63a..0000000 --- a/src/main/kotlin/dev/zapstore/app/Ui.kt +++ /dev/null @@ -1,127 +0,0 @@ -package dev.zapstore.app - -import android.content.Context -import android.graphics.Color -import android.graphics.Typeface -import android.graphics.drawable.GradientDrawable -import android.text.TextUtils -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import coil3.load -import coil3.request.crossfade - -object ZapUi { - val background = Color.rgb(10, 15, 26) - val surface = Color.rgb(15, 20, 27) - val surfaceVariant = Color.rgb(22, 28, 39) - val outline = Color.rgb(45, 55, 72) - val primary = Color.rgb(58, 111, 204) - val textColor = Color.rgb(232, 234, 237) - val muted = Color.rgb(184, 188, 200) - private val iconBackground = Color.rgb(45, 45, 45) - - fun title(context: Context, value: String, size: Float = 24f) = TextView(context).apply { - text = value - textSize = size - typeface = Typeface.create(context.resources.getFont(R.font.inter_display_extra_bold), Typeface.BOLD) - setTextColor(textColor) - } - - fun label(context: Context, value: String, size: Float = 14f) = TextView(context).apply { - text = value - textSize = size - typeface = context.resources.getFont(R.font.inter_regular) - setTextColor(muted) - } - - fun appCard( - context: Context, - app: AppInfo, - release: ReleaseInfo? = null, - onClick: () -> Unit, - ): LinearLayout = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - setPadding(16.dp(context), 14.dp(context), 16.dp(context), 16.dp(context)) - background = rounded(surface, 18.dp(context)) - isClickable = true - isFocusable = true - setOnClickListener { onClick() } - - addView(LinearLayout(context).apply { - gravity = Gravity.CENTER_VERTICAL - addView(appIcon(context, app.name, app.identifier, app.iconUrl), LinearLayout.LayoutParams(52.dp(context), 52.dp(context))) - addView(LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - addView(TextView(context).apply { - text = app.name - textSize = 17f - typeface = Typeface.create(context.resources.getFont(R.font.inter_display_extra_bold), Typeface.BOLD) - setTextColor(textColor) - maxLines = 1 - ellipsize = TextUtils.TruncateAt.END - }) - addView(label(context, buildString { - release?.let { - append(it.version) - it.channel?.let { channel -> append(" · ").append(channel) } - append(" · ") - } - append(app.identifier) - }, 12f)) - }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { - marginStart = 12.dp(context) - }) - }) - if (app.summary.isNotBlank()) { - addView(TextView(context).apply { - text = ZapMarkdown.parse(app.summary) - textSize = 14f - typeface = context.resources.getFont(R.font.inter_regular) - setTextColor(muted) - maxLines = 3 - ellipsize = TextUtils.TruncateAt.END - setPadding(0, 10.dp(context), 0, 0) - }) - } - } - - fun appIcon(context: Context, title: String, identity: String, iconUrl: String?): FrameLayout = - FrameLayout(context).apply { - addView(TextView(context).apply { - text = title.take(1).uppercase() - textSize = 20f - gravity = Gravity.CENTER - typeface = context.resources.getFont(R.font.inter_bold) - setTextColor(Color.WHITE) - background = rounded(iconBackground, 16.dp(context)) - }, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) - iconUrl?.takeIf { it.startsWith("https://") || it.startsWith("http://") }?.let { url -> - val image = ImageView(context).apply { - scaleType = ImageView.ScaleType.CENTER_CROP - contentDescription = "$title icon" - clipToOutline = true - background = rounded(iconBackground, 16.dp(context)) - } - addView(image, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) - image.load(url) { - crossfade(250) - } - } - } - - fun rounded(color: Int, radius: Int) = GradientDrawable().apply { - setColor(color) - cornerRadius = radius.toFloat() - } - - fun View.setVerticalMargin(bottom: Int) { - layoutParams = (layoutParams as? ViewGroup.MarginLayoutParams)?.apply { bottomMargin = bottom } ?: layoutParams - } - - private fun Int.dp(context: Context): Int = (this * context.resources.displayMetrics.density).toInt() -} diff --git a/src/main/kotlin/dev/zapstore/app/ViewModels.kt b/src/main/kotlin/dev/zapstore/app/ViewModels.kt new file mode 100644 index 0000000..ab60220 --- /dev/null +++ b/src/main/kotlin/dev/zapstore/app/ViewModels.kt @@ -0,0 +1,374 @@ +package dev.zapstore.app + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import dev.zapstore.purplequartz.QuerySync +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +const val STACK_ID_ARGUMENT = "stackId" +const val APP_IDENTIFIER_ARGUMENT = "identifier" +const val APP_AUTHOR_ARGUMENT = "author" + +data class HomeUiState( + val searchQuery: String = "", + val submittedSearchQuery: String? = null, + val searchResults: List = emptyList(), + val searchMessage: String? = null, + val isSearching: Boolean = false, + val stacks: List = emptyList(), + val stackApps: Map = emptyMap(), + val stacksLoading: Boolean = true, + val stacksError: String? = null, + val releases: List = emptyList(), + val releaseApps: Map = emptyMap(), + val releasesLoading: Boolean = true, + val releasesMessage: String = "Connecting to relay.zapstore.dev", +) + +class HomeViewModel( + private val repository: CatalogRepository, + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val _uiState = MutableStateFlow( + HomeUiState(searchQuery = savedStateHandle[SEARCH_QUERY_KEY] ?: ""), + ) + val uiState: StateFlow = _uiState.asStateFlow() + + private var stackPreviewJob: Job? = null + private var releaseAppsJob: Job? = null + private var searchJob: Job? = null + private var stackAddresses: List = emptyList() + private var releaseIdentifiers: Set = emptySet() + + init { + observeStacks() + observeLatestReleases() + } + + fun onSearchQueryChanged(value: String) { + savedStateHandle[SEARCH_QUERY_KEY] = value + _uiState.update { it.copy(searchQuery = value) } + } + + fun clearSearch() { + searchJob?.cancel() + savedStateHandle[SEARCH_QUERY_KEY] = "" + _uiState.update { + it.copy( + searchQuery = "", + submittedSearchQuery = null, + searchResults = emptyList(), + searchMessage = null, + isSearching = false, + ) + } + } + + fun submitSearch() { + val query = _uiState.value.searchQuery.trim() + searchJob?.cancel() + if (query.length < MIN_SEARCH_LENGTH) { + _uiState.update { + it.copy( + submittedSearchQuery = null, + searchResults = emptyList(), + searchMessage = null, + isSearching = false, + ) + } + return + } + + _uiState.update { + it.copy( + submittedSearchQuery = query, + searchResults = emptyList(), + searchMessage = "Searching relay.zapstore.dev…", + isSearching = true, + ) + } + searchJob = viewModelScope.launch { + repository.queryRemote( + Filter(kinds = listOf(Catalog.appKind), search = query, limit = 20), + ).collect { state -> + val apps = state.items.map(::AppInfo).distinctBy(AppInfo::address) + _uiState.update { + it.copy( + submittedSearchQuery = query, + searchResults = apps, + searchMessage = state.error?.message ?: when { + apps.isEmpty() && state.sync.isLoading -> "Searching relay.zapstore.dev…" + apps.isEmpty() -> "No apps found for “$query”." + else -> "${apps.size} results for “$query”" + }, + isSearching = apps.isEmpty() && state.sync.isLoading, + ) + } + } + } + } + + private fun observeStacks() { + viewModelScope.launch { + repository.query( + Filter( + authors = listOf(Catalog.communityPubkey), + kinds = listOf(Catalog.appStackKind), + limit = 20, + ), + ).collect { state -> + val stacks = state.items.map(::StackInfo).sortedByDescending { it.event.createdAt } + _uiState.update { + it.copy( + stacks = stacks, + stacksLoading = stacks.isEmpty() && state.sync.isLoading, + stacksError = state.error?.message, + ) + } + observeStackPreviews(stacks) + } + } + } + + private fun observeStackPreviews(stacks: List) { + val addresses = stacks.flatMap(StackInfo::appAddresses).distinct() + if (addresses == stackAddresses) return + stackAddresses = addresses + stackPreviewJob?.cancel() + + val coordinates = addresses.mapNotNull(String::toAppCoordinate) + if (coordinates.isEmpty()) { + _uiState.update { it.copy(stackApps = emptyMap()) } + return + } + + stackPreviewJob = viewModelScope.launch { + repository.query( + Filter( + authors = coordinates.map(AppCoordinate::author).distinct(), + kinds = listOf(Catalog.appKind), + tags = mapOf("d" to coordinates.map(AppCoordinate::identifier).distinct()), + limit = coordinates.size * 2, + ), + ).collect { state -> + _uiState.update { + it.copy(stackApps = state.items.map(::AppInfo).associateBy(AppInfo::address)) + } + } + } + } + + private fun observeLatestReleases() { + viewModelScope.launch { + repository.query( + Filter(kinds = listOf(Catalog.releaseKind), limit = 20), + ).collect { state -> + val releases = state.items.map(::ReleaseInfo) + .sortedByDescending { it.event.createdAt } + .distinctBy { it.appIdentifier ?: it.event.id } + .take(10) + _uiState.update { + it.copy( + releases = releases, + releasesLoading = releases.isEmpty() && state.sync.isLoading, + releasesMessage = state.error?.message ?: "${releases.size} recently updated apps", + ) + } + observeReleaseApps(releases) + } + } + } + + private fun observeReleaseApps(releases: List) { + val identifiers = releases.mapNotNull(ReleaseInfo::appIdentifier).toSet() + if (identifiers == releaseIdentifiers) return + releaseIdentifiers = identifiers + releaseAppsJob?.cancel() + if (identifiers.isEmpty()) { + _uiState.update { it.copy(releaseApps = emptyMap()) } + return + } + + releaseAppsJob = viewModelScope.launch { + repository.query( + Filter( + kinds = listOf(Catalog.appKind), + tags = mapOf("d" to identifiers.toList()), + limit = identifiers.size * 3, + ), + ).collect { state -> + _uiState.update { + it.copy(releaseApps = state.items.map(::AppInfo).associateBy(AppInfo::identifier)) + } + } + } + } + + private companion object { + const val SEARCH_QUERY_KEY = "searchQuery" + const val MIN_SEARCH_LENGTH = 3 + } +} + +data class StackDetailUiState( + val stack: StackInfo? = null, + val appsByAddress: Map = emptyMap(), + val stackLoading: Boolean = true, + val appsLoading: Boolean = false, + val error: String? = null, +) + +class StackDetailViewModel( + private val repository: CatalogRepository, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val stackId = requireNotNull(savedStateHandle.get(STACK_ID_ARGUMENT)) + private val _uiState = MutableStateFlow(StackDetailUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var appsJob: Job? = null + private var appAddresses: List = emptyList() + + init { + viewModelScope.launch { + repository.query( + Filter(ids = listOf(stackId), kinds = listOf(Catalog.appStackKind), limit = 1), + ).collect { state -> + val stack = state.items.firstOrNull()?.let(::StackInfo) + _uiState.update { + it.copy( + stack = stack, + stackLoading = stack == null && state.sync.isLoading, + error = state.error?.message, + ) + } + observeApps(stack) + } + } + } + + private fun observeApps(stack: StackInfo?) { + val addresses = stack?.appAddresses.orEmpty() + if (addresses == appAddresses) return + appAddresses = addresses + appsJob?.cancel() + val coordinates = addresses.mapNotNull(String::toAppCoordinate) + if (coordinates.isEmpty()) { + _uiState.update { it.copy(appsByAddress = emptyMap(), appsLoading = false) } + return + } + + _uiState.update { it.copy(appsByAddress = emptyMap(), appsLoading = true) } + appsJob = viewModelScope.launch { + repository.query( + Filter( + authors = coordinates.map(AppCoordinate::author).distinct(), + kinds = listOf(Catalog.appKind), + tags = mapOf("d" to coordinates.map(AppCoordinate::identifier).distinct()), + limit = coordinates.size * 2, + ), + ).collect { state -> + val apps = state.items.map(::AppInfo).associateBy(AppInfo::address) + _uiState.update { + it.copy( + appsByAddress = apps, + appsLoading = apps.isEmpty() && state.sync.isLoading, + error = state.error?.message ?: it.error, + ) + } + } + } + } +} + +data class AppDetailUiState( + val app: AppInfo? = null, + val release: ReleaseInfo? = null, + val appLoading: Boolean = true, + val releaseLoading: Boolean = true, + val error: String? = null, +) + +class AppDetailViewModel( + private val repository: CatalogRepository, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val identifier = requireNotNull(savedStateHandle.get(APP_IDENTIFIER_ARGUMENT)) + private val author = savedStateHandle.get(APP_AUTHOR_ARGUMENT)?.takeIf(String::isNotBlank) + private val _uiState = MutableStateFlow(AppDetailUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + observeApp() + observeLatestRelease() + } + + private fun observeApp() { + viewModelScope.launch { + repository.query( + Filter( + authors = author?.let(::listOf), + kinds = listOf(Catalog.appKind), + tags = mapOf("d" to listOf(identifier)), + limit = 3, + ), + ).collect { state -> + val app = state.items.firstOrNull()?.let(::AppInfo) + _uiState.update { + it.copy( + app = app, + appLoading = app == null && state.sync.isLoading, + error = state.error?.message ?: it.error, + ) + } + } + } + } + + private fun observeLatestRelease() { + viewModelScope.launch { + repository.query( + Filter( + kinds = listOf(Catalog.releaseKind), + tags = mapOf("i" to listOf(identifier)), + limit = 10, + ), + ).collect { state -> + val release = state.items.map(::ReleaseInfo).maxByOrNull { it.event.createdAt } + _uiState.update { + it.copy( + release = release, + releaseLoading = release == null && state.sync.isLoading, + error = state.error?.message ?: it.error, + ) + } + } + } + } +} + +fun homeViewModelFactory(repository: CatalogRepository): ViewModelProvider.Factory = viewModelFactory { + initializer { HomeViewModel(repository, createSavedStateHandle()) } +} + +fun stackDetailViewModelFactory(repository: CatalogRepository): ViewModelProvider.Factory = viewModelFactory { + initializer { StackDetailViewModel(repository, createSavedStateHandle()) } +} + +fun appDetailViewModelFactory(repository: CatalogRepository): ViewModelProvider.Factory = viewModelFactory { + initializer { AppDetailViewModel(repository, createSavedStateHandle()) } +} + +private val QuerySync.isLoading: Boolean + get() = this == QuerySync.Connecting || this == QuerySync.CatchingUp diff --git a/src/main/kotlin/dev/zapstore/app/ZapstoreApplication.kt b/src/main/kotlin/dev/zapstore/app/ZapstoreApplication.kt new file mode 100644 index 0000000..7b7f038 --- /dev/null +++ b/src/main/kotlin/dev/zapstore/app/ZapstoreApplication.kt @@ -0,0 +1,9 @@ +package dev.zapstore.app + +import android.app.Application + +class ZapstoreApplication : Application() { + val catalogRepository: CatalogRepository by lazy { + PurpleQuartzCatalogRepository(this) + } +} diff --git a/src/main/res/drawable/ic_launcher.xml b/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..e7bcf27 --- /dev/null +++ b/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,12 @@ + + + + diff --git a/src/main/res/font/inter_bold.ttf b/src/main/res/font/inter_bold.ttf index 15e908f..9fb9b75 100644 Binary files a/src/main/res/font/inter_bold.ttf and b/src/main/res/font/inter_bold.ttf differ diff --git a/src/main/res/font/inter_regular.ttf b/src/main/res/font/inter_regular.ttf index c544be4..b7aaca8 100644 Binary files a/src/main/res/font/inter_regular.ttf and b/src/main/res/font/inter_regular.ttf differ diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml new file mode 100644 index 0000000..09121c5 --- /dev/null +++ b/src/main/res/values/strings.xml @@ -0,0 +1,32 @@ + + Zapstore + Search apps + Clear search + Curated stacks + Latest releases + Loading community stacks… + No stacks found. + Loading stack… + Stack not found. + Apps + + %1$d curated app + %1$d curated apps + + Loading apps in this stack… + No apps found in this stack. + Loading app… + App not found. + Published by · %1$s… + Latest release + Version: + Source + License + App ID + Author + Release date + Not available + Back + %1$s icon + %1$s screenshot + diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index 82b1a83..0b2aca9 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -2,8 +2,8 @@