Migrate to Compose

This commit is contained in:
franzap
2026-07-12 18:27:01 -03:00
parent 6ecf3d31d2
commit 0383b336f4
26 changed files with 2143 additions and 1036 deletions
+28 -1
View File
@@ -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)
}
+186 -36
View File
@@ -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
@@ -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<MainActivity>()
@Test
fun navigationHostLaunchesAtHome() {
composeRule.onNodeWithTag("searchField").assertIsDisplayed()
}
}
@@ -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<Array<String>> = emptyArray(),
): Event = Event(
id = id,
pubKey = "1".repeat(64),
createdAt = 1_750_000_000,
kind = kind,
tags = tags,
content = "",
sig = "2".repeat(128),
)
+3 -4
View File
@@ -2,11 +2,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="Zapstore"
android:name=".ZapstoreApplication"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity android:name=".StackDetailActivity" />
<activity android:name=".AppDetailActivity" />
<activity android:name=".SearchActivity" />
<activity
android:name=".MainActivity"
android:exported="true">
+92
View File
@@ -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.
@@ -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)
}
}
+21 -12
View File
@@ -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<QueryState>
fun queryRemote(filter: Filter): Flow<QueryState>
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<dev.zapstore.purplequartz.QueryState> =
client(context).query(
override fun query(filter: Filter): Flow<QueryState> =
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<dev.zapstore.purplequartz.QueryState> =
client(context).query(filter, QuerySource.Local)
fun queryRemote(context: Context, filter: Filter): Flow<dev.zapstore.purplequartz.QueryState> =
client(context).query(
override fun queryRemote(filter: Filter): Flow<QueryState> =
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()
}
}
@@ -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<String, AppInfo>,
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://")
+118 -267
View File
@@ -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<String, AppInfo> = emptyMap()
private var stackApps: Map<String, AppInfo> = 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<ReleaseInfo>) {
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<StackInfo>) {
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<StackInfo>, 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<ReleaseInfo>) {
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()
}
+160 -70
View File
@@ -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("</ul>")
inUnorderedList = false
}
if (inOrderedList) {
html.append("</ol>")
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|(?<!\w)(\*|_)([^*_]+)\6""",
)
fun parse(
value: String,
onOpenUrl: ((String) -> Unit)? = null,
): AnnotatedString = buildAnnotatedString {
value.lines().forEachIndexed { index, rawLine ->
val line = rawLine.trim()
when {
trimmed.isEmpty() -> {
closeLists()
html.append("<br>")
}
line.isEmpty() -> Unit
unordered -> {
if (!inUnorderedList) {
closeLists()
html.append("<ul>")
inUnorderedList = true
}
html.append("<li>").append(inline(trimmed.replaceFirst(Regex("^[-*+]\\s+"), ""))).append("</li>")
}
ordered -> {
if (!inOrderedList) {
closeLists()
html.append("<ol>")
inOrderedList = true
}
html.append("<li>").append(inline(trimmed.replaceFirst(Regex("^\\d+[.)]\\s+"), ""))).append("</li>")
}
else -> {
closeLists()
val heading = Regex("^(#{1,6})\\s+(.+)$").matchEntire(trimmed)
if (heading != null) {
val level = heading.groupValues[1].length
html.append("<h").append(level).append(">")
.append(inline(heading.groupValues[2]))
.append("</h").append(level).append(">")
} else {
html.append(inline(trimmed)).append("<br>")
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("`([^`]+)`")) { "<tt>${it.groupValues[1]}</tt>" }
result = result.replace(Regex("""\[([^\]]+)]\(([^)\s]+)(?:\s+"[^"]*")?\)""")) {
val label = it.groupValues[1]
val url = it.groupValues[2]
"""<a href="${Html.escapeHtml(url)}">$label</a>"""
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""")) { "<b>${it.groupValues[2]}</b>" }
result = result.replace(Regex("""(?<!\w)(\*|_)([^*_]+)\1""")) { "<i>${it.groupValues[2]}</i>" }
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,
)
}
+503
View File
@@ -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))
@@ -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()
}
@@ -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<String, AppInfo> = 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)
}
}
+93
View File
@@ -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,
)
}
-127
View File
@@ -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()
}
@@ -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<AppInfo> = emptyList(),
val searchMessage: String? = null,
val isSearching: Boolean = false,
val stacks: List<StackInfo> = emptyList(),
val stackApps: Map<String, AppInfo> = emptyMap(),
val stacksLoading: Boolean = true,
val stacksError: String? = null,
val releases: List<ReleaseInfo> = emptyList(),
val releaseApps: Map<String, AppInfo> = 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<HomeUiState> = _uiState.asStateFlow()
private var stackPreviewJob: Job? = null
private var releaseAppsJob: Job? = null
private var searchJob: Job? = null
private var stackAddresses: List<String> = emptyList()
private var releaseIdentifiers: Set<String> = 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<StackInfo>) {
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<ReleaseInfo>) {
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<String, AppInfo> = 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<String>(STACK_ID_ARGUMENT))
private val _uiState = MutableStateFlow(StackDetailUiState())
val uiState: StateFlow<StackDetailUiState> = _uiState.asStateFlow()
private var appsJob: Job? = null
private var appAddresses: List<String> = 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<String>(APP_IDENTIFIER_ARGUMENT))
private val author = savedStateHandle.get<String>(APP_AUTHOR_ARGUMENT)?.takeIf(String::isNotBlank)
private val _uiState = MutableStateFlow(AppDetailUiState())
val uiState: StateFlow<AppDetailUiState> = _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
@@ -0,0 +1,9 @@
package dev.zapstore.app
import android.app.Application
class ZapstoreApplication : Application() {
val catalogRepository: CatalogRepository by lazy {
PurpleQuartzCatalogRepository(this)
}
}
+12
View File
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3A6FCC"
android:pathData="M0,0H108V108H0Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M28,26H84L42,74H80V84H20L62,36H28Z" />
</vector>
Binary file not shown.
Binary file not shown.
+32
View File
@@ -0,0 +1,32 @@
<resources>
<string name="app_name">Zapstore</string>
<string name="search_apps">Search apps</string>
<string name="clear_search">Clear search</string>
<string name="curated_stacks">Curated stacks</string>
<string name="latest_releases">Latest releases</string>
<string name="loading_stacks">Loading community stacks…</string>
<string name="no_stacks">No stacks found.</string>
<string name="loading_stack">Loading stack…</string>
<string name="stack_not_found">Stack not found.</string>
<string name="apps">Apps</string>
<plurals name="curated_apps">
<item quantity="one">%1$d curated app</item>
<item quantity="other">%1$d curated apps</item>
</plurals>
<string name="loading_stack_apps">Loading apps in this stack…</string>
<string name="no_stack_apps">No apps found in this stack.</string>
<string name="loading_app">Loading app…</string>
<string name="app_not_found">App not found.</string>
<string name="published_by">Published by · %1$s…</string>
<string name="latest_release">Latest release</string>
<string name="version">Version:</string>
<string name="source">Source</string>
<string name="license">License</string>
<string name="app_id">App ID</string>
<string name="author">Author</string>
<string name="release_date">Release date</string>
<string name="not_available">Not available</string>
<string name="back">Back</string>
<string name="app_icon_description">%1$s icon</string>
<string name="screenshot_description">%1$s screenshot</string>
</resources>
+1 -1
View File
@@ -2,8 +2,8 @@
<style name="AppTheme" parent="android:style/Theme.Material.NoActionBar">
<item name="android:fontFamily">@font/inter_regular</item>
<item name="android:windowActionModeOverlay">true</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
<item name="android:statusBarColor">#0A0F1A</item>
<item name="android:navigationBarColor">#0A0F1A</item>
<item name="android:colorAccent">#3A6FCC</item>
@@ -0,0 +1,23 @@
package dev.zapstore.app
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
val dispatcher: TestDispatcher = StandardTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
@@ -0,0 +1,42 @@
package dev.zapstore.app
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class MarkdownTest {
@Test
fun `parses supported block and inline formatting`() {
val result = ZapMarkdown.parse(
"""
# Heading
- one
1. two
**bold** _italic_ `code`
""".trimIndent(),
)
assertEquals("Heading\n• one\n1. two\nbold italic code", result.text)
assertTrue(result.spanStyles.any { it.item.fontWeight == FontWeight.ExtraBold })
assertTrue(result.spanStyles.any { it.item.fontWeight == FontWeight.Bold })
assertTrue(result.spanStyles.any { it.item.fontStyle == FontStyle.Italic })
}
@Test
fun `creates links only for http urls`() {
val result = ZapMarkdown.parse(
"[site](https://zapstore.dev) [phone](tel:123)",
)
val links = result.getLinkAnnotations(0, result.length)
.mapNotNull { it.item as? LinkAnnotation.Url }
assertEquals(listOf("https://zapstore.dev"), links.map(LinkAnnotation.Url::url))
assertTrue(result.text.contains("site"))
assertTrue(result.text.contains("phone"))
assertFalse(result.text.contains("tel:123"))
}
}
@@ -0,0 +1,133 @@
package dev.zapstore.app
import androidx.lifecycle.SavedStateHandle
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import dev.zapstore.purplequartz.QueryState
import dev.zapstore.purplequartz.QuerySync
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class ViewModelsTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun `home search maps results and cancels the previous query`() = runTest {
val remoteFlows = mutableListOf<MutableSharedFlow<QueryState>>()
val repository = FakeCatalogRepository(
remoteQuery = {
MutableSharedFlow<QueryState>().also(remoteFlows::add)
},
)
val viewModel = HomeViewModel(repository, SavedStateHandle())
runCurrent()
viewModel.onSearchQueryChanged("first")
viewModel.submitSearch()
runCurrent()
assertEquals(1, remoteFlows.single().subscriptionCount.value)
viewModel.onSearchQueryChanged("second")
viewModel.submitSearch()
runCurrent()
assertEquals(0, remoteFlows.first().subscriptionCount.value)
assertEquals(1, remoteFlows.last().subscriptionCount.value)
remoteFlows.last().emit(
queryState(
event(
kind = Catalog.appKind,
tags = arrayOf(
arrayOf("d", "second"),
arrayOf("name", "Second App"),
),
),
),
)
runCurrent()
assertEquals("Second App", viewModel.uiState.value.searchResults.single().name)
assertEquals("1 results for “second”", viewModel.uiState.value.searchMessage)
}
@Test
fun `stack changes cancel the old app query`() = runTest {
val stackFlow = MutableSharedFlow<QueryState>()
val appFlows = mutableListOf<MutableSharedFlow<QueryState>>()
var queryCount = 0
val repository = FakeCatalogRepository(
localQuery = {
if (queryCount++ == 0) {
stackFlow
} else {
MutableSharedFlow<QueryState>().also(appFlows::add)
}
},
)
StackDetailViewModel(
repository,
SavedStateHandle(mapOf(STACK_ID_ARGUMENT to "a".repeat(64))),
)
runCurrent()
stackFlow.emit(stackState("first"))
runCurrent()
assertEquals(1, appFlows.single().subscriptionCount.value)
stackFlow.emit(stackState("second"))
runCurrent()
assertEquals(0, appFlows.first().subscriptionCount.value)
assertEquals(1, appFlows.last().subscriptionCount.value)
}
private fun stackState(identifier: String): QueryState = queryState(
event(
id = "stack-$identifier",
kind = Catalog.appStackKind,
tags = arrayOf(
arrayOf("name", identifier),
arrayOf("a", "${Catalog.appKind}:${"1".repeat(64)}:$identifier"),
),
),
)
}
private class FakeCatalogRepository(
private val localQuery: (Filter) -> Flow<QueryState> = { emptyFlow() },
private val remoteQuery: (Filter) -> Flow<QueryState> = { emptyFlow() },
) : CatalogRepository {
override fun query(filter: Filter): Flow<QueryState> = localQuery(filter)
override fun queryRemote(filter: Filter): Flow<QueryState> = remoteQuery(filter)
override fun refreshConnections() = Unit
}
private fun queryState(vararg events: Event): QueryState =
QueryState(
items = events.toList(),
sync = QuerySync.Complete,
)
private fun event(
id: String = "a".repeat(64),
kind: Int,
tags: Array<Array<String>> = emptyArray(),
): Event = Event(
id = id,
pubKey = "1".repeat(64),
createdAt = 1_750_000_000,
kind = kind,
tags = tags,
content = "",
sig = "2".repeat(128),
)