mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Migrate to Compose
This commit is contained in:
+28
-1
@@ -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
@@ -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),
|
||||
)
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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://")
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
@@ -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>
|
||||
@@ -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),
|
||||
)
|
||||
Reference in New Issue
Block a user