diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6195c96c..b21cc625 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,4 @@ +import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -71,6 +72,15 @@ kotlin { } } +configure { + config.setFrom( + files( + rootProject.file("detekt.yml"), + layout.projectDirectory.file("detekt.yml"), + ), + ) +} + dependencies { implementation(project(":domain")) implementation(project(":data")) @@ -80,11 +90,14 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) implementation(libs.koin.android) implementation(libs.koin.androidx.compose) + implementation(libs.androidx.datastore.preferences) + implementation(libs.coil.compose) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) @@ -97,5 +110,8 @@ dependencies { implementation(libs.androidx.compose.material.icons.extended) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) + implementation(libs.firebase.auth) + implementation(libs.kotlinx.coroutines.play.services) + implementation(libs.play.services.auth) implementation(libs.koin.workmanager) } diff --git a/app/detekt.yml b/app/detekt.yml new file mode 100644 index 00000000..2e5aa217 --- /dev/null +++ b/app/detekt.yml @@ -0,0 +1,14 @@ +# UI (app) module: Jetpack Compose entry points often exceed default complexity limits. +complexity: + LongParameterList: + active: false + LongMethod: + active: false + TooManyFunctions: + active: false + CyclomaticComplexMethod: + active: false + +style: + ReturnCount: + active: false diff --git a/app/google-services.json b/app/google-services.json index 0942b00a..8eeed345 100644 --- a/app/google-services.json +++ b/app/google-services.json @@ -1,23 +1,37 @@ -{ - "project_info": { - "project_number": "123456789", - "project_id": "mock-id" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:123456789:android:mock", - "android_client_info": { - "package_name": "com.itlab.notes" - } - }, - "api_key": [ +{ + "project_info": { + "project_number": "123456789", + "project_id": "mock-id" + }, + "client": [ { - "current_key": "mock-key" + "client_info": { + "mobilesdk_app_id": "1:123456789:android:mock", + "android_client_info": { + "package_name": "com.itlab.notes" + } + }, + "oauth_client": [ + { + "client_id": "mock-id", + "client_type": 1, + "android_info": { + "package_name": "com.itlab.notes", + "certificate_hash": "mock-hash" + } + }, + { + "client_id": "mock-id", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "mock-key" + } + ], + "services": {} } - ], - "services": {} - } - ], - "configuration_version": "1" + ], + "configuration_version": "1" } diff --git a/app/gradle.lockfile b/app/gradle.lockfile index 3fcef764..15f021d7 100644 --- a/app/gradle.lockfile +++ b/app/gradle.lockfile @@ -7,8 +7,8 @@ androidx.activity:activity-ktx:1.12.4=releaseUnitTestRuntimeClasspath androidx.activity:activity-ktx:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.activity:activity:1.12.4=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath 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,debugCompileClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.annotation:annotation-experimental:1.5.0=debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.annotation:annotation-experimental:1.4.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.annotation:annotation-experimental:1.5.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.annotation:annotation-jvm:1.9.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.annotation:annotation:1.9.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.appcompat:appcompat-resources:1.6.1=releaseUnitTestRuntimeClasspath @@ -19,40 +19,31 @@ androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndro androidx.arch.core:core-runtime:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.asynclayoutinflater:asynclayoutinflater:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.autofill:autofill:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -androidx.browser:browser:1.4.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.browser:browser:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.cardview:cardview:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.collection:collection-jvm:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.collection:collection-ktx:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.collection:collection:1.5.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.animation:animation-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.animation:animation-android:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.animation:animation-android:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.animation:animation-core-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.animation:animation-core-android:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-core-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.animation:animation-core-android:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.animation:animation-core:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.animation:animation-core:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation-core:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.animation:animation-core:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.animation:animation:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.animation:animation:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.animation:animation:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.animation:animation:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.foundation:foundation-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.foundation:foundation-android:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.foundation:foundation-android:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.foundation:foundation-layout-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.foundation:foundation-layout-android:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-layout-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.foundation:foundation-layout-android:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.foundation:foundation-layout:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.foundation:foundation-layout:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation-layout:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.foundation:foundation-layout:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.foundation:foundation:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.foundation:foundation:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.foundation:foundation:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.foundation:foundation:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.compose.material3:material3-android:1.3.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.compose.material3:material3-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.material3:material3:1.3.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.compose.material3:material3:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.material:material-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath androidx.compose.material:material-icons-core-android:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.compose.material:material-icons-core-android:1.7.8=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath androidx.compose.material:material-icons-core-desktop:1.7.8=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath @@ -61,98 +52,63 @@ androidx.compose.material:material-icons-core:1.7.8=debugAndroidTestCompileClass androidx.compose.material:material-icons-extended-android:1.7.8=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath androidx.compose.material:material-icons-extended-desktop:1.7.8=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath androidx.compose.material:material-icons-extended:1.7.8=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.material:material-ripple-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.material:material-ripple-android:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-ripple-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.material:material-ripple-android:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.material:material-ripple:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.material:material-ripple:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.material:material-ripple:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.material:material-ripple:1.7.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.material:material:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.runtime:runtime-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.runtime:runtime-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.runtime:runtime-android:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.runtime:runtime-annotation-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.runtime:runtime-annotation-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-annotation-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.runtime:runtime-annotation-android:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.runtime:runtime-annotation:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.runtime:runtime-annotation:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-annotation:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.runtime:runtime-annotation:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.runtime:runtime-retain-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.runtime:runtime-retain-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-retain:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.runtime:runtime-retain:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.compose.runtime:runtime-saveable-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.runtime:runtime-saveable-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-retain-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-retain:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-saveable-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.runtime:runtime-saveable-android:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.runtime:runtime-saveable:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.runtime:runtime-saveable:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime-saveable:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.runtime:runtime-saveable:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.runtime:runtime:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.runtime:runtime:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.runtime:runtime:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.runtime:runtime:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-android:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-geometry-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-geometry-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-geometry-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-geometry-android:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-geometry:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-geometry:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-geometry:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-geometry:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-graphics-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-graphics-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-graphics-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-graphics-android:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-graphics:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-graphics:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-graphics:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-graphics:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-test-android:1.10.5=debugAndroidTestLintChecksClasspath -androidx.compose.ui:ui-test-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath -androidx.compose.ui:ui-test-junit4-android:1.10.5=debugAndroidTestLintChecksClasspath -androidx.compose.ui:ui-test-junit4-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath -androidx.compose.ui:ui-test-junit4:1.10.5=debugAndroidTestLintChecksClasspath -androidx.compose.ui:ui-test-junit4:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath -androidx.compose.ui:ui-test-manifest:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-test-manifest:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.compose.ui:ui-test:1.10.5=debugAndroidTestLintChecksClasspath -androidx.compose.ui:ui-test:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath -androidx.compose.ui:ui-text-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-text-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-test-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.compose.ui:ui-test-junit4-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.compose.ui:ui-test-junit4:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.compose.ui:ui-test-manifest:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-test:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +androidx.compose.ui:ui-text-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-text-android:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-text:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-text:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-text:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-text:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-tooling-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-tooling-android:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.compose.ui:ui-tooling-data-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-tooling-data-android:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.compose.ui:ui-tooling-data:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-tooling-data:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.compose.ui:ui-tooling-preview-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-tooling-preview-android:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-tooling-data-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-tooling-data:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-tooling-preview-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-tooling-preview-android:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-tooling-preview:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-tooling-preview:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling-preview:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-tooling-preview:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-tooling:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-tooling:1.11.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath -androidx.compose.ui:ui-unit-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-unit-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-tooling:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath +androidx.compose.ui:ui-unit-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-unit-android:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-unit:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-unit:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-unit:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-unit:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-util-android:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-util-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-util-android:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-util-android:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui-util:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui-util:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui-util:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui-util:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose.ui:ui:1.10.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose.ui:ui:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose.ui:ui:1.11.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.compose.ui:ui:1.9.2=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.compose:compose-bom:2024.09.00=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.compose:compose-bom:2026.03.00=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.compose:compose-bom:2026.05.00=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.compose:compose-bom:2026.05.00=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.concurrent:concurrent-futures-ktx:1.1.0=debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.concurrent:concurrent-futures-ktx:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath @@ -167,28 +123,28 @@ androidx.core:core-ktx:1.18.0=debugAndroidTestCompileClasspath,debugAndroidTestL androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.core:core:1.17.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.core:core:1.18.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.credentials:credentials-play-services-auth:1.2.0-rc01=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.credentials:credentials:1.2.0-rc01=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.credentials:credentials-play-services-auth:1.2.0-rc01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.credentials:credentials:1.2.0-rc01=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.cursoradapter:cursoradapter:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.customview:customview-poolingcontainer:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath androidx.customview:customview:1.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -androidx.datastore:datastore-android:1.1.7=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath -androidx.datastore:datastore-core-android:1.1.7=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-android:1.1.7=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-core-android:1.1.7=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath androidx.datastore:datastore-core-jvm:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath -androidx.datastore:datastore-core-okio-jvm:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.datastore:datastore-core-okio:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.datastore:datastore-core:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-core-okio-jvm:1.1.7=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-core-okio:1.1.7=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-core:1.1.7=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.datastore:datastore-jvm:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath -androidx.datastore:datastore-preferences-android:1.1.7=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath -androidx.datastore:datastore-preferences-core-android:1.1.7=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-preferences-android:1.1.7=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-preferences-core-android:1.1.7=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath androidx.datastore:datastore-preferences-core-jvm:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath -androidx.datastore:datastore-preferences-core:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-preferences-core:1.1.7=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.datastore:datastore-preferences-external-protobuf:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.datastore:datastore-preferences-jvm:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath androidx.datastore:datastore-preferences-proto:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.datastore:datastore-preferences:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.datastore:datastore:1.1.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.datastore:datastore-preferences:1.1.7=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.datastore:datastore:1.1.7=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.documentfile:documentfile:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.drawerlayout:drawerlayout:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath androidx.drawerlayout:drawerlayout:1.1.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath @@ -196,11 +152,10 @@ androidx.dynamicanimation:dynamicanimation:1.0.0=releaseUnitTestRuntimeClasspath androidx.dynamicanimation:dynamicanimation:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.emoji2:emoji2-views-helper:1.4.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.emoji2:emoji2:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -androidx.fragment:fragment-ktx:1.6.2=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.fragment:fragment-ktx:1.8.9=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.exifinterface:exifinterface:1.3.7=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.fragment:fragment-ktx:1.8.9=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.fragment:fragment:1.3.6=releaseUnitTestRuntimeClasspath -androidx.fragment:fragment:1.6.2=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.fragment:fragment:1.8.9=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.fragment:fragment:1.8.9=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.graphics:graphics-path:1.0.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.graphics:graphics-shapes-android:1.0.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath androidx.graphics:graphics-shapes-desktop:1.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseLintChecksClasspath @@ -249,30 +204,21 @@ androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClass androidx.recyclerview:recyclerview:1.1.0=releaseUnitTestRuntimeClasspath androidx.recyclerview:recyclerview:1.2.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.resourceinspection:resourceinspection-annotation:1.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -androidx.room:room-common-jvm:2.7.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.room:room-common-jvm:2.8.4=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.room:room-common:2.7.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.room:room-common:2.8.4=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.room:room-ktx:2.7.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.room:room-ktx:2.8.4=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.room:room-runtime-android:2.7.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.room:room-runtime-android:2.8.4=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.room:room-runtime:2.7.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.room:room-runtime:2.8.4=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-common-jvm:2.8.4=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-common:2.8.4=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-ktx:2.8.4=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-runtime-android:2.8.4=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.room:room-runtime:2.8.4=debugAndroidTestLintChecksClasspath,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,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate-compose-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate-compose:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate-ktx:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.slidingpanelayout:slidingpanelayout:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.sqlite:sqlite-android:2.5.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.sqlite:sqlite-android:2.6.2=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.sqlite:sqlite-framework-android:2.5.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.sqlite:sqlite-framework-android:2.6.2=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.sqlite:sqlite-framework:2.5.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.sqlite:sqlite-framework:2.6.2=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -androidx.sqlite:sqlite:2.5.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -androidx.sqlite:sqlite:2.6.2=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.sqlite:sqlite-android:2.6.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.sqlite:sqlite-framework-android:2.6.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.sqlite:sqlite-framework:2.6.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +androidx.sqlite:sqlite:2.6.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.swiperefreshlayout:swiperefreshlayout:1.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath androidx.test.espresso:espresso-core:3.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath @@ -301,35 +247,30 @@ androidx.work:work-runtime:2.11.1=debugAndroidTestCompileClasspath,debugCompileC androidx.work:work-runtime:2.9.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath ch.qos.logback:logback-classic:1.3.14=ktlint ch.qos.logback:logback-core:1.3.14=ktlint -co.touchlab:stately-concurrency-jvm:2.0.6=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -co.touchlab:stately-concurrency-jvm:2.1.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -co.touchlab:stately-concurrency:2.0.6=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -co.touchlab:stately-concurrency:2.1.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -co.touchlab:stately-concurrent-collections-jvm:2.0.6=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -co.touchlab:stately-concurrent-collections-jvm:2.1.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -co.touchlab:stately-concurrent-collections:2.0.6=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -co.touchlab:stately-concurrent-collections:2.1.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -co.touchlab:stately-strict-jvm:2.0.6=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -co.touchlab:stately-strict-jvm:2.1.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -co.touchlab:stately-strict:2.0.6=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -co.touchlab:stately-strict:2.1.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +co.touchlab:stately-concurrency-jvm:2.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +co.touchlab:stately-concurrency:2.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +co.touchlab:stately-concurrent-collections-jvm:2.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +co.touchlab:stately-concurrent-collections:2.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +co.touchlab:stately-strict-jvm:2.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +co.touchlab:stately-strict:2.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.android.tools.analytics-library:protos:32.1.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.android.tools.analytics-library:protos:32.2.0=androidLintTool +com.android.tools.analytics-library:protos:32.2.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools.analytics-library:shared:32.1.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.android.tools.analytics-library:shared:32.2.0=androidLintTool +com.android.tools.analytics-library:shared:32.2.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools.analytics-library:tracker:32.2.0=androidLintTool com.android.tools.build:aapt2-proto:9.1.0-14792394=_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.android.tools.build:aapt2-proto:9.2.0-15009934=androidLintTool +com.android.tools.build:aapt2-proto:9.2.0-15009934=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools.build:builder-model:9.2.0=androidLintTool com.android.tools.build:manifest-merger:32.2.0=androidLintTool com.android.tools.ddms:ddmlib:32.1.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action -com.android.tools.ddms:ddmlib:32.2.0=androidLintTool +com.android.tools.ddms:ddmlib:32.2.0=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action com.android.tools.emulator:proto:32.1.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control +com.android.tools.emulator:proto:32.2.0=unified-test-platform-android-test-plugin-host-emulator-control com.android.tools.external.com-intellij:intellij-core:32.2.0=androidLintTool com.android.tools.external.com-intellij:kotlin-compiler:32.2.0=androidLintTool com.android.tools.external.org-jetbrains:uast:32.2.0=androidLintTool com.android.tools.layoutlib:layoutlib-api:32.1.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.android.tools.layoutlib:layoutlib-api:32.2.0=androidLintTool +com.android.tools.layoutlib:layoutlib-api:32.2.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools.lint:lint-api:32.2.0=androidLintTool com.android.tools.lint:lint-checks:32.2.0=androidLintTool com.android.tools.lint:lint-gradle:32.2.0=androidLintTool @@ -337,46 +278,66 @@ com.android.tools.lint:lint-model:32.2.0=androidLintTool com.android.tools.lint:lint-typedef-remover:32.2.0=androidLintTool com.android.tools.lint:lint:32.2.0=androidLintTool com.android.tools.utp:android-device-provider-ddmlib-proto:32.1.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-gradle-work-action +com.android.tools.utp:android-device-provider-ddmlib-proto:32.2.0=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-gradle-work-action com.android.tools.utp:android-device-provider-ddmlib:32.1.0=_internal-unified-test-platform-android-device-provider-ddmlib +com.android.tools.utp:android-device-provider-ddmlib:32.2.0=unified-test-platform-android-device-provider-ddmlib com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:32.1.0=_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:32.2.0=unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-gradle-work-action com.android.tools.utp:android-test-plugin-host-additional-test-output:32.1.0=_internal-unified-test-platform-android-test-plugin-host-additional-test-output +com.android.tools.utp:android-test-plugin-host-additional-test-output:32.2.0=unified-test-platform-android-test-plugin-host-additional-test-output com.android.tools.utp:android-test-plugin-host-apk-installer-proto:32.1.0=_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-apk-installer-proto:32.2.0=unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-gradle-work-action com.android.tools.utp:android-test-plugin-host-apk-installer:32.1.0=_internal-unified-test-platform-android-test-plugin-host-apk-installer +com.android.tools.utp:android-test-plugin-host-apk-installer:32.2.0=unified-test-platform-android-test-plugin-host-apk-installer com.android.tools.utp:android-test-plugin-host-coverage-proto:32.1.0=_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-coverage-proto:32.2.0=unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-gradle-work-action com.android.tools.utp:android-test-plugin-host-coverage:32.1.0=_internal-unified-test-platform-android-test-plugin-host-coverage +com.android.tools.utp:android-test-plugin-host-coverage:32.2.0=unified-test-platform-android-test-plugin-host-coverage com.android.tools.utp:android-test-plugin-host-device-info-proto:32.1.0=_internal-unified-test-platform-android-test-plugin-host-device-info +com.android.tools.utp:android-test-plugin-host-device-info-proto:32.2.0=unified-test-platform-android-test-plugin-host-device-info com.android.tools.utp:android-test-plugin-host-device-info:32.1.0=_internal-unified-test-platform-android-test-plugin-host-device-info +com.android.tools.utp:android-test-plugin-host-device-info:32.2.0=unified-test-platform-android-test-plugin-host-device-info com.android.tools.utp:android-test-plugin-host-emulator-control-proto:32.1.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-emulator-control-proto:32.2.0=unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action com.android.tools.utp:android-test-plugin-host-emulator-control:32.1.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control +com.android.tools.utp:android-test-plugin-host-emulator-control:32.2.0=unified-test-platform-android-test-plugin-host-emulator-control com.android.tools.utp:android-test-plugin-host-logcat-proto:32.1.0=_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-host-logcat-proto:32.2.0=unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-gradle-work-action com.android.tools.utp:android-test-plugin-host-logcat:32.1.0=_internal-unified-test-platform-android-test-plugin-host-logcat +com.android.tools.utp:android-test-plugin-host-logcat:32.2.0=unified-test-platform-android-test-plugin-host-logcat com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:32.1.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action +com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:32.2.0=unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action com.android.tools.utp:android-test-plugin-result-listener-gradle:32.1.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle +com.android.tools.utp:android-test-plugin-result-listener-gradle:32.2.0=unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools.utp:gradle-work-action:32.1.0=_internal-unified-test-platform-gradle-work-action +com.android.tools.utp:gradle-work-action:32.2.0=unified-test-platform-gradle-work-action com.android.tools.utp:utp-common:32.1.0=_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-logcat +com.android.tools.utp:utp-common:32.2.0=unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-logcat com.android.tools:annotations:32.1.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action -com.android.tools:annotations:32.2.0=androidLintTool +com.android.tools:annotations:32.2.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 com.android.tools:common:32.1.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action -com.android.tools:common:32.2.0=androidLintTool +com.android.tools:common:32.2.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 com.android.tools:dvlib:32.1.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.android.tools:dvlib:32.2.0=androidLintTool +com.android.tools:dvlib:32.2.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools:play-sdk-proto:32.2.0=androidLintTool com.android.tools:repository:32.1.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.android.tools:repository:32.2.0=androidLintTool +com.android.tools:repository:32.2.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools:sdk-common:32.1.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.android.tools:sdk-common:32.2.0=androidLintTool +com.android.tools:sdk-common:32.2.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.android.tools:sdklib:32.1.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.android.tools:sdklib:32.2.0=androidLintTool +com.android.tools:sdklib:32.2.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle com.firebaseui:firebase-ui-auth:8.0.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.github.ajalt.clikt:clikt-jvm:5.0.2=ktlint com.github.ajalt.clikt:clikt:5.0.2=ktlint +com.google.accompanist:accompanist-drawablepainter:0.32.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.android.gms:play-services-ads-identifier:18.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.gms:play-services-auth-api-phone:18.0.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.gms:play-services-auth-base:18.0.4=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.gms:play-services-auth:20.7.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.gms:play-services-auth-api-phone:18.0.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.gms:play-services-auth-base:18.0.4=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.gms:play-services-auth:20.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.android.gms:play-services-base:18.5.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath com.google.android.gms:play-services-base:18.9.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.android.gms:play-services-basement:18.9.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.gms:play-services-fido:20.0.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath com.google.android.gms:play-services-fido:20.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.android.gms:play-services-measurement-api:23.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.android.gms:play-services-measurement-base:23.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath @@ -389,33 +350,34 @@ com.google.android.gms:play-services-tasks:18.4.0=debugAndroidTestCompileClasspa com.google.android.libraries.identity.googleid:googleid:1.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.android.material:material:1.10.0=releaseUnitTestRuntimeClasspath com.google.android.material:material:1.13.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.play:core-common:2.0.3=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.play:integrity:1.3.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android.recaptcha:recaptcha:18.6.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.android:annotations:4.1.1.4=_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-core,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.api.grpc:proto-google-common-protos:2.17.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core -com.google.api.grpc:proto-google-common-protos:2.48.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control -com.google.auto.service:auto-service-annotations:1.1.1=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.google.auto.service:auto-service:1.1.1=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.google.auto:auto-common:1.2.1=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.google.code.findbugs:jsr305:3.0.2=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.code.gson:gson:2.10.1=_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.code.gson:gson:2.11.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -com.google.code.gson:gson:2.8.9=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-launcher -com.google.crypto.tink:tink:1.18.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-gradle-work-action -com.google.dagger:dagger:2.48=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action +com.google.android.play:core-common:2.0.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.play:integrity:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android.recaptcha:recaptcha:18.6.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.android:annotations:4.1.1.4=_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-core,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-core +com.google.api.grpc:proto-google-common-protos:2.17.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,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=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +com.google.auto.service:auto-service-annotations:1.1.1=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,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=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,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=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,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:3.0.2=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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 +com.google.code.gson:gson:2.10.1=_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-core,unified-test-platform-gradle-work-action +com.google.code.gson:gson:2.11.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.code.gson:gson:2.8.9=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-launcher,unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher +com.google.crypto.tink:tink:1.18.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-gradle-work-action,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action +com.google.dagger:dagger:2.48=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,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 com.google.errorprone:error_prone_annotations:2.11.0=debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath com.google.errorprone:error_prone_annotations:2.15.0=releaseUnitTestRuntimeClasspath -com.google.errorprone:error_prone_annotations:2.23.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher +com.google.errorprone:error_prone_annotations:2.23.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.errorprone:error_prone_annotations:2.26.0=debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.errorprone:error_prone_annotations:2.28.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool -com.google.errorprone:error_prone_annotations:2.30.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath +com.google.errorprone:error_prone_annotations:2.28.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,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-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.errorprone:error_prone_annotations:2.30.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,unified-test-platform-android-test-plugin-host-emulator-control com.google.firebase:firebase-analytics:23.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:firebase-annotations:17.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.firebase:firebase-appcheck-interop:17.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath com.google.firebase:firebase-appcheck-interop:17.1.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:firebase-appcheck:19.0.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.firebase:firebase-auth-interop:20.0.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.firebase:firebase-auth:24.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.firebase:firebase-auth-interop:20.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.firebase:firebase-auth:24.0.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:firebase-bom:34.12.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:firebase-common:22.0.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:firebase-components:19.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath @@ -426,31 +388,31 @@ com.google.firebase:firebase-installations:19.1.0=debugAndroidTestCompileClasspa com.google.firebase:firebase-measurement-connector:19.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:firebase-storage:22.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.google.firebase:protolite-well-known-types:18.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.guava:failureaccess:1.0.1=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.guava:failureaccess:1.0.2=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool +com.google.guava:failureaccess:1.0.1=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +com.google.guava:failureaccess:1.0.2=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,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:31.1-android=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath -com.google.guava:guava:32.0.1-jre=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher +com.google.guava:guava:32.0.1-jre=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher com.google.guava:guava:32.1.3-android=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.guava:guava:33.3.1-jre=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool +com.google.guava:guava:33.3.1-jre=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,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=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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 com.google.j2objc:j2objc-annotations:1.3=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath -com.google.j2objc:j2objc-annotations:2.8=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher -com.google.j2objc:j2objc-annotations:3.0.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool -com.google.jimfs:jimfs:1.1=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -com.google.protobuf:protobuf-java-util:3.22.3=_internal-unified-test-platform-core -com.google.protobuf:protobuf-java-util:4.28.3=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher +com.google.j2objc:j2objc-annotations:2.8=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher,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=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,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.jimfs:jimfs:1.1=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.google.protobuf:protobuf-java-util:3.22.3=_internal-unified-test-platform-core,unified-test-platform-core +com.google.protobuf:protobuf-java-util:4.28.3=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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 com.google.protobuf:protobuf-java:3.25.5=androidLintTool -com.google.protobuf:protobuf-java:4.28.3=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher +com.google.protobuf:protobuf-java:4.28.3=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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.protobuf:protobuf-javalite:3.25.5=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.protobuf:protobuf-kotlin:4.28.3=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher +com.google.protobuf:protobuf-kotlin:4.28.3=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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.re2j:re2j:1.6=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.google.testing.platform:android-device-provider-local:0.0.9-alpha04=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -com.google.testing.platform:android-driver-instrumentation:0.0.9-alpha04=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-emulator-control -com.google.testing.platform:android-test-plugin:0.0.9-alpha04=_internal-unified-test-platform-android-test-plugin -com.google.testing.platform:core-proto:0.0.9-alpha04=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action -com.google.testing.platform:core:0.0.9-alpha04=_internal-unified-test-platform-core -com.google.testing.platform:launcher:0.0.9-alpha04=_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher +com.google.testing.platform:android-device-provider-local:0.0.9-alpha04=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,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 +com.google.testing.platform:android-driver-instrumentation:0.0.9-alpha04=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control +com.google.testing.platform:android-test-plugin:0.0.9-alpha04=_internal-unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin +com.google.testing.platform:core-proto:0.0.9-alpha04=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +com.google.testing.platform:core:0.0.9-alpha04=_internal-unified-test-platform-core,unified-test-platform-core +com.google.testing.platform:launcher:0.0.9-alpha04=_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,unified-test-platform-gradle-work-action,unified-test-platform-launcher com.jakewharton.timber:timber:5.0.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath com.pinterest.ktlint:ktlint-cli-reporter-baseline:1.5.0=ktlint,ktlintBaselineReporter com.pinterest.ktlint:ktlint-cli-reporter-checkstyle:1.5.0=ktlint @@ -467,17 +429,22 @@ com.pinterest.ktlint:ktlint-logger:1.5.0=ktlint,ktlintBaselineReporter,ktlintRul com.pinterest.ktlint:ktlint-rule-engine-core:1.5.0=ktlint,ktlintBaselineReporter,ktlintRuleset com.pinterest.ktlint:ktlint-rule-engine:1.5.0=ktlint com.pinterest.ktlint:ktlint-ruleset-standard:1.5.0=ktlint,ktlintRuleset -com.squareup.okio:okio-jvm:3.4.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.squareup.okio:okio:3.4.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -com.sun.istack:istack-commons-runtime:3.0.8=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -com.sun.xml.fastinfoset:FastInfoset:1.2.16=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -commons-codec:commons-codec:1.17.1=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -commons-io:commons-io:2.16.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool -commons-logging:commons-logging:1.2=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool +com.squareup.okhttp3:okhttp:4.12.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okio:okio-jvm:3.9.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.squareup.okio:okio:3.9.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +com.sun.istack:istack-commons-runtime:3.0.8=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +com.sun.xml.fastinfoset:FastInfoset:1.2.16=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +commons-codec:commons-codec:1.17.1=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +commons-io:commons-io:2.16.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +commons-logging:commons-logging:1.2=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle dev.drewhamilton.poko:poko-annotations-jvm:0.17.1=detekt dev.drewhamilton.poko:poko-annotations-jvm:0.18.0=ktlint,ktlintBaselineReporter,ktlintRuleset dev.drewhamilton.poko:poko-annotations:0.17.1=detekt dev.drewhamilton.poko:poko-annotations:0.18.0=ktlint,ktlintBaselineReporter,ktlintRuleset +io.coil-kt:coil-base:2.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.coil-kt:coil-compose-base:2.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.coil-kt:coil-compose:2.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.coil-kt:coil:2.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath io.github.davidburstrom.contester:contester-breakpoint:0.2.0=detekt io.github.detekt.sarif4k:sarif4k-jvm:0.6.0=detekt,ktlint,ktlintReporter io.github.detekt.sarif4k:sarif4k:0.6.0=detekt,ktlint,ktlintReporter @@ -507,98 +474,92 @@ io.gitlab.arturbosch.detekt:detekt-rules:1.23.8=detekt io.gitlab.arturbosch.detekt:detekt-tooling:1.23.8=detekt io.gitlab.arturbosch.detekt:detekt-utils:1.23.8=detekt io.grpc:grpc-android:1.62.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.grpc:grpc-api:1.57.2=_internal-unified-test-platform-core +io.grpc:grpc-api:1.57.2=_internal-unified-test-platform-core,unified-test-platform-core io.grpc:grpc-api:1.62.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.grpc:grpc-api:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.grpc:grpc-context:1.57.2=_internal-unified-test-platform-core +io.grpc:grpc-api:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-context:1.57.2=_internal-unified-test-platform-core,unified-test-platform-core io.grpc:grpc-context:1.62.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.grpc:grpc-context:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.grpc:grpc-core:1.57.2=_internal-unified-test-platform-core +io.grpc:grpc-context:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-core:1.57.2=_internal-unified-test-platform-core,unified-test-platform-core io.grpc:grpc-core:1.62.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.grpc:grpc-core:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.grpc:grpc-netty:1.57.2=_internal-unified-test-platform-core -io.grpc:grpc-netty:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-core:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-netty:1.57.2=_internal-unified-test-platform-core,unified-test-platform-core +io.grpc:grpc-netty:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control io.grpc:grpc-okhttp:1.62.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.grpc:grpc-protobuf-lite:1.57.2=_internal-unified-test-platform-core +io.grpc:grpc-protobuf-lite:1.57.2=_internal-unified-test-platform-core,unified-test-platform-core io.grpc:grpc-protobuf-lite:1.62.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.grpc:grpc-protobuf-lite:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.grpc:grpc-protobuf:1.57.2=_internal-unified-test-platform-core -io.grpc:grpc-protobuf:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.grpc:grpc-services:1.57.2=_internal-unified-test-platform-core -io.grpc:grpc-stub:1.57.2=_internal-unified-test-platform-core +io.grpc:grpc-protobuf-lite:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-protobuf:1.57.2=_internal-unified-test-platform-core,unified-test-platform-core +io.grpc:grpc-protobuf:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-services:1.57.2=_internal-unified-test-platform-core,unified-test-platform-core +io.grpc:grpc-stub:1.57.2=_internal-unified-test-platform-core,unified-test-platform-core io.grpc:grpc-stub:1.62.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.grpc:grpc-stub:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control +io.grpc:grpc-stub:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control io.grpc:grpc-util:1.62.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.grpc:grpc-util:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.insert-koin:koin-android:3.5.6=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -io.insert-koin:koin-android:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.insert-koin:koin-androidx-compose:3.5.6=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -io.insert-koin:koin-androidx-compose:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.grpc:grpc-util:1.69.1=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.insert-koin:koin-android:4.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.insert-koin:koin-androidx-compose:4.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath io.insert-koin:koin-androidx-workmanager:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.insert-koin:koin-compose-android:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.insert-koin:koin-compose-jvm:1.1.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -io.insert-koin:koin-compose-viewmodel-android:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.insert-koin:koin-compose-viewmodel:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.insert-koin:koin-compose:1.1.5=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -io.insert-koin:koin-compose:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.insert-koin:koin-core-jvm:3.5.6=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -io.insert-koin:koin-core-jvm:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.insert-koin:koin-core-viewmodel-android:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.insert-koin:koin-core-viewmodel:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.insert-koin:koin-core:3.5.6=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -io.insert-koin:koin-core:4.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.netty:netty-buffer:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-buffer:4.1.93.Final=_internal-unified-test-platform-core -io.netty:netty-codec-http2:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-codec-http2:4.1.93.Final=_internal-unified-test-platform-core -io.netty:netty-codec-http:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-codec-http:4.1.93.Final=_internal-unified-test-platform-core -io.netty:netty-codec-socks:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-codec-socks:4.1.93.Final=_internal-unified-test-platform-core -io.netty:netty-codec:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-codec:4.1.93.Final=_internal-unified-test-platform-core -io.netty:netty-common:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-common:4.1.93.Final=_internal-unified-test-platform-core -io.netty:netty-handler-proxy:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-handler-proxy:4.1.93.Final=_internal-unified-test-platform-core -io.netty:netty-handler:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-handler:4.1.93.Final=_internal-unified-test-platform-core -io.netty:netty-resolver:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-resolver:4.1.93.Final=_internal-unified-test-platform-core -io.netty:netty-transport-native-unix-common:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-transport-native-unix-common:4.1.93.Final=_internal-unified-test-platform-core -io.netty:netty-transport:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control -io.netty:netty-transport:4.1.93.Final=_internal-unified-test-platform-core -io.opencensus:opencensus-api:0.31.0=_internal-unified-test-platform-core -io.opencensus:opencensus-proto:0.2.0=_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher -io.perfmark:perfmark-api:0.26.0=_internal-unified-test-platform-core,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -io.perfmark:perfmark-api:0.27.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control -jakarta.activation:jakarta.activation-api:1.2.1=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -javax.annotation:javax.annotation-api:1.3.2=_internal-unified-test-platform-android-test-plugin-host-emulator-control -javax.inject:javax.inject:1=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.insert-koin:koin-compose-android:4.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.insert-koin:koin-compose-viewmodel-android:4.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.insert-koin:koin-compose-viewmodel:4.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.insert-koin:koin-compose:4.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.insert-koin:koin-core-jvm:4.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.insert-koin:koin-core-viewmodel-android:4.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.insert-koin:koin-core-viewmodel:4.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.insert-koin:koin-core:4.2.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +io.netty:netty-buffer:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-buffer:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.netty:netty-codec-http2:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec-http2:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.netty:netty-codec-http:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec-http:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.netty:netty-codec-socks:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec-socks:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.netty:netty-codec:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-codec:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.netty:netty-common:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-common:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.netty:netty-handler-proxy:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-handler-proxy:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.netty:netty-handler:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-handler:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.netty:netty-resolver:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-resolver:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.netty:netty-transport-native-unix-common:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-transport-native-unix-common:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.netty:netty-transport:4.1.110.Final=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +io.netty:netty-transport:4.1.93.Final=_internal-unified-test-platform-core,unified-test-platform-core +io.opencensus:opencensus-api:0.31.0=_internal-unified-test-platform-core,unified-test-platform-core +io.opencensus:opencensus-proto:0.2.0=_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +io.perfmark:perfmark-api:0.26.0=_internal-unified-test-platform-core,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-core +io.perfmark:perfmark-api:0.27.0=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +jakarta.activation:jakarta.activation-api:1.2.1=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +javax.annotation:javax.annotation-api:1.3.2=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control +javax.inject:javax.inject:1=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,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,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool -net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool -org.apache.commons:commons-compress:1.27.1=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -org.apache.commons:commons-lang3:3.16.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -org.apache.httpcomponents:httpclient:4.5.6=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -org.apache.httpcomponents:httpcore:4.4.16=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -org.apache.httpcomponents:httpmime:4.5.6=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -org.bouncycastle:bcpkix-jdk18on:1.79=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -org.bouncycastle:bcprov-jdk18on:1.79=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -org.bouncycastle:bcutil-jdk18on:1.79=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool +net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,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=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,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=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action +org.apache.commons:commons-compress:1.27.1=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.commons:commons-lang3:3.16.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.httpcomponents:httpclient:4.5.6=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.httpcomponents:httpcore:4.4.16=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.apache.httpcomponents:httpmime:4.5.6=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.bouncycastle:bcpkix-jdk18on:1.79=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.bouncycastle:bcprov-jdk18on:1.79=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.bouncycastle:bcutil-jdk18on:1.79=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.checkerframework:checker-qual:3.12.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath -org.checkerframework:checker-qual:3.33.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher +org.checkerframework:checker-qual:3.33.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher org.checkerframework:checker-qual:3.37.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.checkerframework:checker-qual:3.43.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool +org.checkerframework:checker-qual:3.43.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,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.codehaus.groovy:groovy:3.0.22=androidLintTool -org.codehaus.mojo:animal-sniffer-annotations:1.23=_internal-unified-test-platform-core,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.codehaus.mojo:animal-sniffer-annotations:1.24=_internal-unified-test-platform-android-test-plugin-host-emulator-control +org.codehaus.mojo:animal-sniffer-annotations:1.23=_internal-unified-test-platform-core,debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-core +org.codehaus.mojo:animal-sniffer-annotations:1.24=_internal-unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-emulator-control org.ec4j.core:ec4j-core:1.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset -org.glassfish.jaxb:jaxb-runtime:2.3.2=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool -org.glassfish.jaxb:txw2:2.3.2=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool +org.glassfish.jaxb:jaxb-runtime:2.3.2=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle +org.glassfish.jaxb:txw2:2.3.2=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.hamcrest:hamcrest-library:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath org.jacoco:org.jacoco.agent:0.8.14=androidJacocoAnt @@ -606,136 +567,116 @@ org.jacoco:org.jacoco.ant:0.8.14=androidJacocoAnt org.jacoco:org.jacoco.core:0.8.14=androidJacocoAnt org.jacoco:org.jacoco.report:0.8.14=androidJacocoAnt org.jcommander:jcommander:1.85=detekt -org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.6=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.6=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.6=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.6=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.androidx.savedstate:savedstate-compose:1.3.6=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.androidx.savedstate:savedstate:1.3.6=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.animation:animation-core:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.animation:animation:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.annotation-internal:annotation:1.10.2=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.collection-internal:collection:1.10.2=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.foundation:foundation-layout:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.foundation:foundation:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.runtime:runtime-saveable:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.runtime:runtime:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.runtime:runtime:1.9.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -org.jetbrains.compose.ui:ui-geometry:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.ui:ui-graphics:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.ui:ui-text:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.ui:ui-unit:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.ui:ui-util:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.compose.ui:ui:1.10.2=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +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=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.6=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.6=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,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.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.animation:animation:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.annotation-internal:annotation:1.10.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.collection-internal:collection:1.10.2=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.foundation:foundation-layout:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.foundation:foundation:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.runtime:runtime-saveable:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.runtime:runtime:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui-geometry:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui-graphics:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui-text:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui-unit:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui-util:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.compose.ui:ui:1.10.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.intellij.deps:trove4j:1.0.20200330=detekt,ktlint,ktlintBaselineReporter,ktlintRuleset -org.jetbrains.kotlin:compose-group-mapping:2.3.20=composeMappingProducerClasspath +org.jetbrains.kotlin:compose-group-mapping:2.3.21=composeMappingProducerClasspath org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.22=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath org.jetbrains.kotlin:kotlin-bom:1.8.22=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlin:kotlin-build-tools-api:2.3.20=kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-build-tools-api:2.3.21=kotlinBuildToolsApiClasspath -org.jetbrains.kotlin:kotlin-build-tools-compat:2.3.20=kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-build-tools-compat:2.3.21=kotlinBuildToolsApiClasspath -org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.3.20=kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.3.21=kotlinBuildToolsApiClasspath -org.jetbrains.kotlin:kotlin-build-tools-impl:2.3.20=kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-build-tools-impl:2.3.21=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-build-tools-api:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-compat:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-impl:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-compiler-embeddable:2.0.21=detekt org.jetbrains.kotlin:kotlin-compiler-embeddable:2.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset -org.jetbrains.kotlin:kotlin-compiler-embeddable:2.3.20=kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-compiler-embeddable:2.3.21=kotlinBuildToolsApiClasspath -org.jetbrains.kotlin:kotlin-compiler-runner:2.3.20=kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-compiler-runner:2.3.21=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-compiler-runner:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:2.0.21=kotlinCompilerPluginClasspathReleaseUnitTest org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:2.3.21=kotlin-extension,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease -org.jetbrains.kotlin:kotlin-daemon-client:2.3.20=kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-daemon-client:2.3.21=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-daemon-client:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-daemon-embeddable:2.0.21=detekt org.jetbrains.kotlin:kotlin-daemon-embeddable:2.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset -org.jetbrains.kotlin:kotlin-daemon-embeddable:2.3.20=kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-daemon-embeddable:2.3.21=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.22=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseRuntimeClasspath org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset -org.jetbrains.kotlin:kotlin-reflect:1.8.21=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-reflect:1.8.21=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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.kotlin:kotlin-reflect:2.0.21=detekt -org.jetbrains.kotlin:kotlin-reflect:2.2.10=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool +org.jetbrains.kotlin:kotlin-reflect:2.2.10=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.jetbrains.kotlin:kotlin-script-runtime:2.0.21=detekt org.jetbrains.kotlin:kotlin-script-runtime:2.1.0=ktlint,ktlintBaselineReporter,ktlintRuleset -org.jetbrains.kotlin:kotlin-script-runtime:2.3.20=kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-script-runtime:2.3.21=kotlinBuildToolsApiClasspath -org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21=_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-script-runtime:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21=_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,unified-test-platform-android-test-plugin,unified-test-platform-core +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-launcher,unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-stdlib-common:2.0.21=detekt,releaseUnitTestRuntimeClasspath org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0=ktlintReporter -org.jetbrains.kotlin:kotlin-stdlib-common:2.2.10=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-gradle-work-action -org.jetbrains.kotlin:kotlin-stdlib-common:2.3.20=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -org.jetbrains.kotlin:kotlin-stdlib-common:2.3.21=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:2.2.10=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-gradle-work-action,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-gradle-work-action +org.jetbrains.kotlin:kotlin-stdlib-common:2.3.21=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=detekt,ktlintReporter -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,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-jdk8:1.8.0=detekt,ktlintReporter -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool -org.jetbrains.kotlin:kotlin-stdlib:1.8.21=_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core -org.jetbrains.kotlin:kotlin-stdlib:1.9.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,_internal-unified-test-platform-launcher,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,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:1.8.21=_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-core,unified-test-platform-android-test-plugin,unified-test-platform-core +org.jetbrains.kotlin:kotlin-stdlib:1.9.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-launcher,unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-stdlib:2.0.21=detekt,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains.kotlin:kotlin-stdlib:2.1.0=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset org.jetbrains.kotlin:kotlin-stdlib:2.1.21=composeMappingProducerClasspath -org.jetbrains.kotlin:kotlin-stdlib:2.2.10=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,androidLintTool -org.jetbrains.kotlin:kotlin-stdlib:2.3.20=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath,kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-stdlib:2.3.21=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlin:kotlin-tooling-core:2.3.20=kotlinCompilerClasspath -org.jetbrains.kotlin:kotlin-tooling-core:2.3.21=kotlinBuildToolsApiClasspath -org.jetbrains.kotlinx:atomicfu-jvm:0.22.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher -org.jetbrains.kotlinx:atomicfu:0.22.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.2.10=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-gradle-work-action,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.3.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlin:kotlin-tooling-core:2.3.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlinx:atomicfu-jvm:0.22.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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:atomicfu:0.22.0=_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0=releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +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=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,unified-test-platform-android-test-plugin-result-listener-gradle +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.6.4=detekt,ktlint,ktlintBaselineReporter,ktlintRuleset -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.11.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.9.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.9.0=debugAndroidTestLintChecksClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0=debugAndroidTestLintChecksClasspath -org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.8.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-datetime:0.6.0=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -org.jetbrains.kotlinx:kotlinx-datetime:0.8.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,unified-test-platform-android-test-plugin-result-listener-gradle +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=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,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=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,unified-test-platform-android-test-plugin-result-listener-gradle +org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.8.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-datetime:0.8.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.1=detekt -org.jetbrains.kotlinx:kotlinx-serialization-bom:1.11.0=debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.11.0=debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,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,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +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.4.1=detekt,ktlintReporter -org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0=debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-core:1.4.1=detekt,ktlintReporter -org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,releaseCompileClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.4.1=detekt,ktlintReporter -org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.3=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath -org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0=debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1=detekt,ktlintReporter -org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3=debugAndroidTestLintChecksClasspath,debugUnitTestLintChecksClasspath org.jetbrains.kotlinx:kover-jvm-agent:0.9.8=koverJvmAgent,koverJvmReporter org.jetbrains:annotations:13.0=composeMappingProducerClasspath,detekt,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset -org.jetbrains:annotations:23.0.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains:annotations:23.0.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-driver-instrumentation,_internal-unified-test-platform-android-test-plugin,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,_internal-unified-test-platform-core,_internal-unified-test-platform-gradle-work-action,_internal-unified-test-platform-launcher,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,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,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -org.jvnet.staxex:stax-ex:1.8.1=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,androidLintTool +org.jvnet.staxex:stax-ex:1.8.1=_internal-unified-test-platform-android-test-plugin-result-listener-gradle,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=androidJacocoAnt,androidLintTool org.ow2.asm:asm-tree:9.9=androidJacocoAnt,androidLintTool diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d81259a4..bd0cab25 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,7 @@ android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" + android:label="Notes" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Notes"> diff --git a/app/src/main/java/com/itlab/notes/di/AppModule.kt b/app/src/main/java/com/itlab/AppModule.kt similarity index 52% rename from app/src/main/java/com/itlab/notes/di/AppModule.kt rename to app/src/main/java/com/itlab/AppModule.kt index 722d44e9..223d6442 100644 --- a/app/src/main/java/com/itlab/notes/di/AppModule.kt +++ b/app/src/main/java/com/itlab/AppModule.kt @@ -1,4 +1,4 @@ -package com.itlab.notes.di +package com.itlab import com.itlab.domain.usecase.folderusecase.CreateFolderUseCase import com.itlab.domain.usecase.folderusecase.DeleteFolderUseCase @@ -7,18 +7,33 @@ import com.itlab.domain.usecase.folderusecase.ObserveFoldersUseCase import com.itlab.domain.usecase.folderusecase.UpdateFolderUseCase import com.itlab.domain.usecase.noteusecase.CreateNoteUseCase import com.itlab.domain.usecase.noteusecase.DeleteNoteUseCase +import com.itlab.domain.usecase.noteusecase.GetAllFavoritesUseCase +import com.itlab.domain.usecase.noteusecase.GetNoteUseCase import com.itlab.domain.usecase.noteusecase.GetUserIdUseCase import com.itlab.domain.usecase.noteusecase.MoveNoteToFolderUseCase import com.itlab.domain.usecase.noteusecase.ObserveNotesByFolderUseCase import com.itlab.domain.usecase.noteusecase.ObserveNotesUseCase +import com.itlab.domain.usecase.noteusecase.SearchNotesUseCase +import com.itlab.domain.usecase.noteusecase.SwitchFavoriteUseCase import com.itlab.domain.usecase.noteusecase.UpdateNoteUseCase +import com.itlab.domain.usecase.noteusecase.ValidateDuplicateNoteTitleUseCase +import com.itlab.notes.auth.AppSessionPreferences +import com.itlab.notes.auth.ClearLocalDataOnSignOut +import com.itlab.notes.onboarding.OnboardingPreferences +import com.itlab.notes.onboarding.OnboardingViewModel import com.itlab.notes.ui.NotesUseCases import com.itlab.notes.ui.NotesViewModel -import org.koin.androidx.viewmodel.dsl.viewModel +import com.itlab.notes.ui.auth.AuthViewModel +import org.koin.android.ext.koin.androidApplication +import org.koin.core.module.dsl.viewModel +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val appModule = module { + single { OnboardingPreferences(androidApplication()) } + single { AppSessionPreferences(androidApplication()) } + factory { ValidateDuplicateNoteTitleUseCase(get()) } factory { CreateNoteUseCase(get()) } factory { CreateFolderUseCase(get()) } factory { DeleteFolderUseCase(get(), get()) } @@ -31,6 +46,20 @@ val appModule = factory { MoveNoteToFolderUseCase(get(), get()) } factory { ObserveNotesUseCase(get()) } factory { GetUserIdUseCase(get()) } + factory { SearchNotesUseCase(get()) } + factory { SwitchFavoriteUseCase(get()) } + factory { GetAllFavoritesUseCase(get()) } + factory { GetNoteUseCase(get()) } + factory { UpdateFolderUseCase(get()) } + factory { GetFolderUseCase(get()) } + factory { + ClearLocalDataOnSignOut( + observeNotesUseCase = get(), + deleteNoteUseCase = get(), + observeFoldersUseCase = get(), + deleteFolderUseCase = get(), + ) + } factory { NotesUseCases( createFolderUseCase = get(), @@ -45,12 +74,21 @@ val appModule = moveNoteToFolderUseCase = get(), observeNotesUseCase = get(), getUserIdUseCase = get(), + searchNotesUseCase = get(), + switchFavoriteUseCase = get(), + getAllFavoritesUseCase = get(), + getNoteUseCase = get(), ) } + viewModelOf(::NotesViewModel) + viewModelOf(::OnboardingViewModel) viewModel { - NotesViewModel( - useCases = get(), + AuthViewModel( + firebaseAuth = get(), + app = androidApplication(), + appSessionPreferences = get(), + clearLocalDataOnSignOut = get(), ) } } diff --git a/app/src/main/java/com/itlab/notes/NotesApplication.kt b/app/src/main/java/com/itlab/notes/NotesApplication.kt index 3e85c04b..f8ac769a 100644 --- a/app/src/main/java/com/itlab/notes/NotesApplication.kt +++ b/app/src/main/java/com/itlab/notes/NotesApplication.kt @@ -1,8 +1,8 @@ package com.itlab.notes import android.app.Application +import com.itlab.appModule import com.itlab.data.di.dataModule -import com.itlab.notes.di.appModule import org.koin.android.ext.koin.androidContext import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.core.context.startKoin diff --git a/app/src/main/java/com/itlab/notes/auth/AppSessionPreferences.kt b/app/src/main/java/com/itlab/notes/auth/AppSessionPreferences.kt new file mode 100644 index 00000000..55c79c8d --- /dev/null +++ b/app/src/main/java/com/itlab/notes/auth/AppSessionPreferences.kt @@ -0,0 +1,33 @@ +package com.itlab.notes.auth + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal val Context.appSessionDataStore: DataStore by preferencesDataStore( + name = "app_session_preferences", +) + +class AppSessionPreferences( + private val context: Context, +) { + val continueOffline: Flow = + context.appSessionDataStore.data.map { prefs -> + prefs[CONTINUE_OFFLINE] ?: false + } + + suspend fun setContinueOffline(enabled: Boolean) { + context.appSessionDataStore.edit { prefs -> + prefs[CONTINUE_OFFLINE] = enabled + } + } + + private companion object { + val CONTINUE_OFFLINE = booleanPreferencesKey("continue_offline") + } +} diff --git a/app/src/main/java/com/itlab/notes/auth/ClearLocalDataOnSignOut.kt b/app/src/main/java/com/itlab/notes/auth/ClearLocalDataOnSignOut.kt new file mode 100644 index 00000000..f5c40a59 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/auth/ClearLocalDataOnSignOut.kt @@ -0,0 +1,24 @@ +package com.itlab.notes.auth + +import com.itlab.domain.usecase.folderusecase.DeleteFolderUseCase +import com.itlab.domain.usecase.folderusecase.ObserveFoldersUseCase +import com.itlab.domain.usecase.noteusecase.DeleteNoteUseCase +import com.itlab.domain.usecase.noteusecase.ObserveNotesUseCase +import kotlinx.coroutines.flow.first + +/** Removes all local notes and folders when the user signs out (app-layer only). */ +class ClearLocalDataOnSignOut( + private val observeNotesUseCase: ObserveNotesUseCase, + private val deleteNoteUseCase: DeleteNoteUseCase, + private val observeFoldersUseCase: ObserveFoldersUseCase, + private val deleteFolderUseCase: DeleteFolderUseCase, +) { + suspend operator fun invoke() { + observeNotesUseCase().first().forEach { note -> + deleteNoteUseCase(note.id) + } + observeFoldersUseCase().first().forEach { folder -> + deleteFolderUseCase(folder.id) + } + } +} diff --git a/app/src/main/java/com/itlab/notes/media/ImageRegionLuminance.kt b/app/src/main/java/com/itlab/notes/media/ImageRegionLuminance.kt new file mode 100644 index 00000000..d686f1c6 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/media/ImageRegionLuminance.kt @@ -0,0 +1,68 @@ +package com.itlab.notes.media + +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.get + +/** + * Estimates whether the top-end region of an image is light (for adaptive icon contrast). + */ +object ImageRegionLuminance { + private const val LIGHT_REGION_THRESHOLD = 0.55f + + fun isTopEndRegionLight( + drawable: Drawable, + sampleMaxSize: Int = 96, + ): Boolean { + val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: sampleMaxSize + val height = drawable.intrinsicHeight.takeIf { it > 0 } ?: sampleMaxSize + val scale = minOf(1f, sampleMaxSize.toFloat() / maxOf(width, height)) + val targetWidth = (width * scale).toInt().coerceAtLeast(1) + val targetHeight = (height * scale).toInt().coerceAtLeast(1) + val bitmap = drawable.toBitmap(targetWidth, targetHeight) + return try { + isTopEndRegionLight(bitmap) + } finally { + if (drawable !is BitmapDrawable || drawable.bitmap !== bitmap) { + bitmap.recycle() + } + } + } + + private fun isTopEndRegionLight(bitmap: Bitmap): Boolean { + val width = bitmap.width + val height = bitmap.height + if (width == 0 || height == 0) return true + + val startX = (width * 0.5f).toInt().coerceIn(0, width - 1) + val endY = (height * 0.5f).toInt().coerceAtLeast(1) + val step = maxOf(1, minOf(width, height) / 10) + + var luminanceSum = 0.0 + var count = 0 + var x = startX + while (x < width) { + var y = 0 + while (y < endY) { + val pixel = bitmap[x, y] + val composeColor = + Color( + red = android.graphics.Color.red(pixel) / 255f, + green = android.graphics.Color.green(pixel) / 255f, + blue = android.graphics.Color.blue(pixel) / 255f, + ) + luminanceSum += composeColor.luminance() + count++ + y += step + } + x += step + } + + if (count == 0) return true + return (luminanceSum / count) > LIGHT_REGION_THRESHOLD + } +} diff --git a/app/src/main/java/com/itlab/notes/media/NoteMediaDisplay.kt b/app/src/main/java/com/itlab/notes/media/NoteMediaDisplay.kt new file mode 100644 index 00000000..d8e6511f --- /dev/null +++ b/app/src/main/java/com/itlab/notes/media/NoteMediaDisplay.kt @@ -0,0 +1,21 @@ +package com.itlab.notes.media + +import com.itlab.domain.model.ContentItem +import com.itlab.domain.model.DataSource +import java.io.File + +fun List.withoutTextItems(): List = filterNot { it is ContentItem.Text } + +fun List.imageAttachments(): List = filterIsInstance() + +fun DataSource.toCoilModel(): Any? { + localPath + ?.takeIf { it.isNotBlank() } + ?.let { path -> + val file = File(path) + if (file.isFile && file.canRead() && file.length() > 0L) { + return file + } + } + return remoteUrl?.takeIf { it.isNotBlank() } +} diff --git a/app/src/main/java/com/itlab/notes/media/NoteMediaImport.kt b/app/src/main/java/com/itlab/notes/media/NoteMediaImport.kt new file mode 100644 index 00000000..a51a7b07 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/media/NoteMediaImport.kt @@ -0,0 +1,54 @@ +package com.itlab.notes.media + +import android.content.Context +import android.net.Uri +import android.webkit.MimeTypeMap +import com.itlab.domain.model.ContentItem +import com.itlab.domain.model.DataSource +import java.io.File +import java.util.UUID + +object NoteMediaImport { + private const val SUBDIR = "note_attachments" + + fun importImagesFromUris( + context: Context, + uris: List, + ): List = + uris.mapNotNull { uri -> + runCatching { importImageFromUri(context, uri) }.getOrNull() + } + + fun importImageFromUri( + context: Context, + uri: Uri, + ): ContentItem.Image { + val appContext = context.applicationContext + val resolver = appContext.contentResolver + val mime = resolver.getType(uri) ?: "image/jpeg" + val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg" + val dir = File(appContext.filesDir, SUBDIR).apply { mkdirs() } + val file = File(dir, "${UUID.randomUUID()}.$ext") + resolver.openInputStream(uri)?.use { input -> + file.outputStream().use { out -> input.copyTo(out) } + } ?: error("Cannot read selected image") + require(file.length() > 0L) { "Selected image is empty" } + return ContentItem.Image( + source = DataSource(localPath = file.absolutePath), + mimeType = mime, + ) + } + + fun deleteImportedFileIfOwned( + context: Context, + localPath: String?, + ) { + val path = localPath ?: return + val file = File(path) + if (!file.exists()) return + val root = File(context.applicationContext.filesDir, SUBDIR).absolutePath + if (file.absolutePath.startsWith(root)) { + file.delete() + } + } +} diff --git a/app/src/main/java/com/itlab/notes/onboarding/CoachMarkOverlay.kt b/app/src/main/java/com/itlab/notes/onboarding/CoachMarkOverlay.kt new file mode 100644 index 00000000..c30b24db --- /dev/null +++ b/app/src/main/java/com/itlab/notes/onboarding/CoachMarkOverlay.kt @@ -0,0 +1,225 @@ +package com.itlab.notes.onboarding + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + +@Composable +fun coachMarkOverlay( + step: OnboardingTourStep, + stepIndex: Int, + stepCount: Int, + targetBounds: Rect?, + screenMatchesStep: Boolean, + onSkip: () -> Unit, + onBack: () -> Unit, + onNext: () -> Unit, +) { + val colors = MaterialTheme.colorScheme + val scrim = Color.Black.copy(alpha = 0.62f) + val highlightPadding = 10.dp + val density = LocalDensity.current + val paddedHole = + remember(targetBounds, density) { + targetBounds?.let { bounds -> + val pad = with(density) { highlightPadding.toPx() } + Rect( + left = bounds.left - pad, + top = bounds.top - pad, + right = bounds.right + pad, + bottom = bounds.bottom + pad, + ) + } + } + + Box(modifier = Modifier.fillMaxSize()) { + if (screenMatchesStep && paddedHole != null) { + scrimWithHole( + hole = paddedHole, + scrimColor = scrim, + ) + } else { + Box( + modifier = + Modifier + .fillMaxSize() + .background(scrim) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = {}, + ), + ) + } + + Surface( + modifier = + Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 20.dp, vertical = 28.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + color = colors.surfaceContainerHigh, + tonalElevation = 6.dp, + ) { + Column(modifier = Modifier.padding(20.dp)) { + Text( + text = "Step ${stepIndex + 1} of $stepCount", + style = MaterialTheme.typography.labelMedium, + color = colors.onSurfaceVariant, + ) + Text( + text = step.title, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = colors.onSurface, + modifier = Modifier.padding(top = 4.dp), + ) + Text( + text = step.description, + style = MaterialTheme.typography.bodyMedium, + color = colors.onSurfaceVariant, + modifier = Modifier.padding(top = 8.dp), + ) + if (!screenMatchesStep && step.requiredScreen != null) { + Text( + text = "Follow the hint on screen to continue.", + style = MaterialTheme.typography.bodySmall, + color = colors.primary, + modifier = Modifier.padding(top = 8.dp), + ) + } + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton(onClick = onSkip) { + Text("Skip tour") + } + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (stepIndex > 0) { + TextButton(onClick = onBack) { + Text("Back") + } + } + Button(onClick = onNext) { + Text(if (stepIndex >= stepCount - 1) "Done" else "Next") + } + } + } + } + } + } +} + +@Composable +private fun scrimWithHole( + hole: Rect, + scrimColor: Color, +) { + val interaction = remember { MutableInteractionSource() } + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + val widthPx = constraints.maxWidth.toFloat() + val heightPx = constraints.maxHeight.toFloat() + val holeLeft = hole.left.coerceIn(0f, widthPx) + val holeTop = hole.top.coerceIn(0f, heightPx) + val holeRight = hole.right.coerceIn(holeLeft, widthPx) + val holeBottom = hole.bottom.coerceIn(holeTop, heightPx) + val density = LocalDensity.current + + scrimPanel( + x = 0f, + y = 0f, + width = widthPx, + height = holeTop, + scrimColor = scrimColor, + interaction = interaction, + density = density, + ) + scrimPanel( + x = 0f, + y = holeBottom, + width = widthPx, + height = heightPx - holeBottom, + scrimColor = scrimColor, + interaction = interaction, + density = density, + ) + scrimPanel( + x = 0f, + y = holeTop, + width = holeLeft, + height = holeBottom - holeTop, + scrimColor = scrimColor, + interaction = interaction, + density = density, + ) + scrimPanel( + x = holeRight, + y = holeTop, + width = widthPx - holeRight, + height = holeBottom - holeTop, + scrimColor = scrimColor, + interaction = interaction, + density = density, + ) + } +} + +@Composable +private fun scrimPanel( + x: Float, + y: Float, + width: Float, + height: Float, + scrimColor: Color, + interaction: MutableInteractionSource, + density: androidx.compose.ui.unit.Density, +) { + if (width <= 0f || height <= 0f) return + Box( + modifier = + Modifier + .offset { IntOffset(x.roundToInt(), y.roundToInt()) } + .width(with(density) { width.toDp() }) + .height(with(density) { height.toDp() }) + .background(scrimColor) + .clickable( + interactionSource = interaction, + indication = null, + onClick = {}, + ), + ) +} diff --git a/app/src/main/java/com/itlab/notes/onboarding/OnboardingModifiers.kt b/app/src/main/java/com/itlab/notes/onboarding/OnboardingModifiers.kt new file mode 100644 index 00000000..96e0a3c2 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/onboarding/OnboardingModifiers.kt @@ -0,0 +1,25 @@ +package com.itlab.notes.onboarding + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned + +val LocalOnboardingRegistrar = + staticCompositionLocalOf<((String, Rect?) -> Unit)?> { + null + } + +@Composable +fun Modifier.onboardingTarget(key: String): Modifier { + val registrar = LocalOnboardingRegistrar.current ?: return this + return onGloballyPositioned { coordinates -> + val bounds = coordinates.boundsInRoot() + registrar( + key, + if (bounds.width > 0f && bounds.height > 0f) bounds else null, + ) + } +} diff --git a/app/src/main/java/com/itlab/notes/onboarding/OnboardingPreferences.kt b/app/src/main/java/com/itlab/notes/onboarding/OnboardingPreferences.kt new file mode 100644 index 00000000..efa918ec --- /dev/null +++ b/app/src/main/java/com/itlab/notes/onboarding/OnboardingPreferences.kt @@ -0,0 +1,45 @@ +package com.itlab.notes.onboarding + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal val Context.onboardingDataStore: DataStore by preferencesDataStore( + name = "onboarding_preferences", +) + +class OnboardingPreferences( + private val context: Context, +) { + val welcomeCompleted: Flow = + context.onboardingDataStore.data.map { prefs -> + prefs[WELCOME_COMPLETED] ?: false + } + + val tourCompleted: Flow = + context.onboardingDataStore.data.map { prefs -> + prefs[TOUR_COMPLETED] ?: false + } + + suspend fun setWelcomeCompleted() { + context.onboardingDataStore.edit { prefs -> + prefs[WELCOME_COMPLETED] = true + } + } + + suspend fun setTourCompleted() { + context.onboardingDataStore.edit { prefs -> + prefs[TOUR_COMPLETED] = true + } + } + + private companion object { + val WELCOME_COMPLETED = booleanPreferencesKey("welcome_completed") + val TOUR_COMPLETED = booleanPreferencesKey("tour_completed") + } +} diff --git a/app/src/main/java/com/itlab/notes/onboarding/OnboardingTourSteps.kt b/app/src/main/java/com/itlab/notes/onboarding/OnboardingTourSteps.kt new file mode 100644 index 00000000..bc880649 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/onboarding/OnboardingTourSteps.kt @@ -0,0 +1,108 @@ +package com.itlab.notes.onboarding + +import com.itlab.notes.ui.NotesUiScreen + +enum class OnboardingScreenKind { + Directories, + DirectoryNotes, +} + +data class OnboardingTourStep( + val targetKey: String?, + val title: String, + val description: String, + val requiredScreen: OnboardingScreenKind? = null, + val requiresSignIn: Boolean = false, +) + +fun NotesUiScreen.onboardingScreenKind(): OnboardingScreenKind? = + when (this) { + NotesUiScreen.Directories -> OnboardingScreenKind.Directories + is NotesUiScreen.DirectoryNotes -> OnboardingScreenKind.DirectoryNotes + is NotesUiScreen.NoteEditor -> null + } + +object OnboardingTargets { + const val DIRECTORIES_SEARCH = "directories_search" + const val DIRECTORIES_ADD = "directories_add" + const val DIRECTORIES_FOLDER_ROW = "directories_folder_row" + const val DIRECTORIES_SIGN_OUT = "directories_sign_out" + const val NOTES_FAB = "notes_fab" + const val NOTES_SEARCH = "notes_search" + const val NOTES_NOTE_ROW = "notes_note_row" +} + +fun buildOnboardingTourSteps(showSignOut: Boolean): List = + buildList { + add( + OnboardingTourStep( + targetKey = OnboardingTargets.DIRECTORIES_SEARCH, + title = "Search", + description = "Find directories quickly. Inside a folder you can search notes too.", + requiredScreen = OnboardingScreenKind.Directories, + ), + ) + add( + OnboardingTourStep( + targetKey = OnboardingTargets.DIRECTORIES_ADD, + title = "New directory", + description = "Tap + to create a folder for your notes.", + requiredScreen = OnboardingScreenKind.Directories, + ), + ) + add( + OnboardingTourStep( + targetKey = OnboardingTargets.DIRECTORIES_FOLDER_ROW, + title = "Folders", + description = "Tap to open. Long-press a custom folder to rename or delete it in a dialog.", + requiredScreen = OnboardingScreenKind.Directories, + ), + ) + if (showSignOut) { + add( + OnboardingTourStep( + targetKey = OnboardingTargets.DIRECTORIES_SIGN_OUT, + title = "Sign out", + description = + "Use this when switching accounts. Local notes from the " + + "session are cleared on sign out.", + requiredScreen = OnboardingScreenKind.Directories, + requiresSignIn = true, + ), + ) + } + add( + OnboardingTourStep( + targetKey = null, + title = "Open a folder", + description = "Tap any directory to continue the tour and see note actions.", + requiredScreen = OnboardingScreenKind.DirectoryNotes, + ), + ) + add( + OnboardingTourStep( + targetKey = OnboardingTargets.NOTES_FAB, + title = "New note", + description = "Tap + to create a note in this folder.", + requiredScreen = OnboardingScreenKind.DirectoryNotes, + ), + ) + add( + OnboardingTourStep( + targetKey = OnboardingTargets.NOTES_SEARCH, + title = "Search notes", + description = "Filter notes by title or content in the current folder.", + requiredScreen = OnboardingScreenKind.DirectoryNotes, + ), + ) + add( + OnboardingTourStep( + targetKey = OnboardingTargets.NOTES_NOTE_ROW, + title = "Notes list", + description = + "Tap a note to edit. Long-press to select several notes, then move or delete" + + " them from the top bar.", + requiredScreen = OnboardingScreenKind.DirectoryNotes, + ), + ) + } diff --git a/app/src/main/java/com/itlab/notes/onboarding/OnboardingViewModel.kt b/app/src/main/java/com/itlab/notes/onboarding/OnboardingViewModel.kt new file mode 100644 index 00000000..47ee7c6f --- /dev/null +++ b/app/src/main/java/com/itlab/notes/onboarding/OnboardingViewModel.kt @@ -0,0 +1,182 @@ +package com.itlab.notes.onboarding + +import androidx.compose.ui.geometry.Rect +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.itlab.notes.ui.NotesUiScreen +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +private data class TourCoreState( + val isReady: Boolean, + val welcomeDone: Boolean, + val tourDone: Boolean, + val tourOn: Boolean, + val step: Int, +) + +data class OnboardingUiState( + val isReady: Boolean = false, + val showWelcome: Boolean = false, + val showTour: Boolean = false, + val tourStepIndex: Int = 0, + val showSignOutStep: Boolean = false, + val currentScreenKind: OnboardingScreenKind? = OnboardingScreenKind.Directories, +) + +class OnboardingViewModel( + private val preferences: OnboardingPreferences, +) : ViewModel() { + private val welcomeCompleted = MutableStateFlow(false) + private val tourCompleted = MutableStateFlow(false) + private val tourActive = MutableStateFlow(false) + private val tourStepIndex = MutableStateFlow(0) + private val showSignOutStep = MutableStateFlow(false) + private val currentScreenKind = MutableStateFlow(OnboardingScreenKind.Directories) + private val targetBounds = MutableStateFlow>(emptyMap()) + val targetBoundsState: StateFlow> = targetBounds.asStateFlow() + private val preferencesLoaded = MutableStateFlow(false) + private val pendingTourStart = MutableStateFlow(false) + + val uiState: StateFlow = + combine( + combine( + preferencesLoaded, + welcomeCompleted, + tourCompleted, + tourActive, + tourStepIndex, + ) { loaded, welcomeDone, tourDone, tourOn, step -> + TourCoreState( + isReady = loaded, + welcomeDone = welcomeDone, + tourDone = tourDone, + tourOn = tourOn, + step = step, + ) + }, + showSignOutStep, + currentScreenKind, + ) { core, signOut, screen -> + OnboardingUiState( + isReady = core.isReady, + showWelcome = core.isReady && !core.welcomeDone, + showTour = core.isReady && core.welcomeDone && !core.tourDone && core.tourOn, + tourStepIndex = core.step, + showSignOutStep = signOut, + currentScreenKind = screen, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = OnboardingUiState(isReady = false), + ) + + val tourSteps: StateFlow> = + showSignOutStep + .combine(tourStepIndex) { signOut, _ -> buildOnboardingTourSteps(signOut) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = buildOnboardingTourSteps(false), + ) + + init { + viewModelScope.launch { + preferences.welcomeCompleted.collect { welcomeCompleted.value = it } + } + viewModelScope.launch { + preferences.tourCompleted.collect { tourCompleted.value = it } + } + viewModelScope.launch { + preferences.welcomeCompleted.first() + preferences.tourCompleted.first() + preferencesLoaded.value = true + } + } + + fun startTourIfNeeded() { + if (!welcomeCompleted.value || tourCompleted.value) return + if (tourActive.value) { + pendingTourStart.value = false + return + } + pendingTourStart.value = true + } + + /** Call when the main notes UI is visible (after auth / offline). */ + fun activateTourIfPending() { + if (!pendingTourStart.value || tourCompleted.value || !welcomeCompleted.value) return + pendingTourStart.value = false + tourActive.value = true + tourStepIndex.value = 0 + } + + fun updateShowSignOutStep(show: Boolean) { + showSignOutStep.value = show + } + + fun updateCurrentScreen(screen: NotesUiScreen) { + currentScreenKind.value = screen.onboardingScreenKind() + } + + fun registerTarget( + key: String, + bounds: Rect?, + ) { + targetBounds.update { current -> + if (bounds == null) { + current - key + } else { + current + (key to bounds) + } + } + } + + fun completeWelcome() { + viewModelScope.launch { + preferences.setWelcomeCompleted() + welcomeCompleted.value = true + pendingTourStart.value = true + } + } + + fun skipWelcome() { + completeWelcome() + } + + fun skipTour() { + finishTour() + } + + fun nextTourStep() { + val steps = buildOnboardingTourSteps(showSignOutStep.value) + val clampedIndex = tourStepIndex.value.coerceIn(0, steps.lastIndex) + if (clampedIndex >= steps.lastIndex) { + finishTour() + } else { + tourStepIndex.value = clampedIndex + 1 + } + } + + fun previousTourStep() { + if (tourStepIndex.value > 0) { + tourStepIndex.value -= 1 + } + } + + private fun finishTour() { + viewModelScope.launch { + preferences.setTourCompleted() + tourCompleted.value = true + tourActive.value = false + } + } +} diff --git a/app/src/main/java/com/itlab/notes/onboarding/WelcomeOnboardingScreen.kt b/app/src/main/java/com/itlab/notes/onboarding/WelcomeOnboardingScreen.kt new file mode 100644 index 00000000..4dd79ca7 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/onboarding/WelcomeOnboardingScreen.kt @@ -0,0 +1,220 @@ +package com.itlab.notes.onboarding + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.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.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Cloud +import androidx.compose.material.icons.rounded.Folder +import androidx.compose.material.icons.rounded.Note +import androidx.compose.material.icons.rounded.WavingHand +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.lerp +import kotlinx.coroutines.launch +import kotlin.math.absoluteValue + +private data class WelcomeSlide( + val icon: ImageVector, + val title: String, + val body: String, +) + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun welcomeOnboardingScreen( + onFinished: () -> Unit, + onSkip: () -> Unit, +) { + val slides = + listOf( + WelcomeSlide( + icon = Icons.Rounded.WavingHand, + title = "Welcome to Notes", + body = "A simple workspace for folders, notes, and attachments on your device.", + ), + WelcomeSlide( + icon = Icons.Rounded.Folder, + title = "Organize with directories", + body = "Group notes into folders. Use All Notes and Favorites for quick access.", + ), + WelcomeSlide( + icon = Icons.Rounded.Note, + title = "Write and attach", + body = "Open a note to edit text, add images, and mark favorites.", + ), + WelcomeSlide( + icon = Icons.Rounded.Cloud, + title = "Ready to explore", + body = "Next we will walk you through the main controls in the app.", + ), + ) + val pagerState = rememberPagerState(pageCount = { slides.size }) + val scope = rememberCoroutineScope() + val colors = MaterialTheme.colorScheme + val isLast = pagerState.currentPage == slides.lastIndex + + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = colors.background, + ) { padding -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 24.dp, vertical = 16.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + TextButton(onClick = onSkip) { + Text("Skip") + } + } + HorizontalPager( + state = pagerState, + modifier = + Modifier + .weight(1f) + .fillMaxWidth(), + ) { page -> + welcomeSlideContent(slide = slides[page]) + } + welcomePagerIndicator( + pagerState = pagerState, + pageCount = slides.size, + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 20.dp), + ) + Button( + onClick = { + if (isLast) { + onFinished() + } else { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + }, + modifier = + Modifier + .fillMaxWidth() + .height(48.dp), + ) { + Text(if (isLast) "Get started" else "Next") + } + Spacer(Modifier.height(8.dp)) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun welcomePagerIndicator( + pagerState: PagerState, + pageCount: Int, + modifier: Modifier = Modifier, + activeWidth: Dp = 28.dp, + dotSize: Dp = 8.dp, + dotSpacing: Dp = 10.dp, +) { + val colors = MaterialTheme.colorScheme + val activeColor = colors.primary + val inactiveColor = colors.onSurfaceVariant.copy(alpha = 0.35f) + val scrollPosition by remember { + derivedStateOf { + pagerState.currentPage + pagerState.currentPageOffsetFraction + } + } + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(dotSpacing, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(pageCount) { index -> + val distance = (scrollPosition - index).absoluteValue.coerceIn(0f, 1f) + val width = lerp(activeWidth, dotSize, distance) + val color = lerp(activeColor, inactiveColor, distance) + Box( + modifier = + Modifier + .height(dotSize) + .width(width) + .clip(CircleShape) + .background(color), + ) + } + } +} + +@Composable +private fun welcomeSlideContent(slide: WelcomeSlide) { + val colors = MaterialTheme.colorScheme + Column( + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 8.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = slide.icon, + contentDescription = null, + tint = colors.primary, + modifier = Modifier.size(72.dp), + ) + Spacer(Modifier.height(28.dp)) + Text( + text = slide.title, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold, + color = colors.onSurface, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(12.dp)) + Text( + text = slide.body, + style = MaterialTheme.typography.bodyLarge, + color = colors.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } +} diff --git a/app/src/main/java/com/itlab/notes/ui/NotesApp.kt b/app/src/main/java/com/itlab/notes/ui/NotesApp.kt index 2abb3c45..81bfbf10 100644 --- a/app/src/main/java/com/itlab/notes/ui/NotesApp.kt +++ b/app/src/main/java/com/itlab/notes/ui/NotesApp.kt @@ -1,7 +1,22 @@ package com.itlab.notes.ui +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import com.itlab.notes.onboarding.LocalOnboardingRegistrar +import com.itlab.notes.onboarding.OnboardingViewModel +import com.itlab.notes.onboarding.coachMarkOverlay +import com.itlab.notes.onboarding.welcomeOnboardingScreen +import com.itlab.notes.ui.auth.AuthViewModel +import com.itlab.notes.ui.auth.authScreen import com.itlab.notes.ui.editor.editorScreen +import com.itlab.notes.ui.filterDirectoriesByName import com.itlab.notes.ui.notes.NotesListActions import com.itlab.notes.ui.notes.directoriesScreen import com.itlab.notes.ui.notes.notesListScreen @@ -9,13 +24,99 @@ import org.koin.androidx.compose.koinViewModel @Composable fun notesApp() { + val onboardingViewModel: OnboardingViewModel = koinViewModel() + val onboardingState by onboardingViewModel.uiState.collectAsState() + val tourSteps by onboardingViewModel.tourSteps.collectAsState() + val tourTargetBounds by onboardingViewModel.targetBoundsState.collectAsState() + + if (!onboardingState.isReady) { + return + } + + val authViewModel: AuthViewModel = koinViewModel() + val authState by authViewModel.uiState.collectAsState() + if (!authState.isSessionReady) { + return + } + if (!authState.isSessionActive && !authState.continueOffline) { + authScreen(authViewModel) + return + } + + if (onboardingState.showWelcome) { + welcomeOnboardingScreen( + onFinished = onboardingViewModel::completeWelcome, + onSkip = onboardingViewModel::skipWelcome, + ) + return + } + + val sessionKey = authViewModel.sessionKey ?: return + key(sessionKey) { + LaunchedEffect(sessionKey, onboardingState.showWelcome) { + if (!onboardingState.showWelcome) { + onboardingViewModel.startTourIfNeeded() + onboardingViewModel.activateTourIfPending() + } + } + CompositionLocalProvider( + LocalOnboardingRegistrar provides { targetKey, bounds -> + onboardingViewModel.registerTarget(targetKey, bounds) + }, + ) { + Box(modifier = Modifier.fillMaxSize()) { + notesMain( + authViewModel = authViewModel, + onboardingViewModel = onboardingViewModel, + ) + if (onboardingState.showTour && tourSteps.isNotEmpty()) { + val stepIndex = onboardingState.tourStepIndex.coerceIn(0, tourSteps.lastIndex) + val step = tourSteps[stepIndex] + val screenMatches = + step.requiredScreen == null || + step.requiredScreen == onboardingState.currentScreenKind + coachMarkOverlay( + step = step, + stepIndex = stepIndex, + stepCount = tourSteps.size, + targetBounds = step.targetKey?.let { tourTargetBounds[it] }, + screenMatchesStep = screenMatches, + onSkip = onboardingViewModel::skipTour, + onBack = onboardingViewModel::previousTourStep, + onNext = onboardingViewModel::nextTourStep, + ) + } + } + } + } +} + +@Composable +private fun notesMain( + authViewModel: AuthViewModel, + onboardingViewModel: OnboardingViewModel, +) { val viewModel: NotesViewModel = koinViewModel() + val authState by authViewModel.uiState.collectAsState() val state = viewModel.uiState + LaunchedEffect(state.screen, authState.isSessionActive) { + onboardingViewModel.updateCurrentScreen(state.screen) + onboardingViewModel.updateShowSignOutStep(authState.isSessionActive) + } + when (val screen = state.screen) { NotesUiScreen.Directories -> { directoriesScreen( - directories = state.directories, + directories = + filterDirectoriesByName( + directories = state.directories, + query = state.directorySearchQuery, + ), + searchQuery = state.directorySearchQuery, + onSearchQueryChange = { query -> + viewModel.onEvent(NotesUiEvent.DirectorySearchQueryChanged(query)) + }, onCreateDirectory = { name -> viewModel.onEvent(NotesUiEvent.CreateDirectory(name)) }, @@ -28,14 +129,23 @@ fun notesApp() { onDirectoryClick = { directory -> viewModel.onEvent(NotesUiEvent.OpenDirectory(directory)) }, + showSignOut = authState.isSessionActive, + onSignOut = { authViewModel.signOut() }, + showReturnToSignIn = authState.continueOffline && !authState.isSessionActive, + onReturnToSignIn = { authViewModel.exitOfflineToSignIn() }, ) } is NotesUiScreen.DirectoryNotes -> { notesListScreen( + directoryId = screen.directory.id, directoryName = screen.directory.name, notes = state.notes, - directories = state.directories.filter { it.id != "all" }, + searchQuery = state.notesSearchQuery, + onSearchQueryChange = { query -> + viewModel.onEvent(NotesUiEvent.NotesSearchQueryChanged(query)) + }, + directories = state.directories, actions = NotesListActions( onBack = { viewModel.onEvent(NotesUiEvent.BackToDirectories) }, @@ -59,11 +169,18 @@ fun notesApp() { is NotesUiScreen.NoteEditor -> { editorScreen( directoryName = screen.directory.name, + directoryId = screen.directory.id, note = screen.note, - onBack = { viewModel.onEvent(NotesUiEvent.BackToDirectoryNotes) }, + onBack = { draft -> viewModel.onEvent(NotesUiEvent.LeaveEditor(draft)) }, + onPersist = { draft -> + viewModel.onEvent(NotesUiEvent.PersistNote(draft)) + }, onSave = { updated -> viewModel.onEvent(NotesUiEvent.SaveNote(updated)) }, + onToggleFavorite = { + viewModel.onEvent(NotesUiEvent.ToggleNoteFavorite(screen.note.id)) + }, ) } } diff --git a/app/src/main/java/com/itlab/notes/ui/NotesUiContract.kt b/app/src/main/java/com/itlab/notes/ui/NotesUiContract.kt index 054570f4..1217c1d7 100644 --- a/app/src/main/java/com/itlab/notes/ui/NotesUiContract.kt +++ b/app/src/main/java/com/itlab/notes/ui/NotesUiContract.kt @@ -24,6 +24,8 @@ data class NotesUiState( val screen: NotesUiScreen = NotesUiScreen.Directories, val directories: List = emptyList(), val notes: List = emptyList(), + val notesSearchQuery: String = "", + val directorySearchQuery: String = "", ) sealed interface NotesUiEvent { @@ -49,10 +51,20 @@ sealed interface NotesUiEvent { data object BackToDirectoryNotes : NotesUiEvent + /** Saves pending editor changes (if any), then returns to the notes list. */ + data class LeaveEditor( + val note: NoteItemUi, + ) : NotesUiEvent + data class SaveNote( val note: NoteItemUi, ) : NotesUiEvent + /** Persists editor changes without leaving the editor screen. */ + data class PersistNote( + val note: NoteItemUi, + ) : NotesUiEvent + data class DeleteNote( val noteId: String, ) : NotesUiEvent @@ -66,6 +78,18 @@ sealed interface NotesUiEvent { val noteId: String, val targetDirectoryId: String, ) : NotesUiEvent + + data class NotesSearchQueryChanged( + val query: String, + ) : NotesUiEvent + + data class DirectorySearchQueryChanged( + val query: String, + ) : NotesUiEvent + + data class ToggleNoteFavorite( + val noteId: String, + ) : NotesUiEvent } interface NotesViewModelContract { diff --git a/app/src/main/java/com/itlab/notes/ui/NotesUseCases.kt b/app/src/main/java/com/itlab/notes/ui/NotesUseCases.kt index 7b2ffc16..c4bd321b 100644 --- a/app/src/main/java/com/itlab/notes/ui/NotesUseCases.kt +++ b/app/src/main/java/com/itlab/notes/ui/NotesUseCases.kt @@ -7,10 +7,14 @@ import com.itlab.domain.usecase.folderusecase.ObserveFoldersUseCase import com.itlab.domain.usecase.folderusecase.UpdateFolderUseCase import com.itlab.domain.usecase.noteusecase.CreateNoteUseCase import com.itlab.domain.usecase.noteusecase.DeleteNoteUseCase +import com.itlab.domain.usecase.noteusecase.GetAllFavoritesUseCase +import com.itlab.domain.usecase.noteusecase.GetNoteUseCase import com.itlab.domain.usecase.noteusecase.GetUserIdUseCase import com.itlab.domain.usecase.noteusecase.MoveNoteToFolderUseCase import com.itlab.domain.usecase.noteusecase.ObserveNotesByFolderUseCase import com.itlab.domain.usecase.noteusecase.ObserveNotesUseCase +import com.itlab.domain.usecase.noteusecase.SearchNotesUseCase +import com.itlab.domain.usecase.noteusecase.SwitchFavoriteUseCase import com.itlab.domain.usecase.noteusecase.UpdateNoteUseCase data class NotesUseCases( @@ -26,4 +30,8 @@ data class NotesUseCases( val moveNoteToFolderUseCase: MoveNoteToFolderUseCase, val observeNotesUseCase: ObserveNotesUseCase, val getUserIdUseCase: GetUserIdUseCase, + val searchNotesUseCase: SearchNotesUseCase, + val switchFavoriteUseCase: SwitchFavoriteUseCase, + val getAllFavoritesUseCase: GetAllFavoritesUseCase, + val getNoteUseCase: GetNoteUseCase, ) diff --git a/app/src/main/java/com/itlab/notes/ui/NotesViewModel.kt b/app/src/main/java/com/itlab/notes/ui/NotesViewModel.kt index bc25d8ce..93850534 100644 --- a/app/src/main/java/com/itlab/notes/ui/NotesViewModel.kt +++ b/app/src/main/java/com/itlab/notes/ui/NotesViewModel.kt @@ -8,9 +8,19 @@ import androidx.lifecycle.viewModelScope import com.itlab.domain.model.ContentItem import com.itlab.domain.model.Note import com.itlab.domain.model.NoteFolder +import com.itlab.notes.media.withoutTextItems +import com.itlab.notes.ui.notes.ALL_DIRECTORY_ID import com.itlab.notes.ui.notes.DirectoryItemUi +import com.itlab.notes.ui.notes.FAVORITES_DIRECTORY_ID import com.itlab.notes.ui.notes.NoteItemUi +import com.itlab.notes.ui.notes.RECENT_DIRECTORY_ID +import com.itlab.notes.ui.notes.canCreateNotesInDirectory +import com.itlab.notes.ui.notes.coerceDirectoryNameLength +import com.itlab.notes.ui.notes.isVirtualDirectory +import com.itlab.notes.ui.toSingleLineText import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class NotesViewModel( @@ -48,7 +58,11 @@ class NotesViewModel( is NotesUiEvent.OpenNote -> openNote(event.note) NotesUiEvent.CreateNote -> createNote() is NotesUiEvent.CreateDirectory -> { - val normalized = event.name.trim() + val normalized = + event.name + .toSingleLineText() + .trim() + .coerceDirectoryNameLength() if (normalized.isNotBlank()) { viewModelScope.launch { useCases.createFolderUseCase(NoteFolder(name = normalized)) @@ -58,7 +72,7 @@ class NotesViewModel( is NotesUiEvent.RenameDirectory -> renameDirectory(event) is NotesUiEvent.DeleteDirectory -> deleteDirectory(event.directoryId) is NotesUiEvent.MoveNoteToDirectory -> { - if (event.targetDirectoryId == "all") return + if (isVirtualDirectory(event.targetDirectoryId)) return viewModelScope.launch { useCases.moveNoteToFolderUseCase( folderId = event.targetDirectoryId, @@ -66,19 +80,36 @@ class NotesViewModel( ) } } + is NotesUiEvent.ToggleNoteFavorite -> toggleNoteFavorite(event.noteId) NotesUiEvent.BackToDirectoryNotes -> backToDirectoryNotes() + is NotesUiEvent.LeaveEditor -> leaveEditor(event.note) is NotesUiEvent.SaveNote -> saveNote(event.note) + is NotesUiEvent.PersistNote -> persistNote(event.note) is NotesUiEvent.DeleteNote -> { viewModelScope.launch { useCases.deleteNoteUseCase(event.noteId) } } + is NotesUiEvent.NotesSearchQueryChanged -> onNotesSearchQueryChanged(event.query) + is NotesUiEvent.DirectorySearchQueryChanged -> { + uiState = uiState.copy(directorySearchQuery = event.query) + } } } + private fun onNotesSearchQueryChanged(query: String) { + val directory = (uiState.screen as? NotesUiScreen.DirectoryNotes)?.directory ?: return + uiState = uiState.copy(notesSearchQuery = query) + startNotesCollection(directory, query) + } + private fun renameDirectory(event: NotesUiEvent.RenameDirectory) { - val normalized = event.newName.trim() - if (normalized.isBlank() || event.directoryId == "all") return + val normalized = + event.newName + .toSingleLineText() + .trim() + .coerceDirectoryNameLength() + if (normalized.isBlank() || isVirtualDirectory(event.directoryId)) return viewModelScope.launch { val existingFolder = useCases.getFolderUseCase(event.directoryId) ?: return@launch useCases.updateFolderUseCase(existingFolder.copy(name = normalized)) @@ -86,7 +117,7 @@ class NotesViewModel( } private fun deleteDirectory(directoryId: String) { - if (directoryId == "all") return + if (isVirtualDirectory(directoryId)) return viewModelScope.launch { useCases.deleteFolderUseCase(directoryId) if ((uiState.screen as? NotesUiScreen.DirectoryNotes)?.directory?.id == directoryId) { @@ -100,94 +131,185 @@ class NotesViewModel( uiState.copy( screen = NotesUiScreen.DirectoryNotes(directory = directory), notes = emptyList(), + notesSearchQuery = "", ) + startNotesCollection(directory, searchQuery = "") + } + + private fun startNotesCollection( + directory: DirectoryItemUi, + searchQuery: String, + ) { notesJob?.cancel() - val isAll = directory.id == "all" notesJob = viewModelScope.launch { - val flow = - if (isAll) { - useCases.observeNotesUseCase() - } else { - useCases.observeNotesByFolderUseCase(directory.id) - } - - flow.collect { notes -> + notesFlow(directory, searchQuery).collect { notes -> + val opened = uiState.screen as? NotesUiScreen.DirectoryNotes ?: return@collect uiState = uiState.copy( notes = notes.map { it.toUi() }, + notesSearchQuery = searchQuery, screen = NotesUiScreen.DirectoryNotes( - directory = directory.copy(noteCount = notes.size), + directory = opened.directory.copy(noteCount = notes.size), ), ) } } } + private fun notesFlow( + directory: DirectoryItemUi, + searchQuery: String, + ): Flow> { + val normalizedQuery = searchQuery.trim() + return if (normalizedQuery.isBlank()) { + when (directory.id) { + ALL_DIRECTORY_ID -> useCases.observeNotesUseCase() + FAVORITES_DIRECTORY_ID -> useCases.getAllFavoritesUseCase() + RECENT_DIRECTORY_ID -> + useCases.observeNotesUseCase().map { notes -> + notes.sortedByDescending { it.updatedAt } + } + else -> useCases.observeNotesByFolderUseCase(directory.id) + } + } else { + val searchFlow = + useCases.searchNotesUseCase( + query = normalizedQuery, + folderId = directory.folderIdForSearch(), + ) + when (directory.id) { + FAVORITES_DIRECTORY_ID -> + searchFlow.map { notes -> notes.filter { it.isFavorite } } + RECENT_DIRECTORY_ID -> + searchFlow.map { notes -> + notes.sortedByDescending { it.updatedAt } + } + else -> searchFlow + } + } + } + private val backToDirectories: () -> Unit = { uiState = uiState.copy( screen = NotesUiScreen.Directories, notes = emptyList(), + notesSearchQuery = "", ) } private fun openNote(note: NoteItemUi) { - val dir = (uiState.screen as? NotesUiScreen.DirectoryNotes)?.directory - if (dir != null) { - uiState = - uiState.copy( - screen = NotesUiScreen.NoteEditor(directory = dir, note = note), - ) - } + val dir = (uiState.screen as? NotesUiScreen.DirectoryNotes)?.directory ?: return + notesJob?.cancel() + uiState = + uiState.copy( + screen = NotesUiScreen.NoteEditor(directory = dir, note = note), + ) } private fun createNote() { - val dir = (uiState.screen as? NotesUiScreen.DirectoryNotes)?.directory - if (dir != null) { - val newNote = - Note( - folderId = dir.id.asDomainFolderId(), - userId = useCases.getUserIdUseCase() ?: "anonymous_user", - ).toUi() - uiState = - uiState.copy( - screen = NotesUiScreen.NoteEditor(directory = dir, note = newNote), - ) - } + val dir = (uiState.screen as? NotesUiScreen.DirectoryNotes)?.directory ?: return + if (!canCreateNotesInDirectory(dir.id)) return + notesJob?.cancel() + val userId = useCases.getUserIdUseCase() ?: "local_user" + val newNote = Note(userId = userId, folderId = dir.id.asDomainFolderId()).toUi() + uiState = + uiState.copy( + screen = NotesUiScreen.NoteEditor(directory = dir, note = newNote), + ) } private fun backToDirectoryNotes() { - val editor = uiState.screen as? NotesUiScreen.NoteEditor - if (editor != null) { - uiState = uiState.copy(screen = NotesUiScreen.DirectoryNotes(directory = editor.directory)) + val editor = uiState.screen as? NotesUiScreen.NoteEditor ?: return + val directory = editor.directory + uiState = uiState.copy(screen = NotesUiScreen.DirectoryNotes(directory = directory)) + startNotesCollection(directory, uiState.notesSearchQuery) + } + + private fun toggleNoteFavorite(noteId: String) { + viewModelScope.launch { + useCases.switchFavoriteUseCase(noteId) + val editor = uiState.screen as? NotesUiScreen.NoteEditor + if (editor?.note?.id == noteId) { + uiState = + uiState.copy( + screen = + editor.copy( + note = editor.note.copy(isFavorite = !editor.note.isFavorite), + ), + ) + } + } + } + + private fun persistNote(note: NoteItemUi) { + val editor = uiState.screen as? NotesUiScreen.NoteEditor ?: return + viewModelScope.launch { + persistNoteToRepository(note, editor.directory) + } + } + + private fun leaveEditor(note: NoteItemUi) { + val editor = uiState.screen as? NotesUiScreen.NoteEditor ?: return + viewModelScope.launch { + if (note.title.trim().isNotEmpty()) { + persistNoteToRepository(note, editor.directory) + } + navigateBackToDirectoryNotes(editor.directory) } } private fun saveNote(note: NoteItemUi) { val editor = uiState.screen as? NotesUiScreen.NoteEditor ?: return viewModelScope.launch { - val userId = useCases.getUserIdUseCase() ?: "anonymous_user" - val targetFolderId = note.folderId ?: editor.directory.id.asDomainFolderId() - val existing = latestNotes.firstOrNull { it.id == note.id } + if (!persistNoteToRepository(note, editor.directory)) return@launch + navigateBackToDirectoryNotes(editor.directory) + } + } + + private fun navigateBackToDirectoryNotes(directory: DirectoryItemUi) { + uiState = uiState.copy(screen = NotesUiScreen.DirectoryNotes(directory = directory)) + startNotesCollection(directory, uiState.notesSearchQuery) + } + + private suspend fun persistNoteToRepository( + note: NoteItemUi, + directory: DirectoryItemUi, + ): Boolean { + if (note.title.trim().isEmpty()) return false + val targetFolderId = note.folderId ?: directory.id.asDomainFolderId() + val existing = useCases.getNoteUseCase(note.id) + val result = if (existing != null) { useCases.updateNoteUseCase(existing.applyUiUpdate(note, targetFolderId)) } else { - useCases.createNoteUseCase(note.toDomain(userId = userId, folderId = targetFolderId)) + useCases.createNoteUseCase(note.toDomain(folderId = targetFolderId)) } - uiState = uiState.copy(screen = NotesUiScreen.DirectoryNotes(directory = editor.directory)) + if (result.isFailure) return false + val editor = uiState.screen as? NotesUiScreen.NoteEditor + if (editor?.note?.id == note.id) { + uiState = uiState.copy(screen = editor.copy(note = note)) } + return true } private fun recomputeDirectories() { val countsByFolderId = latestNotes.groupingBy { it.folderId }.eachCount() val allNotesCount = latestNotes.size - val allNotesDir = DirectoryItemUi(id = "all", name = "All Notes", noteCount = allNotesCount) + val favoritesCount = latestNotes.count { it.isFavorite } + val allNotesDir = DirectoryItemUi(id = ALL_DIRECTORY_ID, name = "All Notes", noteCount = allNotesCount) + val favoritesDir = + DirectoryItemUi( + id = FAVORITES_DIRECTORY_ID, + name = "Favorites", + noteCount = favoritesCount, + ) val directories = - listOf(allNotesDir) + + listOf(allNotesDir, favoritesDir) + latestFolders.map { folder -> val count = countsByFolderId[folder.id] ?: 0 folder.toUi(noteCount = count) @@ -217,41 +339,69 @@ internal fun NoteFolder.toUi(noteCount: Int): DirectoryItemUi = internal fun Note.toUi(): NoteItemUi = NoteItemUi( id = id, + userId = userId, title = title, content = contentItems .filterIsInstance() .joinToString("\n") { it.text }, folderId = folderId, + attachments = contentItems.withoutTextItems(), + isFavorite = isFavorite, ) -internal fun NoteItemUi.toDomain( - userId: String, - folderId: String?, -): Note = +internal fun NoteItemUi.toContentItems(): List = + buildList { + if (content.isNotBlank()) add(ContentItem.Text(text = content)) + addAll(attachments.withoutTextItems()) + } + +internal fun NoteItemUi.toDomain(folderId: String?): Note = Note( + userId = userId, id = id, title = title, folderId = folderId, - contentItems = listOf(ContentItem.Text(text = content)), - userId = userId, + contentItems = toContentItems(), + isFavorite = isFavorite, ) internal fun Note.applyUiUpdate( ui: NoteItemUi, targetFolderId: String?, -): Note { - val nonTextContent = contentItems.filterNot { it is ContentItem.Text } - val updatedText = - ui.content - .takeIf { it.isNotBlank() } - ?.let { ContentItem.Text(text = it) } - - return copy( +): Note = + copy( title = ui.title, folderId = targetFolderId, - contentItems = if (updatedText != null) nonTextContent + updatedText else nonTextContent, + contentItems = ui.toContentItems(), + isFavorite = ui.isFavorite, ) -} -internal fun String.asDomainFolderId(): String? = if (this == "all") null else this +internal fun String.asDomainFolderId(): String? = + when (this) { + ALL_DIRECTORY_ID, + RECENT_DIRECTORY_ID, + FAVORITES_DIRECTORY_ID, + -> null + else -> this + } + +internal fun DirectoryItemUi.folderIdForSearch(): String? = + when (id) { + ALL_DIRECTORY_ID, + RECENT_DIRECTORY_ID, + FAVORITES_DIRECTORY_ID, + -> null + else -> id + } + +internal fun filterDirectoriesByName( + directories: List, + query: String, +): List { + val normalized = query.trim() + if (normalized.isBlank()) return directories + return directories.filter { directory -> + directory.name.contains(normalized, ignoreCase = true) + } +} diff --git a/app/src/main/java/com/itlab/notes/ui/SingleLineText.kt b/app/src/main/java/com/itlab/notes/ui/SingleLineText.kt new file mode 100644 index 00000000..b9755919 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/ui/SingleLineText.kt @@ -0,0 +1,6 @@ +package com.itlab.notes.ui + +private val LINE_BREAK_REGEX = Regex("[\r\n\u2028\u2029\u0085]+") + +/** Removes line breaks (typing, paste, or legacy data) so text stays on one line. */ +fun String.toSingleLineText(): String = replace(LINE_BREAK_REGEX, " ") diff --git a/app/src/main/java/com/itlab/notes/ui/auth/AuthScreen.kt b/app/src/main/java/com/itlab/notes/ui/auth/AuthScreen.kt new file mode 100644 index 00000000..d5e00382 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/ui/auth/AuthScreen.kt @@ -0,0 +1,472 @@ +package com.itlab.notes.ui.auth + +import android.app.Activity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Email +import androidx.compose.material.icons.rounded.Visibility +import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.auth.api.signin.GoogleSignInStatusCodes +import com.google.android.gms.common.api.ApiException +import com.itlab.notes.R +import org.koin.androidx.compose.koinViewModel + +@Composable +fun authScreen(viewModel: AuthViewModel = koinViewModel()) { + val state by viewModel.uiState.collectAsState() + val context = LocalContext.current + + val webClientId = stringResource(R.string.default_web_client_id) + val googleSignInEnabled = webClientId.isNotBlank() + + val googleSignInClient = + remember(context, webClientId) { + val options = + GoogleSignInOptions + .Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(webClientId) + .requestEmail() + .build() + GoogleSignIn.getClient(context, options) + } + + val googleLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val data = result.data + if (data == null) { + if (result.resultCode != Activity.RESULT_CANCELED) { + viewModel.reportError("Google sign-in returned no data.") + } else { + viewModel.clearError() + } + return@rememberLauncherForActivityResult + } + try { + val account = + GoogleSignIn + .getSignedInAccountFromIntent(data) + .getResult(ApiException::class.java) + val token = account.idToken + if (token.isNullOrBlank()) { + viewModel.reportError( + "Google sign-in did not return a token. Check Web Client ID in Firebase.", + ) + } else { + viewModel.signInWithGoogle(token) + } + } catch (error: ApiException) { + if (error.statusCode == GoogleSignInStatusCodes.SIGN_IN_CANCELLED) { + viewModel.clearError() + } else { + viewModel.reportError(mapGoogleSignInError(error)) + } + } + } + + val googleUnavailableMessage = + "Google sign-in requires a Web Client ID in Firebase (see default_web_client_id)." + val launchGoogleSignIn = { + if (!googleSignInEnabled) { + viewModel.reportError(googleUnavailableMessage) + } else { + viewModel.clearError() + viewModel.clearSuccess() + googleLauncher.launch(googleSignInClient.signInIntent) + } + } + + Scaffold { padding -> + Box( + modifier = + Modifier + .fillMaxSize() + .padding(padding), + ) { + key(state.step) { + when (state.step) { + AuthScreenStep.ChooseMethod -> + authMethodChoiceContent( + isLoading = state.isLoading, + googleSignInEnabled = googleSignInEnabled, + onGoogleClick = launchGoogleSignIn, + onEmailClick = { viewModel.openEmailStep() }, + onContinueOffline = { viewModel.continueOffline() }, + errorMessage = state.errorMessage, + ) + AuthScreenStep.Email -> + authEmailContent( + state = state, + onBackFromEmail = { viewModel.backFromEmailStep() }, + onSignIn = viewModel::signInWithEmail, + onSignUp = viewModel::signUpWithEmail, + onSwitchToSignUp = { viewModel.switchToSignUpMode() }, + onSwitchToSignIn = { viewModel.switchToSignInMode() }, + onClearError = { viewModel.clearError() }, + onClearSuccess = { viewModel.clearSuccess() }, + ) + } + } + } + } +} + +@Composable +private fun authMethodChoiceContent( + isLoading: Boolean, + googleSignInEnabled: Boolean, + onGoogleClick: () -> Unit, + onEmailClick: () -> Unit, + onContinueOffline: () -> Unit, + errorMessage: String?, +) { + Column( + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Notes", + style = MaterialTheme.typography.headlineMedium, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Choose how you want to sign in", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(40.dp)) + + authMessageBlock(errorMessage = errorMessage, successMessage = null) + + OutlinedButton( + onClick = onGoogleClick, + modifier = + Modifier + .fillMaxWidth() + .height(48.dp), + enabled = !isLoading && googleSignInEnabled, + ) { + if (isLoading) { + authButtonLoadingIndicator( + color = MaterialTheme.colorScheme.primary, + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_google), + contentDescription = null, + modifier = Modifier.authMethodIcon(), + tint = Color.Unspecified, + ) + Text("Continue with Google") + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedButton( + onClick = onEmailClick, + modifier = + Modifier + .fillMaxWidth() + .height(48.dp), + enabled = !isLoading, + ) { + Icon( + imageVector = Icons.Rounded.Email, + contentDescription = null, + modifier = Modifier.authMethodIcon(), + ) + Text("Sign in with Email") + } + + if (!googleSignInEnabled) { + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = "Google sign-in requires a Web Client ID in Firebase (see default_web_client_id).", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + TextButton( + onClick = onContinueOffline, + enabled = !isLoading, + ) { + Text("Continue without signing in") + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun authEmailContent( + state: AuthUiState, + onBackFromEmail: () -> Unit, + onSignIn: (String, String) -> Unit, + onSignUp: (String, String) -> Unit, + onSwitchToSignUp: () -> Unit, + onSwitchToSignIn: () -> Unit, + onClearError: () -> Unit, + onClearSuccess: () -> Unit, +) { + BackHandler(onBack = onBackFromEmail) + + var email by rememberSaveable { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + var passwordVisible by rememberSaveable { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxSize()) { + TopAppBar( + title = { + Text( + text = + if (state.isSignUpMode) { + "Create account" + } else { + "Sign in" + }, + ) + }, + navigationIcon = { + IconButton(onClick = onBackFromEmail, enabled = !state.isLoading) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = "Back", + ) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent, + ), + ) + + Column( + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + OutlinedTextField( + value = email, + onValueChange = { + email = it + onClearError() + onClearSuccess() + }, + shape = MaterialTheme.shapes.medium, + modifier = Modifier.fillMaxWidth(), + label = { Text("Email") }, + singleLine = true, + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next, + ), + enabled = !state.isLoading, + ) + Spacer(modifier = Modifier.height(12.dp)) + OutlinedTextField( + value = password, + onValueChange = { + password = it + onClearError() + onClearSuccess() + }, + shape = MaterialTheme.shapes.medium, + modifier = Modifier.fillMaxWidth(), + label = { Text("Password") }, + singleLine = true, + visualTransformation = + if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + trailingIcon = { + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = + if (passwordVisible) { + Icons.Rounded.VisibilityOff + } else { + Icons.Rounded.Visibility + }, + contentDescription = + if (passwordVisible) { + "Hide password" + } else { + "Show password" + }, + ) + } + }, + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + enabled = !state.isLoading, + ) + + Spacer(modifier = Modifier.height(12.dp)) + authMessageBlock( + errorMessage = state.errorMessage, + successMessage = state.successMessage, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = { + if (state.isSignUpMode) { + onSignUp(email, password) + } else { + onSignIn(email, password) + } + }, + modifier = + Modifier + .fillMaxWidth() + .height(48.dp), + enabled = !state.isLoading, + ) { + if (state.isLoading) { + authButtonLoadingIndicator( + color = MaterialTheme.colorScheme.onPrimary, + ) + } else { + Text( + text = + if (state.isSignUpMode) { + "Create account" + } else { + "Sign in" + }, + ) + } + } + + TextButton( + onClick = if (state.isSignUpMode) onSwitchToSignIn else onSwitchToSignUp, + enabled = !state.isLoading, + ) { + Text( + text = + if (state.isSignUpMode) { + "Already have an account? Sign in" + } else { + "Need an account? Create one" + }, + ) + } + } + } +} + +private fun Modifier.authMethodIcon(): Modifier = + size(30.dp) + .padding(end = 12.dp) + +@Composable +private fun authButtonLoadingIndicator(color: Color = LocalContentColor.current) { + CircularProgressIndicator( + modifier = Modifier.size(22.dp), + strokeWidth = 2.dp, + color = color, + ) +} + +@Composable +private fun authMessageBlock( + errorMessage: String?, + successMessage: String?, +) { + when { + !errorMessage.isNullOrBlank() -> + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + !successMessage.isNullOrBlank() -> + Text( + text = successMessage, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +private fun mapGoogleSignInError(error: ApiException): String = + when (error.statusCode) { + GoogleSignInStatusCodes.DEVELOPER_ERROR -> + "Google Sign-In configuration error. Add SHA-1 fingerprint in Firebase Console." + else -> error.message ?: "Google sign-in failed (code ${error.statusCode})." + } diff --git a/app/src/main/java/com/itlab/notes/ui/auth/AuthViewModel.kt b/app/src/main/java/com/itlab/notes/ui/auth/AuthViewModel.kt new file mode 100644 index 00000000..6e68b8b5 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/ui/auth/AuthViewModel.kt @@ -0,0 +1,330 @@ +package com.itlab.notes.ui.auth + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException +import com.google.firebase.auth.FirebaseAuthInvalidUserException +import com.google.firebase.auth.FirebaseAuthUserCollisionException +import com.google.firebase.auth.FirebaseAuthWeakPasswordException +import com.google.firebase.auth.GoogleAuthProvider +import com.itlab.notes.R +import com.itlab.notes.auth.AppSessionPreferences +import com.itlab.notes.auth.ClearLocalDataOnSignOut +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await + +enum class AuthScreenStep { + ChooseMethod, + Email, +} + +data class AuthUiState( + val step: AuthScreenStep = AuthScreenStep.ChooseMethod, + val isSessionReady: Boolean = false, + /** User may enter the notes app (explicit sign-in or restored Firebase session). */ + val isSessionActive: Boolean = false, + val continueOffline: Boolean = false, + val isLoading: Boolean = false, + val isSignUpMode: Boolean = false, + val errorMessage: String? = null, + val successMessage: String? = null, +) + +class AuthViewModel( + private val firebaseAuth: FirebaseAuth, + private val app: Application, + private val appSessionPreferences: AppSessionPreferences, + private val clearLocalDataOnSignOut: ClearLocalDataOnSignOut, +) : ViewModel() { + private var shouldActivateSession = firebaseAuth.currentUser != null + + private val _uiState = + MutableStateFlow( + AuthUiState( + isSessionActive = firebaseAuth.currentUser != null && shouldActivateSession, + ), + ) + val uiState: StateFlow = _uiState.asStateFlow() + + val sessionKey: String? + get() = + when { + _uiState.value.continueOffline -> OFFLINE_SESSION_KEY + _uiState.value.isSessionActive -> firebaseAuth.currentUser?.uid + else -> null + } + + private val authStateListener = + FirebaseAuth.AuthStateListener { auth -> + val signedIn = auth.currentUser != null + val sessionActive = signedIn && shouldActivateSession + _uiState.update { + it.copy( + isSessionActive = sessionActive, + continueOffline = if (sessionActive) false else it.continueOffline, + isLoading = false, + ) + } + if (sessionActive) { + viewModelScope.launch { appSessionPreferences.setContinueOffline(false) } + } + } + + init { + firebaseAuth.addAuthStateListener(authStateListener) + viewModelScope.launch { + appSessionPreferences.continueOffline.collect { offline -> + _uiState.update { state -> + val sessionActive = firebaseAuth.currentUser != null && shouldActivateSession + state.copy( + continueOffline = if (sessionActive) false else offline, + isSessionReady = true, + ) + } + } + } + } + + override fun onCleared() { + firebaseAuth.removeAuthStateListener(authStateListener) + super.onCleared() + } + + fun openEmailStep() { + _uiState.update { + it.copy( + step = AuthScreenStep.Email, + isSignUpMode = false, + errorMessage = null, + successMessage = null, + ) + } + } + + fun backToMethodChoice() { + _uiState.update { + it.copy( + step = AuthScreenStep.ChooseMethod, + isSignUpMode = false, + errorMessage = null, + successMessage = null, + isLoading = false, + ) + } + } + + fun switchToSignUpMode() { + _uiState.update { + it.copy( + isSignUpMode = true, + errorMessage = null, + successMessage = null, + ) + } + } + + fun switchToSignInMode() { + _uiState.update { + it.copy( + isSignUpMode = false, + errorMessage = null, + successMessage = null, + ) + } + } + + fun backFromEmailStep() { + if (_uiState.value.isSignUpMode) { + switchToSignInMode() + } else { + backToMethodChoice() + } + } + + fun continueOffline() { + _uiState.update { it.copy(continueOffline = true, errorMessage = null) } + viewModelScope.launch { appSessionPreferences.setContinueOffline(true) } + } + + private suspend fun clearOfflineSession() { + appSessionPreferences.setContinueOffline(false) + } + + /** Leaves offline mode and returns to the sign-in choice screen. */ + fun exitOfflineToSignIn() { + viewModelScope.launch { + runCatching { clearLocalDataOnSignOut() } + clearOfflineSession() + _uiState.update { + it.copy( + step = AuthScreenStep.ChooseMethod, + continueOffline = false, + isSessionActive = false, + isLoading = false, + errorMessage = null, + successMessage = null, + ) + } + } + } + + fun clearError() { + _uiState.update { it.copy(errorMessage = null) } + } + + fun clearSuccess() { + _uiState.update { it.copy(successMessage = null) } + } + + fun reportError(message: String) { + _uiState.update { it.copy(isLoading = false, errorMessage = message) } + } + + fun signInWithEmail( + email: String, + password: String, + ) { + val trimmedEmail = email.trim() + if (trimmedEmail.isEmpty() || password.isEmpty()) { + _uiState.update { it.copy(errorMessage = "Email and password are required.") } + return + } + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null, successMessage = null) } + shouldActivateSession = true + runCatching { + firebaseAuth.signInWithEmailAndPassword(trimmedEmail, password).await() + clearLocalDataOnSignOut() + }.onFailure { error -> + shouldActivateSession = false + _uiState.update { + it.copy( + isLoading = false, + errorMessage = mapAuthError(error), + ) + } + } + } + } + + fun signUpWithEmail( + email: String, + password: String, + ) { + val trimmedEmail = email.trim() + if (trimmedEmail.isEmpty() || password.isEmpty()) { + _uiState.update { it.copy(errorMessage = "Email and password are required.") } + return + } + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null, successMessage = null) } + shouldActivateSession = false + runCatching { + firebaseAuth.createUserWithEmailAndPassword(trimmedEmail, password).await() + firebaseAuth.signOut() + }.onSuccess { + _uiState.update { + it.copy( + isLoading = false, + isSessionActive = false, + successMessage = + "Account created. Switch to Sign in below when you are ready.", + errorMessage = null, + ) + } + }.onFailure { error -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = mapAuthError(error), + ) + } + } + } + } + + fun signInWithGoogle(idToken: String) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null, successMessage = null) } + shouldActivateSession = true + runCatching { + val credential = GoogleAuthProvider.getCredential(idToken, null) + firebaseAuth.signInWithCredential(credential).await() + clearLocalDataOnSignOut() + }.onFailure { error -> + shouldActivateSession = false + _uiState.update { + it.copy( + isLoading = false, + errorMessage = mapAuthError(error), + ) + } + } + } + } + + fun signOut() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null, successMessage = null) } + shouldActivateSession = false + runCatching { + clearLocalDataOnSignOut() + clearOfflineSession() + firebaseAuth.signOut() + signOutGoogle() + }.onFailure { error -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = error.message ?: "Sign out failed.", + ) + } + } + _uiState.update { + it.copy( + step = AuthScreenStep.ChooseMethod, + continueOffline = false, + isLoading = false, + isSessionActive = false, + ) + } + } + } + + private suspend fun signOutGoogle() { + val webClientId = app.getString(R.string.default_web_client_id) + if (webClientId.isBlank()) return + val options = + GoogleSignInOptions + .Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(webClientId) + .requestEmail() + .build() + GoogleSignIn.getClient(app, options).signOut().await() + } + + private fun mapAuthError(error: Throwable): String = + when (error) { + is FirebaseAuthInvalidUserException -> + "No account found for this email. Try creating an account." + is FirebaseAuthInvalidCredentialsException -> + "Invalid email or password." + is FirebaseAuthWeakPasswordException -> + "Password must be at least 6 characters." + is FirebaseAuthUserCollisionException -> + "An account with this email already exists. Sign in instead." + else -> error.message ?: "Authentication failed. Please try again." + } + + companion object { + const val OFFLINE_SESSION_KEY = "offline" + } +} diff --git a/app/src/main/java/com/itlab/notes/ui/editor/EditorScreen.kt b/app/src/main/java/com/itlab/notes/ui/editor/EditorScreen.kt index aa826b2a..ab49fc79 100644 --- a/app/src/main/java/com/itlab/notes/ui/editor/EditorScreen.kt +++ b/app/src/main/java/com/itlab/notes/ui/editor/EditorScreen.kt @@ -1,39 +1,208 @@ package com.itlab.notes.ui.editor +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints 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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.AddPhotoAlternate +import androidx.compose.material.icons.rounded.BrokenImage +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.ExpandLess +import androidx.compose.material.icons.rounded.ExpandMore +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.StarBorder import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.itlab.domain.model.ContentItem +import com.itlab.domain.usecase.noteusecase.ValidateDuplicateNoteTitleUseCase +import com.itlab.notes.media.ImageRegionLuminance +import com.itlab.notes.media.NoteMediaImport +import com.itlab.notes.media.imageAttachments +import com.itlab.notes.media.toCoilModel +import com.itlab.notes.ui.asDomainFolderId import com.itlab.notes.ui.notes.NoteItemUi +import com.itlab.notes.ui.toSingleLineText +import kotlinx.coroutines.delay +import org.koin.compose.koinInject +import kotlin.math.roundToInt + +private const val EDITOR_TOP_BAR_TITLE_MAX_LENGTH = 35 + +private val EditorHorizontalGutter = 15.dp +private val EditorHorizontalContentPadding = 15.dp +private val EditorContentScrollBottomInset = 120.dp +private val EditorContentScrollTopInset = 16.dp +private val EditorContentFieldMinHeight = 160.dp +private const val EDITOR_AUTOSAVE_DEBOUNCE_MS = 600L + +private data class EditorAttachmentsViewerState( + val images: List, + val initialIndex: Int, +) + +private fun String.truncateForEditorTopBar(): String = take(EDITOR_TOP_BAR_TITLE_MAX_LENGTH) + +private const val EDITOR_AI_UI_PREVIEW = true + +private const val EDITOR_AI_PREVIEW_SUMMARY = + "This note is about planning the product launch: goals for the week, " + + "open questions for the team, and a short list of next steps." +private val editorAiPreviewTags = + listOf("Work", "Planning", "Product", "Follow-up", "Study", "Study", "Study") @OptIn(ExperimentalMaterial3Api::class) @Composable fun editorScreen( directoryName: String, + directoryId: String, note: NoteItemUi, - onBack: () -> Unit, + onBack: (NoteItemUi) -> Unit, + onPersist: (NoteItemUi) -> Unit, onSave: (NoteItemUi) -> Unit, + onToggleFavorite: () -> Unit, ) { val colors = MaterialTheme.colorScheme - val editorVm = remember(note.id) { EditorViewModel(initialNote = note) } + val context = LocalContext.current + val validateDuplicateTitle: ValidateDuplicateNoteTitleUseCase = koinInject() + val initialNote = remember(note.id) { note } + val editorVm = remember(note.id) { EditorViewModel(initialNote = initialNote) } + var attachmentsViewer by remember { mutableStateOf(null) } + val targetFolderId = note.folderId ?: directoryId.asDomainFolderId() + var titleDuplicate by remember { mutableStateOf(false) } + val trimmedTitle = editorVm.title.trim() + val titleHasDuplicate = titleDuplicate && trimmedTitle.isNotEmpty() + + fun persistDraftIfNeeded(force: Boolean = false) { + if (titleHasDuplicate || trimmedTitle.isEmpty()) return + val draft = editorVm.buildUpdatedNote() + if (!force && draft == initialNote) return + onPersist(draft) + } + + LaunchedEffect(editorVm.title, editorVm.content, editorVm.attachments, titleHasDuplicate) { + if (editorVm.buildUpdatedNote() == initialNote) return@LaunchedEffect + delay(EDITOR_AUTOSAVE_DEBOUNCE_MS) + if (!titleHasDuplicate) { + persistDraftIfNeeded() + } + } + + LaunchedEffect(editorVm.title, targetFolderId, note.id) { + titleDuplicate = + validateDuplicateTitle( + title = editorVm.title, + folderId = targetFolderId, + excludeNoteId = note.id, + ) + } + + LaunchedEffect(note.isFavorite) { + editorVm.syncFavoriteFromNote(note.isFavorite) + } + + val leaveEditor = { + onBack(editorVm.buildUpdatedNote()) + } + + BackHandler { + if (attachmentsViewer != null) { + attachmentsViewer = null + } else { + leaveEditor() + } + } + + val pickImages = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickMultipleVisualMedia(), + ) { uris: List -> + if (uris.isEmpty()) return@rememberLauncherForActivityResult + val imported = NoteMediaImport.importImagesFromUris(context, uris) + editorVm.addAttachments(imported) + persistDraftIfNeeded(force = true) + } Scaffold( containerColor = colors.background, @@ -41,21 +210,78 @@ fun editorScreen( editorTopBar( directoryName = directoryName, title = editorVm.title, - onBack = onBack, + isFavorite = note.isFavorite, + onBack = leaveEditor, + onToggleFavorite = onToggleFavorite, + onAddImage = { + pickImages.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), + ) + }, ) }, floatingActionButton = { editorFab( onClick = { onSave(editorVm.buildUpdatedNote()) }, + enabled = trimmedTitle.isNotEmpty() && !titleHasDuplicate, ) }, ) { paddingValues -> - editorContent( - title = editorVm.title, - content = editorVm.content, - onTitleChange = editorVm::onTitleChange, - onContentChange = editorVm::onContentChange, - modifier = Modifier.padding(paddingValues), + Column( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + if (EDITOR_AI_UI_PREVIEW && editorAiPreviewTags.isNotEmpty()) { + editorAiTagsBar( + tags = editorAiPreviewTags, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = EditorHorizontalGutter, vertical = 8.dp), + ) + } + editorContent( + title = editorVm.title, + titleHasDuplicate = titleHasDuplicate, + content = editorVm.content, + attachments = editorVm.attachments, + aiSummary = if (EDITOR_AI_UI_PREVIEW) EDITOR_AI_PREVIEW_SUMMARY else null, + onTitleChange = editorVm::onTitleChange, + onContentChange = editorVm::onContentChange, + onAttachmentClick = { item -> + if (item !is ContentItem.Image) return@editorContent + val images = editorVm.attachments.imageAttachments() + val index = images.indexOfFirst { it.id == item.id } + if (index >= 0) { + attachmentsViewer = + EditorAttachmentsViewerState( + images = images, + initialIndex = index, + ) + } + }, + onRemoveAttachment = { item -> + if (item is ContentItem.Image) { + NoteMediaImport.deleteImportedFileIfOwned(context, item.source.localPath) + } + editorVm.removeAttachment(item.id) + persistDraftIfNeeded(force = true) + }, + modifier = + Modifier + .weight(1f) + .fillMaxWidth(), + ) + } + } + + attachmentsViewer?.let { viewer -> + editorFullScreenAttachmentsViewer( + images = viewer.images, + initialIndex = viewer.initialIndex, + onDismiss = { attachmentsViewer = null }, ) } } @@ -65,25 +291,55 @@ fun editorScreen( private fun editorTopBar( directoryName: String, title: String, + isFavorite: Boolean, onBack: () -> Unit, + onToggleFavorite: () -> Unit, + onAddImage: () -> Unit, ) { val colors = MaterialTheme.colorScheme + val topBarTitle = + (if (title.isBlank()) directoryName else title).truncateForEditorTopBar() CenterAlignedTopAppBar( title = { Text( - text = if (title.isBlank()) directoryName else title, + text = topBarTitle, color = colors.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp), ) }, navigationIcon = { IconButton(onClick = onBack) { Icon( - Icons.AutoMirrored.Filled.ArrowBack, + Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = null, tint = colors.onSurface, ) } }, + actions = { + IconButton(onClick = onToggleFavorite) { + Icon( + imageVector = if (isFavorite) Icons.Rounded.Star else Icons.Rounded.StarBorder, + contentDescription = + if (isFavorite) { + "Remove from favorites" + } else { + "Add to favorites" + }, + tint = if (isFavorite) colors.primary else colors.onSurface, + ) + } + IconButton(onClick = onAddImage) { + Icon( + Icons.Rounded.AddPhotoAlternate, + contentDescription = "Add images", + tint = colors.onSurface, + ) + } + }, colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent, @@ -96,14 +352,18 @@ private fun editorTopBar( } @Composable -private fun editorFab(onClick: () -> Unit) { +private fun editorFab( + onClick: () -> Unit, + enabled: Boolean = true, +) { val colors = MaterialTheme.colorScheme FloatingActionButton( - onClick = onClick, + onClick = { if (enabled) onClick() }, + modifier = Modifier.alpha(if (enabled) 1f else 0.4f), containerColor = colors.primary, ) { Icon( - Icons.Default.Check, + Icons.Rounded.Check, contentDescription = null, tint = colors.onPrimary, ) @@ -113,55 +373,608 @@ private fun editorFab(onClick: () -> Unit) { @Composable private fun editorContent( title: String, + titleHasDuplicate: Boolean, content: String, + attachments: List, + aiSummary: String?, onTitleChange: (String) -> Unit, onContentChange: (String) -> Unit, + onAttachmentClick: (ContentItem) -> Unit, + onRemoveAttachment: (ContentItem) -> Unit, modifier: Modifier = Modifier, ) { - val colors = MaterialTheme.colorScheme + val scrollState = rememberScrollState() Column( modifier = modifier .fillMaxSize() - .padding(horizontal = 16.dp, vertical = 12.dp), + .verticalScroll(scrollState) + .padding(horizontal = EditorHorizontalGutter, vertical = 12.dp), ) { - editorTitleField( - value = title, - onValueChange = onTitleChange, + if (!aiSummary.isNullOrBlank()) { + editorCollapsibleSummaryCard( + summary = aiSummary, + modifier = Modifier.padding(bottom = 10.dp), + ) + } + + editorTitleSection( + title = title, + titleHasDuplicate = titleHasDuplicate, + onTitleChange = onTitleChange, ) editorContentField( value = content, onValueChange = onContentChange, + scrollState = scrollState, modifier = Modifier.padding(top = 12.dp), ) + + if (attachments.isNotEmpty()) { + editorAttachmentsRow( + attachments = attachments, + onAttachmentClick = onAttachmentClick, + onRemove = onRemoveAttachment, + modifier = Modifier.padding(top = 12.dp), + ) + } + + Spacer(modifier = Modifier.height(88.dp)) + } +} + +@Composable +private fun editorAttachmentsRow( + attachments: List, + onAttachmentClick: (ContentItem) -> Unit, + onRemove: (ContentItem) -> Unit, + modifier: Modifier = Modifier, +) { + LazyRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(vertical = 4.dp), + ) { + items( + items = attachments, + key = { it.id }, + ) { item -> + when (item) { + is ContentItem.Image -> editorImageThumbnail(item, onAttachmentClick, onRemove) + is ContentItem.File -> + Surface( + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = + Modifier + .height(88.dp) + .width(120.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { onAttachmentClick(item) }, + ) { + Box(Modifier.fillMaxSize()) { + Text( + text = item.name, + style = MaterialTheme.typography.labelSmall, + maxLines = 3, + modifier = + Modifier + .align(Alignment.Center) + .padding(horizontal = 8.dp, vertical = 20.dp), + ) + IconButton( + onClick = { onRemove(item) }, + modifier = Modifier.align(Alignment.TopEnd), + ) { + Icon(Icons.Rounded.Close, contentDescription = null) + } + } + } + is ContentItem.Link -> + Surface( + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = + Modifier + .height(88.dp) + .width(120.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { onAttachmentClick(item) }, + ) { + Box(Modifier.fillMaxSize()) { + Text( + text = item.title ?: item.url, + style = MaterialTheme.typography.labelSmall, + maxLines = 3, + modifier = + Modifier + .align(Alignment.Center) + .padding(horizontal = 8.dp, vertical = 20.dp), + ) + IconButton( + onClick = { onRemove(item) }, + modifier = Modifier.align(Alignment.TopEnd), + ) { + Icon(Icons.Rounded.Close, contentDescription = null) + } + } + } + is ContentItem.Text -> { } + } + } + } +} + +@Composable +private fun editorImageThumbnail( + image: ContentItem.Image, + onAttachmentClick: (ContentItem) -> Unit, + onRemove: (ContentItem) -> Unit, +) { + val context = LocalContext.current + val colors = MaterialTheme.colorScheme + val model = + remember(image.id, image.source.localPath, image.source.remoteUrl) { + image.source.toCoilModel() + } + var closeIconTint by remember(image.id) { mutableStateOf(Color.White) } + Box { + Surface( + shape = RoundedCornerShape(8.dp), + tonalElevation = 1.dp, + modifier = + Modifier + .size(88.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { onAttachmentClick(image) }, + ) { + if (model != null) { + AsyncImage( + model = + ImageRequest + .Builder(context) + .data(model) + .crossfade(true) + .allowHardware(false) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + onSuccess = { state -> + val isLightRegion = + ImageRegionLuminance.isTopEndRegionLight(state.result.drawable) + closeIconTint = if (isLightRegion) Color.Black else Color.White + }, + onError = { closeIconTint = colors.onSurfaceVariant }, + ) + } else { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Rounded.BrokenImage, + contentDescription = null, + tint = colors.onSurfaceVariant, + ) + } + } + } + IconButton( + onClick = { onRemove(image) }, + modifier = + Modifier + .align(Alignment.TopEnd) + .size(28.dp), + ) { + Icon( + Icons.Rounded.Close, + contentDescription = null, + tint = closeIconTint, + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun editorFullScreenAttachmentsViewer( + images: List, + initialIndex: Int, + onDismiss: () -> Unit, +) { + if (images.isEmpty()) return + + val context = LocalContext.current + val colors = MaterialTheme.colorScheme + val overlayBackground = colors.surface.copy(alpha = 0.88f) + val safeInitialIndex = initialIndex.coerceIn(0, images.lastIndex) + val pagerState = + rememberPagerState( + initialPage = safeInitialIndex, + pageCount = { images.size }, + ) + + Dialog( + onDismissRequest = onDismiss, + properties = + DialogProperties( + usePlatformDefaultWidth = false, + decorFitsSystemWindows = false, + ), + ) { + val dismissInteractionSource = remember { MutableInteractionSource() } + Box( + modifier = + Modifier + .fillMaxSize() + .background(overlayBackground) + .clickable( + interactionSource = dismissInteractionSource, + indication = null, + ) { onDismiss() }, + ) { + HorizontalPager( + state = pagerState, + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 8.dp, vertical = 48.dp), + ) { page -> + val item = images[page] + val model = + remember(item.id, item.source.localPath, item.source.remoteUrl) { + item.source.toCoilModel() + } + BoxWithConstraints( + modifier = + Modifier + .fillMaxSize() + .clickable( + interactionSource = remember(page) { MutableInteractionSource() }, + indication = null, + ) { onDismiss() }, + contentAlignment = Alignment.Center, + ) { + if (model != null) { + val absorbImageTap = remember(item.id) { MutableInteractionSource() } + AsyncImage( + model = + ImageRequest + .Builder(context) + .data(model) + .crossfade(false) + .allowHardware(false) + .build(), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = + Modifier + .sizeIn(maxWidth = maxWidth, maxHeight = maxHeight) + .wrapContentSize() + .clickable( + interactionSource = absorbImageTap, + indication = null, + onClick = {}, + ), + ) + } else { + Icon( + imageVector = Icons.Rounded.BrokenImage, + contentDescription = null, + tint = colors.onSurfaceVariant, + modifier = Modifier.size(48.dp), + ) + } + } + } + if (images.size > 1) { + Text( + text = "${pagerState.currentPage + 1} / ${images.size}", + style = MaterialTheme.typography.labelLarge, + color = colors.onSurfaceVariant, + modifier = + Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 24.dp), + ) + } + IconButton( + onClick = onDismiss, + modifier = + Modifier + .align(Alignment.TopEnd) + .padding(top = 20.dp, end = 8.dp), + ) { + Icon( + Icons.Rounded.Close, + contentDescription = null, + tint = colors.onSurface, + ) + } + } } } @Composable -private fun editorTitleField( +private fun editorAiTagsBar( + tags: List, + modifier: Modifier = Modifier, +) { + val colors = MaterialTheme.colorScheme + LazyRow( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 0.dp), + ) { + itemsIndexed( + items = tags, + key = { index, tag -> "$index-$tag" }, + ) { _, tag -> + FilterChip( + selected = true, + onClick = { }, + label = { + Text( + text = tag, + style = MaterialTheme.typography.labelLarge, + ) + }, + shape = MaterialTheme.shapes.extraLarge, + colors = + FilterChipDefaults.filterChipColors( + containerColor = colors.surfaceContainerHigh, + labelColor = colors.onSurface, + iconColor = colors.onSurface, + selectedContainerColor = colors.secondaryContainer, + selectedLabelColor = colors.onSecondaryContainer, + selectedLeadingIconColor = colors.onSecondaryContainer, + ), + border = + FilterChipDefaults.filterChipBorder( + enabled = true, + selected = true, + borderColor = colors.outline.copy(alpha = 0.35f), + selectedBorderColor = Color.Transparent, + ), + ) + } + } +} + +@Composable +private fun editorCollapsibleSummaryCard( + summary: String, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(true) } + val colors = MaterialTheme.colorScheme + + Surface( + color = colors.surfaceContainer, + shape = MaterialTheme.shapes.large, + modifier = modifier.fillMaxWidth(), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { expanded = !expanded } + .padding(horizontal = EditorHorizontalContentPadding, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "AI Summary", + style = MaterialTheme.typography.titleSmall, + color = colors.onSurface, + modifier = Modifier.weight(1f), + ) + Icon( + imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, + contentDescription = + if (expanded) { + "Collapse summary" + } else { + "Expand summary" + }, + tint = colors.onSurfaceVariant, + ) + } + AnimatedVisibility( + visible = expanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + Text( + text = summary, + style = MaterialTheme.typography.bodyMedium, + color = colors.onSurface, + modifier = + Modifier.padding( + start = EditorHorizontalContentPadding, + end = EditorHorizontalContentPadding, + bottom = 14.dp, + ), + ) + } + } + } +} + +@Composable +private fun editorPlainTextField( value: String, onValueChange: (String) -> Unit, + placeholder: String, + modifier: Modifier = Modifier, + textStyle: TextStyle = MaterialTheme.typography.bodyLarge, + singleLine: Boolean = false, + minLines: Int = 1, + stripLineBreaks: Boolean = false, ) { val colors = MaterialTheme.colorScheme - TextField( + val interactionSource = remember { MutableInteractionSource() } + + BasicTextField( value = value, - onValueChange = onValueChange, - placeholder = { Text("Title") }, - singleLine = true, + onValueChange = { newValue -> + onValueChange(if (stripLineBreaks) newValue.toSingleLineText() else newValue) + }, + modifier = modifier.fillMaxWidth(), + textStyle = textStyle.copy(color = colors.onSurface), + cursorBrush = SolidColor(colors.primary), + singleLine = singleLine, + maxLines = if (singleLine) 1 else Int.MAX_VALUE, + minLines = if (singleLine) 1 else minLines, + keyboardOptions = + if (singleLine) { + KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Next, + ) + } else { + KeyboardOptions.Default + }, + interactionSource = interactionSource, + decorationBox = { innerTextField -> + editorPlainTextFieldDecoration( + value = value, + placeholder = placeholder, + textStyle = textStyle, + singleLine = singleLine, + interactionSource = interactionSource, + content = innerTextField, + ) + }, + ) +} + +@Composable +private fun editorPlainTextFieldDecoration( + value: String, + placeholder: String, + textStyle: TextStyle, + singleLine: Boolean, + interactionSource: MutableInteractionSource, + content: @Composable () -> Unit, +) { + val colors = MaterialTheme.colorScheme + TextFieldDefaults.DecorationBox( + value = value, + innerTextField = content, + enabled = true, + singleLine = singleLine, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + placeholder = { + Text( + text = placeholder, + style = textStyle, + color = colors.onSurfaceVariant, + ) + }, colors = TextFieldDefaults.colors( - focusedTextColor = colors.onSurface, - unfocusedTextColor = colors.onSurface, - focusedPlaceholderColor = colors.onSurfaceVariant, - unfocusedPlaceholderColor = colors.onSurfaceVariant, - focusedContainerColor = colors.background, - unfocusedContainerColor = colors.background, + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, disabledIndicatorColor = Color.Transparent, errorIndicatorColor = Color.Transparent, ), + contentPadding = + PaddingValues( + horizontal = EditorHorizontalContentPadding, + vertical = 8.dp, + ), + ) +} + +private fun editorScrollTargetForCursor( + scrollState: ScrollState, + fieldTopInScrollPx: Float, + textLayoutResult: TextLayoutResult, + cursorOffset: Int, + bottomInsetPx: Float, + topInsetPx: Float, +): Int? { + if (scrollState.viewportSize <= 0) return null + val safeOffset = cursorOffset.coerceIn(0, textLayoutResult.layoutInput.text.length) + val cursorRect = textLayoutResult.getCursorRect(safeOffset) + val cursorTopInScroll = fieldTopInScrollPx + cursorRect.top + val cursorBottomInScroll = fieldTopInScrollPx + cursorRect.bottom + val viewportTop = scrollState.value.toFloat() + val viewportBottom = viewportTop + scrollState.viewportSize - bottomInsetPx + + val targetScroll = + when { + cursorBottomInScroll > viewportBottom -> + (cursorBottomInScroll - scrollState.viewportSize + bottomInsetPx) + .roundToInt() + .coerceIn(0, scrollState.maxValue) + cursorTopInScroll < viewportTop + topInsetPx -> + (cursorTopInScroll - topInsetPx).roundToInt().coerceIn(0, scrollState.maxValue) + else -> return null + } + return targetScroll.takeIf { it != scrollState.value } +} + +private val EditorTitleDuplicateErrorLineHeight = 20.dp + +@Composable +private fun editorTitleSection( + title: String, + titleHasDuplicate: Boolean, + onTitleChange: (String) -> Unit, +) { + Column(modifier = Modifier.fillMaxWidth()) { + editorTitleField( + value = title, + onValueChange = onTitleChange, + ) + Box( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = EditorHorizontalContentPadding) + .heightIn(min = EditorTitleDuplicateErrorLineHeight) + .padding(top = 4.dp), + ) { + if (titleHasDuplicate) { + Text( + text = "A note with that name already exists.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + } + } +} + +@Composable +private fun editorTitleField( + value: String, + onValueChange: (String) -> Unit, +) { + editorPlainTextField( + value = value, + onValueChange = onValueChange, + placeholder = "Title", + singleLine = true, + stripLineBreaks = true, + textStyle = MaterialTheme.typography.titleLarge, ) } @@ -169,27 +982,68 @@ private fun editorTitleField( private fun editorContentField( value: String, onValueChange: (String) -> Unit, + scrollState: ScrollState, modifier: Modifier = Modifier, ) { val colors = MaterialTheme.colorScheme - TextField( - value = value, - onValueChange = onValueChange, - modifier = modifier, - placeholder = { Text("Input") }, - minLines = 12, - colors = - TextFieldDefaults.colors( - focusedTextColor = colors.onSurface, - unfocusedTextColor = colors.onSurface, - focusedPlaceholderColor = colors.onSurfaceVariant, - unfocusedPlaceholderColor = colors.onSurfaceVariant, - focusedContainerColor = colors.background, - unfocusedContainerColor = colors.background, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - ), + val density = LocalDensity.current + val interactionSource = remember { MutableInteractionSource() } + val textStyle = MaterialTheme.typography.bodyLarge + val bottomInsetPx = with(density) { EditorContentScrollBottomInset.toPx() } + val topInsetPx = with(density) { EditorContentScrollTopInset.toPx() } + + var textFieldValue by remember { mutableStateOf(TextFieldValue(text = value, selection = TextRange(value.length))) } + var textLayoutResult by remember { mutableStateOf(null) } + var fieldTopInScrollPx by remember { mutableFloatStateOf(0f) } + + LaunchedEffect(value) { + if (textFieldValue.text != value) { + textFieldValue = TextFieldValue(text = value, selection = TextRange(value.length)) + } + } + + LaunchedEffect(textFieldValue, textLayoutResult, fieldTopInScrollPx, scrollState.viewportSize) { + val layout = textLayoutResult ?: return@LaunchedEffect + val targetScroll = + editorScrollTargetForCursor( + scrollState = scrollState, + fieldTopInScrollPx = fieldTopInScrollPx, + textLayoutResult = layout, + cursorOffset = textFieldValue.selection.end, + bottomInsetPx = bottomInsetPx, + topInsetPx = topInsetPx, + ) ?: return@LaunchedEffect + scrollState.animateScrollTo(targetScroll) + } + + BasicTextField( + value = textFieldValue, + onValueChange = { updated -> + textFieldValue = updated + if (updated.text != value) { + onValueChange(updated.text) + } + }, + onTextLayout = { textLayoutResult = it }, + modifier = + modifier + .fillMaxWidth() + .sizeIn(minHeight = EditorContentFieldMinHeight) + .onGloballyPositioned { coordinates -> + fieldTopInScrollPx = coordinates.positionInParent().y + }, + textStyle = textStyle.copy(color = colors.onSurface), + cursorBrush = SolidColor(colors.primary), + interactionSource = interactionSource, + decorationBox = { innerTextField -> + editorPlainTextFieldDecoration( + value = textFieldValue.text, + placeholder = "Input", + textStyle = textStyle, + singleLine = false, + interactionSource = interactionSource, + content = innerTextField, + ) + }, ) } diff --git a/app/src/main/java/com/itlab/notes/ui/editor/EditorViewModel.kt b/app/src/main/java/com/itlab/notes/ui/editor/EditorViewModel.kt index 4e448ffb..cab4d3dc 100644 --- a/app/src/main/java/com/itlab/notes/ui/editor/EditorViewModel.kt +++ b/app/src/main/java/com/itlab/notes/ui/editor/EditorViewModel.kt @@ -3,31 +3,63 @@ package com.itlab.notes.ui.editor import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import com.itlab.domain.model.ContentItem +import com.itlab.notes.media.withoutTextItems import com.itlab.notes.ui.notes.NoteItemUi +import com.itlab.notes.ui.toSingleLineText class EditorViewModel( initialNote: NoteItemUi, ) { private val noteId: String = initialNote.id + private val userId: String = initialNote.userId + private val folderId: String? = initialNote.folderId - var title: String by mutableStateOf(initialNote.title) + var title: String by mutableStateOf(initialNote.title.toSingleLineText()) private set var content: String by mutableStateOf(initialNote.content) private set + var attachments: List by mutableStateOf(initialNote.attachments.withoutTextItems()) + private set + + var isFavorite: Boolean by mutableStateOf(initialNote.isFavorite) + private set + + fun syncFavoriteFromNote(value: Boolean) { + isFavorite = value + } + fun onTitleChange(newTitle: String) { - title = newTitle + title = newTitle.toSingleLineText() } fun onContentChange(newContent: String) { content = newContent } + fun addAttachment(item: ContentItem) { + attachments = attachments + item + } + + fun addAttachments(items: List) { + if (items.isEmpty()) return + attachments = attachments + items + } + + fun removeAttachment(id: String) { + attachments = attachments.filterNot { it.id == id } + } + fun buildUpdatedNote(): NoteItemUi = NoteItemUi( id = noteId, + userId = userId, title = title, content = content, + folderId = folderId, + attachments = attachments, + isFavorite = isFavorite, ) } diff --git a/app/src/main/java/com/itlab/notes/ui/notes/AppEmptyState.kt b/app/src/main/java/com/itlab/notes/ui/notes/AppEmptyState.kt new file mode 100644 index 00000000..929d7486 --- /dev/null +++ b/app/src/main/java/com/itlab/notes/ui/notes/AppEmptyState.kt @@ -0,0 +1,96 @@ +package com.itlab.notes.ui.notes + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Folder +import androidx.compose.material.icons.rounded.SearchOff +import androidx.compose.material3.Icon +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.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun appEmptyState( + icon: ImageVector, + title: String, + message: String, + modifier: Modifier = Modifier, +) { + val colors = MaterialTheme.colorScheme + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxWidth(), + ) { + Box( + modifier = + Modifier + .clip(MaterialTheme.shapes.medium) + .background(colors.surfaceContainer), + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.padding(14.dp).size(32.dp), + tint = colors.onSurfaceVariant, + ) + } + Spacer(Modifier.height(16.dp)) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = colors.onSurface, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(8.dp)) + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + color = colors.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 50.dp), + ) + } +} + +@Composable +fun notesSearchEmptyState(modifier: Modifier = Modifier) { + appEmptyState( + icon = Icons.Rounded.SearchOff, + title = "No results found", + message = "Try a different search term or check another folder.", + modifier = modifier, + ) +} + +@Composable +fun directoriesSearchEmptyState(modifier: Modifier = Modifier) { + appEmptyState( + icon = Icons.Rounded.SearchOff, + title = "No results found", + message = "Try a different search term.", + modifier = modifier, + ) +} + +@Composable +fun directoriesEmptyState(modifier: Modifier = Modifier) { + appEmptyState( + icon = Icons.Rounded.Folder, + title = "No directories yet", + message = "Tap + to create a directory and organize your notes.", + modifier = modifier, + ) +} diff --git a/app/src/main/java/com/itlab/notes/ui/notes/AppSearchField.kt b/app/src/main/java/com/itlab/notes/ui/notes/AppSearchField.kt new file mode 100644 index 00000000..aea4f72a --- /dev/null +++ b/app/src/main/java/com/itlab/notes/ui/notes/AppSearchField.kt @@ -0,0 +1,169 @@ +package com.itlab.notes.ui.notes + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp + +@Composable +fun appSearchField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + placeholderText: String = "Search", +) { + var isFocused by remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current + val interactionSource = remember { MutableInteractionSource() } + val onClearClick: () -> Unit = { + onValueChange("") + isFocused = false + focusManager.clearFocus(force = true) + } + + Box( + modifier = + modifier + .fillMaxWidth() + .height(56.dp), + contentAlignment = Alignment.Center, + ) { + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = + Modifier + .fillMaxWidth() + .height(48.dp) + .onFocusChanged { isFocused = it.isFocused }, + singleLine = true, + interactionSource = interactionSource, + textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface), + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + decorationBox = { content -> + appSearchFieldDecorationBox( + input = + AppSearchFieldDecorationInput( + value = value, + isFocused = isFocused, + placeholderText = placeholderText, + onClearClick = onClearClick, + ), + interactionSource = interactionSource, + ) { + content() + } + }, + ) + } +} + +@Composable +private fun appSearchFieldDecorationBox( + input: AppSearchFieldDecorationInput, + interactionSource: MutableInteractionSource, + content: @Composable () -> Unit, +) { + TextFieldDefaults.DecorationBox( + value = input.value, + innerTextField = content, + enabled = true, + singleLine = true, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + placeholder = { + Text( + input.placeholderText, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + leadingIcon = { + appSearchFieldLeadingIcon(isFocused = input.isFocused) + }, + trailingIcon = { + appSearchFieldTrailingIcon( + isFocused = input.isFocused, + onClearClick = input.onClearClick, + ) + }, + shape = CircleShape, + colors = + TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + cursorColor = MaterialTheme.colorScheme.primary, + ), + contentPadding = PaddingValues(start = 24.dp, end = 16.dp, top = 4.dp, bottom = 4.dp), + ) +} + +@Composable +private fun appSearchFieldLeadingIcon(isFocused: Boolean) { + Icon( + imageVector = Icons.Rounded.Search, + contentDescription = null, + tint = + if (isFocused) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = + Modifier + .padding(start = 24.dp) + .size(25.dp), + ) +} + +@Composable +private fun appSearchFieldTrailingIcon( + isFocused: Boolean, + onClearClick: () -> Unit, +) { + if (isFocused) { + IconButton(onClick = onClearClick) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +private data class AppSearchFieldDecorationInput( + val value: String, + val isFocused: Boolean, + val placeholderText: String, + val onClearClick: () -> Unit, +) diff --git a/app/src/main/java/com/itlab/notes/ui/notes/DirectoriesScreen.kt b/app/src/main/java/com/itlab/notes/ui/notes/DirectoriesScreen.kt index eac273bd..4e3c18c2 100644 --- a/app/src/main/java/com/itlab/notes/ui/notes/DirectoriesScreen.kt +++ b/app/src/main/java/com/itlab/notes/ui/notes/DirectoriesScreen.kt @@ -1,64 +1,143 @@ +@file:Suppress("TooManyFunctions") + package com.itlab.notes.ui.notes +import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +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.RowScope 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ChevronRight -import androidx.compose.material.icons.filled.Stars -import androidx.compose.material3.AlertDialog +import androidx.compose.material.icons.automirrored.rounded.Login +import androidx.compose.material.icons.automirrored.rounded.Logout +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.AllInbox +import androidx.compose.material.icons.rounded.ChevronRight +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.Folder +import androidx.compose.material.icons.rounded.FolderCopy +import androidx.compose.material.icons.rounded.Schedule +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.TextFields +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.itlab.notes.onboarding.OnboardingTargets +import com.itlab.notes.onboarding.onboardingTarget +import com.itlab.notes.ui.toSingleLineText + +private const val DIRECTORY_NAME_TAKEN_ERROR = "A directory with this name already exists" @OptIn(ExperimentalMaterial3Api::class) @Composable fun directoriesScreen( directories: List, + searchQuery: String, + onSearchQueryChange: (String) -> Unit, onCreateDirectory: (String) -> Unit, onDeleteDirectory: (DirectoryItemUi) -> Unit, onRenameDirectory: (DirectoryItemUi, String) -> Unit, onDirectoryClick: (DirectoryItemUi) -> Unit, + showSignOut: Boolean = false, + onSignOut: () -> Unit = {}, + showReturnToSignIn: Boolean = false, + onReturnToSignIn: () -> Unit = {}, ) { val colors = MaterialTheme.colorScheme + val focusManager = LocalFocusManager.current var showCreateDialog by remember { mutableStateOf(false) } + BackHandler(enabled = showCreateDialog) { + showCreateDialog = false + } + Scaffold( + modifier = + Modifier + .fillMaxSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { + focusManager.clearFocus(force = true) + }, containerColor = colors.background, topBar = { directoriesTopBar( + showSignOut = showSignOut, + onSignOut = onSignOut, + showReturnToSignIn = showReturnToSignIn, + onReturnToSignIn = onReturnToSignIn, onAddDirectoryClick = { showCreateDialog = true }, ) }, ) { paddingValues -> directoriesList( directories = directories, + searchQuery = searchQuery, + onSearchQueryChange = onSearchQueryChange, onDirectoryLongClick = onDeleteDirectory, onDirectoryRename = onRenameDirectory, onDirectoryClick = onDirectoryClick, @@ -66,48 +145,206 @@ fun directoriesScreen( ) } if (showCreateDialog) { - var directoryName by remember { mutableStateOf("") } - AlertDialog( + directoriesCreateDirectoryDialog( + directories = directories, onDismissRequest = { showCreateDialog = false }, - title = { Text("New Directory") }, - text = { - OutlinedTextField( - value = directoryName, - onValueChange = { directoryName = it }, - label = { Text("Directory name") }, - singleLine = true, + onCreateDirectory = onCreateDirectory, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun directoriesCreateDirectoryDialog( + directories: List, + onDismissRequest: () -> Unit, + onCreateDirectory: (String) -> Unit, +) { + var directoryName by remember { mutableStateOf("") } + val trimmedName = directoryName.trim() + val nameAlreadyExists = + trimmedName.isNotEmpty() && + directories.any { dir -> + !isSpecialDirectory(dir.id) && + dir.name.trim().equals(trimmedName, ignoreCase = true) + } + universalBasicAlertDialog( + onDismissRequest = onDismissRequest, + slots = + UniversalBasicAlertDialogSlots( + icon = Icons.Rounded.Folder, + iconContainerColor = MaterialTheme.colorScheme.surfaceContainer, + iconTintColor = MaterialTheme.colorScheme.onSurfaceVariant, + title = { + Text( + text = "Create Directory", + fontWeight = FontWeight.W400, + ) + }, + input = { + directoryOutlinedTextField( + modifier = Modifier.padding(top = 5.dp), + value = directoryName, + onValueChange = { directoryName = it }, + placeholderText = "Enter directory name...", + isError = nameAlreadyExists, + errorMessage = if (nameAlreadyExists) DIRECTORY_NAME_TAKEN_ERROR else null, + requestInitialFocus = true, + ) + }, + actions = { + TextButton( + onClick = onDismissRequest, + contentPadding = PaddingValues(horizontal = 12.dp), + ) { + Text("Cancel") + } + + Spacer(modifier = Modifier.width(4.dp)) + + Button( + onClick = { + onCreateDirectory(directoryName) + onDismissRequest() + }, + enabled = trimmedName.isNotEmpty() && !nameAlreadyExists, + contentPadding = PaddingValues(horizontal = 16.dp), + shape = MaterialTheme.shapes.medium, + ) { + Text("Create") + } + }, + ), + ) +} + +internal data class UniversalBasicAlertDialogSlots( + val icon: ImageVector, + val iconContainerColor: Color, + val iconTintColor: Color, + val title: @Composable () -> Unit, + val input: @Composable () -> Unit, + val actions: @Composable RowScope.() -> Unit, +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun universalBasicAlertDialog( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false), + slots: UniversalBasicAlertDialogSlots, +) { + BasicAlertDialog( + onDismissRequest = onDismissRequest, + modifier = + modifier + .fillMaxWidth(0.87f) + .sizeIn(maxWidth = 560.dp), + properties = properties, + ) { + val dialogFocusManager = LocalFocusManager.current + Surface( + shape = MaterialTheme.shapes.extraLarge, + ) { + Box { + Box( + modifier = + Modifier + .matchParentSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { + dialogFocusManager.clearFocus(force = true) + }, ) - }, - confirmButton = { - TextButton( - onClick = { - onCreateDirectory(directoryName) - showCreateDialog = false - }, - enabled = directoryName.trim().isNotEmpty(), + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, ) { - Text("Create") - } - }, - dismissButton = { - TextButton(onClick = { showCreateDialog = false }) { - Text("Cancel") + Box( + Modifier + .clip(MaterialTheme.shapes.medium) + .background(slots.iconContainerColor), + ) { + Icon( + slots.icon, + modifier = + Modifier + .padding(all = 14.dp) + .size(30.dp), + contentDescription = null, + tint = slots.iconTintColor, + ) + } + Spacer(Modifier.height(10.dp)) + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onSurface, + ) { + ProvideTextStyle(MaterialTheme.typography.headlineMedium) { + slots.title() + } + } + Spacer(Modifier.height(5.dp)) + slots.input() + Spacer(Modifier.height(10.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + slots.actions(this) + } } - }, - ) + } + } } } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun directoriesTopBar(onAddDirectoryClick: () -> Unit) { +private fun directoriesTopBar( + showSignOut: Boolean, + onSignOut: () -> Unit, + showReturnToSignIn: Boolean, + onReturnToSignIn: () -> Unit, + onAddDirectoryClick: () -> Unit, +) { val colors = MaterialTheme.colorScheme + val showAccountAction = showSignOut || showReturnToSignIn CenterAlignedTopAppBar( title = { Text("Directories", color = colors.onSurface) }, actions = { - IconButton(onClick = onAddDirectoryClick) { + if (showAccountAction) { + IconButton( + onClick = if (showSignOut) onSignOut else onReturnToSignIn, + modifier = + if (showSignOut) { + Modifier.onboardingTarget(OnboardingTargets.DIRECTORIES_SIGN_OUT) + } else { + Modifier + }, + ) { + Icon( + imageVector = + if (showSignOut) { + Icons.AutoMirrored.Rounded.Logout + } else { + Icons.AutoMirrored.Rounded.Login + }, + contentDescription = if (showSignOut) "Sign out" else "Sign in", + tint = colors.onSurface, + ) + } + } + IconButton( + onClick = onAddDirectoryClick, + modifier = Modifier.onboardingTarget(OnboardingTargets.DIRECTORIES_ADD), + ) { Icon( - Icons.Default.Add, + Icons.Rounded.Add, contentDescription = null, tint = colors.onSurface, ) @@ -124,143 +361,318 @@ private fun directoriesTopBar(onAddDirectoryClick: () -> Unit) { ) } +private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = + pointerInput(Unit) { + detectTapGestures( + onTap = { focusManager.clearFocus(force = true) }, + ) + } + @Composable private fun directoriesList( directories: List, + searchQuery: String, + onSearchQueryChange: (String) -> Unit, onDirectoryLongClick: (DirectoryItemUi) -> Unit, onDirectoryRename: (DirectoryItemUi, String) -> Unit, onDirectoryClick: (DirectoryItemUi) -> Unit, modifier: Modifier = Modifier, ) { - var directoryPendingDelete by remember { mutableStateOf(null) } - var directoryPendingRename by remember { mutableStateOf(null) } - LazyColumn(modifier = modifier.padding(horizontal = 16.dp)) { - items(directories) { dir -> - directoryRow( - directory = dir, - onClick = { onDirectoryClick(dir) }, - onLongClick = { - if (dir.id != "all") { - directoryPendingDelete = dir - } - }, + var pendingDelete by remember { mutableStateOf(null) } + var pendingRename by remember { mutableStateOf(null) } + val focusManager = LocalFocusManager.current + + BackHandler(enabled = pendingRename != null || pendingDelete != null) { + when { + pendingRename != null -> pendingRename = null + pendingDelete != null -> pendingDelete = null + } + } + + val allNotesDirectory = remember(directories) { directories.firstOrNull { it.id == ALL_DIRECTORY_ID } } + val favoritesDirectory = + remember(directories) { directories.firstOrNull { it.id == FAVORITES_DIRECTORY_ID } } + val regularDirectories = + remember(directories) { + directories.filter { it.id != ALL_DIRECTORY_ID && it.id != FAVORITES_DIRECTORY_ID } + } + val totalNotesCount = + allNotesDirectory?.noteCount ?: directories.sumOf { it.noteCount } + val recentDirectory = + remember(totalNotesCount) { + DirectoryItemUi(id = RECENT_DIRECTORY_ID, name = "Recent", noteCount = totalNotesCount) + } + val isSearchActive = searchQuery.isNotBlank() + + Column( + modifier = modifier.fillMaxSize().clearFocusOnTap(focusManager).padding(horizontal = 12.dp), + ) { + directorySearchBar( + query = searchQuery, + onQueryChange = onSearchQueryChange, + modifier = Modifier.onboardingTarget(OnboardingTargets.DIRECTORIES_SEARCH), + ) + LazyColumn( + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(bottom = 12.dp), + ) { + fun LazyListScope.addSection( + title: String, + dirs: List, + tourHighlightDirectoryId: String? = null, + ) { + if (dirs.isEmpty()) return + item { sectionTitle(title = title) } + item { + directoriesBlock( + directories = dirs, + onDirectoryClick = onDirectoryClick, + onDirectoryLongClick = { pendingDelete = it }, + tourHighlightDirectoryId = tourHighlightDirectoryId, + ) + } + } + + item { + directoriesHeroPanel( + directoriesCount = regularDirectories.size, + totalNotesCount = totalNotesCount, + ) + } + allNotesDirectory?.let { allNotes -> + addSection("Everything", listOf(allNotes)) + } + favoritesDirectory?.let { favorites -> + addSection("Favorite notes", listOf(favorites)) + } + if (!isSearchActive) { + addSection("Continue working", listOf(recentDirectory)) + } + addSection( + title = "Regular directories", + dirs = regularDirectories, + tourHighlightDirectoryId = + regularDirectories.firstOrNull()?.id ?: allNotesDirectory?.id, ) + + when { + isSearchActive && directories.isEmpty() -> { + item { + Box( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 80.dp) + .heightIn(min = 220.dp), + contentAlignment = Alignment.Center, + ) { + directoriesSearchEmptyState() + } + } + } + !isSearchActive && regularDirectories.isEmpty() -> { + item { + Box( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 80.dp) + .heightIn(min = 220.dp), + contentAlignment = Alignment.Center, + ) { + directoriesEmptyState() + } + } + } + } } } - directoryPendingDelete?.let { dir -> + + pendingDelete?.let { dir -> directoryActionsDialog( directory = dir, onDelete = { onDirectoryLongClick(dir) - directoryPendingDelete = null + pendingDelete = null }, onRename = { - directoryPendingDelete = null - directoryPendingRename = dir + pendingDelete = null + pendingRename = dir }, - onDismiss = { directoryPendingDelete = null }, + onDismiss = { pendingDelete = null }, ) } - directoryPendingRename?.let { dir -> + pendingRename?.let { dir -> directoryRenameDialog( + directories = directories, directory = dir, onSave = { newName -> onDirectoryRename(dir, newName) - directoryPendingRename = null + pendingRename = null }, - onDismiss = { directoryPendingRename = null }, + onDismiss = { pendingRename = null }, ) } } @Composable -private fun directoryActionsDialog( - directory: DirectoryItemUi, - onDelete: () -> Unit, - onRename: () -> Unit, - onDismiss: () -> Unit, +private fun directoriesHeroPanel( + directoriesCount: Int, + totalNotesCount: Int, ) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text("Directory actions") }, - text = { Text("Choose action for \"${directory.name}\"") }, - confirmButton = { - TextButton(onClick = onDelete) { - Text("Delete") - } - }, - dismissButton = { - TextButton(onClick = onRename) { - Text("Rename") + val colors = MaterialTheme.colorScheme + Surface( + color = colors.surfaceContainer, + shape = MaterialTheme.shapes.large, + modifier = Modifier.padding(top = 10.dp, bottom = 6.dp), + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Rounded.FolderCopy, + contentDescription = null, + tint = colors.primary, + modifier = Modifier.size(25.dp), + ) + Spacer(Modifier.width(12.dp)) + Column { + Text( + text = "Workspace", + style = MaterialTheme.typography.titleMedium, + color = colors.onSurface, + ) + Text( + text = "$totalNotesCount notes • $directoriesCount directories", + style = MaterialTheme.typography.bodyMedium, + color = colors.onSurfaceVariant, + ) } - }, + } + } +} + +@Composable +private fun sectionTitle(title: String) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 8.dp, top = 10.dp, bottom = 6.dp), ) } @Composable -private fun directoryRenameDialog( - directory: DirectoryItemUi, - onSave: (String) -> Unit, - onDismiss: () -> Unit, +private fun directoriesBlock( + directories: List, + onDirectoryClick: (DirectoryItemUi) -> Unit, + onDirectoryLongClick: (DirectoryItemUi) -> Unit, + tourHighlightDirectoryId: String? = null, ) { - var renameName by remember(directory.id) { mutableStateOf(directory.name) } - AlertDialog( - onDismissRequest = onDismiss, - title = { Text("Rename directory") }, - text = { - OutlinedTextField( - value = renameName, - onValueChange = { renameName = it }, - label = { Text("Directory name") }, - singleLine = true, - ) - }, - confirmButton = { - TextButton( - onClick = { onSave(renameName) }, - enabled = renameName.trim().isNotEmpty(), - ) { - Text("Save") - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text("Cancel") + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = MaterialTheme.shapes.large, + ) { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + directories.forEachIndexed { index, dir -> + directoryRow( + directory = dir, + onClick = { + onDirectoryClick(dir) + }, + onLongClick = { + if (!isSpecialDirectory(dir.id)) { + onDirectoryLongClick(dir) + } + }, + modifier = + Modifier + .padding(horizontal = 12.dp, vertical = 0.dp) + .then( + if (dir.id == tourHighlightDirectoryId) { + Modifier.onboardingTarget(OnboardingTargets.DIRECTORIES_FOLDER_ROW) + } else { + Modifier + }, + ), + ) + if (index < directories.lastIndex) { + directoriesListDivider() + } } - }, + } + } +} + +@Composable +fun directorySearchBar( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + appSearchField( + value = query, + onValueChange = onQueryChange, + modifier = modifier, + placeholderText = "Search directories", ) } +private fun isSpecialDirectory(directoryId: String): Boolean = isVirtualDirectory(directoryId) + @OptIn(ExperimentalFoundationApi::class) @Composable private fun directoryRow( directory: DirectoryItemUi, onClick: () -> Unit, onLongClick: () -> Unit, + modifier: Modifier = Modifier, ) { val colors = MaterialTheme.colorScheme + val isAllNotes = directory.id == ALL_DIRECTORY_ID + val isFavorites = directory.id == FAVORITES_DIRECTORY_ID Row( modifier = - Modifier + modifier .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) .combinedClickable( onClick = onClick, onLongClick = onLongClick, - ).padding(vertical = 12.dp), + ).padding(horizontal = 12.dp, vertical = 11.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( - Icons.Default.Stars, + imageVector = + when { + directory.id == RECENT_DIRECTORY_ID -> Icons.Rounded.Schedule + isFavorites -> Icons.Rounded.Star + isAllNotes -> Icons.Rounded.AllInbox + else -> Icons.Rounded.Folder + }, contentDescription = null, - tint = colors.primary, - modifier = Modifier.size(24.dp), + tint = + if (isAllNotes || isFavorites) { + colors.primary + } else { + colors.onSurfaceVariant + }, + modifier = Modifier.size(25.dp), ) - Spacer(Modifier.width(16.dp)) + Spacer(Modifier.width(12.dp)) Text( text = directory.name, color = colors.onSurface, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) Surface( color = colors.surfaceVariant, @@ -273,10 +685,256 @@ private fun directoryRow( style = MaterialTheme.typography.labelSmall, ) } + Spacer(Modifier.width(5.dp)) Icon( - Icons.Default.ChevronRight, + Icons.Rounded.ChevronRight, contentDescription = null, tint = colors.onSurfaceVariant, ) } } + +@Composable +private fun directoriesListDivider() { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = + Modifier + .padding(horizontal = 10.dp) + .fillMaxWidth(0.9f), + contentAlignment = Alignment.Center, + ) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + thickness = 1.dp, + ) + } + } +} + +@Composable +private fun directoryActionsDialog( + directory: DirectoryItemUi, + onDelete: () -> Unit, + onRename: () -> Unit, + onDismiss: () -> Unit, +) { + universalBasicAlertDialog( + onDismissRequest = onDismiss, + slots = + UniversalBasicAlertDialogSlots( + icon = Icons.Rounded.Edit, + iconContainerColor = MaterialTheme.colorScheme.surfaceContainer, + iconTintColor = MaterialTheme.colorScheme.onSurfaceVariant, + title = { + Text("Directory actions") + }, + input = { + Text( + text = "Choose action for \"${directory.name}\"", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + }, + actions = { + TextButton(onClick = onRename) { + Text("Rename") + } + + Spacer(modifier = Modifier.width(4.dp)) + + Button( + onClick = onDelete, + contentPadding = PaddingValues(horizontal = 16.dp), + shape = MaterialTheme.shapes.medium, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + Text("Delete") + } + }, + ), + ) +} + +@Composable +private fun directoryRenameDialog( + directories: List, + directory: DirectoryItemUi, + onSave: (String) -> Unit, + onDismiss: () -> Unit, +) { + var renameName by remember(directory.id) { mutableStateOf(directory.name.toSingleLineText()) } + val trimmedName = renameName.trim() + val nameAlreadyExists = + trimmedName.isNotEmpty() && + directories.any { dir -> + !isSpecialDirectory(dir.id) && + dir.id != directory.id && + dir.name.trim().equals(trimmedName, ignoreCase = true) + } + universalBasicAlertDialog( + onDismissRequest = onDismiss, + slots = + UniversalBasicAlertDialogSlots( + icon = Icons.Rounded.Edit, + iconContainerColor = MaterialTheme.colorScheme.surfaceContainer, + iconTintColor = MaterialTheme.colorScheme.onSurfaceVariant, + title = { + Text("Rename directory") + }, + input = { + directoryOutlinedTextField( + modifier = Modifier.padding(top = 5.dp), + value = renameName, + onValueChange = { renameName = it }, + placeholderText = "Enter directory name...", + isError = nameAlreadyExists, + errorMessage = if (nameAlreadyExists) DIRECTORY_NAME_TAKEN_ERROR else null, + requestInitialFocus = true, + ) + }, + actions = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + + Spacer(modifier = Modifier.width(4.dp)) + + Button( + onClick = { onSave(renameName) }, + enabled = trimmedName.isNotEmpty() && renameName != directory.name && !nameAlreadyExists, + contentPadding = PaddingValues(horizontal = 16.dp), + shape = MaterialTheme.shapes.medium, + ) { + Text("Save") + } + }, + ), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun directoryOutlinedTextField( + value: String, + onValueChange: (String) -> Unit, + placeholderText: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isError: Boolean = false, + errorMessage: String? = null, + requestInitialFocus: Boolean = false, +) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(requestInitialFocus) { + if (requestInitialFocus) { + focusRequester.requestFocus() + } + } + val interactionSource = remember { MutableInteractionSource() } + val scheme = MaterialTheme.colorScheme + val shape = MaterialTheme.shapes.medium + val colors = + OutlinedTextFieldDefaults.colors( + focusedContainerColor = scheme.surfaceContainer, + unfocusedContainerColor = scheme.surfaceContainer, + disabledContainerColor = Color.Transparent, + focusedTextColor = scheme.onSurfaceVariant, + unfocusedTextColor = scheme.onSurfaceVariant, + disabledTextColor = scheme.onSurfaceVariant.copy(alpha = 0.38f), + focusedBorderColor = scheme.outline, + unfocusedBorderColor = scheme.outline.copy(alpha = 0.5f), + disabledBorderColor = scheme.outline.copy(alpha = 0.5f), + cursorColor = scheme.primary, + errorBorderColor = scheme.error, + errorCursorColor = scheme.error, + ) + val textStyle = MaterialTheme.typography.bodyLarge.copy(color = scheme.onSurfaceVariant) + val contentPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp) + + BasicTextField( + value = value, + onValueChange = { newValue -> + onValueChange(newValue.toSingleLineText().coerceDirectoryNameLength()) + }, + modifier = + modifier + .fillMaxWidth() + .then( + if (requestInitialFocus) { + Modifier.focusRequester(focusRequester) + } else { + Modifier + }, + ), + enabled = enabled, + textStyle = textStyle, + singleLine = true, + maxLines = 1, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.Words, + imeAction = ImeAction.Done, + ), + cursorBrush = SolidColor(scheme.primary), + interactionSource = interactionSource, + decorationBox = { innerTextField -> + OutlinedTextFieldDefaults.DecorationBox( + value = value, + innerTextField = innerTextField, + enabled = enabled, + singleLine = true, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + isError = isError, + supportingText = + if (isError && errorMessage != null) { + { + Text( + text = errorMessage, + style = MaterialTheme.typography.bodySmall, + color = scheme.error, + ) + } + } else { + null + }, + placeholder = { + Text( + text = placeholderText, + style = MaterialTheme.typography.bodyLarge, + color = scheme.onSurfaceVariant.copy(alpha = 0.5f), + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.TextFields, + contentDescription = null, + tint = scheme.onSurfaceVariant, + modifier = Modifier.size(22.dp), + ) + }, + colors = colors, + contentPadding = contentPadding, + container = { + OutlinedTextFieldDefaults.Container( + enabled = enabled, + isError = isError, + interactionSource = interactionSource, + colors = colors, + shape = shape, + ) + }, + ) + }, + ) +} diff --git a/app/src/main/java/com/itlab/notes/ui/notes/DirectoryIds.kt b/app/src/main/java/com/itlab/notes/ui/notes/DirectoryIds.kt new file mode 100644 index 00000000..2ca3e55e --- /dev/null +++ b/app/src/main/java/com/itlab/notes/ui/notes/DirectoryIds.kt @@ -0,0 +1,17 @@ +package com.itlab.notes.ui.notes + +internal const val ALL_DIRECTORY_ID = "all" +internal const val RECENT_DIRECTORY_ID = "recent" +internal const val FAVORITES_DIRECTORY_ID = "favorites" + +/** Max stored/displayed length for user-created folder names. */ +internal const val DIRECTORY_NAME_MAX_LENGTH = 40 + +internal fun String.coerceDirectoryNameLength(): String = take(DIRECTORY_NAME_MAX_LENGTH) + +internal fun isVirtualDirectory(directoryId: String): Boolean = + directoryId == ALL_DIRECTORY_ID || + directoryId == RECENT_DIRECTORY_ID || + directoryId == FAVORITES_DIRECTORY_ID + +internal fun canCreateNotesInDirectory(directoryId: String): Boolean = directoryId != ALL_DIRECTORY_ID diff --git a/app/src/main/java/com/itlab/notes/ui/notes/NoteItemUi.kt b/app/src/main/java/com/itlab/notes/ui/notes/NoteItemUi.kt index 618a112f..fe0d0abf 100644 --- a/app/src/main/java/com/itlab/notes/ui/notes/NoteItemUi.kt +++ b/app/src/main/java/com/itlab/notes/ui/notes/NoteItemUi.kt @@ -1,8 +1,13 @@ package com.itlab.notes.ui.notes +import com.itlab.domain.model.ContentItem + data class NoteItemUi( val id: String, + val userId: String, val title: String, val content: String, val folderId: String? = null, + val attachments: List = emptyList(), + val isFavorite: Boolean = false, ) diff --git a/app/src/main/java/com/itlab/notes/ui/notes/NotesScreen.kt b/app/src/main/java/com/itlab/notes/ui/notes/NotesScreen.kt index 64de47cb..0bc60649 100644 --- a/app/src/main/java/com/itlab/notes/ui/notes/NotesScreen.kt +++ b/app/src/main/java/com/itlab/notes/ui/notes/NotesScreen.kt @@ -1,95 +1,154 @@ package com.itlab.notes.ui.notes -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState +import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.AlertDialog +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.automirrored.rounded.CompareArrows +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Folder +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.material3.SwipeToDismissBox -import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import kotlin.math.abs +import com.itlab.notes.onboarding.OnboardingTargets +import com.itlab.notes.onboarding.onboardingTarget @OptIn(ExperimentalMaterial3Api::class) @Composable fun notesListScreen( + directoryId: String, directoryName: String, notes: List, + searchQuery: String, + onSearchQueryChange: (String) -> Unit, directories: List, actions: NotesListActions, ) { val colors = MaterialTheme.colorScheme + val selectedNoteIds = remember { mutableStateListOf() } + var showMoveDialog by remember { mutableStateOf(false) } + var showDeleteDialog by remember { mutableStateOf(false) } + val isSelectionMode = selectedNoteIds.isNotEmpty() + val selectedCount = selectedNoteIds.size + val clearSelection = { + selectedNoteIds.clear() + showMoveDialog = false + showDeleteDialog = false + } + val deleteSelected = { + notes.filter { it.id in selectedNoteIds }.forEach { note -> + actions.onNoteDelete(note) + } + clearSelection() + } + val handleBack = { + when { + showDeleteDialog -> showDeleteDialog = false + showMoveDialog -> showMoveDialog = false + isSelectionMode -> clearSelection() + else -> actions.onBack() + } + } + + BackHandler(onBack = handleBack) Scaffold( containerColor = colors.background, topBar = { notesTopBar( directoryName = directoryName, - onBack = actions.onBack, + selectedCount = selectedCount, + onBack = handleBack, + onMoveSelected = { showMoveDialog = true }, + onDeleteSelected = { showDeleteDialog = true }, ) }, floatingActionButton = { - notesFab(onAddNoteClick = actions.onAddNoteClick) + if (!isSelectionMode && canCreateNotesInDirectory(directoryId)) { + notesFab(onAddNoteClick = actions.onAddNoteClick) + } }, ) { paddingValues -> - notesListContent( - notes = notes, - paddingValues = paddingValues, - directories = directories, - actions = - NotesListContentActions( - onNoteDelete = actions.onNoteDelete, - onNoteMove = actions.onNoteMove, - onNoteClick = actions.onNoteClick, - ), - ) + Box(Modifier.fillMaxSize()) { + notesListContent( + notes = notes, + searchQuery = searchQuery, + onSearchQueryChange = onSearchQueryChange, + paddingValues = paddingValues, + selectedNoteIds = selectedNoteIds, + actions = + NotesListContentActions( + onNoteDelete = actions.onNoteDelete, + onNoteClick = actions.onNoteClick, + ), + ) + if (showMoveDialog && selectedNoteIds.isNotEmpty()) { + notesMoveNotesDialog( + currentDirectoryId = directoryId, + directories = directories, + onDismissRequest = { showMoveDialog = false }, + onFolderChosen = { folderId -> + selectedNoteIds.forEach { noteId -> actions.onNoteMove(noteId, folderId) } + selectedNoteIds.clear() + showMoveDialog = false + }, + ) + } + if (showDeleteDialog && selectedNoteIds.isNotEmpty()) { + notesDeleteConfirmationDialog( + selectedCount = selectedCount, + onDismissRequest = { showDeleteDialog = false }, + onConfirmDelete = deleteSelected, + ) + } + } } } @@ -103,7 +162,6 @@ data class NotesListActions( private data class NotesListContentActions( val onNoteDelete: (NoteItemUi) -> Unit, - val onNoteMove: (noteId: String, directoryId: String) -> Unit, val onNoteClick: (NoteItemUi) -> Unit, ) @@ -111,20 +169,54 @@ private data class NotesListContentActions( @Composable private fun notesTopBar( directoryName: String, + selectedCount: Int, onBack: () -> Unit, + onMoveSelected: () -> Unit, + onDeleteSelected: () -> Unit, ) { val colors = MaterialTheme.colorScheme CenterAlignedTopAppBar( - title = { Text(directoryName, color = colors.onSurface) }, + title = { + Text( + text = if (selectedCount > 0) "$selectedCount selected" else directoryName, + color = colors.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), + ) + }, navigationIcon = { IconButton(onClick = onBack) { Icon( - Icons.AutoMirrored.Filled.ArrowBack, + imageVector = + if (selectedCount > 0) { + Icons.Rounded.Close + } else { + Icons.AutoMirrored.Rounded.ArrowBack + }, contentDescription = null, tint = colors.onSurface, ) } }, + actions = { + if (selectedCount > 0) { + IconButton(onClick = onMoveSelected) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.CompareArrows, + contentDescription = null, + tint = colors.onSurface, + ) + } + IconButton(onClick = onDeleteSelected) { + Icon( + imageVector = Icons.Rounded.Delete, + contentDescription = null, + tint = colors.onSurface, + ) + } + } + }, colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent, @@ -136,15 +228,209 @@ private fun notesTopBar( ) } +@Composable +private fun notesMoveNotesDialog( + currentDirectoryId: String, + directories: List, + onDismissRequest: () -> Unit, + onFolderChosen: (String) -> Unit, +) { + val moveTargets = + remember(directories, currentDirectoryId) { + directories.filter { !isVirtualDirectory(it.id) && it.id != currentDirectoryId } + } + universalBasicAlertDialog( + onDismissRequest = onDismissRequest, + slots = + UniversalBasicAlertDialogSlots( + icon = Icons.AutoMirrored.Rounded.CompareArrows, + iconContainerColor = MaterialTheme.colorScheme.surfaceContainer, + iconTintColor = MaterialTheme.colorScheme.onSurfaceVariant, + title = { + Text( + text = "Move to folder", + fontWeight = FontWeight.W400, + ) + }, + input = { + notesMoveTargetsBlock( + directories = moveTargets, + onFolderChosen = onFolderChosen, + ) + }, + actions = { + TextButton( + onClick = onDismissRequest, + contentPadding = PaddingValues(horizontal = 12.dp), + ) { + Text("Cancel") + } + }, + ), + ) +} + +@Composable +private fun notesMoveTargetsBlock( + directories: List, + onFolderChosen: (String) -> Unit, +) { + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = MaterialTheme.shapes.large, + modifier = Modifier.fillMaxWidth(), + ) { + if (directories.isEmpty()) { + Box( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 20.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = "No other folders to move to", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } else { + Column( + modifier = + Modifier + .fillMaxWidth() + .heightIn(max = 180.dp) + .verticalScroll(rememberScrollState()), + ) { + directories.forEachIndexed { index, dir -> + notesMoveTargetRow( + directory = dir, + onClick = { onFolderChosen(dir.id) }, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 0.dp), + ) + if (index < directories.lastIndex) { + notesMoveTargetsDivider() + } + } + } + } + } +} + +@Composable +private fun notesMoveTargetRow( + directory: DirectoryItemUi, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val colors = MaterialTheme.colorScheme + Row( + modifier = + modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 11.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Rounded.Folder, + contentDescription = null, + tint = colors.onSurfaceVariant, + modifier = Modifier.size(25.dp), + ) + Spacer(Modifier.width(12.dp)) + Text( + text = directory.name, + color = colors.onSurface, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun notesMoveTargetsDivider() { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = + Modifier + .padding(horizontal = 10.dp) + .fillMaxWidth(0.9f), + contentAlignment = Alignment.Center, + ) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.55f), + thickness = 1.dp, + ) + } + } +} + +@Composable +private fun notesDeleteConfirmationDialog( + selectedCount: Int, + onDismissRequest: () -> Unit, + onConfirmDelete: () -> Unit, +) { + universalBasicAlertDialog( + onDismissRequest = onDismissRequest, + slots = + UniversalBasicAlertDialogSlots( + icon = Icons.Rounded.Delete, + iconContainerColor = MaterialTheme.colorScheme.errorContainer, + iconTintColor = MaterialTheme.colorScheme.onErrorContainer, + title = { + Text( + text = "Delete selected notes?", + ) + }, + input = { + Text( + text = "This will permanently delete $selectedCount note(s).", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + }, + actions = { + TextButton( + onClick = onDismissRequest, + contentPadding = PaddingValues(horizontal = 12.dp), + ) { + Text("Cancel") + } + Button( + onClick = onConfirmDelete, + contentPadding = PaddingValues(horizontal = 16.dp), + shape = MaterialTheme.shapes.medium, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + Text("Delete") + } + }, + ), + ) +} + @Composable private fun notesFab(onAddNoteClick: () -> Unit) { val colors = MaterialTheme.colorScheme FloatingActionButton( onClick = onAddNoteClick, + modifier = Modifier.onboardingTarget(OnboardingTargets.NOTES_FAB), containerColor = colors.primary, ) { Icon( - Icons.Default.Add, + Icons.Rounded.Add, contentDescription = null, tint = colors.onPrimary, ) @@ -154,167 +440,123 @@ private fun notesFab(onAddNoteClick: () -> Unit) { @Composable private fun notesListContent( notes: List, + searchQuery: String, + onSearchQueryChange: (String) -> Unit, paddingValues: androidx.compose.foundation.layout.PaddingValues, - directories: List, + selectedNoteIds: MutableList, actions: NotesListContentActions, ) { - var pendingMoveNote by remember { mutableStateOf(null) } + val isSelectionMode = selectedNoteIds.isNotEmpty() Column( modifier = Modifier .padding(paddingValues) .padding(horizontal = 16.dp), ) { - searchField() + searchField( + query = searchQuery, + onQueryChange = onSearchQueryChange, + ) + + val isSearchActive = searchQuery.isNotBlank() LazyColumn( verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(top = 4.dp), ) { + if (notes.isEmpty() && isSearchActive) { + item { + Box( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 80.dp) + .heightIn(min = 220.dp), + contentAlignment = Alignment.Center, + ) { + notesSearchEmptyState() + } + } + } + val tourNoteId = notes.firstOrNull()?.id items( items = notes, key = { note -> note.id }, ) { note -> notesListItem( note = note, - onDelete = { actions.onNoteDelete(note) }, - onClick = { actions.onNoteClick(note) }, - onMoveRequest = { pendingMoveNote = note }, + isSelected = note.id in selectedNoteIds, + modifier = + if (note.id == tourNoteId) { + Modifier.onboardingTarget(OnboardingTargets.NOTES_NOTE_ROW) + } else { + Modifier + }, + onClick = { + if (isSelectionMode) { + if (note.id in selectedNoteIds) { + selectedNoteIds.remove(note.id) + } else { + selectedNoteIds.add(note.id) + } + } else { + actions.onNoteClick(note) + } + }, + onLongClick = { + if (note.id !in selectedNoteIds) { + selectedNoteIds.add(note.id) + } + }, ) } } } - pendingMoveNote?.let { note -> - moveNoteDialog( - directories = directories, - onMoveTo = { directoryId -> - actions.onNoteMove(note.id, directoryId) - pendingMoveNote = null - }, - onDismiss = { pendingMoveNote = null }, - ) - } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun notesListItem( note: NoteItemUi, - onDelete: () -> Unit, + isSelected: Boolean, + modifier: Modifier = Modifier, onClick: () -> Unit, - onMoveRequest: () -> Unit, -) { - var isDeleteDispatched by remember(note.id) { mutableStateOf(false) } - val dismissState = - rememberSwipeToDismissBoxState( - positionalThreshold = { totalDistance -> totalDistance * 0.22f }, - ) - val swipeProgress by - remember(dismissState) { - derivedStateOf { - dismissState.progress.coerceIn(0f, 1f) - } - } - val swipeOffsetPx by - remember(dismissState) { - derivedStateOf { - kotlin.runCatching { abs(dismissState.requireOffset()) }.getOrDefault(0f) - } - } - LaunchedEffect(dismissState.targetValue, isDeleteDispatched) { - if (!isDeleteDispatched && dismissState.targetValue == SwipeToDismissBoxValue.EndToStart) { - isDeleteDispatched = true - onDelete() - } - } - SwipeToDismissBox( - state = dismissState, - enableDismissFromStartToEnd = false, - backgroundContent = { - swipeDeleteBackground( - isActive = dismissState.targetValue == SwipeToDismissBoxValue.EndToStart, - swipeProgress = swipeProgress, - swipeOffsetPx = swipeOffsetPx, - ) - }, - ) { - noteCard( - note = note, - onClick = onClick, - onLongClick = onMoveRequest, - ) - } -} - -@Composable -private fun swipeDeleteBackground( - isActive: Boolean, - swipeProgress: Float, - swipeOffsetPx: Float, + onLongClick: () -> Unit, ) { - val colors = MaterialTheme.colorScheme - val density = LocalDensity.current - val clampedProgress = swipeProgress.coerceIn(0f, 1f) - val activeScale by animateFloatAsState( - targetValue = if (isActive) 1f else (0.9f + clampedProgress * 0.1f), - label = "deleteIconScale", - ) - val activeAlpha by animateFloatAsState( - targetValue = if (isActive) 1f else (0.62f + clampedProgress * 0.28f), - label = "deleteIconAlpha", + noteCard( + note = note, + isSelected = isSelected, + modifier = modifier, + onClick = onClick, + onLongClick = onLongClick, ) - BoxWithConstraints( - modifier = Modifier.fillMaxSize().padding(vertical = 1.dp), - contentAlignment = Alignment.CenterEnd, - ) { - val maxWidth = maxWidth - val gapFromCard = 8.dp - val targetWidth = - with(density) { (swipeOffsetPx - gapFromCard.toPx()).coerceAtLeast(0f).toDp() } - .coerceAtMost(maxWidth) - val animatedWidth by animateDpAsState(targetValue = targetWidth, label = "deleteBackgroundWidth") - Surface( - color = colors.errorContainer.copy(alpha = 0.6f), - shape = RoundedCornerShape(16.dp), - modifier = - Modifier - .fillMaxHeight() - .width(animatedWidth), - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null, - tint = colors.onErrorContainer.copy(alpha = activeAlpha), - modifier = - Modifier.graphicsLayer( - scaleX = activeScale, - scaleY = activeScale, - ), - ) - } - } - } } @Composable @OptIn(ExperimentalFoundationApi::class) private fun noteCard( note: NoteItemUi, + isSelected: Boolean, + modifier: Modifier = Modifier, onClick: () -> Unit, onLongClick: () -> Unit, ) { val colors = MaterialTheme.colorScheme Card( - colors = CardDefaults.cardColors(containerColor = colors.surfaceVariant), - shape = RoundedCornerShape(16.dp), + colors = + CardDefaults.cardColors( + containerColor = + if (isSelected) { + colors.surfaceContainerHighest + } else { + colors.surfaceContainer + }, + ), + shape = MaterialTheme.shapes.large, modifier = - Modifier + modifier .fillMaxWidth() + .clip(MaterialTheme.shapes.large) .combinedClickable( onClick = onClick, onLongClick = onLongClick, @@ -323,85 +565,59 @@ private fun noteCard( Column(modifier = Modifier.padding(16.dp).fillMaxWidth()) { Text( text = note.title, - color = colors.onSurface, + color = + if (isSelected) { + colors.onPrimaryContainer + } else { + colors.onSurface + }, style = MaterialTheme.typography.titleMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(8.dp)) Text( - text = note.content, - color = colors.onSurfaceVariant, + text = noteCardDescriptionText(note), + color = + if (isSelected) { + colors.onPrimaryContainer + } else if (note.content.isBlank()) { + colors.onSurfaceVariant.copy(alpha = 0.7f) + } else { + colors.onSurfaceVariant + }, style = MaterialTheme.typography.bodySmall, - maxLines = 4, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) } } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun moveNoteDialog( - directories: List, - onMoveTo: (String) -> Unit, - onDismiss: () -> Unit, -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text("Move note") }, - text = { - Column { - directories.forEach { dir -> - TextButton( - onClick = { onMoveTo(dir.id) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(dir.name) - } - } - } - }, - confirmButton = {}, - dismissButton = { - TextButton(onClick = onDismiss) { - Text("Cancel") - } - }, - ) -} +private fun noteCardDescriptionText(note: NoteItemUi): String = + buildString { + append(if (note.content.isNotBlank()) note.content else "No description") + if (note.attachments.isNotEmpty()) { + append(" · ") + append(note.attachments.size) + append(" attachment") + if (note.attachments.size != 1) append("s") + } + } @Composable -private fun searchField() { - val colors = MaterialTheme.colorScheme - - Surface( - color = colors.surfaceVariant.copy(alpha = 0.65f), - shape = RoundedCornerShape(24.dp), +private fun searchField( + query: String, + onQueryChange: (String) -> Unit, +) { + appSearchField( + value = query, + onValueChange = onQueryChange, modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - Icons.Default.Menu, - contentDescription = null, - tint = colors.onSurfaceVariant, - ) - Text( - text = "Hinted search text", - color = colors.onSurfaceVariant, - modifier = - Modifier - .padding(horizontal = 16.dp) - .weight(1f), - ) - Icon( - Icons.Default.Search, - contentDescription = null, - tint = colors.onSurfaceVariant, - ) - } - } + .padding(vertical = 16.dp) + .onboardingTarget(OnboardingTargets.NOTES_SEARCH), + placeholderText = "Search notes", + ) } diff --git a/app/src/main/res/drawable/ic_google.xml b/app/src/main/res/drawable/ic_google.xml new file mode 100644 index 00000000..2db8d7e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_google.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index 97483cfe..00000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Notes - \ No newline at end of file diff --git a/domain/src/main/java/com/itlab/domain/usecase/noteusecase/SearchNotesUseCase.kt b/domain/src/main/java/com/itlab/domain/usecase/noteusecase/SearchNotesUseCase.kt index f827c5f4..8d487671 100644 --- a/domain/src/main/java/com/itlab/domain/usecase/noteusecase/SearchNotesUseCase.kt +++ b/domain/src/main/java/com/itlab/domain/usecase/noteusecase/SearchNotesUseCase.kt @@ -9,12 +9,17 @@ import kotlinx.coroutines.flow.map class SearchNotesUseCase( private val repo: NotesRepository, ) { - operator fun invoke(query: String): Flow> { + operator fun invoke( + query: String, + folderId: String? = null, + ): Flow> { val normalizedQuery = query.trim().lowercase() if (normalizedQuery.isBlank()) return repo.observeNotes() return repo.observeNotes().map { notes -> - notes.filter { note -> note.matches(normalizedQuery) } + notes + .filter { note -> folderId == null || note.folderId == folderId } + .filter { note -> note.matches(normalizedQuery) } } } diff --git a/domain/src/main/java/com/itlab/domain/usecase/noteusecase/ValidateDuplicateNoteTitleUseCase.kt b/domain/src/main/java/com/itlab/domain/usecase/noteusecase/ValidateDuplicateNoteTitleUseCase.kt new file mode 100644 index 00000000..8b3d5766 --- /dev/null +++ b/domain/src/main/java/com/itlab/domain/usecase/noteusecase/ValidateDuplicateNoteTitleUseCase.kt @@ -0,0 +1,23 @@ +package com.itlab.domain.usecase.noteusecase + +import com.itlab.domain.repository.NotesRepository +import kotlinx.coroutines.flow.first + +class ValidateDuplicateNoteTitleUseCase( + private val repo: NotesRepository, +) { + /** @return true when another note in the same folder already has this title. */ + suspend operator fun invoke( + title: String, + folderId: String?, + excludeNoteId: String, + ): Boolean { + val normalizedTitle = title.trim() + if (normalizedTitle.isEmpty()) return false + return repo.observeNotes().first().any { existing -> + existing.id != excludeNoteId && + existing.folderId == folderId && + existing.title.trim().equals(normalizedTitle, ignoreCase = true) + } + } +} diff --git a/domain/src/test/java/com/itlab/domain/SearchNotesUseCaseTest.kt b/domain/src/test/java/com/itlab/domain/SearchNotesUseCaseTest.kt index 12645466..5e38349f 100644 --- a/domain/src/test/java/com/itlab/domain/SearchNotesUseCaseTest.kt +++ b/domain/src/test/java/com/itlab/domain/SearchNotesUseCaseTest.kt @@ -79,6 +79,37 @@ class SearchNotesUseCaseTest { assertEquals(0, result.size) } + @Test + fun `invoke should filter by folder when folderId is set`() = + runBlocking { + val now = Instant.fromEpochMilliseconds(System.currentTimeMillis()) + val notes = + listOf( + Note( + userId = "u1", + id = "1", + title = "Shopping List", + folderId = "folder_a", + createdAt = now, + updatedAt = now, + ), + Note( + userId = "u1", + id = "2", + title = "Shopping budget", + folderId = "folder_b", + createdAt = now, + updatedAt = now, + ), + ) + coEvery { repo.observeNotes() } returns flowOf(notes) + + val result = searchNotesUseCase("shop", folderId = "folder_a").first() + + assertEquals(1, result.size) + assertEquals("1", result[0].id) + } + @Test fun `invoke should cover all branches of content matching`() = runBlocking { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 003ad344..e16856a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,9 @@ workManager = "2.9.0" androidx-work-testing = "2.9.0" lifecycleViewmodelKtx = "2.10.0" koin = "4.2.1" +coilCompose = "2.7.0" +datastore = "1.1.7" +playServicesAuth = "20.7.0" [libraries] mockk-agent-jvm = { group = "io.mockk", name = "mockk-agent-jvm", version.ref = "mockk" } @@ -42,6 +45,7 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } @@ -64,6 +68,8 @@ androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle- androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coilCompose" } +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } mockk = { group = "io.mockk", name = "mockk", version = "1.14.9" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } androidx-test-core = { group = "androidx.test", name = "core", version = "1.7.0" } @@ -73,6 +79,7 @@ kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "ko firebase-firestore = { group = "com.google.firebase", name = "firebase-firestore" } firebase-storage = { group = "com.google.firebase", name = "firebase-storage" } firebase-auth = { group = "com.google.firebase", name = "firebase-auth" } +play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuth" } firebase-ui-auth = { group = "com.firebaseui", name = "firebase-ui-auth", version = "8.0.2" } androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager" } androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidx-work-testing" }