From 50fb3f13a0d92da4d275e98a7498bf6852847c51 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 21 Nov 2025 09:29:41 -0500 Subject: [PATCH 01/56] adding js to shared --- build.gradle.kts | 3 ++- shared/build.gradle.kts | 42 +++++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index d03c079b..3b9b194a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,7 +37,8 @@ subprojects { } } } - +/* tasks.register("clean") { delete(rootProject.layout.buildDirectory) } +*/ diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index bdaf0388..0ec3edd7 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -34,11 +34,7 @@ android { ) main.manifest.srcFile("src/androidMain/AndroidManifest.xml") } -} - -version = "1.0" -android { configurations { create("androidTestApi") create("androidTestDebugApi") @@ -49,6 +45,8 @@ android { } } +version = "1.0" + kotlin { compilerOptions { // common compiler options applied to all Kotlin source sets @@ -61,13 +59,16 @@ kotlin { iosX64() iosArm64() iosSimulatorArm64() + js(IR) { + browser() + binaries.executable() + } version = "1.0" sourceSets { commonMain.dependencies { api(libs.kermit) - api(libs.kermit.crashlytics) api(libs.kotlinx.coroutines.core) api(libs.kotlinx.datetime) api(libs.multiplatformSettings.core) @@ -80,16 +81,29 @@ kotlin { implementation(libs.koin.core) implementation(libs.korio) } - androidMain.dependencies { - implementation(libs.sqldelight.driver.android) - implementation(libs.kotlinx.coroutines.android) - implementation(libs.ktor.client.okhttp) - implementation(libs.androidx.core) + val mobileMain by creating { + dependsOn(commonMain.get()) + dependencies { + api(libs.kermit.crashlytics) + } + } + + androidMain { + dependsOn(mobileMain) + dependencies { + implementation(libs.sqldelight.driver.android) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.ktor.client.okhttp) + implementation(libs.androidx.core) + } } - iosMain.dependencies { - implementation(libs.sqldelight.driver.ios) - implementation(libs.sqliter) - implementation(libs.ktor.client.ios) + iosMain { + dependsOn(mobileMain) + dependencies { + implementation(libs.sqldelight.driver.ios) + implementation(libs.sqliter) + implementation(libs.ktor.client.ios) + } } all { From a5955b8252ca95468fa5846a6cb61026edb90364 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 21 Nov 2025 09:54:06 -0500 Subject: [PATCH 02/56] cleaning up code --- build.gradle.kts | 3 +- gradle/libs.versions.toml | 8 +++-- ios/build.gradle.kts | 2 -- shared/build.gradle.kts | 5 +++ .../kotlin/co/touchlab/droidcon/Koin.js.kt | 35 +++++++++++++++++++ .../impl/SqlDelightDriverFactory.js.kt | 9 +++++ .../droidcon/service/JsNotificationService.kt | 25 +++++++++++++ .../co/touchlab/droidcon/util/Platform.js.kt | 5 +++ .../util/formatter/JsDateFormatter.kt | 13 +++++++ 9 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt create mode 100644 shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt create mode 100644 shared/src/jsMain/kotlin/co/touchlab/droidcon/service/JsNotificationService.kt create mode 100644 shared/src/jsMain/kotlin/co/touchlab/droidcon/util/Platform.js.kt create mode 100644 shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt diff --git a/build.gradle.kts b/build.gradle.kts index 3b9b194a..571c0560 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,11 +23,12 @@ subprojects { } } + /* tasks.withType(KotlinCompile::class).all { kotlinOptions { jvmTarget = "1.8" } - } + }*/ configure { version.set("1.4.0") enableExperimentalRules.set(true) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1ea9aba..c09f2b4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ kotlin = "2.1.10" android-gradle-plugin = "8.9.0" coroutines = "1.10.1" kotlinx-datetime = "0.6.1" -ktor = "3.0.3" +ktor = "3.3.2" stately = "2.1.0" java = "21" @@ -22,7 +22,7 @@ sqliter = "1.3.1" hyperdrive = "0.1.148" multiplatformSettings = "1.2.0" -sqlDelight = "2.0.2" +sqlDelight = "2.2.1" firebase-bom = "33.10.0" firebase-crashlytics-gradle = "3.0.3" gms-google-services = "4.4.2" @@ -49,11 +49,12 @@ uuid = "0.8.3" ktlint = "12.1.1" coil = "3.1.0" zoomimage = "1.1.0-alpha06" -skie = "0.10.1" +skie = "0.10.8" [libraries] coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } sqliter = { module = "co.touchlab:sqliter-driver", version.ref = "sqliter" } compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose-compiler" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } @@ -88,6 +89,7 @@ multiplatformSettings-test = { module = "com.russhwolf:multiplatform-settings-te sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" } sqldelight-driver-ios = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" } +sqldelight-driver-js = { module = "app.cash.sqldelight:web-worker-driver", version.ref = "sqlDelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqlDelight" } sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" } diff --git a/ios/build.gradle.kts b/ios/build.gradle.kts index abf76243..c61a2a46 100644 --- a/ios/build.gradle.kts +++ b/ios/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { @@ -49,7 +48,6 @@ kotlin { framework { baseName = "DroidconKit" isStatic = true - embedBitcodeMode = BitcodeEmbeddingMode.DISABLE freeCompilerArgs += listOf( "-linker-option", "-framework", "-linker-option", "Metal", diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 0ec3edd7..e1daa449 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -105,6 +105,11 @@ kotlin { implementation(libs.ktor.client.ios) } } + jsMain.dependencies { + implementation(libs.ktor.client.cio) + implementation(libs.sqldelight.driver.js) + + } all { languageSettings.apply { diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt new file mode 100644 index 00000000..00502571 --- /dev/null +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt @@ -0,0 +1,35 @@ +package co.touchlab.droidcon + +import app.cash.sqldelight.db.SqlDriver +import co.touchlab.droidcon.application.service.NotificationService +import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory +import co.touchlab.droidcon.service.JsNotificationService +import co.touchlab.droidcon.util.formatter.DateFormatter +import co.touchlab.droidcon.util.formatter.JsDateFormatter +import co.touchlab.kermit.CommonWriter +import co.touchlab.kermit.Logger +import co.touchlab.kermit.StaticConfig +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.cio.CIO +import org.koin.dsl.module + +actual val platformModule: org.koin.core.module.Module = module { + single { + SqlDelightDriverFactory().createDriver() + } + + single { + CIO.create() + } + + single { + get() + } + + single { + JsDateFormatter() + } + + val baseKermit = Logger(config = StaticConfig(logWriterList = listOf(CommonWriter())), tag = "Droidcon") + factory { (tag: String?) -> if (tag != null) baseKermit.withTag(tag) else baseKermit } +} diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt new file mode 100644 index 00000000..e34ce5c0 --- /dev/null +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt @@ -0,0 +1,9 @@ +package co.touchlab.droidcon.domain.repository.impl + +import app.cash.sqldelight.db.SqlDriver + +actual class SqlDelightDriverFactory { + actual fun createDriver(): SqlDriver { + TODO("Not yet implemented") + } +} diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/service/JsNotificationService.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/service/JsNotificationService.kt new file mode 100644 index 00000000..deabdf4c --- /dev/null +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/service/JsNotificationService.kt @@ -0,0 +1,25 @@ +package co.touchlab.droidcon.service + +import co.touchlab.droidcon.application.service.Notification +import co.touchlab.droidcon.application.service.NotificationService +import co.touchlab.droidcon.domain.entity.Session +import kotlinx.datetime.Instant + +class JsNotificationService : NotificationService { + override suspend fun initialize(): Boolean = false + + override suspend fun schedule( + notification: Notification.Local, + title: String, + body: String, + delivery: Instant, + dismiss: Instant?, + ) { + } + + override suspend fun cancel(sessionIds: List) { + } + + override fun setHandler(notificationHandler: DeepLinkNotificationHandler) { + } +} diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/Platform.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/Platform.js.kt new file mode 100644 index 00000000..dd90f177 --- /dev/null +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/Platform.js.kt @@ -0,0 +1,5 @@ +package co.touchlab.droidcon.util + +internal actual fun printThrowable(t: Throwable) { + t.printStackTrace() +} diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt new file mode 100644 index 00000000..2ca10e21 --- /dev/null +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt @@ -0,0 +1,13 @@ +package co.touchlab.droidcon.util.formatter + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime + +class JsDateFormatter() : DateFormatter { + + override fun monthWithDay(date: LocalDate): String? = null + + override fun timeOnly(dateTime: LocalDateTime): String? = null + + override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime): String = "" +} From f4cbb4e1ddec771d74cc9873c1ecdeb0a0006541 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 21 Nov 2025 10:15:51 -0500 Subject: [PATCH 03/56] initial call --- build.gradle.kts | 5 +- gradle/libs.versions.toml | 56 +++++++++---------- gradle/wrapper/gradle-wrapper.properties | 2 +- ios/build.gradle.kts | 2 - .../kotlin/co/touchlab/droidcon/Koin.kt | 4 +- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index d03c079b..f5fe5500 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,8 +24,9 @@ subprojects { } tasks.withType(KotlinCompile::class).all { - kotlinOptions { - jvmTarget = "1.8" + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") } } configure { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1ea9aba..58a205cd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,54 +2,54 @@ ## SDK Versions minSdk = "21" targetSdk = "35" -compileSdk = "35" +compileSdk = "36" # Dependencies -kotlin = "2.1.10" +kotlin = "2.2.21" ## Gradle Plugin version must be compatible with Multiplatform ## https://kotlinlang.org/docs/multiplatform-compatibility-guide.html#version-compatibility -android-gradle-plugin = "8.9.0" -coroutines = "1.10.1" -kotlinx-datetime = "0.6.1" -ktor = "3.0.3" +android-gradle-plugin = "8.13.1" +coroutines = "1.10.2" +kotlinx-datetime = "0.7.1" +ktor = "3.3.2" stately = "2.1.0" java = "21" -kermit = "2.0.5" -sqliter = "1.3.1" +kermit = "2.0.8" +sqliter = "1.3.3" hyperdrive = "0.1.148" -multiplatformSettings = "1.2.0" -sqlDelight = "2.0.2" -firebase-bom = "33.10.0" -firebase-crashlytics-gradle = "3.0.3" -gms-google-services = "4.4.2" +multiplatformSettings = "1.3.0" +sqlDelight = "2.2.1" +firebase-bom = "34.6.0" +firebase-crashlytics-gradle = "3.0.6" +gms-google-services = "4.4.4" # TODO: Update Compose libraries. There is currently a conflicing issue with the HorizontalPager -compose-androidx-ui = "1.7.8" +compose-androidx-ui = "1.9.5" compose-compiler = "1.5.15" -composeNavigation = "2.9.5" -compose-jb = "1.7.3" +composeNavigation = "2.9.6" +compose-jb = "1.9.3" -splashscreen = "1.0.1" +splashscreen = "1.2.0" junit = "4.13.2" -junitKtx = "1.2.1" -imageLoader = "1.2.2.1" +junitKtx = "1.3.0" +imageLoader = "1.10.0" korio = "4.0.10" # Sample - Android -androidx-core = "1.15.0" -androidx-lifecycle = "2.8.7" -androidx-activity-compose = "1.10.1" +androidx-core = "1.17.0" +androidx-lifecycle = "2.10.0" +androidx-activity-compose = "1.12.0" android-desugaring = "2.1.5" -koin = "4.0.2" -uuid = "0.8.3" -ktlint = "12.1.1" -coil = "3.1.0" -zoomimage = "1.1.0-alpha06" -skie = "0.10.1" +koin = "4.1.1" +uuid = "0.8.4" +ktlint = "14.0.1" +coil = "3.3.0" +zoomimage = "1.4.0" +skie = "0.10.8" [libraries] coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793..37f853b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/ios/build.gradle.kts b/ios/build.gradle.kts index abf76243..c61a2a46 100644 --- a/ios/build.gradle.kts +++ b/ios/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { @@ -49,7 +48,6 @@ kotlin { framework { baseName = "DroidconKit" isStatic = true - embedBitcodeMode = BitcodeEmbeddingMode.DISABLE freeCompilerArgs += listOf( "-linker-option", "-framework", "-linker-option", "Metal", diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index 6786b0fa..00ac9815 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -49,8 +49,9 @@ import co.touchlab.droidcon.domain.service.impl.json.AboutJsonResourceDataSource import co.touchlab.droidcon.domain.service.impl.json.JsonResourceReader import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout +import kotlin.time.Clock +import kotlin.time.ExperimentalTime import kotlinx.coroutines.runBlocking -import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import kotlinx.serialization.json.Json import org.koin.core.KoinApplication @@ -91,6 +92,7 @@ val booleanAdapter = object : ColumnAdapter { override fun encode(value: Boolean): Long = if (value) 1L else 0L } +@OptIn(ExperimentalTime::class) private val coreModule = module { single { DroidconDatabase.invoke( From c89be89c45cc982d53246a290533e27dcf49fd25 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 21 Nov 2025 10:21:02 -0500 Subject: [PATCH 04/56] adding compiler arg --- build.gradle.kts | 7 +++++++ ios/build.gradle.kts | 1 + shared-ui/build.gradle.kts | 1 + shared/build.gradle.kts | 1 + 4 files changed, 10 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index f5fe5500..377a8daf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { @@ -29,6 +30,12 @@ subprojects { freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") } } + + tasks.withType(KotlinNativeCompile::class).all { + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + } + } configure { version.set("1.4.0") enableExperimentalRules.set(true) diff --git a/ios/build.gradle.kts b/ios/build.gradle.kts index c61a2a46..873a09b7 100644 --- a/ios/build.gradle.kts +++ b/ios/build.gradle.kts @@ -22,6 +22,7 @@ kotlin { languageSettings.apply { optIn("kotlin.RequiresOptIn") optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") + optIn("kotlin.time.ExperimentalTime") } } iosMain.dependencies { diff --git a/shared-ui/build.gradle.kts b/shared-ui/build.gradle.kts index 9828407a..74510b38 100644 --- a/shared-ui/build.gradle.kts +++ b/shared-ui/build.gradle.kts @@ -106,6 +106,7 @@ kotlin { languageSettings.apply { optIn("kotlin.RequiresOptIn") optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") + optIn("kotlin.time.ExperimentalTime") } } matching { it.name.endsWith("Test") }.configureEach { diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index bdaf0388..4b22ada9 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -96,6 +96,7 @@ kotlin { languageSettings.apply { optIn("kotlin.RequiresOptIn") optIn("kotlinx.coroutines.ExperimentalCoroutinesApi") + optIn("kotlin.time.ExperimentalTime") } } From 12ad1e71cc5305ec6fae1b81dafb8eeadc2ec5a1 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 21 Nov 2025 11:13:37 -0500 Subject: [PATCH 05/56] Updating Dependencies and fixing deprecation warnings --- .../java/co/touchlab/droidcon/android/MainApp.kt | 4 ++-- gradle/libs.versions.toml | 13 ++++++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 2 +- .../ui/session/SessionDetailView.kt | 6 +++--- .../ui/session/SpeakerDetailView.kt | 4 ++-- .../util/LocalDateTime+startOfMinute.kt | 2 +- .../util/NavigationController.kt | 6 +++--- .../viewmodel/session/SessionDetailViewModel.kt | 2 +- .../droidcon/service/AndroidNotificationService.kt | 2 +- .../application/service/NotificationService.kt | 2 +- .../co/touchlab/droidcon/domain/entity/Session.kt | 2 +- .../repository/impl/SqlDelightSessionRepository.kt | 6 ++++-- .../impl/adapter/InstantSqlDelightAdapter.kt | 2 +- .../droidcon/domain/service/DateTimeService.kt | 2 +- .../domain/service/impl/DefaultDateTimeService.kt | 4 ++-- .../domain/service/impl/DefaultFeedbackService.kt | 3 +-- .../domain/service/impl/DefaultScheduleService.kt | 2 +- .../domain/service/impl/DefaultSyncService.kt | 2 +- .../sqldelight/co/touchlab/droidcon/db/Session.sq | 2 +- .../iosMain/kotlin/co/touchlab/droidcon/Koin.ios.kt | 1 + .../droidcon/service/IOSNotificationService.kt | 2 +- 22 files changed, 39 insertions(+), 34 deletions(-) diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt index 13ec2c1a..9c1cec2f 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt @@ -14,8 +14,8 @@ import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule import co.touchlab.droidcon.util.ClasspathResourceReader -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase +import com.google.firebase.Firebase +import com.google.firebase.analytics.analytics import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ObservableSettings import com.russhwolf.settings.SharedPreferencesSettings diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 58a205cd..7ae85ade 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] ## SDK Versions -minSdk = "21" -targetSdk = "35" +minSdk = "23" +targetSdk = "36" compileSdk = "36" # Dependencies @@ -23,6 +23,9 @@ hyperdrive = "0.1.148" multiplatformSettings = "1.3.0" sqlDelight = "2.2.1" +firebase-analytics = "22.5.0" +firebase-crashlytics = "19.4.4" +firebase-messaging = "24.1.2" firebase-bom = "34.6.0" firebase-crashlytics-gradle = "3.0.6" gms-google-services = "4.4.4" @@ -59,9 +62,9 @@ compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } -firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx", version = "_" } -firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx", version = "_" } -firebase-messaging = { module = "com.google.firebase:firebase-messaging-ktx", version = "_" } +firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "firebase-analytics" } +firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "firebase-crashlytics" } +firebase-messaging = { module = "com.google.firebase:firebase-messaging-ktx", version.ref = "firebase-messaging" } hyperdrive-multiplatformx-api = { module = "org.brightify.hyperdrive:multiplatformx-api", version.ref = "hyperdrive" } android-desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugaring" } uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b1..d4081da4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index 55116d7d..a06e880c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,7 +18,7 @@ dependencyResolutionManagement { } plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0") + id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0") } include(":shared", ":shared-ui", ":android", ":ios") diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt index 5ddcc439..ca7be9c6 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt @@ -21,9 +21,9 @@ import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.outlined.BookmarkAdd import androidx.compose.material3.Button -import androidx.compose.material3.Divider 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 @@ -102,7 +102,7 @@ internal fun SessionDetailView(viewModel: SessionDetailViewModel) { val title by viewModel.observeTitle.observeAsState() val locationInfo by viewModel.observeInfo.observeAsState() HeaderView(title, locationInfo) - Divider() + HorizontalDivider() } if (state != SessionDetailViewModel.SessionState.Ended) { val isAttending by viewModel.observeIsAttending.observeAsState() @@ -167,7 +167,7 @@ internal fun SessionDetailView(viewModel: SessionDetailViewModel) { textAlign = TextAlign.Center, ) - Divider() + HorizontalDivider() speakers.forEach { speaker -> SpeakerView(speaker) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SpeakerDetailView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SpeakerDetailView.kt index c16ad3dd..8f5589ef 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SpeakerDetailView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SpeakerDetailView.kt @@ -14,8 +14,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Language -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -80,7 +80,7 @@ internal fun SpeakerDetailView(viewModel: SpeakerDetailViewModel) { SocialView(WebLink.fromUrl(it), "linkedin") } - Divider() + HorizontalDivider() viewModel.bio?.let { BioView(it, viewModel.bioWebLinks) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/LocalDateTime+startOfMinute.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/LocalDateTime+startOfMinute.kt index 00317ab1..281fecbe 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/LocalDateTime+startOfMinute.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/LocalDateTime+startOfMinute.kt @@ -5,4 +5,4 @@ package co.touchlab.droidcon.util import kotlinx.datetime.LocalDateTime val LocalDateTime.startOfMinute: LocalDateTime - get() = LocalDateTime(year, month, dayOfMonth, hour, minute) + get() = LocalDateTime(year, month, day, hour, minute) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt index 66eaa404..4068c74b 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt @@ -4,7 +4,7 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.with +import androidx.compose.animation.togetherWith import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.RememberObserver @@ -260,9 +260,9 @@ internal fun NavigationStack(key: Any?, links: NavigationStackScope.() -> Unit, targetState = activeLinkComposables, transitionSpec = { if (initialState.indexOfLast { it.body != null } < targetState.indexOfLast { it.body != null }) { - slideInHorizontally(initialOffsetX = { it }) with slideOutHorizontally(targetOffsetX = { -it }) + slideInHorizontally(initialOffsetX = { it }).togetherWith(slideOutHorizontally(targetOffsetX = { -it })) } else { - slideInHorizontally(initialOffsetX = { -it }) with slideOutHorizontally(targetOffsetX = { it }) + slideInHorizontally(initialOffsetX = { -it }).togetherWith(slideOutHorizontally(targetOffsetX = { it })) } }, contentAlignment = Alignment.BottomCenter, diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt index ca6ce08d..758a830e 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt @@ -11,11 +11,11 @@ import co.touchlab.droidcon.dto.WebLink import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.droidcon.viewmodel.FeedbackDialogViewModel +import kotlin.time.Instant import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map -import kotlinx.datetime.Instant import org.brightify.hyperdrive.multiplatformx.BaseViewModel import org.brightify.hyperdrive.multiplatformx.property.asFlow import org.brightify.hyperdrive.multiplatformx.property.flatMapLatest diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/service/AndroidNotificationService.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/service/AndroidNotificationService.kt index 6f4392e9..c3ab1086 100644 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/service/AndroidNotificationService.kt +++ b/shared/src/androidMain/kotlin/co/touchlab/droidcon/service/AndroidNotificationService.kt @@ -21,7 +21,7 @@ import co.touchlab.kermit.Logger import com.russhwolf.settings.ObservableSettings import com.russhwolf.settings.get import com.russhwolf.settings.set -import kotlinx.datetime.Instant +import kotlin.time.Instant import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationService.kt index 6c9a1f54..c1bdcbe1 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationService.kt @@ -2,7 +2,7 @@ package co.touchlab.droidcon.application.service import co.touchlab.droidcon.domain.entity.Session import co.touchlab.droidcon.service.DeepLinkNotificationHandler -import kotlinx.datetime.Instant +import kotlin.time.Instant interface NotificationService { suspend fun initialize(): Boolean diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/Session.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/Session.kt index 726812a0..bef750d4 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/Session.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/entity/Session.kt @@ -1,7 +1,7 @@ package co.touchlab.droidcon.domain.entity import co.touchlab.droidcon.domain.service.DateTimeService -import kotlinx.datetime.Instant +import kotlin.time.Instant class Session( private val dateTimeService: DateTimeService, diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt index 0a3ba597..2c0c9041 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt @@ -9,10 +9,10 @@ import co.touchlab.droidcon.domain.entity.Room import co.touchlab.droidcon.domain.entity.Session import co.touchlab.droidcon.domain.repository.SessionRepository import co.touchlab.droidcon.domain.service.DateTimeService +import kotlin.time.Instant import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.datetime.Instant class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, private val sessionQueries: SessionQueries) : BaseRepository(), @@ -73,7 +73,9 @@ class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, ) } - override fun doDelete(id: Session.Id, conferenceId: Long) = sessionQueries.deleteById(id.value, conferenceId) + override fun doDelete(id: Session.Id, conferenceId: Long) { + sessionQueries.deleteById(id.value, conferenceId) + } override fun contains(id: Session.Id, conferenceId: Long): Boolean = sessionQueries.existsById(id.value, conferenceId).executeAsOne().toBoolean() diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/adapter/InstantSqlDelightAdapter.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/adapter/InstantSqlDelightAdapter.kt index 0aecc959..decd3e4c 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/adapter/InstantSqlDelightAdapter.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/adapter/InstantSqlDelightAdapter.kt @@ -1,7 +1,7 @@ package co.touchlab.droidcon.domain.repository.impl.adapter import app.cash.sqldelight.ColumnAdapter -import kotlinx.datetime.Instant +import kotlin.time.Instant object InstantSqlDelightAdapter : ColumnAdapter { diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/DateTimeService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/DateTimeService.kt index 9ac6703b..35f31a0f 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/DateTimeService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/DateTimeService.kt @@ -1,6 +1,6 @@ package co.touchlab.droidcon.domain.service -import kotlinx.datetime.Instant +import kotlin.time.Instant import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultDateTimeService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultDateTimeService.kt index e944f1eb..8563aaeb 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultDateTimeService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultDateTimeService.kt @@ -1,8 +1,8 @@ package co.touchlab.droidcon.domain.service.impl import co.touchlab.droidcon.domain.service.DateTimeService -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant +import kotlin.time.Clock +import kotlin.time.Instant import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt index e9e169a5..620b5e6b 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt @@ -6,9 +6,8 @@ import co.touchlab.droidcon.domain.service.FeedbackService import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ObservableSettings import com.russhwolf.settings.set +import kotlin.time.Clock import kotlinx.coroutines.flow.first -import kotlinx.datetime.Clock -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @OptIn(ExperimentalSettingsApi::class) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt index debaf85b..56f55f44 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt @@ -4,7 +4,7 @@ import co.touchlab.droidcon.domain.entity.Session import co.touchlab.droidcon.domain.repository.SessionRepository import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.ScheduleService -import kotlinx.datetime.Instant +import kotlin.time.Instant class DefaultScheduleService( private val sessionRepository: SessionRepository, diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt index d05df721..af4ff279 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt @@ -24,13 +24,13 @@ import co.touchlab.droidcon.domain.service.impl.dto.SpeakersDto.LinkType import co.touchlab.droidcon.domain.service.impl.dto.SponsorSessionsDto import co.touchlab.droidcon.domain.service.impl.dto.SponsorsDto import co.touchlab.kermit.Logger +import kotlin.time.Instant import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.datetime.DateTimeUnit -import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.minus diff --git a/shared/src/commonMain/sqldelight/co/touchlab/droidcon/db/Session.sq b/shared/src/commonMain/sqldelight/co/touchlab/droidcon/db/Session.sq index 75922387..e2dc8c28 100644 --- a/shared/src/commonMain/sqldelight/co/touchlab/droidcon/db/Session.sq +++ b/shared/src/commonMain/sqldelight/co/touchlab/droidcon/db/Session.sq @@ -1,4 +1,4 @@ -import kotlinx.datetime.Instant; +import kotlin.time.Instant; import kotlin.Int; CREATE TABLE sessionTable( diff --git a/shared/src/iosMain/kotlin/co/touchlab/droidcon/Koin.ios.kt b/shared/src/iosMain/kotlin/co/touchlab/droidcon/Koin.ios.kt index bd43fcbe..6f997c7c 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/droidcon/Koin.ios.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/droidcon/Koin.ios.kt @@ -78,6 +78,7 @@ fun Koin.get(objCProtocol: ObjCProtocol, qualifier: Qualifier?): Any { return get(kClazz, qualifier, null) } +@OptIn(BetaInteropApi::class) fun Koin.getAny(objCObject: ObjCObject, qualifier: Qualifier?, parameters: ParametersDefinition?): Any { val kclass = when (objCObject) { is ObjCClass -> getOriginalKotlinClass(objCObject) diff --git a/shared/src/iosMain/kotlin/co/touchlab/droidcon/service/IOSNotificationService.kt b/shared/src/iosMain/kotlin/co/touchlab/droidcon/service/IOSNotificationService.kt index 68ae3637..08e40cae 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/droidcon/service/IOSNotificationService.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/droidcon/service/IOSNotificationService.kt @@ -7,7 +7,7 @@ import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.SyncService import co.touchlab.droidcon.util.wrapMultiThreadCallback import co.touchlab.kermit.Logger -import kotlinx.datetime.Instant +import kotlin.time.Instant import kotlinx.datetime.toNSDate import platform.Foundation.NSCalendar import platform.Foundation.NSCalendarUnitDay From 5b9acd140af3246aca5ab47f6abc55f341fe6b50 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 21 Nov 2025 11:16:15 -0500 Subject: [PATCH 06/56] Update Koin.kt --- shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index 00ac9815..797f7037 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -92,7 +92,6 @@ val booleanAdapter = object : ColumnAdapter { override fun encode(value: Boolean): Long = if (value) 1L else 0L } -@OptIn(ExperimentalTime::class) private val coreModule = module { single { DroidconDatabase.invoke( From f233d2784635feb84c21bf92754eaea4097f1882 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 21 Nov 2025 11:58:29 -0500 Subject: [PATCH 07/56] Update Koin.kt --- shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index 797f7037..72bd2f04 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -50,7 +50,6 @@ import co.touchlab.droidcon.domain.service.impl.json.JsonResourceReader import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout import kotlin.time.Clock -import kotlin.time.ExperimentalTime import kotlinx.coroutines.runBlocking import kotlinx.datetime.TimeZone import kotlinx.serialization.json.Json From a975001f9a6107fc554ba75ef7136b2a062043bd Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 21 Nov 2025 12:00:12 -0500 Subject: [PATCH 08/56] Update Koin.kt --- shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index 797f7037..72bd2f04 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -50,7 +50,6 @@ import co.touchlab.droidcon.domain.service.impl.json.JsonResourceReader import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout import kotlin.time.Clock -import kotlin.time.ExperimentalTime import kotlinx.coroutines.runBlocking import kotlinx.datetime.TimeZone import kotlinx.serialization.json.Json From 94ae645f81fa093ae9deca73b209d163b0eb4fdf Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 21 Nov 2025 17:11:59 -0500 Subject: [PATCH 09/56] Finalizing JS for Shared --- build.gradle.kts | 4 +- kotlin-js-store/yarn.lock | 3051 +++++++++++++++++ shared/build.gradle.kts | 10 +- .../kotlin/co/touchlab/droidcon/Koin.kt | 4 +- .../droidcon/service/JsNotificationService.kt | 8 +- .../util/formatter/JsDateFormatter.kt | 2 +- 6 files changed, 3066 insertions(+), 13 deletions(-) create mode 100644 kotlin-js-store/yarn.lock diff --git a/build.gradle.kts b/build.gradle.kts index e46d1273..3759fcb7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,7 @@ subprojects { } } - /* + tasks.withType(KotlinCompile::class).all { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) @@ -36,7 +36,7 @@ subprojects { compilerOptions { freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") } - }*/ + } configure { version.set("1.4.0") enableExperimentalRules.set(true) diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock new file mode 100644 index 00000000..f8802357 --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,3051 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@^0.6.1": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz#f13c7c205915eb91ae54c557f5e92bddd8be0e83" + integrity sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + +"@jsonjoy.com/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/buffers@^1.0.0", "@jsonjoy.com/buffers@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz#8d99c7f67eaf724d3428dfd9826c6455266a5c83" + integrity sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA== + +"@jsonjoy.com/codegen@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz#5c23f796c47675f166d23b948cdb889184b93207" + integrity sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g== + +"@jsonjoy.com/json-pack@^1.11.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz#93f8dd57fe3a3a92132b33d1eb182dcd9e7629fa" + integrity sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg== + dependencies: + "@jsonjoy.com/base64" "^1.1.2" + "@jsonjoy.com/buffers" "^1.2.0" + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/json-pointer" "^1.0.2" + "@jsonjoy.com/util" "^1.9.0" + hyperdyperid "^1.2.0" + thingies "^2.5.0" + tree-dump "^1.1.0" + +"@jsonjoy.com/json-pointer@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz#049cb530ac24e84cba08590c5e36b431c4843408" + integrity sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg== + dependencies: + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/util" "^1.9.0" + +"@jsonjoy.com/util@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.9.0.tgz#7ee95586aed0a766b746cd8d8363e336c3c47c46" + integrity sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ== + dependencies: + "@jsonjoy.com/buffers" "^1.0.0" + "@jsonjoy.com/codegen" "^1.0.0" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + +"@types/body-parser@*": + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.12": + version "2.8.19" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" + integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.7": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz#74f47555b3d804b54cb7030e6f9aa0c7485cfc5b" + integrity sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express-serve-static-core@^4.17.21", "@types/express-serve-static-core@^4.17.33": + version "4.19.7" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz#f1d306dcc03b1aafbfb6b4fe684cce8a31cffc10" + integrity sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.5.tgz#3ba069177caa34ab96585ca23b3984d752300cdc" + integrity sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "^1" + +"@types/express@^4.17.21": + version "4.17.25" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.25.tgz#070c8c73a6fee6936d65c195dbbfb7da5026649b" + integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "^1" + +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + +"@types/http-proxy@^1.17.8": + version "1.17.17" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.17.tgz#d9e2c4571fe3507343cb210cd41790375e59a533" + integrity sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw== + dependencies: + "@types/node" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.14" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.14.tgz#006c2616ccd65550560c2757d8472eb6d3ecea0b" + integrity sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@>=10.0.0": + version "24.10.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.1.tgz#91e92182c93db8bd6224fca031e2370cef9a8f01" + integrity sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ== + dependencies: + undici-types "~7.16.0" + +"@types/qs@*": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/retry@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" + integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== + +"@types/send@*": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74" + integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ== + dependencies: + "@types/node" "*" + +"@types/send@<1": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.6.tgz#aeb5385be62ff58a52cd5459daa509ae91651d25" + integrity sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@^1", "@types/serve-static@^1.15.5": + version "1.15.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.10.tgz#768169145a778f8f5dfcb6360aead414a3994fee" + integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "<1" + +"@types/sockjs@^0.3.36": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + +"@types/ws@^8.5.10": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" + +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" + +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-3.0.1.tgz#76ac285b9658fa642ce238c276264589aa2b6b57" + integrity sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA== + +"@webpack-cli/info@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-3.0.1.tgz#3cff37fabb7d4ecaab6a8a4757d3826cf5888c63" + integrity sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ== + +"@webpack-cli/serve@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-3.0.1.tgz#bd8b1f824d57e30faa19eb78e4c0951056f72f00" + integrity sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-phases@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" + integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== + +acorn@^8.15.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +baseline-browser-mapping@^2.8.25: + version "2.8.30" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz#5c7420acc2fd20f3db820a40c6521590a671d137" + integrity sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@1.20.3, body-parser@^1.19.0: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.3.0.tgz#80d867430b5a0da64e82a8047fc1e355bdb71722" + integrity sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.24.0: + version "4.28.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929" + integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ== + dependencies: + baseline-browser-mapping "^2.8.25" + caniuse-lite "^1.0.30001754" + electron-to-chromium "^1.5.249" + node-releases "^2.0.27" + update-browserslist-db "^1.1.4" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001754: + version "1.0.30001756" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz#fe80104631102f88e58cad8aa203a2c3e5ec9ebd" + integrity sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.1, chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +compressible@~2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.8.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== + dependencies: + bytes "3.1.2" + compressible "~2.0.18" + debug "2.6.9" + negotiator "~0.6.4" + on-headers "~1.1.0" + safe-buffer "5.2.1" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +cookie@~0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.3, cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== + +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0, debug@^4.3.4, debug@^4.3.5: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +default-browser-id@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.1.tgz#f7a7ccb8f5104bf8e0f71ba3b1ccfa5eafdb21e8" + integrity sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q== + +default-browser@^5.2.1: + version "5.4.0" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.4.0.tgz#b55cf335bb0b465dd7c961a02cd24246aa434287" + integrity sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" + integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.249: + version "1.5.259" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz#d4393167ec14c5a046cebaec3ddf3377944ce965" + integrity sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + +engine.io@~6.6.0: + version "6.6.4" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.4.tgz#0a89a3e6b6c1d4b0c2a2a637495e7c149ec8d8ee" + integrity sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g== + dependencies: + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.7.2" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + +enhanced-resolve@^5.17.2: + version "5.18.3" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz#9b5f4c5c076b8787c78fe540392ce76a88855b44" + integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@~2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.2.tgz#22a5ed2fd7ce0cbcff1d1474cf4909a44bdb6e85" + integrity sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + punycode "^1.4.1" + safe-regex-test "^1.1.0" + +envinfo@^7.14.0: + version "7.20.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.20.0.tgz#3fd9de69fb6af3e777a017dfa033676368d67dd7" + integrity sha512-+zUomDcLXsVkQ37vUqWBvQwLaLlj8eZPSi61llaEFAVBY5mhcXdaSw1pSJVl4yTYD5g/gEfpNl28YYk4IPvrrg== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +express@^4.21.2: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-uri@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + +follow-redirects@^1.0.0: + version "1.15.11" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== + +foreground-child@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regex.js@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz#2b323728271d133830850e32311f40766c5f6413" + integrity sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ== + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^10.4.5: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3, glob@^7.1.7: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" + integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== + +http-proxy-middleware@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" + integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-network-error@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.3.0.tgz#2ce62cbca444abd506f8a900f39d20b898d37512" + integrity sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== + dependencies: + is-inside-container "^1.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isbinaryfile@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" + integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-yaml@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.1.tgz#4eafd31bbe684a747a6e8f3e4ad373e53979ced4" + integrity sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ== + dependencies: + glob "^7.1.3" + minimatch "^9.0.3" + webpack-merge "^4.1.5" + +karma@6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.4.tgz#dfa5a426cf5a8b53b43cd54ef0d0d09742351492" + integrity sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w== + dependencies: + "@colors/colors" "1.5.0" + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.7.2" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kotlin-web-helpers@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-2.1.0.tgz#6cd4b0f0dc3baea163929c8638155b8d19c55a74" + integrity sha512-NAJhiNB84tnvJ5EQx7iER3GWw7rsTZkX9HVHZpe7E3dDBD/dhTzqgSwNU3MfQjniy2rB04bP24WM9Z32ntUWRg== + dependencies: + format-util "^1.0.5" + +launch-editor@^2.6.1: + version "2.12.0" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.12.0.tgz#cc740f4e0263a6b62ead2485f9896e545321f817" + integrity sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg== + dependencies: + picocolors "^1.1.1" + shell-quote "^1.8.3" + +loader-runner@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3" + integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^4.43.1: + version "4.51.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.51.0.tgz#f33b5eff5e2faa01bfacc02aacf23ec7d8c84c94" + integrity sha512-4zngfkVM/GpIhC8YazOsM6E8hoB33NP0BCESPOA6z7qaL6umPJNqkO8CNYaLV2FB2MV6H1O3x2luHHOSqppv+A== + dependencies: + "@jsonjoy.com/json-pack" "^1.11.0" + "@jsonjoy.com/util" "^1.9.0" + glob-to-regex.js "^1.0.1" + thingies "^2.5.0" + tree-dump "^1.0.3" + tslib "^2.0.0" + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.3, minimatch@^9.0.4, minimatch@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@11.7.1: + version "11.7.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.7.1.tgz#91948fecd624fb4bd154ed260b7e1ad3910d7c7a" + integrity sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A== + dependencies: + browser-stdout "^1.3.1" + chokidar "^4.0.1" + debug "^4.3.5" + diff "^7.0.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^10.4.5" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^9.0.5" + ms "^2.1.3" + picocolors "^1.1.1" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^9.2.0" + yargs "^17.7.2" + yargs-parser "^21.1.1" + yargs-unparser "^2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-releases@^2.0.27: + version "2.0.27" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" + integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1, on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +open@^10.0.3: + version "10.2.0" + resolved "https://registry.yarnpkg.com/open/-/open-10.2.0.tgz#b9d855be007620e80b6fb05fac98141fe62db73c" + integrity sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA== + dependencies: + default-browser "^5.2.1" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + wsl-utils "^0.1.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-retry@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-6.2.1.tgz#81828f8dc61c6ef5a800585491572cc9892703af" + integrity sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ== + dependencies: + "@types/retry" "0.12.2" + is-network-error "^1.0.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rfdc@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-applescript@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911" + integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" + integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" + integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +socket.io-adapter@~2.5.2: + version "2.5.5" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" + integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg== + dependencies: + debug "~4.3.4" + ws "~8.17.1" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.7.2: + version "4.8.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a" + integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.6.0" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-loader@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-5.0.0.tgz#f593a916e1cc54471cfc8851b905c8a845fc7e38" + integrity sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA== + dependencies: + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== + dependencies: + ansi-regex "^6.0.1" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" + integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== + +terser-webpack-plugin@^5.3.11: + version "5.3.14" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" + +terser@^5.31.1: + version "5.44.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.1.tgz#e391e92175c299b8c284ad6ded609e37303b0a9c" + integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.15.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +thingies@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-2.5.0.tgz#5f7b882c933b85989f8466b528a6247a6881e04f" + integrity sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tmp@^0.2.1: + version "0.2.5" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tree-dump@^1.0.3, tree-dump@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.1.0.tgz#ab29129169dc46004414f5a9d4a3c6e89f13e8a4" + integrity sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA== + +tslib@^2.0.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +ua-parser-js@^0.7.30: + version "0.7.41" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.41.tgz#9f6dee58c389e8afababa62a4a2dc22edb69a452" + integrity sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg== + +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz#7802aa2ae91477f255b86e0e46dbc787a206ad4a" + integrity sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +watchpack@^2.4.1: + version "2.4.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947" + integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-6.0.1.tgz#a1ce25da5ba077151afd73adfa12e208e5089207" + integrity sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw== + dependencies: + "@discoveryjs/json-ext" "^0.6.1" + "@webpack-cli/configtest" "^3.0.1" + "@webpack-cli/info" "^3.0.1" + "@webpack-cli/serve" "^3.0.1" + colorette "^2.0.14" + commander "^12.1.0" + cross-spawn "^7.0.3" + envinfo "^7.14.0" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^6.0.1" + +webpack-dev-middleware@^7.4.2: + version "7.4.5" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz#d4e8720aa29cb03bc158084a94edb4594e3b7ac0" + integrity sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA== + dependencies: + colorette "^2.0.10" + memfs "^4.43.1" + mime-types "^3.0.1" + on-finished "^2.4.1" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz#96a143d50c58fef0c79107e61df911728d7ceb39" + integrity sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg== + dependencies: + "@types/bonjour" "^3.5.13" + "@types/connect-history-api-fallback" "^1.5.4" + "@types/express" "^4.17.21" + "@types/express-serve-static-core" "^4.17.21" + "@types/serve-index" "^1.9.4" + "@types/serve-static" "^1.15.5" + "@types/sockjs" "^0.3.36" + "@types/ws" "^8.5.10" + ansi-html-community "^0.0.8" + bonjour-service "^1.2.1" + chokidar "^3.6.0" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + express "^4.21.2" + graceful-fs "^4.2.6" + http-proxy-middleware "^2.0.9" + ipaddr.js "^2.1.0" + launch-editor "^2.6.1" + open "^10.0.3" + p-retry "^6.2.0" + schema-utils "^4.2.0" + selfsigned "^2.4.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^7.4.2" + ws "^8.18.0" + +webpack-merge@^4.1.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-merge@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" + integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.1" + +webpack-sources@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" + integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== + +webpack@5.100.2: + version "5.100.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.100.2.tgz#e2341facf9f7de1d702147c91bcb65b693adf9e8" + integrity sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw== + dependencies: + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.8" + "@types/json-schema" "^7.0.15" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.15.0" + acorn-import-phases "^1.0.3" + browserslist "^4.24.0" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.2" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^4.3.2" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.11" + watchpack "^2.4.1" + webpack-sources "^3.3.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +workerpool@^9.2.0: + version "9.3.4" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.3.4.tgz#f6c92395b2141afd78e2a889e80cb338fe9fca41" + integrity sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@8.18.3, ws@^8.18.0: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== + +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + +wsl-utils@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/wsl-utils/-/wsl-utils-0.1.0.tgz#8783d4df671d4d50365be2ee4c71917a0557baab" + integrity sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw== + dependencies: + is-wsl "^3.1.0" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index eb3bcfbb..d280cc84 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -105,10 +105,18 @@ kotlin { implementation(libs.ktor.client.ios) } } + + // Connect iOS target source sets to iosMain + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + + iosX64Main.dependsOn(iosMain.get()) + iosArm64Main.dependsOn(iosMain.get()) + iosSimulatorArm64Main.dependsOn(iosMain.get()) jsMain.dependencies { implementation(libs.ktor.client.cio) implementation(libs.sqldelight.driver.js) - } all { diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index 72bd2f04..5db98fc7 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -49,8 +49,8 @@ import co.touchlab.droidcon.domain.service.impl.json.AboutJsonResourceDataSource import co.touchlab.droidcon.domain.service.impl.json.JsonResourceReader import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout +import korlibs.io.async.runBlockingNoJs import kotlin.time.Clock -import kotlinx.coroutines.runBlocking import kotlinx.datetime.TimeZone import kotlinx.serialization.json.Json import org.koin.core.KoinApplication @@ -135,7 +135,7 @@ private val coreModule = module { // Add ConferenceConfigProvider single { val conferenceRepository: ConferenceRepository = get() - val selectedConference = runBlocking { + val selectedConference = runBlockingNoJs { conferenceRepository.getSelected() } DefaultConferenceConfigProvider( diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/service/JsNotificationService.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/service/JsNotificationService.kt index deabdf4c..6ad8f0cf 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/service/JsNotificationService.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/service/JsNotificationService.kt @@ -8,13 +8,7 @@ import kotlinx.datetime.Instant class JsNotificationService : NotificationService { override suspend fun initialize(): Boolean = false - override suspend fun schedule( - notification: Notification.Local, - title: String, - body: String, - delivery: Instant, - dismiss: Instant?, - ) { + override suspend fun schedule(notification: Notification.Local, title: String, body: String, delivery: Instant, dismiss: Instant?) { } override suspend fun cancel(sessionIds: List) { diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt index 2ca10e21..2244fff7 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt @@ -3,7 +3,7 @@ package co.touchlab.droidcon.util.formatter import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime -class JsDateFormatter() : DateFormatter { +class JsDateFormatter : DateFormatter { override fun monthWithDay(date: LocalDate): String? = null From 54aba14d63aa9b4649e1a20e64623dad4d0367c6 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 5 Dec 2025 13:55:40 -0500 Subject: [PATCH 10/56] Update build.gradle.kts --- shared-ui/build.gradle.kts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/shared-ui/build.gradle.kts b/shared-ui/build.gradle.kts index 74510b38..cf6a2fe1 100644 --- a/shared-ui/build.gradle.kts +++ b/shared-ui/build.gradle.kts @@ -66,6 +66,10 @@ kotlin { iosX64() iosArm64() iosSimulatorArm64() + js(IR) { + browser() + binaries.executable() + } version = "1.0" @@ -74,7 +78,6 @@ kotlin { implementation(projects.shared) api(libs.kermit) - api(libs.kermit.crashlytics) api(libs.kotlinx.coroutines.core) api(libs.kotlinx.datetime) api(libs.multiplatformSettings.core) @@ -100,8 +103,22 @@ kotlin { implementation(libs.zoomimage.composeResources) implementation(libs.hyperdrive.multiplatformx.api) - // implementation(libs.hyperdrive.multiplatformx.compose) } + val mobileMain by creating { + dependsOn(commonMain.get()) + dependencies { + api(libs.kermit.crashlytics) + } + } + + androidMain { + dependsOn(mobileMain) + } + iosMain { + dependsOn(mobileMain) + } + + all { languageSettings.apply { optIn("kotlin.RequiresOptIn") From b5e594491df620891c0c3eee0d20cf7146663147 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 5 Dec 2025 16:42:23 -0500 Subject: [PATCH 11/56] finalizing shared-ui --- shared-ui/build.gradle.kts | 11 +++ .../droidcon/ui/util/LocalImage.jvm.kt | 4 + .../ui/util/LocalImage.kt | 4 +- .../viewmodel/WaitForLoadedContextModel.kt | 5 +- .../touchlab/droidcon/ui/util/LocalImage.kt | 5 ++ .../settings/PlatformSpecificSettings.js.kt | 7 ++ .../co.touchlab.droidcon/ui/theme/Type.js.kt | 5 ++ .../co.touchlab.droidcon/ui/util/Dialog.js.kt | 24 ++++++ .../ui/util/LocalImage.js.kt | 74 +++++++++++++++++++ .../ui/util/NavigationBackPressWrapper.js.kt | 9 +++ .../kotlin/co/touchlab/droidcon/Koin.kt | 1 - 11 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/settings/PlatformSpecificSettings.js.kt create mode 100644 shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/theme/Type.js.kt create mode 100644 shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Dialog.js.kt create mode 100644 shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt create mode 100644 shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/NavigationBackPressWrapper.js.kt diff --git a/shared-ui/build.gradle.kts b/shared-ui/build.gradle.kts index cf6a2fe1..9d1eec8c 100644 --- a/shared-ui/build.gradle.kts +++ b/shared-ui/build.gradle.kts @@ -118,6 +118,17 @@ kotlin { dependsOn(mobileMain) } + val iosArm64Main by getting { + dependsOn(iosMain.get()) + } + + val iosSimulatorArm64Main by getting { + dependsOn(iosMain.get()) + } + + val iosX64Main by getting { + dependsOn(iosMain.get()) + } all { languageSettings.apply { diff --git a/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.jvm.kt b/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.jvm.kt index 6f7813f4..761ff22e 100644 --- a/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.jvm.kt +++ b/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.jvm.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import co.touchlab.droidcon.ui.theme.Dimensions +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers // Use of the function getIdentifier is discouraged, but we need to use it since the drawable names are defined in the common code for both // platforms and on each platform we need to get the drawable according to provided name. @@ -53,3 +55,5 @@ internal actual fun __LocalImage(imageResourceName: String, modifier: Modifier, } } } + +actual val IODispatcher: CoroutineDispatcher = Dispatchers.IO diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt index b5680f41..3fbfe73e 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt @@ -2,10 +2,12 @@ package co.touchlab.droidcon.ui.util import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier - +import kotlinx.coroutines.CoroutineDispatcher @Composable internal expect fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?) +expect val IODispatcher: CoroutineDispatcher + @Composable internal fun LocalImage(imageResourceName: String, modifier: Modifier = Modifier, contentDescription: String? = null) { __LocalImage(imageResourceName, modifier, contentDescription) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt index e54a7ee6..6c4dd701 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt @@ -4,9 +4,8 @@ import co.touchlab.droidcon.application.gateway.SettingsGateway import co.touchlab.droidcon.domain.entity.Conference import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.SyncService +import co.touchlab.droidcon.ui.util.IODispatcher import co.touchlab.kermit.Logger -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -36,7 +35,7 @@ class WaitForLoadedContextModel( suspend fun watchConferenceChanges() { lifecycle.whileAttached { - withContext(Dispatchers.IO) { + withContext(IODispatcher) { try { syncService.syncConferences() } catch (e: Exception) { diff --git a/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.kt b/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.kt index 75e2aad5..734999a1 100644 --- a/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.kt +++ b/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.kt @@ -19,6 +19,9 @@ import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.layout.ContentScale import co.touchlab.droidcon.ui.theme.Dimensions +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import platform.UIKit.UIImage @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) @@ -49,3 +52,5 @@ internal actual fun __LocalImage(imageResourceName: String, modifier: Modifier, } } } + +actual val IODispatcher: CoroutineDispatcher = Dispatchers.IO diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/settings/PlatformSpecificSettings.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/settings/PlatformSpecificSettings.js.kt new file mode 100644 index 00000000..f061b757 --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/settings/PlatformSpecificSettings.js.kt @@ -0,0 +1,7 @@ +package co.touchlab.droidcon.ui.settings + +import co.touchlab.droidcon.viewmodel.settings.SettingsViewModel +@androidx.compose.runtime.Composable +internal actual fun PlatformSpecificSettingsView(viewModel: SettingsViewModel) { + // Add settings specific for JS here. +} diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/theme/Type.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/theme/Type.js.kt new file mode 100644 index 00000000..edb0bc5d --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/theme/Type.js.kt @@ -0,0 +1,5 @@ +package co.touchlab.droidcon.ui.theme + +import androidx.compose.ui.text.font.FontFamily + +actual val montserratFontFamily: FontFamily = FontFamily.Default diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Dialog.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Dialog.js.kt new file mode 100644 index 00000000..6e7cfb7d --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Dialog.js.kt @@ -0,0 +1,24 @@ +package co.touchlab.droidcon.ui.util + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +internal actual fun Dialog(dismiss: () -> Unit, content: @Composable (() -> Unit)) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.3f)) + .clickable(interactionSource = MutableInteractionSource(), indication = null) { }, + contentAlignment = Alignment.Center, + ) { + content() + } +} diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt new file mode 100644 index 00000000..4abbd124 --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt @@ -0,0 +1,74 @@ +package co.touchlab.droidcon.ui.util + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.layout.ContentScale +import co.touchlab.droidcon.ui.theme.Dimensions +import kotlinx.browser.window +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.await +import org.jetbrains.skia.Image.Companion.makeFromEncoded +import org.khronos.webgl.Int8Array + +@Composable +internal actual fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?) { + val painter: Painter? by produceState(null, imageResourceName) { + value = try { + val fetched = window.fetch("drawable/$imageResourceName").await() + if (fetched.ok) { + val bytes = Int8Array(fetched.arrayBuffer().await()).unsafeCast() + makeFromEncoded(bytes).toComposeImageBitmap().let(::BitmapPainter) + } else { + null + } + } catch (e: Exception) { + null + } + } + + val currentPainter = painter + if (currentPainter != null) { + Image( + painter = currentPainter, + contentDescription = contentDescription, + modifier = modifier, + contentScale = ContentScale.FillWidth, + ) + } else { + Row( + modifier = modifier.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(Dimensions.Padding.half)), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.Warning, + contentDescription = contentDescription, + modifier = Modifier.padding(Dimensions.Padding.half), + tint = Color.White, + ) + Text("Image not supported", modifier = Modifier.padding(Dimensions.Padding.default), color = Color.White) + Spacer(modifier = Modifier.weight(1f)) + } + } +} + +actual val IODispatcher: CoroutineDispatcher = Dispatchers.Default diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/NavigationBackPressWrapper.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/NavigationBackPressWrapper.js.kt new file mode 100644 index 00000000..dc494370 --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/NavigationBackPressWrapper.js.kt @@ -0,0 +1,9 @@ +package co.touchlab.droidcon.ui.util + +import androidx.compose.runtime.Composable + +@Composable +internal actual fun NavigationBackPressWrapper(content: @Composable (() -> Unit)) { + // For now no back press wrapping is needed on Android. + content() +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index b8002b62..5db98fc7 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -51,7 +51,6 @@ import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout import korlibs.io.async.runBlockingNoJs import kotlin.time.Clock -import kotlinx.coroutines.runBlocking import kotlinx.datetime.TimeZone import kotlinx.serialization.json.Json import org.koin.core.KoinApplication From 7830f969ab9890b6b5a6c175b7219c369dec8323 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 8 Jan 2026 11:33:11 -0500 Subject: [PATCH 12/56] Adding small Web Project from Template --- build.gradle.kts | 1 - gradle/libs.versions.toml | 8 ++-- settings.gradle.kts | 2 +- web/build.gradle.kts | 32 +++++++++++++ .../kotlin/org/example/project/Platform.js.kt | 7 +++ .../org/example/project/Platform.wasmJs.kt | 7 +++ .../drawable/compose-multiplatform.xml | 44 ++++++++++++++++++ .../webMain/kotlin/org/example/project/App.kt | 45 +++++++++++++++++++ .../kotlin/org/example/project/Greeting.kt | 7 +++ .../kotlin/org/example/project/Main2.kt | 11 +++++ .../kotlin/org/example/project/Platform.kt | 7 +++ web/src/webMain/resources/index.html | 20 +++++++++ web/src/webMain/resources/styles.css | 7 +++ 13 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 web/build.gradle.kts create mode 100644 web/src/jsMain/kotlin/org/example/project/Platform.js.kt create mode 100644 web/src/wasmJsMain/kotlin/org/example/project/Platform.wasmJs.kt create mode 100644 web/src/webMain/composeResources/drawable/compose-multiplatform.xml create mode 100644 web/src/webMain/kotlin/org/example/project/App.kt create mode 100644 web/src/webMain/kotlin/org/example/project/Greeting.kt create mode 100644 web/src/webMain/kotlin/org/example/project/Main2.kt create mode 100644 web/src/webMain/kotlin/org/example/project/Platform.kt create mode 100644 web/src/webMain/resources/index.html create mode 100644 web/src/webMain/resources/styles.css diff --git a/build.gradle.kts b/build.gradle.kts index 3759fcb7..30a3cbfa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,6 @@ subprojects { } } - tasks.withType(KotlinCompile::class).all { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43ff4e33..81fa735d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,10 +9,10 @@ kotlin = "2.2.21" ## Gradle Plugin version must be compatible with Multiplatform ## https://kotlinlang.org/docs/multiplatform-compatibility-guide.html#version-compatibility -android-gradle-plugin = "8.13.1" +android-gradle-plugin = "8.13.2" coroutines = "1.10.2" kotlinx-datetime = "0.7.1" -ktor = "3.3.2" +ktor = "3.3.3" stately = "2.1.0" java = "21" @@ -26,12 +26,12 @@ sqlDelight = "2.2.1" firebase-analytics = "22.5.0" firebase-crashlytics = "19.4.4" firebase-messaging = "24.1.2" -firebase-bom = "34.6.0" +firebase-bom = "34.7.0" firebase-crashlytics-gradle = "3.0.6" gms-google-services = "4.4.4" # TODO: Update Compose libraries. There is currently a conflicing issue with the HorizontalPager -compose-androidx-ui = "1.9.5" +compose-androidx-ui = "1.10.0" compose-compiler = "1.5.15" composeNavigation = "2.9.6" compose-jb = "1.9.3" diff --git a/settings.gradle.kts b/settings.gradle.kts index a06e880c..8542b849 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,7 +21,7 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0") } -include(":shared", ":shared-ui", ":android", ":ios") +include(":shared", ":shared-ui", ":android", ":ios", ":web") rootProject.name = "Droidcon" diff --git a/web/build.gradle.kts b/web/build.gradle.kts new file mode 100644 index 00000000..bef1aad7 --- /dev/null +++ b/web/build.gradle.kts @@ -0,0 +1,32 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.composeCompiler) +} + +kotlin { + js { + browser() + binaries.executable() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + commonMain.dependencies { + + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material3) + } + commonTest.dependencies { + // implementation(libs.kotlin.test) + } + } +} diff --git a/web/src/jsMain/kotlin/org/example/project/Platform.js.kt b/web/src/jsMain/kotlin/org/example/project/Platform.js.kt new file mode 100644 index 00000000..14d8ff21 --- /dev/null +++ b/web/src/jsMain/kotlin/org/example/project/Platform.js.kt @@ -0,0 +1,7 @@ +package org.example.project + +class JsPlatform : Platform { + override val name: String = "Web with Kotlin/JS" +} + +actual fun getPlatform(): Platform = JsPlatform() diff --git a/web/src/wasmJsMain/kotlin/org/example/project/Platform.wasmJs.kt b/web/src/wasmJsMain/kotlin/org/example/project/Platform.wasmJs.kt new file mode 100644 index 00000000..7db26af4 --- /dev/null +++ b/web/src/wasmJsMain/kotlin/org/example/project/Platform.wasmJs.kt @@ -0,0 +1,7 @@ +package org.example.project + +class WasmPlatform : Platform { + override val name: String = "Web with Kotlin/Wasm" +} + +actual fun getPlatform(): Platform = WasmPlatform() diff --git a/web/src/webMain/composeResources/drawable/compose-multiplatform.xml b/web/src/webMain/composeResources/drawable/compose-multiplatform.xml new file mode 100644 index 00000000..1ffc948c --- /dev/null +++ b/web/src/webMain/composeResources/drawable/compose-multiplatform.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/webMain/kotlin/org/example/project/App.kt b/web/src/webMain/kotlin/org/example/project/App.kt new file mode 100644 index 00000000..3c0e48f3 --- /dev/null +++ b/web/src/webMain/kotlin/org/example/project/App.kt @@ -0,0 +1,45 @@ +package org.example.project + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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 + +@Composable +fun App() { + MaterialTheme { + var showContent by remember { mutableStateOf(false) } + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.primaryContainer) + .safeContentPadding() + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Button(onClick = { showContent = !showContent }) { + Text("Click me!") + } + AnimatedVisibility(showContent) { + val greeting = remember { Greeting().greet() } + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Compose: $greeting") + } + } + } + } +} diff --git a/web/src/webMain/kotlin/org/example/project/Greeting.kt b/web/src/webMain/kotlin/org/example/project/Greeting.kt new file mode 100644 index 00000000..42a53688 --- /dev/null +++ b/web/src/webMain/kotlin/org/example/project/Greeting.kt @@ -0,0 +1,7 @@ +package org.example.project + +class Greeting { + private val platform = getPlatform() + + fun greet(): String = "Hello, ${platform.name}!" +} diff --git a/web/src/webMain/kotlin/org/example/project/Main2.kt b/web/src/webMain/kotlin/org/example/project/Main2.kt new file mode 100644 index 00000000..6d79ecb8 --- /dev/null +++ b/web/src/webMain/kotlin/org/example/project/Main2.kt @@ -0,0 +1,11 @@ +package org.example.project + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + ComposeViewport { + App() + } +} diff --git a/web/src/webMain/kotlin/org/example/project/Platform.kt b/web/src/webMain/kotlin/org/example/project/Platform.kt new file mode 100644 index 00000000..d4b5d197 --- /dev/null +++ b/web/src/webMain/kotlin/org/example/project/Platform.kt @@ -0,0 +1,7 @@ +package org.example.project + +interface Platform { + val name: String +} + +expect fun getPlatform(): Platform diff --git a/web/src/webMain/resources/index.html b/web/src/webMain/resources/index.html new file mode 100644 index 00000000..890a8093 --- /dev/null +++ b/web/src/webMain/resources/index.html @@ -0,0 +1,20 @@ + + + + + + KotlinProject + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/webMain/resources/styles.css b/web/src/webMain/resources/styles.css new file mode 100644 index 00000000..0549b10f --- /dev/null +++ b/web/src/webMain/resources/styles.css @@ -0,0 +1,7 @@ +html, body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} \ No newline at end of file From 636285cd1b6874a7d6b41a8074c89a56223fba11 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 8 Jan 2026 12:32:05 -0500 Subject: [PATCH 13/56] Updating Web Call --- gradle/libs.versions.toml | 1 + .../co.touchlab.droidcon/ui/MainView.kt | 11 +++++ web/build.gradle.kts | 11 +++-- .../kotlin/org/example/project/Platform.js.kt | 7 --- .../touchlab/droidcon/web}/App.kt | 5 +- .../droidcon/web/DependencyInjection.kt | 46 +++++++++++++++++++ .../kotlin/co/touchlab/droidcon/web/Main2.kt | 14 ++++++ .../droidcon/web/WebAnalyticsService.kt | 9 ++++ .../web/service/DefaultParseUrlViewService.kt | 17 +++++++ .../NotificationLocalizedStringFactory.kt | 13 ++++++ .../droidcon/web/util/WebResourceReader.kt | 7 +++ .../web/util/formatter/WebDateFormatter.kt | 14 ++++++ .../kotlin/org/example/project/Greeting.kt | 7 --- .../kotlin/org/example/project/Main2.kt | 11 ----- .../kotlin/org/example/project/Platform.kt | 7 --- 15 files changed, 142 insertions(+), 38 deletions(-) create mode 100644 shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/MainView.kt delete mode 100644 web/src/jsMain/kotlin/org/example/project/Platform.js.kt rename web/src/webMain/kotlin/{org/example/project => co/touchlab/droidcon/web}/App.kt (91%) create mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt create mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt create mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt create mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt create mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt create mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt create mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/util/formatter/WebDateFormatter.kt delete mode 100644 web/src/webMain/kotlin/org/example/project/Greeting.kt delete mode 100644 web/src/webMain/kotlin/org/example/project/Main2.kt delete mode 100644 web/src/webMain/kotlin/org/example/project/Platform.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81fa735d..bf49e8a4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,6 +58,7 @@ skie = "0.10.8" coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +multiplatform-settings-make-observable = { module = "com.russhwolf:multiplatform-settings-make-observable", version.ref = "multiplatformSettings" } sqliter = { module = "co.touchlab:sqliter-driver", version.ref = "sqliter" } compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose-compiler" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/MainView.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/MainView.kt new file mode 100644 index 00000000..265a4ab2 --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/MainView.kt @@ -0,0 +1,11 @@ +package co.touchlab.droidcon.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel + +@Composable +fun MainView(waitForLoadedContextModel: WaitForLoadedContextModel) { + MainComposeView(waitForLoadedContextModel = waitForLoadedContextModel, modifier = Modifier) +} + diff --git a/web/build.gradle.kts b/web/build.gradle.kts index bef1aad7..01351b26 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -7,23 +7,28 @@ plugins { } kotlin { - js { + js(IR) { browser() binaries.executable() } + /* @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() binaries.executable() - } + }*/ sourceSets { commonMain.dependencies { - + implementation(projects.shared) + implementation(projects.sharedUi) + implementation(libs.koin.core) implementation(compose.ui) implementation(compose.foundation) implementation(compose.material3) + implementation(libs.multiplatformSettings.core) + implementation(libs.multiplatform.settings.make.observable) } commonTest.dependencies { // implementation(libs.kotlin.test) diff --git a/web/src/jsMain/kotlin/org/example/project/Platform.js.kt b/web/src/jsMain/kotlin/org/example/project/Platform.js.kt deleted file mode 100644 index 14d8ff21..00000000 --- a/web/src/jsMain/kotlin/org/example/project/Platform.js.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.example.project - -class JsPlatform : Platform { - override val name: String = "Web with Kotlin/JS" -} - -actual fun getPlatform(): Platform = JsPlatform() diff --git a/web/src/webMain/kotlin/org/example/project/App.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/App.kt similarity index 91% rename from web/src/webMain/kotlin/org/example/project/App.kt rename to web/src/webMain/kotlin/co/touchlab/droidcon/web/App.kt index 3c0e48f3..44e04b69 100644 --- a/web/src/webMain/kotlin/org/example/project/App.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/App.kt @@ -1,4 +1,4 @@ -package org.example.project +package co.touchlab.droidcon.web import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background @@ -32,12 +32,11 @@ fun App() { Text("Click me!") } AnimatedVisibility(showContent) { - val greeting = remember { Greeting().greet() } Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { - Text("Compose: $greeting") + Text("Compose: Hello") } } } diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt new file mode 100644 index 00000000..7436ea23 --- /dev/null +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt @@ -0,0 +1,46 @@ +package co.touchlab.droidcon.web + +import co.touchlab.droidcon.application.service.NotificationSchedulingService +import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.droidcon.domain.service.impl.ResourceReader +import co.touchlab.droidcon.initKoin +import co.touchlab.droidcon.service.ParseUrlViewService +import co.touchlab.droidcon.ui.uiModule +import co.touchlab.droidcon.util.formatter.DateFormatter +import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel +import co.touchlab.droidcon.web.service.DefaultParseUrlViewService +import co.touchlab.droidcon.web.util.NotificationLocalizedStringFactory +import co.touchlab.droidcon.web.util.WebResourceReader +import co.touchlab.droidcon.web.util.formatter.WebDateFormatter +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ObservableSettings +import com.russhwolf.settings.Settings +import com.russhwolf.settings.StorageSettings +import org.koin.core.KoinApplication +import org.koin.dsl.module +import com.russhwolf.settings.observable.makeObservable + +@OptIn(ExperimentalSettingsApi::class) +fun startKoin(): KoinApplication = + initKoin( + module { + single { + val storageSettings: Settings = StorageSettings() + storageSettings.makeObservable() + } + single { WebResourceReader() } + + single { WebDateFormatter() } + + single { NotificationLocalizedStringFactory() } + + single { WebAnalyticsService() } + + single { DefaultParseUrlViewService() } + } + uiModule, + ) + +@OptIn(ExperimentalWasmJsInterop::class) +@Suppress("unused") +val KoinApplication.waitForLoadedContextModel: WaitForLoadedContextModel + get() = get() diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt new file mode 100644 index 00000000..6638bd10 --- /dev/null +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt @@ -0,0 +1,14 @@ +package co.touchlab.droidcon.web + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport +import co.touchlab.droidcon.ui.MainView + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + val koinApplication = startKoin() + val viewModel = koinApplication.waitForLoadedContextModel + ComposeViewport { + MainView(viewModel) + } +} diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt new file mode 100644 index 00000000..3961c1c5 --- /dev/null +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt @@ -0,0 +1,9 @@ +package co.touchlab.droidcon.web + +import co.touchlab.droidcon.domain.service.AnalyticsService + +class WebAnalyticsService : AnalyticsService { + override fun logEvent(name: String, params: Map) { + //TODO("Not yet implemented") + } +} diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt new file mode 100644 index 00000000..415d6fef --- /dev/null +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt @@ -0,0 +1,17 @@ +package co.touchlab.droidcon.web.service + +import co.touchlab.droidcon.dto.WebLink +import co.touchlab.droidcon.service.ParseUrlViewService + +class DefaultParseUrlViewService : ParseUrlViewService { + + private val urlRegex = + "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)".toRegex() + + override fun parse(text: String): List = urlRegex.findAll(text).map { + WebLink( + it.range, + it.value + ) + }.toList() +} diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt new file mode 100644 index 00000000..404d3444 --- /dev/null +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt @@ -0,0 +1,13 @@ +package co.touchlab.droidcon.web.util + +import co.touchlab.droidcon.application.service.NotificationSchedulingService + +class NotificationLocalizedStringFactory() : NotificationSchedulingService.LocalizedStringFactory { + + override fun reminderTitle(roomName: String?): String = "" + override fun reminderBody(sessionTitle: String): String = "" + + override fun feedbackTitle(): String = "" + + override fun feedbackBody(): String = "" +} diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt new file mode 100644 index 00000000..f8d80617 --- /dev/null +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt @@ -0,0 +1,7 @@ +package co.touchlab.droidcon.web.util + +import co.touchlab.droidcon.domain.service.impl.ResourceReader + +class WebResourceReader : ResourceReader { + override fun readResource(name: String): String = "" +} diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/formatter/WebDateFormatter.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/formatter/WebDateFormatter.kt new file mode 100644 index 00000000..a211e6fc --- /dev/null +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/formatter/WebDateFormatter.kt @@ -0,0 +1,14 @@ +package co.touchlab.droidcon.web.util.formatter + +import co.touchlab.droidcon.util.formatter.DateFormatter +import kotlin.getValue +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime + +class WebDateFormatter : DateFormatter { + override fun monthWithDay(date: LocalDate): String? = null + + override fun timeOnly(dateTime: LocalDateTime): String? = null + + override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime): String = "" +} diff --git a/web/src/webMain/kotlin/org/example/project/Greeting.kt b/web/src/webMain/kotlin/org/example/project/Greeting.kt deleted file mode 100644 index 42a53688..00000000 --- a/web/src/webMain/kotlin/org/example/project/Greeting.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.example.project - -class Greeting { - private val platform = getPlatform() - - fun greet(): String = "Hello, ${platform.name}!" -} diff --git a/web/src/webMain/kotlin/org/example/project/Main2.kt b/web/src/webMain/kotlin/org/example/project/Main2.kt deleted file mode 100644 index 6d79ecb8..00000000 --- a/web/src/webMain/kotlin/org/example/project/Main2.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.example.project - -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.window.ComposeViewport - -@OptIn(ExperimentalComposeUiApi::class) -fun main() { - ComposeViewport { - App() - } -} diff --git a/web/src/webMain/kotlin/org/example/project/Platform.kt b/web/src/webMain/kotlin/org/example/project/Platform.kt deleted file mode 100644 index d4b5d197..00000000 --- a/web/src/webMain/kotlin/org/example/project/Platform.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.example.project - -interface Platform { - val name: String -} - -expect fun getPlatform(): Platform From 9b216190e3cb7919ab4388439584d7a917042db7 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 8 Jan 2026 12:36:57 -0500 Subject: [PATCH 14/56] Update WaitForLoadedContextModel.kt --- .../co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt index 6c4dd701..45842878 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt @@ -6,12 +6,14 @@ import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.SyncService import co.touchlab.droidcon.ui.util.IODispatcher import co.touchlab.kermit.Logger +import kotlin.js.JsExport import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.brightify.hyperdrive.multiplatformx.BaseViewModel +@JsExport // TODO: Try this? class WaitForLoadedContextModel( private val conferenceConfigProvider: ConferenceConfigProvider, applicationViewModelFactory: ApplicationViewModel.Factory, From 664a786d56008f4eeb48da2deed9fdce6b4af5d9 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 22 Jan 2026 09:42:56 -0500 Subject: [PATCH 15/56] Updating sqldelight driver to be suspend and fixing some issues with koin --- .../impl/SqlDelightDriverFactory.android.kt | 2 +- .../impl/SqlDelightDriverFactory.kt | 2 +- .../impl/SqlDelightDriverFactory.ios.kt | 2 +- .../impl/SqlDelightDriverFactory.js.kt | 11 +++-- web/build.gradle.kts | 2 + .../kotlin/co/touchlab/droidcon/web/App.kt | 44 ------------------- .../kotlin/co/touchlab/droidcon/web/Main2.kt | 6 ++- 7 files changed, 18 insertions(+), 51 deletions(-) delete mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/App.kt diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt index 143c4574..e741b399 100644 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt +++ b/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt @@ -6,5 +6,5 @@ import app.cash.sqldelight.driver.android.AndroidSqliteDriver import co.touchlab.droidcon.db.DroidconDatabase actual class SqlDelightDriverFactory(private val context: Context) { - actual fun createDriver(): SqlDriver = AndroidSqliteDriver(DroidconDatabase.Schema, context, "droidcon.db") + actual suspend fun createDriver(): SqlDriver = AndroidSqliteDriver(DroidconDatabase.Schema, context, "droidcon.db") } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.kt index 6d71e5ae..76e0aa60 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.kt @@ -3,5 +3,5 @@ package co.touchlab.droidcon.domain.repository.impl import app.cash.sqldelight.db.SqlDriver expect class SqlDelightDriverFactory { - fun createDriver(): SqlDriver + suspend fun createDriver(): SqlDriver } diff --git a/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt b/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt index b20c448c..bdd95cef 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt @@ -5,5 +5,5 @@ import app.cash.sqldelight.driver.native.NativeSqliteDriver import co.touchlab.droidcon.db.DroidconDatabase actual class SqlDelightDriverFactory { - actual fun createDriver(): SqlDriver = NativeSqliteDriver(DroidconDatabase.Schema, "droidcon.db") + actual suspend fun createDriver(): SqlDriver = NativeSqliteDriver(DroidconDatabase.Schema, "droidcon.db") } diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt index e34ce5c0..f22eae52 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt @@ -1,9 +1,14 @@ package co.touchlab.droidcon.domain.repository.impl import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.worker.WebWorkerDriver +import app.cash.sqldelight.driver.worker.expected.Worker +import co.touchlab.droidcon.db.DroidconDatabase actual class SqlDelightDriverFactory { - actual fun createDriver(): SqlDriver { - TODO("Not yet implemented") - } + actual suspend fun createDriver(): SqlDriver = WebWorkerDriver( + Worker( + js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)"""), + ), + ).also { DroidconDatabase.Schema.create(it).await() } } diff --git a/web/build.gradle.kts b/web/build.gradle.kts index 01351b26..7dad3404 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -29,6 +29,8 @@ kotlin { implementation(compose.material3) implementation(libs.multiplatformSettings.core) implementation(libs.multiplatform.settings.make.observable) + implementation(libs.koin.compose) + implementation("androidx.lifecycle:lifecycle-viewmodel: 2.10.0") } commonTest.dependencies { // implementation(libs.kotlin.test) diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/App.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/App.kt deleted file mode 100644 index 44e04b69..00000000 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/App.kt +++ /dev/null @@ -1,44 +0,0 @@ -package co.touchlab.droidcon.web - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.safeContentPadding -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -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 - -@Composable -fun App() { - MaterialTheme { - var showContent by remember { mutableStateOf(false) } - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.primaryContainer) - .safeContentPadding() - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Button(onClick = { showContent = !showContent }) { - Text("Click me!") - } - AnimatedVisibility(showContent) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text("Compose: Hello") - } - } - } - } -} diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt index 6638bd10..f1d8117f 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt @@ -3,12 +3,16 @@ package co.touchlab.droidcon.web import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport import co.touchlab.droidcon.ui.MainView +import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel +import org.koin.compose.koinInject @OptIn(ExperimentalComposeUiApi::class) fun main() { val koinApplication = startKoin() - val viewModel = koinApplication.waitForLoadedContextModel + ComposeViewport { + val viewModel:WaitForLoadedContextModel = koinInject() // Works MainView(viewModel) + } } From c76364eaa00472ef3c4d1018dd0d69f4b1620781 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Tue, 10 Feb 2026 15:02:19 -0500 Subject: [PATCH 16/56] temporary merges for JS --- gradle/libs.versions.toml | 1 + .../viewmodel/WaitForLoadedContextModel.kt | 6 +- .../session/BaseSessionListViewModel.kt | 2 +- .../viewmodel/session/SessionDayViewModel.kt | 2 +- .../session/SessionDetailViewModel.kt | 5 +- .../impl/SqlDelightDriverFactory.android.kt | 2 +- .../util/formatter/AndroidDateFormatter.kt | 11 ++-- .../kotlin/co/touchlab/droidcon/Koin.kt | 8 +-- .../DefaultNotificationSchedulingService.kt | 13 ++++- .../gateway/impl/DefaultSessionGateway.kt | 50 ++++++++++++----- .../gateway/impl/DefaultSponsorGateway.kt | 23 +++++--- .../impl/SqlDelightDriverFactory.kt | 2 +- .../service/ConferenceConfigProvider.kt | 18 +++--- .../service/impl/DefaultApiDataSource.kt | 29 +++++----- .../impl/DefaultConferenceConfigProvider.kt | 51 +++++++++++------ .../service/impl/DefaultScheduleService.kt | 1 + .../domain/service/impl/DefaultSyncService.kt | 26 ++++++--- .../impl/SqlDelightDriverFactory.ios.kt | 2 +- .../impl/SqlDelightDriverFactory.js.kt | 9 +-- web/build.gradle.kts | 4 +- .../co/touchlab/droidcon/web/KoinTest.kt | 55 +++++++++++++++++++ .../droidcon/web/DependencyInjection.kt | 26 +++++---- 22 files changed, 236 insertions(+), 110 deletions(-) create mode 100644 web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf49e8a4..a2ece09c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -118,6 +118,7 @@ compose-navigation = { module = "androidx.navigation:navigation-compose", versio koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } zoomimage-composeResources = { module = "io.github.panpf.zoomimage:zoomimage-compose-resources", version.ref = "zoomimage" } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt index 45842878..75913d56 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt @@ -12,8 +12,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.brightify.hyperdrive.multiplatformx.BaseViewModel - -@JsExport // TODO: Try this? class WaitForLoadedContextModel( private val conferenceConfigProvider: ConferenceConfigProvider, applicationViewModelFactory: ApplicationViewModel.Factory, @@ -47,7 +45,9 @@ class WaitForLoadedContextModel( launch { conferenceConfigProvider.observeChanges().collect { conference -> - _state.emit(State.Ready(conference)) + if (conference != null) { + _state.emit(State.Ready(conference)) + } } } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt index 415110f3..157c6d93 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt @@ -43,7 +43,7 @@ abstract class BaseSessionListViewModel( .groupBy { it.session.startsAt.toConferenceDateTime( dateTimeService, - conferenceConfigProvider.getConferenceTimeZone(), + conferenceConfigProvider.getConferenceTimeZone() ?: kotlinx.datetime.TimeZone.UTC, ).date } .map { (date, items) -> diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt index 7a2c3210..f7b05eed 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt @@ -27,7 +27,7 @@ class SessionDayViewModel( .groupBy { it.session.startsAt.toConferenceDateTime( dateTimeService, - conferenceTimeZone = conferenceConfigProvider.getConferenceTimeZone(), + conferenceTimeZone = conferenceConfigProvider.getConferenceTimeZone() ?: kotlinx.datetime.TimeZone.UTC, ).startOfMinute } .map { (startsAt, items) -> diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt index 758a830e..97681885 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt @@ -57,9 +57,10 @@ class SessionDetailViewModel( listOfNotNull( it.room?.name, with(dateTimeService) { + val timeZone = conferenceConfigProvider.getConferenceTimeZone() ?: kotlinx.datetime.TimeZone.UTC dateFormatter.timeOnlyInterval( - it.session.startsAt.toConferenceDateTime(conferenceConfigProvider.getConferenceTimeZone()), - it.session.endsAt.toConferenceDateTime(conferenceConfigProvider.getConferenceTimeZone()), + it.session.startsAt.toConferenceDateTime(timeZone), + it.session.endsAt.toConferenceDateTime(timeZone), ) }, ).joinToString() diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt index e741b399..143c4574 100644 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt +++ b/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt @@ -6,5 +6,5 @@ import app.cash.sqldelight.driver.android.AndroidSqliteDriver import co.touchlab.droidcon.db.DroidconDatabase actual class SqlDelightDriverFactory(private val context: Context) { - actual suspend fun createDriver(): SqlDriver = AndroidSqliteDriver(DroidconDatabase.Schema, context, "droidcon.db") + actual fun createDriver(): SqlDriver = AndroidSqliteDriver(DroidconDatabase.Schema, context, "droidcon.db") } diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/formatter/AndroidDateFormatter.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/formatter/AndroidDateFormatter.kt index 7b8ec5c1..9a7418df 100644 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/formatter/AndroidDateFormatter.kt +++ b/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/formatter/AndroidDateFormatter.kt @@ -14,24 +14,25 @@ class AndroidDateFormatter(private val dateTimeService: DateTimeService, private DateFormatter { // Get timezone from ConferenceConfigProvider - private val conferenceTimeZone get() = conferenceConfigProvider.getConferenceTimeZone() + private val conferenceTimeZone: kotlinx.datetime.TimeZone? + get() = conferenceConfigProvider.getConferenceTimeZone() // Create formatters as properties to ensure they use the current conference timezone private val shortDateFormat get() = SimpleDateFormat("MMM d", Locale.getDefault()).apply { - timeZone = java.util.TimeZone.getTimeZone(conferenceTimeZone.id) + timeZone = java.util.TimeZone.getTimeZone(conferenceTimeZone?.id ?: "UTC") } private val minuteHourTimeFormat get() = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()) - .apply { timeZone = java.util.TimeZone.getTimeZone(conferenceTimeZone.id) } + .apply { timeZone = java.util.TimeZone.getTimeZone(conferenceTimeZone?.id ?: "UTC") } override fun monthWithDay(date: LocalDate): String = shortDateFormat.format( - Date(with(dateTimeService) { date.atTime(0, 0).fromConferenceDateTime(conferenceTimeZone) }.toEpochMilliseconds()), + Date(with(dateTimeService) { date.atTime(0, 0).fromConferenceDateTime(conferenceTimeZone ?: kotlinx.datetime.TimeZone.UTC) }.toEpochMilliseconds()), ).uppercase() override fun timeOnly(dateTime: LocalDateTime): String? = minuteHourTimeFormat.format( - Date(with(dateTimeService) { dateTime.fromConferenceDateTime(conferenceTimeZone) }.toEpochMilliseconds()), + Date(with(dateTimeService) { dateTime.fromConferenceDateTime(conferenceTimeZone ?: kotlinx.datetime.TimeZone.UTC) }.toEpochMilliseconds()), ) override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime): String = diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index 5db98fc7..41b8e22f 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -49,7 +49,6 @@ import co.touchlab.droidcon.domain.service.impl.json.AboutJsonResourceDataSource import co.touchlab.droidcon.domain.service.impl.json.JsonResourceReader import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout -import korlibs.io.async.runBlockingNoJs import kotlin.time.Clock import kotlinx.datetime.TimeZone import kotlinx.serialization.json.Json @@ -134,13 +133,9 @@ private val coreModule = module { // Add ConferenceConfigProvider single { - val conferenceRepository: ConferenceRepository = get() - val selectedConference = runBlockingNoJs { - conferenceRepository.getSelected() - } DefaultConferenceConfigProvider( conferenceRepository = get(), - initialConference = selectedConference, + initialConference = null, ) } @@ -195,6 +190,7 @@ private val coreModule = module { conferenceRepository = get(), ) } + single(qualifier(DefaultSyncService.DataSource.Kind.Api)) { DefaultApiDataSource( client = get(), diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt index a0bf096d..6d35cff6 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt @@ -48,11 +48,12 @@ class DefaultNotificationSchedulingService( } override suspend fun runScheduling() { + val conferenceId = conferenceConfigProvider.getConferenceId() ?: return notificationService.initialize() coroutineScope { launch { scheduleNotifications( - sessionRepository.observeAllAttending(conferenceConfigProvider.getConferenceId()), + sessionRepository.observeAllAttending(conferenceId), settingsRepository.settings, ) } @@ -60,9 +61,10 @@ class DefaultNotificationSchedulingService( } override suspend fun rescheduleAll() { + val conferenceId = conferenceConfigProvider.getConferenceId() ?: return scheduledNotifications = emptyList() scheduleNotifications( - sessionRepository.observeAllAttending(conferenceConfigProvider.getConferenceId()).take(1), + sessionRepository.observeAllAttending(conferenceId).take(1), settingsRepository.settings.take(1), ) } @@ -90,7 +92,12 @@ class DefaultNotificationSchedulingService( for (session in newSessions) { if (isRemindersEnabled) { val roomName = session.room?.let { - roomRepository.get(it, conferenceConfigProvider.getConferenceId()).name + val conferenceId = conferenceConfigProvider.getConferenceId() + if (conferenceId != null) { + roomRepository.get(it, conferenceId).name + } else { + null + } } val reminderDelivery = session.startsAt.plus(NotificationSchedulingService.REMINDER_DELIVERY_START_OFFSET, DateTimeUnit.MINUTE) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt index d8a4004b..ca78baab 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt @@ -10,6 +10,7 @@ import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.ScheduleService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map class DefaultSessionGateway( @@ -20,10 +21,15 @@ class DefaultSessionGateway( private val conferenceConfigProvider: ConferenceConfigProvider, ) : SessionGateway { - private val conferenceId get() = conferenceConfigProvider.getConferenceId() + private val conferenceId: Long? + get() = conferenceConfigProvider.getConferenceId() override fun observeSchedule(): Flow> = conferenceConfigProvider.observeChanges().flatMapLatest { conf -> - sessionRepository.observeAll(conf.id) + if (conf == null) { + flowOf(emptyList()) + } else { + sessionRepository.observeAll(conf.id) + } }.map { sessions -> sessions.map { session -> scheduleItemForSession(session) @@ -31,32 +37,46 @@ class DefaultSessionGateway( } override fun observeAgenda(): Flow> = conferenceConfigProvider.observeChanges().flatMapLatest { conf -> - sessionRepository.observeAllAttending(conf.id) + if (conf == null) { + flowOf(emptyList()) + } else { + sessionRepository.observeAllAttending(conf.id) + } }.map { sessions -> sessions.map { session -> scheduleItemForSession(session) } } - override fun observeScheduleItem(id: Session.Id): Flow = sessionRepository.observe(id, conferenceId).map { session -> - scheduleItemForSession(session) + override fun observeScheduleItem(id: Session.Id): Flow { + val confId = conferenceId ?: throw IllegalStateException("Conference ID is not available") + return sessionRepository.observe(id, confId).map { session -> + scheduleItemForSession(session) + } } - private suspend fun scheduleItemForSession(session: Session): ScheduleItem = ScheduleItem( - session, - scheduleService.isInConflict(session), - session.room?.let { roomRepository.find(it, conferenceId) }, - profileRepository.getSpeakersBySession(session.id, conferenceId), - ) + private suspend fun scheduleItemForSession(session: Session): ScheduleItem { + val confId = conferenceId ?: throw IllegalStateException("Conference ID is not available") + return ScheduleItem( + session, + scheduleService.isInConflict(session), + session.room?.let { roomRepository.find(it, confId) }, + profileRepository.getSpeakersBySession(session.id, confId), + ) + } override suspend fun setAttending(session: Session, attending: Boolean) { - sessionRepository.setRsvp(session.id, Session.RSVP(attending, false), conferenceId) + val confId = conferenceId ?: throw IllegalStateException("Conference ID is not available") + sessionRepository.setRsvp(session.id, Session.RSVP(attending, false), confId) } override suspend fun setFeedback(session: Session, feedback: Session.Feedback) { - sessionRepository.setFeedback(session.id, feedback, conferenceId) + val confId = conferenceId ?: throw IllegalStateException("Conference ID is not available") + sessionRepository.setFeedback(session.id, feedback, confId) } - override suspend fun getScheduleItem(id: Session.Id): ScheduleItem? = - sessionRepository.find(id, conferenceId)?.let { scheduleItemForSession(it) } + override suspend fun getScheduleItem(id: Session.Id): ScheduleItem? { + val confId = conferenceId ?: return null + return sessionRepository.find(id, confId)?.let { scheduleItemForSession(it) } + } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt index 3b8b844e..927483fa 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt @@ -18,19 +18,28 @@ class DefaultSponsorGateway( private val conferenceConfigProvider: ConferenceConfigProvider, ) : SponsorGateway { - override fun observeSponsors(): Flow> = - sponsorGroupRepository.observeAll(conferenceConfigProvider.getConferenceId()).map { groups -> + override fun observeSponsors(): Flow> { + val conferenceId = conferenceConfigProvider.getConferenceId() + ?: return kotlinx.coroutines.flow.flowOf(emptyList()) + return sponsorGroupRepository.observeAll(conferenceId).map { groups -> groups.map { group -> SponsorGroupWithSponsors( group, - sponsorRepository.allByGroupName(group.name, conferenceConfigProvider.getConferenceId()), + sponsorRepository.allByGroupName(group.name, conferenceId), ) } } + } - override fun observeSponsorById(id: Sponsor.Id): Flow = - sponsorRepository.observe(id, conferenceConfigProvider.getConferenceId()) + override fun observeSponsorById(id: Sponsor.Id): Flow { + val conferenceId = conferenceConfigProvider.getConferenceId() + ?: throw IllegalStateException("Conference ID is not available") + return sponsorRepository.observe(id, conferenceId) + } - override suspend fun getRepresentatives(sponsorId: Sponsor.Id): List = - profileRepository.getSponsorRepresentatives(sponsorId, conferenceConfigProvider.getConferenceId()) + override suspend fun getRepresentatives(sponsorId: Sponsor.Id): List { + val conferenceId = conferenceConfigProvider.getConferenceId() + ?: throw IllegalStateException("Conference ID is not available") + return profileRepository.getSponsorRepresentatives(sponsorId, conferenceId) + } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.kt index 76e0aa60..6d71e5ae 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.kt @@ -3,5 +3,5 @@ package co.touchlab.droidcon.domain.repository.impl import app.cash.sqldelight.db.SqlDriver expect class SqlDelightDriverFactory { - suspend fun createDriver(): SqlDriver + fun createDriver(): SqlDriver } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt index 39d44353..ff0dce13 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt @@ -5,19 +5,19 @@ import kotlinx.coroutines.flow.Flow import kotlinx.datetime.TimeZone interface ConferenceConfigProvider { - fun getConferenceId(): Long - fun getConferenceTimeZone(): TimeZone - fun getProjectId(): String - fun getCollectionName(): String - fun getApiKey(): String - fun getScheduleId(): String - fun showVenueMap(): Boolean - fun observeChanges(): Flow + fun getConferenceId(): Long? + fun getConferenceTimeZone(): TimeZone? + fun getProjectId(): String? + fun getCollectionName(): String? + fun getApiKey(): String? + fun getScheduleId(): String? + fun showVenueMap(): Boolean? + fun observeChanges(): Flow /** * Get the currently selected conference */ - suspend fun getSelectedConference(): Conference + suspend fun getSelectedConference(): Conference? /** * Initiates conference observation. diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt index bc1de1d5..c15420c3 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt @@ -20,32 +20,35 @@ class DefaultApiDataSource( private val json: Json, private val conferenceConfigProvider: ConferenceConfigProvider, ) : DefaultSyncService.DataSource { - override suspend fun getSpeakers(): List { + override suspend fun getSpeakers(): List? { + val scheduleId = conferenceConfigProvider.getScheduleId() ?: return null val jsonString = client.get { // We want to use the same scheduleId for speakers and schedule - sessionize("/api/v2/${conferenceConfigProvider.getScheduleId()}/view/speakers") + sessionize("/api/v2/$scheduleId/view/speakers") }.bodyAsText() return json.decodeFromString(ListSerializer(SpeakersDto.SpeakerDto.serializer()), jsonString) } - override suspend fun getSchedule(): List { + override suspend fun getSchedule(): List? { + val scheduleId = conferenceConfigProvider.getScheduleId() ?: return null val jsonString = client.get { - sessionize("/api/v2/${conferenceConfigProvider.getScheduleId()}/view/gridtable") + sessionize("/api/v2/$scheduleId/view/gridtable") }.bodyAsText() return json.decodeFromString(ListSerializer(ScheduleDto.DayDto.serializer()), jsonString) } - override suspend fun getSponsorSessions(): List { + override suspend fun getSponsorSessions(): List? { + val scheduleId = conferenceConfigProvider.getScheduleId() ?: return null val jsonString = client.get { - sessionize("/api/v2/${conferenceConfigProvider.getScheduleId()}/view/sessions") + sessionize("/api/v2/$scheduleId/view/sessions") }.bodyAsText() return json.decodeFromString(ListSerializer(SponsorSessionsDto.SessionGroupDto.serializer()), jsonString) } - override suspend fun getSponsors(): SponsorsDto.SponsorCollectionDto { - val projectId = conferenceConfigProvider.getProjectId() - val collectionName = conferenceConfigProvider.getCollectionName() - val apiKey = conferenceConfigProvider.getApiKey() + override suspend fun getSponsors(): SponsorsDto.SponsorCollectionDto? { + val projectId = conferenceConfigProvider.getProjectId() ?: return null + val collectionName = conferenceConfigProvider.getCollectionName() ?: return null + val apiKey = conferenceConfigProvider.getApiKey() ?: return null val databaseName = "(default)" // This could be moved to ConferenceConfigProvider if needed val jsonString = client.get { @@ -54,9 +57,9 @@ class DefaultApiDataSource( return json.decodeFromString(SponsorsDto.SponsorCollectionDto.serializer(), jsonString) } - suspend fun getConferences(): ConferencesDto.ConferenceCollectionDto { - val projectId = conferenceConfigProvider.getProjectId() - val apiKey = conferenceConfigProvider.getApiKey() + suspend fun getConferences(): ConferencesDto.ConferenceCollectionDto? { + val projectId = conferenceConfigProvider.getProjectId() ?: return null + val apiKey = conferenceConfigProvider.getApiKey() ?: return null val databaseName = "(default)" val conferenceListCollection = "conferenceListMobile" diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt index 52e98884..10df081a 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt @@ -7,41 +7,58 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map import kotlinx.datetime.TimeZone -class DefaultConferenceConfigProvider(private val conferenceRepository: ConferenceRepository, initialConference: Conference) : - ConferenceConfigProvider { +class DefaultConferenceConfigProvider( + private val conferenceRepository: ConferenceRepository, + initialConference: Conference? = null +) : ConferenceConfigProvider { private val log = Logger.withTag("DefaultConferenceConfigProvider") - private val _currentConferenceState = MutableStateFlow(initialConference) - val currentConferenceState: StateFlow = _currentConferenceState + private val _currentConferenceState = MutableStateFlow(initialConference) + val currentConferenceState: StateFlow = _currentConferenceState - private val currentConference: Conference + private val currentConference: Conference? get() = currentConferenceState.value - override fun getConferenceId(): Long = currentConference.id + override fun getConferenceId(): Long? = currentConference?.id - override fun getConferenceTimeZone(): TimeZone = currentConference.timeZone + override fun getConferenceTimeZone(): TimeZone? = currentConference?.timeZone - override fun getProjectId(): String = "droidcon-148cc" + override fun getProjectId(): String? = "droidcon-148cc" - override fun getCollectionName(): String = currentConference.collectionName + override fun getCollectionName(): String? = currentConference?.collectionName - override fun getApiKey(): String = currentConference.apiKey + override fun getApiKey(): String? = currentConference?.apiKey - override fun getScheduleId(): String = currentConference.scheduleId + override fun getScheduleId(): String? = currentConference?.scheduleId - override fun showVenueMap(): Boolean = true // Default to true, will be configurable per conference later + override fun showVenueMap(): Boolean? = true // Default to true, will be configurable per conference later - override fun observeChanges(): Flow = conferenceRepository.observeSelected() + override fun observeChanges(): Flow = conferenceRepository.observeSelected() + .map { it } + .catch { emit(null) } // Implementation of the interface method to get the currently selected conference - override suspend fun getSelectedConference(): Conference = conferenceRepository.getSelected() + override suspend fun getSelectedConference(): Conference? = try { + conferenceRepository.getSelected() + } catch (e: Exception) { + log.w { "No conference selected: ${e.message}" } + null + } // Implementation of the interface method to load the conference asynchronously // Also sets up continuous observation of conference changes override suspend fun loadSelectedConference() { - conferenceRepository.observeSelected().collect { conference -> - _currentConferenceState.emit(conference) - } + conferenceRepository.observeSelected() + .map { it } + .catch { e -> + log.w(e) { "Error observing selected conference, emitting null" } + emit(null) + } + .collect { conference -> + _currentConferenceState.value = conference + } } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt index 56f55f44..557c860a 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt @@ -17,6 +17,7 @@ class DefaultScheduleService( } val sessionRange = session.startsAt.rangeTo(session.endsAt) val conferenceId = conferenceConfigProvider.getConferenceId() + ?: return false return sessionRepository.allAttending(conferenceId).any { otherSession -> otherSession.id != session.id && sessionRange.intersects(otherSession.startsAt.rangeTo(otherSession.endsAt)) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt index af4ff279..505087d0 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt @@ -193,15 +193,19 @@ class DefaultSyncService( // DB Transactions for db mods are ridiculously faster than non-trans changes. Also, if something fails, thd db will roll back. // The repo architecture will likely need to change. Everything is suspend and unconcerned with thread, but that's not good practice. - db.transaction { - updateSpeakersFromDataSource(speakerDtos, conference) - updateScheduleFromDataSource(days, conference) + if (speakerDtos != null && days != null) { + db.transaction { + updateSpeakersFromDataSource(speakerDtos, conference) + updateScheduleFromDataSource(days, conference) + } } // Sponsors may fail due to firebase errors, so we'll do this separate val sponsors = dataSource.getSponsors() - db.transaction { - updateSponsorsFromDataSource(sponsorSessionsGroups, sponsors, conference) + if (sponsorSessionsGroups != null && sponsors != null) { + db.transaction { + updateSponsorsFromDataSource(sponsorSessionsGroups, sponsors, conference) + } } } @@ -216,6 +220,10 @@ class DefaultSyncService( // Get conferences from Firestore val conferencesFromFirestore = apiDataSource.getConferences() + ?: run { + log.w { "Unable to sync conferences: conference configuration is not available" } + return + } // Get all local conferences (need to collect from Flow first) val localConferences = conferenceRepository.observeAll().first() @@ -513,12 +521,12 @@ class DefaultSyncService( Api, } - suspend fun getSpeakers(): List + suspend fun getSpeakers(): List? - suspend fun getSchedule(): List + suspend fun getSchedule(): List? - suspend fun getSponsorSessions(): List + suspend fun getSponsorSessions(): List? - suspend fun getSponsors(): SponsorsDto.SponsorCollectionDto + suspend fun getSponsors(): SponsorsDto.SponsorCollectionDto? } } diff --git a/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt b/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt index bdd95cef..b20c448c 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt @@ -5,5 +5,5 @@ import app.cash.sqldelight.driver.native.NativeSqliteDriver import co.touchlab.droidcon.db.DroidconDatabase actual class SqlDelightDriverFactory { - actual suspend fun createDriver(): SqlDriver = NativeSqliteDriver(DroidconDatabase.Schema, "droidcon.db") + actual fun createDriver(): SqlDriver = NativeSqliteDriver(DroidconDatabase.Schema, "droidcon.db") } diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt index f22eae52..137b43b6 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt @@ -6,9 +6,10 @@ import app.cash.sqldelight.driver.worker.expected.Worker import co.touchlab.droidcon.db.DroidconDatabase actual class SqlDelightDriverFactory { - actual suspend fun createDriver(): SqlDriver = WebWorkerDriver( + actual fun createDriver(): SqlDriver = + WebWorkerDriver( Worker( - js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)"""), - ), - ).also { DroidconDatabase.Schema.create(it).await() } + js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)""") + ) + ) } diff --git a/web/build.gradle.kts b/web/build.gradle.kts index 7dad3404..ca001e92 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -33,7 +33,9 @@ kotlin { implementation("androidx.lifecycle:lifecycle-viewmodel: 2.10.0") } commonTest.dependencies { - // implementation(libs.kotlin.test) + implementation("org.jetbrains.kotlin:kotlin-test:2.2.21") + //implementation(libs.kotlin.test) + implementation(libs.koin.test) } } } diff --git a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt new file mode 100644 index 00000000..2677c0a1 --- /dev/null +++ b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt @@ -0,0 +1,55 @@ +package co.touchlab.droidcon.web + +import co.touchlab.droidcon.application.service.NotificationSchedulingService +import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.droidcon.domain.service.impl.ResourceReader +import co.touchlab.droidcon.service.JsNotificationService +import co.touchlab.droidcon.service.ParseUrlViewService +import co.touchlab.droidcon.ui.uiModule +import co.touchlab.droidcon.util.formatter.DateFormatter +import co.touchlab.droidcon.web.service.DefaultParseUrlViewService +import co.touchlab.droidcon.web.util.NotificationLocalizedStringFactory +import co.touchlab.droidcon.web.util.WebResourceReader +import co.touchlab.droidcon.web.util.formatter.WebDateFormatter +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ObservableSettings +import com.russhwolf.settings.Settings +import com.russhwolf.settings.StorageSettings +import com.russhwolf.settings.observable.makeObservable +import kotlin.test.Test +import org.koin.dsl.koinApplication +import org.koin.dsl.module +import org.koin.test.KoinTest +import org.koin.test.check.checkModules + +class MyTest : KoinTest { + + @OptIn(ExperimentalSettingsApi::class) + @Test + fun `should inject my components`() { + val mainModule = module { + single { + val storageSettings: Settings = StorageSettings() + storageSettings.makeObservable() + } + single { WebResourceReader() } + + single { WebDateFormatter() } + + single { NotificationLocalizedStringFactory() } + + single { WebAnalyticsService() } + + single { DefaultParseUrlViewService() } + + // Provide JsNotificationService which is required by the JS platform module + single { JsNotificationService() } + } + + koinApplication { + modules(mainModule,uiModule) + checkModules() + } + } +} + diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt index 7436ea23..09160bef 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt @@ -4,6 +4,7 @@ import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin +import co.touchlab.droidcon.service.JsNotificationService import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule import co.touchlab.droidcon.util.formatter.DateFormatter @@ -23,21 +24,24 @@ import com.russhwolf.settings.observable.makeObservable @OptIn(ExperimentalSettingsApi::class) fun startKoin(): KoinApplication = initKoin( - module { - single { - val storageSettings: Settings = StorageSettings() - storageSettings.makeObservable() - } - single { WebResourceReader() } + module { + single { + val storageSettings: Settings = StorageSettings() + storageSettings.makeObservable() + } + single { WebResourceReader() } - single { WebDateFormatter() } + single { WebDateFormatter() } - single { NotificationLocalizedStringFactory() } + single { NotificationLocalizedStringFactory() } - single { WebAnalyticsService() } + single { WebAnalyticsService() } - single { DefaultParseUrlViewService() } - } + uiModule, + single { DefaultParseUrlViewService() } + + // Provide JsNotificationService which is required by the JS platform module + single { JsNotificationService() } + } + uiModule ) @OptIn(ExperimentalWasmJsInterop::class) From 0c78ac88597524192c91604c14ffcd400d921916 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 11 Feb 2026 12:17:12 -0500 Subject: [PATCH 17/56] Moving ViewModels to factory --- kotlin-js-store/yarn.lock | 643 +++++++++++++++++- .../co.touchlab.droidcon/ui/UiModule.kt | 115 ++-- .../viewmodel/ApplicationViewModel.kt | 48 +- .../viewmodel/WaitForLoadedContextModel.kt | 3 +- .../viewmodel/settings/SettingsViewModel.kt | 10 +- 5 files changed, 699 insertions(+), 120 deletions(-) diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index f8802357..bb1dbd9c 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@cashapp/sqldelight-sqljs-worker@2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@cashapp/sqldelight-sqljs-worker/-/sqldelight-sqljs-worker-2.2.1.tgz#c71776a9dddfc435d4f1e64317a7039d447ea024" + integrity sha512-cj/llgS1T94t7rz63fI7pbi+jJx+vQofCT58KyMZb9XVRuoxb4taB5wbbBa4e/iljiuN5XIGGPFx+5PvtVh3LQ== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -113,6 +118,27 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -232,7 +258,7 @@ dependencies: "@types/node" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -487,6 +513,11 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + ajv-keywords@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" @@ -494,6 +525,16 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ajv@^8.0.0, ajv@^8.9.0: version "8.17.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" @@ -549,11 +590,37 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +asn1.js@^4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + base64id@2.0.0, base64id@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" @@ -574,6 +641,16 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: + version "4.12.2" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.2.tgz#3d8fed6796c24e177737f7cc5172ee04ef39ec99" + integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== + +bn.js@^5.2.1, bn.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.2.tgz#82c09f9ebbb17107cd72cb7fd39bd1f9d0aaa566" + integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw== + body-parser@1.20.3, body-parser@^1.19.0: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" @@ -622,11 +699,71 @@ braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" +brorand@^1.0.1, brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + browser-stdout@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== +browserify-aes@^1.0.4, browserify-aes@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0, browserify-rsa@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.1.tgz#06e530907fe2949dc21fc3c2e2302e10b1437238" + integrity sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ== + dependencies: + bn.js "^5.2.1" + randombytes "^2.1.0" + safe-buffer "^5.2.1" + +browserify-sign@^4.0.0: + version "4.2.5" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.5.tgz#3979269fa8af55ba18aac35deef11b45515cd27d" + integrity sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw== + dependencies: + bn.js "^5.2.2" + browserify-rsa "^4.1.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.6.1" + inherits "^2.0.4" + parse-asn1 "^5.1.9" + readable-stream "^2.3.8" + safe-buffer "^5.2.1" + browserslist@^4.24.0: version "4.28.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929" @@ -643,6 +780,19 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== + +buffer@6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + bundle-name@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" @@ -655,7 +805,7 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== @@ -663,7 +813,17 @@ call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: es-errors "^1.3.0" function-bind "^1.1.2" -call-bound@^1.0.2, call-bound@^1.0.3: +call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== @@ -716,6 +876,15 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.7" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.7.tgz#bd094bfef42634ccfd9e13b9fc73274997111e39" + integrity sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA== + dependencies: + inherits "^2.0.4" + safe-buffer "^5.2.1" + to-buffer "^1.2.2" + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -837,6 +1006,18 @@ cookie@~0.7.2: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +copy-webpack-plugin@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz#2d2c460c4c4695ec0a58afb2801a1205256c4e6b" + integrity sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA== + dependencies: + fast-glob "^3.2.7" + glob-parent "^6.0.1" + globby "^11.0.3" + normalize-path "^3.0.0" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -850,6 +1031,37 @@ cors@~2.8.5: object-assign "^4" vary "^1" +create-ecdh@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" + integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== + dependencies: + bn.js "^4.1.0" + elliptic "^6.5.3" + +create-hash@^1.1.0, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -859,6 +1071,23 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" +crypto-browserify@3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" @@ -908,6 +1137,15 @@ default-browser@^5.2.1: bundle-name "^4.1.0" default-browser-id "^5.0.0" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" @@ -923,6 +1161,14 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== +des.js@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" + integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -943,6 +1189,22 @@ diff@^7.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + dns-packet@^5.2.2: version "5.6.1" resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" @@ -984,6 +1246,19 @@ electron-to-chromium@^1.5.249: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz#d4393167ec14c5a046cebaec3ddf3377944ce965" integrity sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ== +elliptic@^6.5.3, elliptic@^6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" + integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -1047,7 +1322,7 @@ envinfo@^7.14.0: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.20.0.tgz#3fd9de69fb6af3e777a017dfa033676368d67dd7" integrity sha512-+zUomDcLXsVkQ37vUqWBvQwLaLlj8eZPSi61llaEFAVBY5mhcXdaSw1pSJVl4yTYD5g/gEfpNl28YYk4IPvrrg== -es-define-property@^1.0.1: +es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== @@ -1124,6 +1399,14 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + express@^4.21.2: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" @@ -1166,11 +1449,27 @@ extend@^3.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -fast-deep-equal@^3.1.3: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.2.7, fast-glob@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + fast-uri@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" @@ -1181,6 +1480,13 @@ fastest-levenshtein@^1.0.12: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== +fastq@^1.6.0: + version "1.20.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" + integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== + dependencies: + reusify "^1.0.4" + faye-websocket@^0.11.3: version "0.11.4" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" @@ -1252,6 +1558,13 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== +for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + foreground-child@^3.1.0: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" @@ -1304,7 +1617,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -1328,13 +1641,20 @@ get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" -glob-parent@~5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob-to-regex.js@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz#2b323728271d133830850e32311f40766c5f6413" @@ -1369,7 +1689,19 @@ glob@^7.1.3, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" -gopd@^1.2.0: +globby@^11.0.3: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== @@ -1389,6 +1721,13 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" @@ -1401,6 +1740,24 @@ has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" +hash-base@^3.0.0, hash-base@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.2.tgz#79d72def7611c3f6e3c3b5730652638001b10a74" + integrity sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg== + dependencies: + inherits "^2.0.4" + readable-stream "^2.3.8" + safe-buffer "^5.2.1" + to-buffer "^1.2.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -1413,6 +1770,15 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -1493,6 +1859,16 @@ iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + import-local@^3.0.2: version "3.2.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" @@ -1509,7 +1885,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1541,6 +1917,11 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" @@ -1563,7 +1944,7 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-glob@^4.0.1, is-glob@~4.0.1: +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -1614,6 +1995,13 @@ is-regex@^1.2.1: has-tostringtag "^1.0.2" hasown "^2.0.2" +is-typed-array@^1.1.14: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -1626,6 +2014,11 @@ is-wsl@^3.1.0: dependencies: is-inside-container "^1.0.0" +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -1676,6 +2069,11 @@ json-parse-even-better-errors@^2.3.1: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + json-schema-traverse@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" @@ -1821,6 +2219,15 @@ math-intrinsics@^1.1.0: resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -1848,12 +2255,17 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.2: +micromatch@^4.0.2, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -1861,6 +2273,14 @@ micromatch@^4.0.2: braces "^3.0.3" picomatch "^2.3.1" +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -1895,11 +2315,16 @@ mime@^2.5.2: resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== -minimalistic-assert@^1.0.0: +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -2056,6 +2481,11 @@ open@^10.0.3: is-inside-container "^1.0.0" wsl-utils "^0.1.0" +os-browserify@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -2103,11 +2533,27 @@ package-json-from-dist@^1.0.0: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== +parse-asn1@^5.0.0, parse-asn1@^5.1.9: + version "5.1.9" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.9.tgz#8dd24c3ea8da77dffbc708d94eaf232fd6156e95" + integrity sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg== + dependencies: + asn1.js "^4.10.1" + browserify-aes "^1.2.0" + evp_bytestokey "^1.0.3" + pbkdf2 "^3.1.5" + safe-buffer "^5.2.1" + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +path-browserify@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -2141,6 +2587,23 @@ path-to-regexp@0.1.12: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pbkdf2@^3.0.3, pbkdf2@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.5.tgz#444a59d7a259a95536c56e80c89de31cc01ed366" + integrity sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ== + dependencies: + create-hash "^1.2.0" + create-hmac "^1.1.7" + ripemd160 "^2.0.3" + safe-buffer "^5.2.1" + sha.js "^2.4.12" + to-buffer "^1.2.1" + picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -2158,6 +2621,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +possible-typed-array-names@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -2171,11 +2639,28 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + qjobs@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" @@ -2188,13 +2673,26 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" -randombytes@^2.1.0: +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -2210,7 +2708,7 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" -readable-stream@^2.0.1: +readable-stream@^2.0.1, readable-stream@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -2223,7 +2721,7 @@ readable-stream@^2.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6: +readable-stream@^3.0.6, readable-stream@^3.5.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -2292,6 +2790,11 @@ retry@^0.13.1: resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + rfdc@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" @@ -2304,12 +2807,27 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.3.tgz#9be54e4ba5e3559c8eee06a25cd7648bbccdf5a8" + integrity sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA== + dependencies: + hash-base "^3.1.2" + inherits "^2.0.4" + run-applescript@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911" integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -2333,6 +2851,15 @@ safe-regex-test@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +schema-utils@^3.1.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.2: version "4.3.3" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" @@ -2375,7 +2902,7 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^6.0.2: +serialize-javascript@^6.0.0, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== @@ -2405,6 +2932,18 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -2415,6 +2954,15 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +sha.js@^2.4.0, sha.js@^2.4.12, sha.js@^2.4.8: + version "2.4.12" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f" + integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w== + dependencies: + inherits "^2.0.4" + safe-buffer "^5.2.1" + to-buffer "^1.2.0" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -2484,6 +3032,11 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + socket.io-adapter@~2.5.2: version "2.5.5" resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" @@ -2571,6 +3124,11 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +sql.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-1.8.0.tgz#cb45d957e17a2239662fe2f614c9b678990867a6" + integrity sha512-3HD8pSkZL+5YvYUI8nlvNILs61ALqq34xgmF+BHpqxe68yZIJ1H+sIVIODvni25+CcxHUxDyrTJUL0lE/m7afw== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -2581,6 +3139,14 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +stream-browserify@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" + integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== + dependencies: + inherits "~2.0.4" + readable-stream "^3.5.0" + streamroller@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" @@ -2701,6 +3267,15 @@ tmp@^0.2.1: resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== +to-buffer@^1.2.0, to-buffer@^1.2.1, to-buffer@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.2.tgz#ffe59ef7522ada0a2d1cb5dfe03bb8abc3cdc133" + integrity sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw== + dependencies: + isarray "^2.0.5" + safe-buffer "^5.2.1" + typed-array-buffer "^1.0.3" + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -2731,6 +3306,15 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + ua-parser-js@^0.7.30: version "0.7.41" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.41.tgz#9f6dee58c389e8afababa62a4a2dc22edb69a452" @@ -2759,6 +3343,13 @@ update-browserslist-db@^1.1.4: escalade "^3.2.0" picocolors "^1.1.1" +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -2779,6 +3370,11 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +vm-browserify@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== + void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" @@ -2930,6 +3526,19 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== +which-typed-array@^1.1.16: + version "1.1.20" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.20.tgz#3fdb7adfafe0ea69157b1509f3a1cd892bd1d122" + integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + which@^1.2.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt index 734612ed..b5988997 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt @@ -22,34 +22,51 @@ import org.koin.core.parameter.parametersOf import org.koin.dsl.module val uiModule = module { - // MARK: View model factories. + single { SessionDetailScrollStateStorage() } + + // Factories for parameterized ViewModels (used by the ViewModels below) + single { SessionBlockViewModel.Factory(sessionListItemFactory = get(), dateFormatter = get()) } single { - ApplicationViewModel.Factory( - scheduleFactory = get(), - agendaFactory = get(), - sponsorsFactory = get(), - settingsFactory = get(), + SessionDayViewModel.Factory( + sessionBlockFactory = get(), + dateFormatter = get(), + dateTimeService = get(), + conferenceConfigProvider = get(), + sessionDetailScrollStateStorage = get(), + ) + } + single { SessionListItemViewModel.Factory(dateTimeService = get()) } + single { + SessionDetailViewModel.Factory( + sessionGateway = get(), + speakerListItemFactory = get(), + speakerDetailFactory = get(), + dateFormatter = get(), + dateTimeService = get(), + parseUrlViewService = get(), + settingsGateway = get(), feedbackDialogFactory = get(), - syncService = get(), - notificationSchedulingService = get(), - notificationService = get(), feedbackService = get(), - settingsGateway = get(), - conferenceRepository = get(), + conferenceConfigProvider = get(), + notificationService = get(), ) } - + single { SpeakerListItemViewModel.Factory() } + single { SpeakerDetailViewModel.Factory(parseUrlViewService = get()) } + single { SponsorGroupViewModel.Factory(sponsorGroupItemFactory = get()) } + single { SponsorGroupItemViewModel.Factory() } single { - WaitForLoadedContextModel( - conferenceConfigProvider = get(), - applicationViewModelFactory = get(), - syncService = get(), - settingsGateway = get(), + SponsorDetailViewModel.Factory( + sponsorGateway = get(), + speakerListItemFactory = get(), + speakerDetailFactory = get(), ) } + single { FeedbackDialogViewModel.Factory(sessionGateway = get(), get(parameters = { parametersOf("FeedbackDialogViewModel") })) } + // ViewModels provided by Koin single { - ScheduleViewModel.Factory( + ScheduleViewModel( sessionGateway = get(), sessionDayFactory = get(), sessionDetailFactory = get(), @@ -59,7 +76,7 @@ val uiModule = module { ) } single { - AgendaViewModel.Factory( + AgendaViewModel( sessionGateway = get(), sessionDayFactory = get(), sessionDetailFactory = get(), @@ -68,46 +85,42 @@ val uiModule = module { conferenceConfigProvider = get(), ) } - single { SessionBlockViewModel.Factory(sessionListItemFactory = get(), dateFormatter = get()) } single { - SessionDayViewModel.Factory( - sessionBlockFactory = get(), - dateFormatter = get(), - dateTimeService = get(), - conferenceConfigProvider = get(), - sessionDetailScrollStateStorage = get(), + SponsorListViewModel( + sponsorGateway = get(), + sponsorGroupFactory = get(), + sponsorDetailFactory = get(), ) } - single { SessionListItemViewModel.Factory(dateTimeService = get()) } - + single { AboutViewModel(aboutRepository = get(), parseUrlViewService = get()) } single { - SessionDetailViewModel.Factory( - sessionGateway = get(), - speakerListItemFactory = get(), - speakerDetailFactory = get(), - dateFormatter = get(), - dateTimeService = get(), - parseUrlViewService = get(), + SettingsViewModel( settingsGateway = get(), + about = get(), + conferenceRepository = get(), + ) + } + single { + ApplicationViewModel( + schedule = get(), + agenda = get(), + sponsors = get(), + settings = get(), feedbackDialogFactory = get(), + syncService = get(), + notificationSchedulingService = get(), + notificationService = get(), feedbackService = get(), + settingsGateway = get(), + conferenceRepository = get(), + ) + } + single { + WaitForLoadedContextModel( conferenceConfigProvider = get(), - notificationService = get(), + applicationViewModel = get(), + syncService = get(), + settingsGateway = get(), ) } - single { SpeakerListItemViewModel.Factory() } - - single { SpeakerDetailViewModel.Factory(parseUrlViewService = get()) } - - single { SponsorListViewModel.Factory(sponsorGateway = get(), sponsorGroupFactory = get(), sponsorDetailFactory = get()) } - single { SponsorGroupViewModel.Factory(sponsorGroupItemFactory = get()) } - single { SponsorGroupItemViewModel.Factory() } - single { SponsorDetailViewModel.Factory(sponsorGateway = get(), speakerListItemFactory = get(), speakerDetailFactory = get()) } - - single { SettingsViewModel.Factory(settingsGateway = get(), aboutFactory = get(), conferenceRepository = get()) } - single { AboutViewModel.Factory(aboutRepository = get(), parseUrlViewService = get()) } - - single { FeedbackDialogViewModel.Factory(sessionGateway = get(), get(parameters = { parametersOf("FeedbackDialogViewModel") })) } - - single { SessionDetailScrollStateStorage() } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt index 9ea81320..3fe4b1fd 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt @@ -20,10 +20,10 @@ import org.brightify.hyperdrive.multiplatformx.property.MutableObservablePropert import org.brightify.hyperdrive.multiplatformx.property.ObservableProperty class ApplicationViewModel( - scheduleFactory: ScheduleViewModel.Factory, - agendaFactory: AgendaViewModel.Factory, - sponsorsFactory: SponsorListViewModel.Factory, - settingsFactory: SettingsViewModel.Factory, + val schedule: ScheduleViewModel, + val agenda: AgendaViewModel, + val sponsors: SponsorListViewModel, + val settings: SettingsViewModel, private val feedbackDialogFactory: FeedbackDialogViewModel.Factory, private val syncService: SyncService, private val notificationSchedulingService: NotificationSchedulingService, @@ -34,45 +34,11 @@ class ApplicationViewModel( ) : BaseViewModel(), DeepLinkNotificationHandler { - class Factory( - private val scheduleFactory: ScheduleViewModel.Factory, - private val agendaFactory: AgendaViewModel.Factory, - private val sponsorsFactory: SponsorListViewModel.Factory, - private val settingsFactory: SettingsViewModel.Factory, - private val feedbackDialogFactory: FeedbackDialogViewModel.Factory, - private val syncService: SyncService, - private val notificationSchedulingService: NotificationSchedulingService, - private val notificationService: NotificationService, - private val feedbackService: FeedbackService, - private val settingsGateway: SettingsGateway, - private val conferenceRepository: ConferenceRepository, - ) { - - fun create(): ApplicationViewModel { - val applicationViewModel = ApplicationViewModel( - scheduleFactory = scheduleFactory, - agendaFactory = agendaFactory, - sponsorsFactory = sponsorsFactory, - settingsFactory = settingsFactory, - feedbackDialogFactory = feedbackDialogFactory, - syncService = syncService, - notificationSchedulingService = notificationSchedulingService, - notificationService = notificationService, - feedbackService = feedbackService, - settingsGateway = settingsGateway, - conferenceRepository = conferenceRepository, - ) - notificationService.setHandler(applicationViewModel) - return applicationViewModel - } - } - private val log = Logger.withTag("ApplicationViewModel") - val schedule by managed(scheduleFactory.create()) - val agenda by managed(agendaFactory.create()) - val sponsors by managed(sponsorsFactory.create()) - val settings by managed(settingsFactory.create()) + init { + notificationService.setHandler(this) + } var presentedFeedback: FeedbackDialogViewModel? by managed(null) val observePresentedFeedback by observe(::presentedFeedback) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt index 75913d56..fd92a2de 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.withContext import org.brightify.hyperdrive.multiplatformx.BaseViewModel class WaitForLoadedContextModel( private val conferenceConfigProvider: ConferenceConfigProvider, - applicationViewModelFactory: ApplicationViewModel.Factory, + val applicationViewModel: ApplicationViewModel, private val syncService: SyncService, private val settingsGateway: SettingsGateway, ) : BaseViewModel() { @@ -25,7 +25,6 @@ class WaitForLoadedContextModel( private val _state = MutableStateFlow(State.Loading) val state: StateFlow = _state - val applicationViewModel by managed(applicationViewModelFactory.create()) private val log = Logger.withTag("WaitForLoadedContextModel") diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt index 2a4d1c59..d5f663a6 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt @@ -10,7 +10,7 @@ import org.brightify.hyperdrive.multiplatformx.property.ObservableProperty class SettingsViewModel( settingsGateway: SettingsGateway, - private val aboutFactory: AboutViewModel.Factory, + val about: AboutViewModel, private val conferenceRepository: ConferenceRepository, ) : BaseViewModel() { private val log = Logger.withTag("SettingsViewModel") @@ -47,7 +47,6 @@ class SettingsViewModel( private val _selectedConference = MutableObservableProperty(null) val selectedConference: ObservableProperty = _selectedConference - val about by managed(aboutFactory.create()) override suspend fun whileAttached() { // Load conferences @@ -81,11 +80,4 @@ class SettingsViewModel( } } - class Factory( - private val settingsGateway: SettingsGateway, - private val aboutFactory: AboutViewModel.Factory, - private val conferenceRepository: ConferenceRepository, - ) { - fun create() = SettingsViewModel(settingsGateway, aboutFactory, conferenceRepository) - } } From adc50fc52cad3d2f54a56a6b1d421801ea56a842 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 11 Feb 2026 13:26:08 -0500 Subject: [PATCH 18/56] Adding favicon and attempting to fix sqldelight for web --- kotlin-js-store/yarn.lock | 628 +----------------- shared/build.gradle.kts | 14 + .../domain/repository/ProfileRepository.kt | 4 +- .../droidcon/domain/repository/Repository.kt | 10 +- .../domain/repository/impl/BaseRepository.kt | 14 +- .../impl/SqlDelightProfileRepository.kt | 8 +- .../impl/SqlDelightRoomRepository.kt | 4 +- .../impl/SqlDelightSessionRepository.kt | 4 +- .../impl/SqlDelightSponsorGroupRepository.kt | 4 +- .../impl/SqlDelightSponsorRepository.kt | 4 +- .../domain/service/impl/DefaultSyncService.kt | 6 +- web/build.gradle.kts | 33 +- web/src/webMain/resources/favicon.png | Bin 0 -> 46706 bytes web/src/webMain/resources/index.html | 1 + web/webpack.config.d/sqljs-config.js | 16 + 15 files changed, 95 insertions(+), 655 deletions(-) create mode 100644 web/src/webMain/resources/favicon.png create mode 100644 web/webpack.config.d/sqljs-config.js diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index bb1dbd9c..1f91e1a2 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -118,27 +118,6 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -258,7 +237,7 @@ dependencies: "@types/node" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -513,11 +492,6 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - ajv-keywords@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" @@ -525,16 +499,6 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - ajv@^8.0.0, ajv@^8.9.0: version "8.17.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" @@ -590,37 +554,11 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -asn1.js@^4.10.1: - version "4.10.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" - integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - base64id@2.0.0, base64id@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" @@ -641,16 +579,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.2" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.2.tgz#3d8fed6796c24e177737f7cc5172ee04ef39ec99" - integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== - -bn.js@^5.2.1, bn.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.2.tgz#82c09f9ebbb17107cd72cb7fd39bd1f9d0aaa566" - integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw== - body-parser@1.20.3, body-parser@^1.19.0: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" @@ -699,71 +627,11 @@ braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - browser-stdout@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -browserify-aes@^1.0.4, browserify-aes@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.1.tgz#06e530907fe2949dc21fc3c2e2302e10b1437238" - integrity sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ== - dependencies: - bn.js "^5.2.1" - randombytes "^2.1.0" - safe-buffer "^5.2.1" - -browserify-sign@^4.0.0: - version "4.2.5" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.5.tgz#3979269fa8af55ba18aac35deef11b45515cd27d" - integrity sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw== - dependencies: - bn.js "^5.2.2" - browserify-rsa "^4.1.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.6.1" - inherits "^2.0.4" - parse-asn1 "^5.1.9" - readable-stream "^2.3.8" - safe-buffer "^5.2.1" - browserslist@^4.24.0: version "4.28.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929" @@ -780,19 +648,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== - -buffer@6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - bundle-name@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" @@ -805,7 +660,7 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== @@ -813,17 +668,7 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply- es-errors "^1.3.0" function-bind "^1.1.2" -call-bind@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - -call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: +call-bound@^1.0.2, call-bound@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== @@ -876,15 +721,6 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.7" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.7.tgz#bd094bfef42634ccfd9e13b9fc73274997111e39" - integrity sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA== - dependencies: - inherits "^2.0.4" - safe-buffer "^5.2.1" - to-buffer "^1.2.2" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -1006,18 +842,6 @@ cookie@~0.7.2: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== -copy-webpack-plugin@9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz#2d2c460c4c4695ec0a58afb2801a1205256c4e6b" - integrity sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA== - dependencies: - fast-glob "^3.2.7" - glob-parent "^6.0.1" - globby "^11.0.3" - normalize-path "^3.0.0" - schema-utils "^3.1.1" - serialize-javascript "^6.0.0" - core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -1031,37 +855,6 @@ cors@~2.8.5: object-assign "^4" vary "^1" -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" @@ -1071,23 +864,6 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" -crypto-browserify@3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" @@ -1137,15 +913,6 @@ default-browser@^5.2.1: bundle-name "^4.1.0" default-browser-id "^5.0.0" -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - define-lazy-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" @@ -1161,14 +928,6 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== -des.js@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" - integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -1189,22 +948,6 @@ diff@^7.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - dns-packet@^5.2.2: version "5.6.1" resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" @@ -1246,19 +989,6 @@ electron-to-chromium@^1.5.249: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz#d4393167ec14c5a046cebaec3ddf3377944ce965" integrity sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ== -elliptic@^6.5.3, elliptic@^6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" - integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -1322,7 +1052,7 @@ envinfo@^7.14.0: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.20.0.tgz#3fd9de69fb6af3e777a017dfa033676368d67dd7" integrity sha512-+zUomDcLXsVkQ37vUqWBvQwLaLlj8eZPSi61llaEFAVBY5mhcXdaSw1pSJVl4yTYD5g/gEfpNl28YYk4IPvrrg== -es-define-property@^1.0.0, es-define-property@^1.0.1: +es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== @@ -1399,14 +1129,6 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - express@^4.21.2: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" @@ -1449,27 +1171,11 @@ extend@^3.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: +fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.7, fast-glob@^3.2.9: - version "3.3.3" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" - integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.8" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - fast-uri@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" @@ -1480,13 +1186,6 @@ fastest-levenshtein@^1.0.12: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== -fastq@^1.6.0: - version "1.20.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" - integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== - dependencies: - reusify "^1.0.4" - faye-websocket@^0.11.3: version "0.11.4" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" @@ -1558,13 +1257,6 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== -for-each@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" - integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== - dependencies: - is-callable "^1.2.7" - foreground-child@^3.1.0: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" @@ -1617,7 +1309,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -1641,20 +1333,13 @@ get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob-parent@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - glob-to-regex.js@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz#2b323728271d133830850e32311f40766c5f6413" @@ -1689,19 +1374,7 @@ glob@^7.1.3, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" -globby@^11.0.3: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -gopd@^1.0.1, gopd@^1.2.0: +gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== @@ -1721,13 +1394,6 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" @@ -1740,24 +1406,6 @@ has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" -hash-base@^3.0.0, hash-base@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.2.tgz#79d72def7611c3f6e3c3b5730652638001b10a74" - integrity sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg== - dependencies: - inherits "^2.0.4" - readable-stream "^2.3.8" - safe-buffer "^5.2.1" - to-buffer "^1.2.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -1770,15 +1418,6 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -1859,16 +1498,6 @@ iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore@^5.2.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - import-local@^3.0.2: version "3.2.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" @@ -1885,7 +1514,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1917,11 +1546,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" @@ -1944,7 +1568,7 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -1995,13 +1619,6 @@ is-regex@^1.2.1: has-tostringtag "^1.0.2" hasown "^2.0.2" -is-typed-array@^1.1.14: - version "1.1.15" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" - integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== - dependencies: - which-typed-array "^1.1.16" - is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -2014,11 +1631,6 @@ is-wsl@^3.1.0: dependencies: is-inside-container "^1.0.0" -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -2069,11 +1681,6 @@ json-parse-even-better-errors@^2.3.1: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - json-schema-traverse@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" @@ -2219,15 +1826,6 @@ math-intrinsics@^1.1.0: resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -2255,17 +1853,12 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.2, micromatch@^4.0.8: +micromatch@^4.0.2: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -2273,14 +1866,6 @@ micromatch@^4.0.2, micromatch@^4.0.8: braces "^3.0.3" picomatch "^2.3.1" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -2315,16 +1900,11 @@ mime@^2.5.2: resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: +minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -2481,11 +2061,6 @@ open@^10.0.3: is-inside-container "^1.0.0" wsl-utils "^0.1.0" -os-browserify@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== - p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -2533,17 +2108,6 @@ package-json-from-dist@^1.0.0: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== -parse-asn1@^5.0.0, parse-asn1@^5.1.9: - version "5.1.9" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.9.tgz#8dd24c3ea8da77dffbc708d94eaf232fd6156e95" - integrity sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg== - dependencies: - asn1.js "^4.10.1" - browserify-aes "^1.2.0" - evp_bytestokey "^1.0.3" - pbkdf2 "^3.1.5" - safe-buffer "^5.2.1" - parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -2587,23 +2151,6 @@ path-to-regexp@0.1.12: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pbkdf2@^3.0.3, pbkdf2@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.5.tgz#444a59d7a259a95536c56e80c89de31cc01ed366" - integrity sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ== - dependencies: - create-hash "^1.2.0" - create-hmac "^1.1.7" - ripemd160 "^2.0.3" - safe-buffer "^5.2.1" - sha.js "^2.4.12" - to-buffer "^1.2.1" - picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -2621,11 +2168,6 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -possible-typed-array-names@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" - integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -2639,28 +2181,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - qjobs@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" @@ -2673,26 +2198,13 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: +randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -2708,7 +2220,7 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" -readable-stream@^2.0.1, readable-stream@^2.3.8: +readable-stream@^2.0.1: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -2721,7 +2233,7 @@ readable-stream@^2.0.1, readable-stream@^2.3.8: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.5.0: +readable-stream@^3.0.6: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -2790,11 +2302,6 @@ retry@^0.13.1: resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== -reusify@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" - integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== - rfdc@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" @@ -2807,27 +2314,12 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.3.tgz#9be54e4ba5e3559c8eee06a25cd7648bbccdf5a8" - integrity sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA== - dependencies: - hash-base "^3.1.2" - inherits "^2.0.4" - run-applescript@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911" integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q== -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -2851,15 +2343,6 @@ safe-regex-test@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -schema-utils@^3.1.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.2: version "4.3.3" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" @@ -2902,7 +2385,7 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^6.0.0, serialize-javascript@^6.0.2: +serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== @@ -2932,18 +2415,6 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" -set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -2954,15 +2425,6 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sha.js@^2.4.0, sha.js@^2.4.12, sha.js@^2.4.8: - version "2.4.12" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f" - integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w== - dependencies: - inherits "^2.0.4" - safe-buffer "^5.2.1" - to-buffer "^1.2.0" - shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -3032,11 +2494,6 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - socket.io-adapter@~2.5.2: version "2.5.5" resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" @@ -3139,14 +2596,6 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-browserify@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - streamroller@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" @@ -3267,15 +2716,6 @@ tmp@^0.2.1: resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== -to-buffer@^1.2.0, to-buffer@^1.2.1, to-buffer@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.2.tgz#ffe59ef7522ada0a2d1cb5dfe03bb8abc3cdc133" - integrity sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw== - dependencies: - isarray "^2.0.5" - safe-buffer "^5.2.1" - typed-array-buffer "^1.0.3" - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -3306,15 +2746,6 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typed-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" - integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-typed-array "^1.1.14" - ua-parser-js@^0.7.30: version "0.7.41" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.41.tgz#9f6dee58c389e8afababa62a4a2dc22edb69a452" @@ -3343,13 +2774,6 @@ update-browserslist-db@^1.1.4: escalade "^3.2.0" picocolors "^1.1.1" -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -3370,11 +2794,6 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -vm-browserify@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" @@ -3526,19 +2945,6 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -which-typed-array@^1.1.16: - version "1.1.20" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.20.tgz#3fdb7adfafe0ea69157b1509f3a1cd892bd1d122" - integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - call-bound "^1.0.4" - for-each "^0.3.5" - get-proto "^1.0.1" - gopd "^1.2.0" - has-tostringtag "^1.0.2" - which@^1.2.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index d280cc84..1a0203b8 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -62,6 +62,8 @@ kotlin { js(IR) { browser() binaries.executable() + + } version = "1.0" @@ -117,6 +119,16 @@ kotlin { jsMain.dependencies { implementation(libs.ktor.client.cio) implementation(libs.sqldelight.driver.js) + //implementation(devNpm("copy-webpack-plugin", "9.1.0")) + implementation(npm("sql.js", "1.8.0")) + implementation(npm("@cashapp/sqldelight-sqljs-worker", "2.2.1")) +/* + implementation(npm("path-browserify", "1.0.1")) + implementation(npm("crypto-browserify", "3.12.0")) + implementation(npm("os-browserify", "0.3.0")) + implementation(npm("buffer", "6.0.3")) + implementation(npm("stream-browserify", "3.0.0")) + implementation(npm("vm-browserify", "1.1.2"))*/ } all { @@ -136,5 +148,7 @@ kotlin { sqldelight { databases.create("DroidconDatabase") { packageName.set("co.touchlab.droidcon.db") + generateAsync.set(true) + } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepository.kt index 61cd3d02..148bea82 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepository.kt @@ -8,9 +8,9 @@ interface ProfileRepository : Repository { suspend fun getSpeakersBySession(id: Session.Id, conferenceId: Long): List - fun setSessionSpeakers(session: Session, speakers: List, conferenceId: Long) + suspend fun setSessionSpeakers(session: Session, speakers: List, conferenceId: Long) - fun setSponsorRepresentatives(sponsor: Sponsor, representatives: List, conferenceId: Long) + suspend fun setSponsorRepresentatives(sponsor: Sponsor, representatives: List, conferenceId: Long) suspend fun getSponsorRepresentatives(sponsorId: Sponsor.Id, conferenceId: Long): List diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/Repository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/Repository.kt index ecd978fd..e9075b09 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/Repository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/Repository.kt @@ -18,15 +18,15 @@ interface Repository> { fun observeAll(conferenceId: Long): Flow> - fun add(entity: ENTITY, conferenceId: Long) + suspend fun add(entity: ENTITY, conferenceId: Long) - fun remove(entity: ENTITY, conferenceId: Long): Boolean + suspend fun remove(entity: ENTITY, conferenceId: Long): Boolean - fun remove(id: ID, conferenceId: Long): Boolean + suspend fun remove(id: ID, conferenceId: Long): Boolean - fun update(entity: ENTITY, conferenceId: Long) + suspend fun update(entity: ENTITY, conferenceId: Long) - fun addOrUpdate(entity: ENTITY, conferenceId: Long) + suspend fun addOrUpdate(entity: ENTITY, conferenceId: Long) fun contains(id: ID, conferenceId: Long): Boolean } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/BaseRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/BaseRepository.kt index 08baf2af..1bac0c25 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/BaseRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/BaseRepository.kt @@ -14,7 +14,7 @@ abstract class BaseRepository> : Repository< override suspend fun all(conferenceId: Long): List = observeAll(conferenceId).first() - override fun add(entity: ENTITY, conferenceId: Long) { + override suspend fun add(entity: ENTITY, conferenceId: Long) { if (!contains(entity.id, conferenceId)) { doUpsert(entity, conferenceId) } else { @@ -23,9 +23,9 @@ abstract class BaseRepository> : Repository< } } - override fun remove(entity: ENTITY, conferenceId: Long) = remove(entity.id, conferenceId) + override suspend fun remove(entity: ENTITY, conferenceId: Long) = remove(entity.id, conferenceId) - override fun remove(id: ID, conferenceId: Long): Boolean { + override suspend fun remove(id: ID, conferenceId: Long): Boolean { val idExists = contains(id, conferenceId) if (idExists) { doDelete(id, conferenceId) @@ -33,7 +33,7 @@ abstract class BaseRepository> : Repository< return idExists } - override fun update(entity: ENTITY, conferenceId: Long) { + override suspend fun update(entity: ENTITY, conferenceId: Long) { if (contains(entity.id, conferenceId)) { doUpsert(entity, conferenceId) } else { @@ -42,11 +42,11 @@ abstract class BaseRepository> : Repository< } } - override fun addOrUpdate(entity: ENTITY, conferenceId: Long) = doUpsert(entity, conferenceId) + override suspend fun addOrUpdate(entity: ENTITY, conferenceId: Long) = doUpsert(entity, conferenceId) - protected abstract fun doUpsert(entity: ENTITY, conferenceId: Long) + protected abstract suspend fun doUpsert(entity: ENTITY, conferenceId: Long) - protected abstract fun doDelete(id: ID, conferenceId: Long) + protected abstract suspend fun doDelete(id: ID, conferenceId: Long) protected fun Long.toBoolean(): Boolean = this != 0L diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt index e0caa96f..a375bc2e 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt @@ -25,7 +25,7 @@ class SqlDelightProfileRepository( override suspend fun getSpeakersBySession(id: Session.Id, conferenceId: Long): List = profileQueries.selectBySession(id.value, conferenceId, ::profileFactory).executeAsList() - override fun setSessionSpeakers(session: Session, speakers: List, conferenceId: Long) { + override suspend fun setSessionSpeakers(session: Session, speakers: List, conferenceId: Long) { speakerQueries.deleteBySessionId(session.id.value, conferenceId) speakers.forEachIndexed { index, speakerId -> speakerQueries.insertUpdate( @@ -37,7 +37,7 @@ class SqlDelightProfileRepository( } } - override fun setSponsorRepresentatives(sponsor: Sponsor, representatives: List, conferenceId: Long) { + override suspend fun setSponsorRepresentatives(sponsor: Sponsor, representatives: List, conferenceId: Long) { representativeQueries.deleteBySponsorId( sponsorName = sponsor.id.name, sponsorGroupName = sponsor.id.group, @@ -74,7 +74,7 @@ class SqlDelightProfileRepository( override fun observeAll(conferenceId: Long): Flow> = profileQueries.selectAll(conferenceId, ::profileFactory).asFlow().mapToList(Dispatchers.Main) - override fun doUpsert(entity: Profile, conferenceId: Long) { + override suspend fun doUpsert(entity: Profile, conferenceId: Long) { profileQueries.upsert( id = entity.id.value, conferenceId = conferenceId, @@ -88,7 +88,7 @@ class SqlDelightProfileRepository( ) } - override fun doDelete(id: Profile.Id, conferenceId: Long) { + override suspend fun doDelete(id: Profile.Id, conferenceId: Long) { profileQueries.delete(id.value, conferenceId) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt index b1657dd5..e21719c4 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt @@ -25,7 +25,7 @@ class SqlDelightRoomRepository(private val roomQueries: RoomQueries) : override fun observeAll(conferenceId: Long): Flow> = roomQueries.selectAll(conferenceId, ::roomFactory).asFlow().mapToList(Dispatchers.Main) - override fun doUpsert(entity: Room, conferenceId: Long) { + override suspend fun doUpsert(entity: Room, conferenceId: Long) { roomQueries.upsert( id = entity.id.value, conferenceId = conferenceId, @@ -33,7 +33,7 @@ class SqlDelightRoomRepository(private val roomQueries: RoomQueries) : ) } - override fun doDelete(id: Room.Id, conferenceId: Long) { + override suspend fun doDelete(id: Room.Id, conferenceId: Long) { roomQueries.deleteById(id.value, conferenceId) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt index 2c0c9041..4ea6407e 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt @@ -55,7 +55,7 @@ class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, override fun observeAll(conferenceId: Long): Flow> = sessionQueries.allSessions(conferenceId, ::sessionFactory).asFlow().mapToList(Dispatchers.Main) - override fun doUpsert(entity: Session, conferenceId: Long) { + override suspend fun doUpsert(entity: Session, conferenceId: Long) { sessionQueries.upsert( id = entity.id.value, conferenceId = conferenceId, @@ -73,7 +73,7 @@ class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, ) } - override fun doDelete(id: Session.Id, conferenceId: Long) { + override suspend fun doDelete(id: Session.Id, conferenceId: Long) { sessionQueries.deleteById(id.value, conferenceId) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorGroupRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorGroupRepository.kt index a037acae..41770949 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorGroupRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorGroupRepository.kt @@ -32,7 +32,7 @@ class SqlDelightSponsorGroupRepository(private val sponsorGroupQueries: SponsorG override fun contains(id: SponsorGroup.Id, conferenceId: Long): Boolean = sponsorGroupQueries.existsByName(id.value, conferenceId).executeAsOne().toBoolean() - override fun doUpsert(entity: SponsorGroup, conferenceId: Long) { + override suspend fun doUpsert(entity: SponsorGroup, conferenceId: Long) { sponsorGroupQueries.upsert( name = entity.id.value, conferenceId = conferenceId, @@ -41,7 +41,7 @@ class SqlDelightSponsorGroupRepository(private val sponsorGroupQueries: SponsorG ) } - override fun doDelete(id: SponsorGroup.Id, conferenceId: Long) { + override suspend fun doDelete(id: SponsorGroup.Id, conferenceId: Long) { sponsorGroupQueries.deleteByName(id.value, conferenceId) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorRepository.kt index eb28e598..1c0584e5 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorRepository.kt @@ -34,7 +34,7 @@ class SqlDelightSponsorRepository(private val sponsorQueries: SponsorQueries) : override fun allSync(conferenceId: Long): List = sponsorQueries.selectAll(conferenceId, ::sponsorFactory).executeAsList() - override fun doUpsert(entity: Sponsor, conferenceId: Long) { + override suspend fun doUpsert(entity: Sponsor, conferenceId: Long) { sponsorQueries.upsert( name = entity.id.name, groupName = entity.id.group, @@ -46,7 +46,7 @@ class SqlDelightSponsorRepository(private val sponsorQueries: SponsorQueries) : ) } - override fun doDelete(id: Sponsor.Id, conferenceId: Long) { + override suspend fun doDelete(id: Sponsor.Id, conferenceId: Long) { sponsorQueries.deleteById(id.name, id.group, conferenceId) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt index 505087d0..d8f901dd 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt @@ -322,7 +322,7 @@ class DefaultSyncService( } } - private fun updateSpeakersFromDataSource(speakerDtos: List, conference: Conference) { + private suspend fun updateSpeakersFromDataSource(speakerDtos: List, conference: Conference) { val profiles = speakerDtos.map(::profileFactory) val conferenceId = conference.id @@ -339,7 +339,7 @@ class DefaultSyncService( private fun dateFromString(dateTimeString: String): String = dateTimeString.split("T")[0] private fun timeFromString(dateTimeString: String): String = dateTimeString.split("T")[1] - private fun updateScheduleFromDataSource(_days: List, conference: Conference) { + private suspend fun updateScheduleFromDataSource(_days: List, conference: Conference) { val originalToAdjustedDateMap = _days.flatMap { dayDto -> dayDto.rooms.flatMap { roomDto -> roomDto.sessions } }.map { sessionDto -> dateFromString(sessionDto.startsAt) }.toSet().toList().sorted().mapIndexed { index, date -> @@ -444,7 +444,7 @@ class DefaultSyncService( } } - private fun updateSponsorsFromDataSource( + private suspend fun updateSponsorsFromDataSource( sponsorSessionsGroups: List, sponsors: SponsorsDto.SponsorCollectionDto, conference: Conference, diff --git a/web/build.gradle.kts b/web/build.gradle.kts index ca001e92..0a3fadfa 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -20,22 +20,25 @@ kotlin { }*/ sourceSets { - commonMain.dependencies { - implementation(projects.shared) - implementation(projects.sharedUi) - implementation(libs.koin.core) - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(libs.multiplatformSettings.core) - implementation(libs.multiplatform.settings.make.observable) - implementation(libs.koin.compose) - implementation("androidx.lifecycle:lifecycle-viewmodel: 2.10.0") + val jsMain by getting { + dependencies { + implementation(projects.shared) + implementation(projects.sharedUi) + implementation(libs.koin.core) + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material) + implementation(libs.multiplatformSettings.core) + implementation(libs.multiplatform.settings.make.observable) + implementation(libs.koin.compose) + } } - commonTest.dependencies { - implementation("org.jetbrains.kotlin:kotlin-test:2.2.21") - //implementation(libs.kotlin.test) - implementation(libs.koin.test) + val jsTest by getting { + dependencies { + implementation("org.jetbrains.kotlin:kotlin-test:2.2.21") + //implementation(libs.kotlin.test) + implementation(libs.koin.test) + } } } } diff --git a/web/src/webMain/resources/favicon.png b/web/src/webMain/resources/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..d3ed8bae1f73258be418beca0abd6937675cc4f2 GIT binary patch literal 46706 zcmeFYWmpt{*f6?)f`oul(kLiMcb6h1sg!_pcXup`fYOb0hqN>-AtEK+y-3%ROT+G- zS^m%aem@`1b)6577w+uN{N|qD-8tXXRplSzQQ?6=pofYIZ{CAI_kgeWKo2m1!x6Z3 z68NEPBO{|OZ{cX+YGcOmUj8+Mx~r3=jlBg3r0{J>%fOmOi&PTg=0&1F6JDUQ^j7q8 zMt!_m@r_Vo&w-KPLeEf5e~%!kJD;DIRoYypi=|+F;idY$2rjw%MhF~wynx51dusTm zg{&_VSvncWSnH7Q%1>pxd@T=P!%Rb^G0>G^|BH;nEWbk$tKs|BQWamxy0}1^rA1jv z-CtzFtiCjm9d!kaVm#eu`5H<-WZ8?M|7wD?sch*A+kg6Emu_Hs=^AWLD{K1|)b0sG z$jX$1E*U|eQr@TtVa!X)sIGtip?k^o$3WDPI42+?WPm`*d_^~uQP7H&Lf(Eqm7?(V zqvJ<^0&3}e=7Q(wVADx?TBQSd##hhE%AV`yMzwjq!ei0=xyKOv0CQdaahDVMbJj%l zQcwuWKtFV%11!StdPRI3=1+IEI_@Fnp7$zZ`to;-eJg6dls*f?JKL<4qqJcvzELcJ z?QO+MRpzW%RC9mu<$m{*c6D|p_DG)N%t!eBia188DaCw*NEtpekv}oU<&86&C-441 z9>pY{j)~W7MUI|ne28%k!FfAYZ7u* zs@gb-q;JaBhDQ(mQh23SMDy{BjS+^MN{Ft1#P=k-EdNRItL;tp?svhpR z$n>=H-Hubz6R+s(ho9LPzedxJ(YdmFfuN}X9wYFx(eXk8#!t-wO zWkdOAg9d?6E2%k;ckVyu$Mf_`Ilp`#J^=;rxG`fDUw;g|TsW#)**MZ)@#>Ott8$@t zrgx)v-D;|m{>SM|(X7@y(pA!r5g4A>?y%XeVFuS%pDx*4P^_!%q6*SH2JN!^F#eA3vnA zda?5qi>d8t8KZWkkOQh%pXocni_h(R`f`rBCewu!jAEDdj`bat7UlvK7_D>O$Vco$wbOEfJp^7e8+!{9VvA^BtK71OWw zujf)J-fpkgjTCZVh4sUJTzW!lDiZzFRB(F6fIR$K&-&`2cUZxRG|C7Hw@sv zBnI{W{Vs>W0>ZjG511rOlK=URUiIHU1>k`Gb$4{?XKVq17(j||UTJz~z?Qr+G|$>l z$8N)??QIm-n0fCtNii@O@jN4Wik>7qnQ)9;PrLoh(QNy-KB6tRQ&m~>88$WRPezq{ zFF&)hL)aIr8dOk-PZU<64zS_DRJZo3Eyfu6#jd%zkptLZ;PRG#M|PEe_plBVdM~Ex zlbq*Krd0d3Hgr~drboz5q!$KmV`Zb#D*a~>)SWdc1x#sD)Os(nmzOppi z1KJXMY}Pd5#hSk)Y~CBtIEN7ELGD|L|FhcJR(o{= z6>QnWMy7ahoDEW8Km%c76@Zl4`$!-)lE-{oYng`50B3@}pfI_BUVU~|B^}C$KJllY zXa0p~xM|GfREVh*I}fDi%oPmk^S^tlaIHia0nZIsHh;`*_=l4XZf_PJv)!BdJC5Yx0>)(D9jf1fSp?0W}oGt_?0ksOVau(wV&(yn(1wF`C`n0z}t1rKmI#4|1V zRdEr2?V*!ON(YU;K=bsMPGk*lmj3Fq za)}!4aocZQbzr#6%>(SNi~dg;;E&Mjm=xoqT}a8l)BKcoY^&#Z9QQc-PoTAy^28FE zP02%rXXxDpF)5DIs4Z@wuCI?s)asZsYJEK}=IfEOjC>Mg;Y`J?!VErl0ywTzv$Oej>g_7bz7*)USY%vCe(^!y5O42)AiWV{w)?^k zmWYue4Tt&^H^#tI*pKTQLf7a`yq%b_QbW;?`?4wc;qi-Osf&;D9v&qzYBo+WP%op! zd%3LohTrCr;;)0u@1!6%bCuFEaZT%B@4S!SD^*tTMTI1Tz#SO%UJ<35 z$c9-)td3i-`fpo|GSj!;)$f@zpqWBE3A==5PEB=gWgiOX$$Q`IADrzWe#%?=vI1JL z@6SMj7MGx}Jqsr!R9;ZebfHNd_(<0CMgBg)`4?glVIQbt-=ig*Ax~qC3ne5J2GH)z z)0I98+-^3K)wIZ+9+=Z-f43q7FTyI3L+HG(?ZQ_~G_NglIZU19C#`yFw*eXIpS0Ya z#H}#d&rdDUHgC}#=FmIzKQ_|WXFBhr8TtB3iYnIj4o>pNe8dy^cd)o#dM7DohVg|f z#K~3O#WH;gea}2Rd}#QU6vO{)Dp^Fgi}Y$wP9jc$--K|4VKJal=t*SyD|c2QjMyJ{ z+J*T;8bA?*nY#0XpDlEj z5jWlEq26zYd%@)ytCY!!^4Rea=<9Dx;*)j`{?0O%>39WP=5{o~WBQ6zI>9K&9aoxV z1y4OCbI9-L-uqJ+1X&a z4?pwRz8RTLl^#5#UOiwQJSEUzYHoyMXx%Y~7_d4g%IC9;jf>;tw1ZHi0L7-lShG46 zS90bnJCKYF8ro}N0k%pmD+dRbTqm{n?E2F`y_`O(<%+Q1mkYfkJc8O`=w+ifU=ci0 zgoqW86nG&8Vh3L#xayY2kv}(Ipx7{iWzmqXQ=G4sw<5$Za~c~H!nVic0~{P&ZtzC> zsh|t}(${Ev5Hw7dp1B6Q`YBK6Ke{lT4BJ7WdiSDU6hye3-EYQa_Q^OSz(X=T6JsmG5BKegW>;Ft-ktY+<*WfWS@h#o-UJYTe} zrdFOlVoy%wG`x6Qu#7$<3^4nO;mO_9P2Vlo$tJ@7LSvV1U}FY=+v)ox_)m z@#GmtgdBaA8yg$>V!V>PS7m+=2{jhA{}OJ5&Y%AwxgRDu$TJ5pe%fR5%o8c();n^f z`8Ix%7}nHyu$HWTy^8ne6$lhxRW|4Rk1u^*3lt=2kSNsY$gLP}9-D-J(&Ml_yG*&` zql%(Y`?xifDHhDtIy$$O`UAe`Mhm`DDk+EUJ(|fVUunA^^k^DJiE30AUS1bw^BW|2 zm|c|r_CY8_2{5usc*@Z-D2K#{B0X7LP0H02q-ksC(3bs%24~xJ@HxYi#Di8o$LG?` z-F)+D6Knk;Jj-cJiuWzg31!~i32$mtoH^u=U$8d>9&j+xozaqohkh)_LU;Oy3! zoN3J9x2k<&U8>PWY=TF3OdR4?ntp(G50JxiPY+o?F{#2<=(Y?|w}ZI&aI%i|>aL{b zoyhwpIA3+t^0_VtK%uIt!QDVgH#&1Hle_^Kg&Sddg_meW98L012>9H_aNZaHd_O8G z4mzwB+~(IcBDwI$Z(Z2x+kmll8Qp_uU?s@iBHHT9<62b)nrNtt{IwuzcgUf!d&(FP zvc)dV())RL4gldyImq1f)-*;O<=h9-lA@Dxd7y|#+V_MFwi6l)O?k;MAh)vDlgV-4;CN;HeC4U;&kCRd{+ zTzhF9I&<_kP75nIu8+FfXBpQVu0mdf>77acr?Nh{DJXsYF5F;Pr0a}sl$TYdc!m?JmGJt3i7sK-EaM+EN1G2Z@nfXVG=UI2Wq?G&H%-V6@ZpRZDOr~kk; zc<>j&&aXpCKqjhR4nLMf`id=C^QSqxNSHmJ+~NKnre8bBiv9YIQV+%E4T*17I*?P% z9(Hn`D3QUxNe0;g&n=^d7V@uayIL~=ky3=$IVA>oZ9F=;^dqHvJ|8bVy ze-r`r3#=~_#zfmTed_lRya!=u3cJ77PeDCb#Jg88==9+p;y>QZ-o;?W^I9}Nap+I$ z8|5b}F-`xw)O0r8@D&?pyrLpbp00mUB_dbp?h!s`XF&8dJZH_|8!Bo14_E-A{4n?k*4dd!eVIbE%i3T_NT>{bjR5HV7c>?4VL}}?dd)TZ z`>gS6S$?j7QM$+_YQ_p8HNgW+$7ppZHX<@!$N}$9;Pyj1PbWTeK{T7C<-Yc)UE{v- z;H}5~FLe9=w50Ms8QRW2*ZzrOeSACu9KtWl0gVe=5rpft$}eo@`RyzU$F?F`QGs3o z0p<+Z&j-fW`!|1@sdVPl1-5~Eqen+=apJ$BF?Js|k9TZ(dh?fUNqbMnjlKoK=mp(r zLtZBvS68XE-n!1H2w7}xQ4?w~Uh98{IaFXDZXWLLAm=m$UGm2XdTF>$}nUb06Ma zk&Wws({6>)CzFz%ihjO6@S)C0(+2I(nJJC~IC{D@I``L-xOChY!r z@ExV>R^PaJHsr>T)YLvCVSZtlEJRhfGf@#BbyBV?kukc~$ta4UfOYqXiqD_hCO6CfxoM!HG(*GGF zt6agJIx^AtP8kS-70g!c^HugA=cwK3J|&+-l5I+?q-U^AqDRSvP*B&M%MlbNh40Me zPgXvwAYYFr*-pk84?$yDw=$S9CRjLg*50CN>!Rk7^uyr(X{hWkKzOi#h&tip#!q=c z$5XYekJR5vwBEg4-(nNRn}q}L$k+1rKqINgo|dYIcJ!us05oM_U$LEm2^j2 zt+K2-V7o6Lf9s*sjTK8y&iSuQ()SVW)-(Iir9!Hzt)Y7_3UEM<{?|4~aq|8@<^QtN z>3x3o60HghGGyr)6RY~*z-ya-KuFw{S;aSFFtd8rb0z|iC{k9iM2pD%PIh@GeVW4> zk@n+8dm~U9VdCcFo~C|P$9xONbA|fdPmbuYNcx57Uqf!e_Oq=BF3)nk!jb9hHr>`H zhppi+$vIuF*0|xhPl07Yk@*TIJ|dX)NSMk9$16AB{L}x=^G#^z8b{k9n!Q~XD2%q7 z&#asb{eQwM8dvo`tGbg%)mfGS`w0SIfOrR1Dx|2XzE~sX(nIjmiR^Giqmy%<@ZS-h z9bO|(552CfP(f8l`PI6ZnI7izMr_^F`)pc$hj%b(Y6dfe%{1o0)OD@UjA8ohl6I7Ir@YPbFGXwU(`juZBb) z4D~is3NM?;dR2F?qGkixy4{pbL91gipkm`NdKd63B#{_cP;Ajl{7vQ?({-2a*Mykv z?hjst)gxPxzPb9~b4wt~17b)(EI6`AG78%=jT}8u<=dA4TEJg687=U;xLwX%6irx# zq}fkDCX6ZRJAPfnzD{sULstZfR6d2#leYo9s%&eR+lsKm^|e&3;&>I~mPE5#TF`gm zQW>8(6Ba2C008|k)n1h=Y5Z3RbPhZa)&mn70CoxKO4d7LxHF;B*Ry03lymu}G5ms| zPEDVtvt>LslEUim4N+HV^Yl62iJ2szHX;O@vy9+NtO}_FW5o5ZwV2-edlfm)S$ywI zmCY*ecQsF)KXh}gB1iv%>yXOPc9k|ZPHOhJrQEY!CYPDOmTY*(bp1ckb7+6%}Ap zxolPTZvhtlL9Y*Tzv-ga)6=s?q_XZ!-$$M|o+C$sOCoJ4KudUaY)ciPm6kGIWlu*P zAK(-^OMkn!?kr7+v*9cqEyvVNsudY;`jxe)WbCnk$TLf;Sy<$Uj7Jg7_5%+om2JV) zL3FbD@yliIjZa$iqYOAgjf)=>_QmDU5cY*3W#XR)i`a6@KHMxZjiwrW6s*WA)-_#T z&B1IxgqNNf3FTJMZy3Gi+o)_V+W3u6tIC|>I|g|F(1O~re@j*;)8E_tRG&C8AZ*Q8Mj`)6{?k{Sz! zwN*Xq6+Aw6gS0r20FnpVGW@)gIwhvzdbO{oVbx_U>#X?Re?M2&zNWaA4nGt2Zg8k(LiHwjv4EBhR-YPqtzXhpFgd!?J`y9PlYB>PB7aM!mEK^rxPeetcY) z9~ti7%!ft8*c$NjKVaVZXCnExK3YpYW{763rDm-_Kh8L5_2EkuFj1P+vLv`&0?{oU z&jcCo;=>#x292gjhkfkmG=xlRHx5HDo9k`h(z}{^l)#uF)ebR! z?X|*V(5=z3Sm>S{g!B-k+?}4=o1QV8Cs1&Q44)J&<5xihBK2cVyx|HlilL5;TjD1|szkX8AnjLH9fxSBwW4HS^ z)^8kVP3>k)Z%)6_D28uF!F966C1Hu@tNoef`(xQ(NwxYwsReA0-$U+|#9Zj*dI(T^ zwjazR8frHK;j<|5p%j6P1R(KYGF`(rn1`=$ZkV4TTX#FpE3B+IvS)KPvX%$8xcLTHVg%6`dpkO)BXmKb<@>$HLqtVLP;qOk(<3>tB@np!#&&3*>eG)DQ zI!Q=osEtC)E=qV>Dju?B5cb9^k#X`iWJ3zf>U+5Pn+KRQXeoaQrm^z|{rma|+zZU9=k14!H|=~@?iU60LOPifJ(yVpq)Dq;iO%1* zSeBk0gPR^_J^K0$l;g9KQ)i)jo_2FGf5BI+Kn6}<$?v|uN7pZQEjE^Yi&(L6lyEuF(g**glu2Whn&QqWVB2H0Jt!_p79v&YP z_L7M!>ZC!VMSwV+$8W7c;0x#6r)rJrjWq|kts_~(S*L47ic+>kLL!6|utXuwA>AAG ztd6K$aLd(s>hf(sKl8;#=22N_soMf+76wR|07KgEyxqwtK;u!wgDEm=A%AzB&SJIo z3~!6Bcgv0@v7dtT-r4L)NgOV{|Md~5|LX%xo`_EQ|lK1v(QyGs-4%b@Dx$N#SFr>*FP`}(0HdqqNXjkfJnAG^os_d2ydLo)!V(g{t`Ui z5(bxS7vrKa?rjkw>)7zAOKTL-lR>rFWTWB9l$v@U}GnsrG_n zeZ*^5()5fJG8MtTa3)2H^(ZGI9H1Wl!+O7@z3N@+Jx+vBb)I{mJ| zlt-a5pXNS}JPPM{eV5GXGbJlkoS=9XymSA4E+;lQ)x>pS;h9o{*+jE+*e!698kNi_ zo0?S2Gvx%y5?^u|cOs;@3ARkVn0dw1gZdYFOXKl{S%>&ukn&{H&`A%9&E)0N*i_Gs z{f{07E`A&Vfpx62Kg!#GSOdB&gw#vd-_U{3fE0v28(4mm1D@oXCFUfY4`#p$Y1sgH zuZLX2lS7HBqkinq!9%Nz;z=y-vw=FDaTtQ@W~hxiO3e>%RO|6EMuV}^E2=oz9p?IP zIS~(V5>dB5O0H?QY#Jv;@m?dqn4oP;FA#4x1)G7|2z}=^eV_E$*Sho0MGFc7tJ5ka zKd6VKfYYC09RphmOpwiQTF@DK1E>#m8;5hisT>?2JYuV2U%H5s*?%4Tda0s^tb;{r zH!oQOS-os+@;0_-FwFun>_ubNn-<);eSP-nxstzr1CxVNa^ABpir`}jfH!1Cd>%jwg)r_#Kf*1zWMtIq&eL%Q@6Uc0rY(Y;w6}|BON7QqCFQ%IO3S6`R+<3vKK`zq?Qa?XBNA0^)Ea$=g@sW^ zTZUJb*;rmVK4%H)a_tu}JXS6on)bL3$6^RgsBg%GhFv`_>)T z&xD%+LYI_8Y2;VoeQFK$jMcjb@_3U!F7LI$(NVs5C{SX6AM~bE^=i3;6ETtk(JtI1`vtn!55 zy7ReW%UblunBpy-@4fkpF7gNJbTr!(7}ECxvukc^PGZO@6ds%5GpyBVr)TMN`}YpW zT<;M&MEs>+(mJZjPQ;nNVoPo2Y^$sCSg@@1Kkg3hNZ|8psL0E!x^>_5u~gMnQnl6UKtYm{vF5WLcg*DZ1=q? zy8j8K?eBYT%gi(Z?o!OBSIr36**jeus#Eryb?cT%2 z6O1e@0xArK03cAi?(OdSp#H$TNR6(~z(>|b_!s2asyhw01bD2POXsH|J_zwo%^=O* zo878=WZ?DE?XORfB)E!BoUDPU|5tu*gP}2o?-%{^c zkN+vT9Lz5?`(v;@SJhIE88q|^RPa`qrR{vVP)vLDEluAWgP~9gt_thfMIAQB9QkaC z1X`H7D#RlDceXlc{tsDXB9O-E1nLe0ra&1;TEPCr@ncfh4!vv#2~l z0%RL#B5*&U84UIlM~rkdtwk?NA$fSe%S$O+SS%V>Ew1jYT~S#KDgLF91txizCcsF| z%ejeG*C1umlL0PNU8`plV9;{3!e5G_vf3Ei$0|LaQudnF4G+*s{7PVBi^5$So9zF~ ztDHKH5)K@V7U>yBDFzGJCcc!7`RI6@sE^$uH&$M7g!6#x5h#8$0mpTQD|cBc_FLY& zoq81K5}Xr(ep1qzi%sb|X_tPyfeVqu-er&7{OOJlVo zm4K)-_3As34W)Iz@Vb@@b?0ERTsK*rvIQ;1Wi zjGxf=siUrxo^qgggt6V$%W*EWhs{rkq#GZAg2$@6Yj)F2>&rl+wGIPI)bv&wgao0T zG$MPsZApAj5Llypn7txT;c;F9M-x@v61Ttopb&}klZ&H!+lL3tH4inV{$7$+Mv#I&{guMwoL6d zRh*YPyAO&W*U!PC+|8s8UnuS05;nzFTF(f((9Rg#21}pgtfev5H^@zv-cxK0@MAca zaq6qmeh###e?79Ecl5a!2?02mV=6=RJ@IK zfB4YFrIJG#G|D^;BGByPs%ouCqy>K(TrU%7_YqKnyZniq?wcbze3?D{!}BR9R`OoZ zCm`GST;!mQCQmS6O3s7|G3fnV*ZQ7R#MRQzX#_q7m;&?}3Hq|UXsPLo_oQNTrzI1W zX23Kr^95Xo^Zov+qA4w`Y`tSttr6(iEQW-i8*+g{&$-`DCaxoWK7FhLloHTWaQtR>+8z# z91bD#0Yd%S-{fAi#?f@Q*-T1tK{SMaSAuA)bz1ZympDQX$b+kqv^A=rSp#&u| z4Q_A~z}Sk~dBb@*wM@Y6y0yO>GOGI@@qEw2cAGQ~f9uLml~h5cen#A@W95(9 z!~`ls1i>Ql`Wquy>`&!5iD-DS zJDJ)+oo_tc(u`Y4x9cunclvLjfq&DOpVj9gjnKZ~z{qO3u_y2!Us-sHrt*1T zbBtCB%}k*-##ako#TfDU3&f$AMQViD%b!k0+kVS-z-*agJ^rf~0P#xe=TG~w0HYL_ zTRxZjX!%GhYuwPqHLN=`nvFotuFl?et5XE>wBsYBr~SrZt5WDAw2gg)cyf5gz^Kwx z{~&7vNVxq&$kh&%R|Sx-VlDbL9-V65lOKUA#!oI40kxXNtF)T|p-daYW)s&hBL+2L zW8lrpvnq*{kH3|KL*6Py4J4CkY9H+HQVmvkZ&PFB=o}&oW0OK~*$<9N_*(NvPLney zb4$EdrmVkdD<778hw{MZud&k07mQpEz2}BrJLG=Bdc5FD&uP~L9@K@7!P}b6c-`Tu zWz6qAq8oQ(lnQ3t;OHB8SQfS!f2 zJzjULK{|M|H#!i-9kR*UnNRv3eu>cIclTLX4X1%GCFgfc)WEf7a2hG8>v~eU$#R)1 z)GldGi2P=HXk2!9dZne+jX{Bkf6(wd*-96!+V>3vgdP)MG^*{B}x1Teqw-doU zUp$&*qbk0wv)PNtswdj4*K&*d5C-k2=bn9fJ-@tIdaxEaE$rK_v-A?FnERyQWr|SL zW$58lNFk17?epWfq2Z(En8TM@f3}UV93d6^4GZ&=m9L3x?bv{!1gac_6m36{Jo|fx zjYi$)I5+~z7%HuGbFj&E0RZvHy+)(gSNOX0%b8mX@&uzX0LQbX4uR6G03dQ| z2#>B3AH&Rg)qc81mzy<9PPgsmHz(`j9hN$mt-TkdD(0tE5BK-uT0}VvoG|&LK(W?K zGCoMbS6rN|McaSq1!++ca#5-^EvHl{S5c4M$Ez9R-dZX%7$CPcAuDb?AFDM zqd)(vTc&&BXP90-aehxu=`>R8+~+Tr-N6w3stnIHPLL!qXw_YiQt)fb;o;Y2=uSt~ zNGU<7fkHf`^7zk`I?c?5^D%>~Y{^uThxnqNlVJgNFd%&^H3-229MyU@KkdbYT!KWc z_Db<@#SR#+(6ru7!1SnI45;A=>dn-nk&;AoCl>v64*=B^06cCAaJ6B0^I>tf%C|H_pGgs2N{ftwC_$ z?&$_A@Dyzdx~^?{kgqK0QqQmLc!IeW5can9?LSKCYS%*;k2u-h#wY4*pr+vvprUg1 zui>+Cv9qGu-g)a5PzqItgxA$rhFE^okaGO=m5ouYwI;W04v+xpp8@b!C&*e*?KS;>kzUrQ6Zoe_D_KZm3-9 z;856@<}#T*5{Y3PbdT!rd`QP`u~j)|0A)68b2H2ac|(n$kJqAXb)GvQ37leJd%bu) z`f15Yq#M^B+s%Bl{$?b=KBf=d>^7GbMMMXL^b%MmHzjUU#`pL|3 zbc02Bw~FTZ4rYsEw55WJo)nT)*$X?_)N{yh^CWBgpBPupKQH)J#_znc?4@>VQajw@ zPZ-@#E^)IWQ8^wc_^hs}YfI{#fiwSqaDlPvc)->eCKq~=?fhfM4TLu@k#GWa?O5e^ zf$z5(Qw~|vShh2XD@st4tbH)l-_(5yu4OImPoH2jFrO8-6c`W0cc0kl_{#{mCXJp7 zL_mW9GzWHSi)&?PKh#+RAdD8fIZp0Xu8!UxfzkQV<+8X{1hA;>KOQ$3k4YQ$agPyD z+SYfI_)AG9yw}-=?M5U;8Nuw2B$>J|f82l#k`DqgganU=_MR=@fG5QdKNpwoH^XXY zv4VBe{Pg#6b@)pR9{7#|nLJ!8WnQZwx%`dQlDg1Zns3{|k$}5}hS(OgT`hi%eE&o3 zYy7OmA%NG>q+o}F{Q(JD9A&_Z=p_VB@*f^h<%is_3J^J-d04#E8e`F!YS|UPX#Hkn zV&!3SpDxk9#3qEpmLh&fZUjDby@r&b2Q1K*ly0UJw8~Twl=H3*BWYTq6y6=8Q6=qp2|Y9*h;s*q{e z=#`r0R*cyxPz8SK3o0?Nvzxcnf`pT%1)d@Mr5v>(cpQ`-p@*=r^VZwilj`gnv)!B9 z`!Uf%TMYnPB@GSS+4(+1|4Pi}?|)mjx*py7GZGp17$}&$Q0CUsN-Zr_&X-Te!+$6u zf?cL1m|m!^uzL{QQ1^z4x=YL}w7~)2et~?k=6REg8N=W`ee3XT3}d*P?*RgQa<~UX zi?25ZqigAAZ>k2^nuu~lX**s6Hq!^3?}RWuY6vZ0;}953w@T;h74&PUFDaSHjq7Jh zRjwH*)YN>!tk$3~uCvtN3hmpSS}N1_XuGzZN#^Ku*Lp~hHHz`;P{XtQ$u?a=8sFIr zPSI_F;y!C#sjFpgYqtP8g6CJpR##c_U4rKD=`n7Jw}g#J3=>vS6dOz zd+FuZs4ud|exqwO2PADBP}Coiee`IYk(xZ0Ip-2opZ==@m(S~^>swsqTa$@Nw(l2G zOpIICs*q;w_zxuIX+tBsZs#4Qky%&2pz&+ib)6hRnk+AQ76bi;kq9#IjXv^6@q8%n zwHmig{Kol$+r@sF_h#yCe$2E_#3Snz$*>Nr(>cED3b1{m)HJ&0TwqIT&fT|+Y;ara8K*E} z|IXW;kynLDkk+ITv@2CF=s2|k`<$nk>E^V|%;)r6F&ZVC?^R_3=MAvzR&EGrmP|p> zp^P4`aoKA?@TA{Z*1At=WdVhvnuQx5CmXv6dnnCp14omCes$4{b+~LmyMny@A4`o4 z)wzxLfK=e<;KtB9?V1e*u@7uz?Frx5-!R5pl5^PFI9k3Z<_&4WFy2!VZEf0S$ICkV zWq6HXo`F&C>3OEwNx765eErbCIkUp%zPjYl7y=|197*9GZ**yx^^$9n7chnG$a|%TPz-W@@ecc3@Z2;7S?abbzS_B%Eo0ZbxcA zIRMCa9`qW=BDcUbz=SsAg{zzYz8-44aDPO60y@h5o$XZ2^NT0!Na8YQ#4%+=YB>;) zBPaj-FxmS)KB2vI1KGf-Ig5veTr)d4ytGgg`mgh9uxskUp=%j~u{X(NMPDSi#8FVH z?mm6bH2-6#My4HGy7c}>l6#-pEO9zsOY0>iQ)ySQwrr-A)5)W%I-nkZUwZV?>Oj1I zdO7E#K4fN&6=&@VBTldLqvsple{hlRuABv~ghYGnz11ZHP2p038am1w2T=K2jVet| zTKlZ0<=(C{iX9?A+bnN%nazC~1skDzqz|tUtn}WW6Ddf`Lwe?OUIx5;Qsx_Yaw2^C`>xRLuG-r7+J&CXk>5rP6_!Iam4*iXmv~FSoQ6e;fO59s z#c}VCp>?A`%Iot+YE`ic$AyMJT;@Ofz$m{?HSq3R8w_dsHw2&?Nq2XF9h+kr_XCF; zc1K_Qf{bznH>V{lm)gJ2>fIY8vTK=yZ{$uyVzsw2%#50A`u0_6*2_Q6i3Iu&<;OJ0 zPYBQ+MhUgVNO&$Uc`+Mes*_%Q!a1pI*9%)B%)xzz9h6zM!|Fy4Z%eNEk!h>*6wm6N zb0;Jc@?{~3lb%uaZ?+&VRauzLQNKH|)-_B~7j(}<2YceCiK)RDU^OIR9 zRGascmIQLa*&1CQ%z|&7 z3=(kVSHDY_0ZvC!de}`9*t8uIfwymuy5gl!B34%?dEF6ip(pC~^(&NoyTFWJFCBmS zhE+C4-vEiD0yY`$#X?c~l~g#7D4dFuvvjj5TR=48@!`7Z*SvQ>eCt&WbAiULd;mmt zpB40J(OKp^l5%>M5g^$W{K9ETbn;>sc+AbHAZEvPQgPiRccMJd>CbSHLsS~LXaO1q zj1?bR6!S7%{lNoua{4UGi`sB`4_>6(uvI=w;1J#Sx>wC4R^2~8Rh~d-z*n_UMPL&P z`uAWk%fX-!I}S1bw2jPHu^HN!32$rmHx+z2hg(0pUuk&v^*G#}wAX zRONC|^&z9Ya88)hoOuil5Q*295gSa2Ko?8b4;dJFmp&r7q?+4qnS9+aY)l1}E|#;? zt7i^9X6gxCpy0Ow$Fk@w_G?eo+xC@E*okW;4Fl~dHx*seyntx)t<-UtN(0R%d6{xD zI-mn|+F)#m?X+O|C<3EkL`%n~v~+de4^V`w4pjOMP1UYzQFFix3oZ{x zK|s#GV~i1G8cqjvv+B!!%>Wf!AJz|1nfL1L&bU#<-mX^tq?{-$oA>Wcn@-1@GToQK z9XCIF>n0iemRf!xE^dG!OmM5k!AFUYRc~HQ+y;67vy~d_I?CH!6qJ9Zu+MrVpd0rB z`IS;vF(*ccWdaUtZI@N;cTopEg`kb;iCXzyrQNabnxpPVx2WRAgii&;ETG99Q;c7K z=($__gx9#N1w=k1NN9S@cVGdbaJ2By49W)--?@{>)ggXxf&yN`2mprh9Q!-N9Fzag zEC3Ka9Y98?{meI&7ZP7?LWgleRdYZ&!S_M;fj&#j#RZsde84d z1C22LU2~&1=Fi??O0Q{0%s(ec>U>P;MxK(&r`h@Cp!KI7qALN{ofyPwSqBN%&BiKF zwE1Cj4r{uI0w(|K&M%yC&wz>XVPP3KA4TlXH@F~`d3r;F>?KD}+x)83?>!lrJL$vb zH81w%-2tPm3g4U~-Pqgc0N%t%wW^~9LH$NL+^o3I`%~-`LJq?ytyp_wUPrql4y}VAlh}O~q)V}9jHOia6a3G4n>C?}@ni;9q zCE()E`M2$;&lzAG{k>6cfy39WtR+)c{80K@Q>(kp28h_cZd>3%i&R_`U#y7TkC%-E zRqHHNWtW=->fm zZ+Of)8E1-)LBD{Nx+W$coifs>5Q|JzKX7{ktpx{kbzc)q#ah0;)wT)1Bc7oUV5<7- zq2YY@TQ^sq%dXN7pj%mQU_{y~_fyZ3Pn$cahhE6zOQnXo|~@~Cl> z<%ldn5jnZB|A)1=jH;@O`bRe)4T4BFh=O!?NeGhCCEcxbgA&r+-6`EI4bt76hwg@R zH_vZy_(8v-58e*1i8tZ zy-?M3Ghz7k?;HItBg6-pWDKI8!KJa}moe5j(m8|U5t0&Pd{rNu#X~ny1uUaGy5Dkr zZ4rS%>nv8$t|S9(oBBn>3zKM`7j&oXPFCXZw{2_Bws&qY03!^x!s(4#=x+GF#{Pt| z9fUA-zFz{K7Kri)Xn*8lL@VS#!DOF4GO~K$CG~Da4k17i3PX-Haems_e<~7#jqY3* zn|s-205(@~@rR|I2hDe{R-xE%*?CpJVdwQ?dxKW`}+AU zGUyzR`<6c*@8TlI<8>ff0TjrDxL+pQL{hTm*BTurNHfX`jr+_9JUO^?KVA_9X7`8S zx*|M7IJGto)Jn(OslEu^1(jryAFrH&#u%7eiCNA$Mm|G8M-gjK?K+Q{9-P zKQVC?egcPug+4U>#o2+VQp<(XCDx_44O91K)6X>)(-t9$c(gMl@Z#_|IPeMzZ!j@o z=Vw90!L!-k+k5TZfKl4k$kNe;*VQzhPCyD%65E!?hyZF69@hIK|I(tT{i2zq&LQ^!eWDZt25|%2JH1S2cg{{Lt6G=mgqW$P&X_fNeTZ72=lvv zgdl7|5e5{enAW*o%LDgJgt4bY=;UxWtE;^~sGrdliV6`)B~PGaLMJWxd?yZ1H|;Ab zC8>DYR5B+Sn0Ax0GmnpQjoUvF7YwnETYO%jU2pN9<>W23BGpK_u@u?CFHk-9=DD1F z{9@~f(@l*n{MG&DGpJl=*pCD)vn4Qbm1-Z{oq|_TCgjHcW_dQfX8-=ZP)6fHSxvYa zEiL82tl={TRl?)%aw>r~{8&37#erX?Sm86FoVD_R;=}gsdq^%KImXS(B5sJ|y(vi6 z3d>r7)^85a&(6&QQ$=h}tDe>(0wKV5q&M*OOQZYGGx4D4OQHg)qd$$<-aKCL+E{XK zz>t2Uh;3!Dr6OT5!EvGG;gnhuJ*QP-pj*k{O8}fF(A8Xyl;7DI5+6$ zscuXou<{xC2G!C0<5Pd`?cka8ge|*|)smA|$>cmlH+uDXrpdv)bN5%?T%p2#L3)*Q z3s%UFS?%5KLvKkKaQ{>Mn(_QF<-COYqksABr<@HeF63_)K~6cr{v5J${Dk z4;aL0-o{m1>M_k2rf0x8p`NnH)A1G<)Z3dvf7lfgXm^p zw|DiAK^N9D7%+O7QLjVNrK}CSad~z<0TsUmEU>-ME&1X)=BA%aX?)9*)AinX8p7@M z2}ZdN37H`fE!Mdl>orjz{jgizOehaf<+n;LB*|thkT&Z4dy^KTknU#Qz6@0Tm{u9y z-i``a3l5$l^N5Uz|4>%;x+^k4=U+09(&}0EDw$T@pMjHv(_-;R+#jB_(Vr&3`C-oL zLk^7rKTJrwagi4+`PmmY2B9wxM+lMV^(k6yEg!M>r+$<2sa_0F^YHK(xW3Muj#kqh z)@+)>cYbO*(0olC=O5!r(hZJ<;{}DkHr>ZkGW^#Edpm~ zSLN<}8!er@;RMe>H|cdIeq5p7$uwWDUWg&s4_SC1>#$Bm8(6Nd&x@>&S`)OzA5^9|u+I z$Wyka{3ooJe$9soM@gK!%kzi(^;X=O9@-uz2q;Y8O23)k?+{5W73+$CMKbQ1dU1TP zzls4B*pl1;rpUfih$oxjt^Y+N=ONCZ(IgD>E@}l_8o6#P(-DqlyHe)~w-5cYxA%js z*Jfx6s+hvmQGLPD+qx1_EXpj+UhvHNXTWCxR&&a3TYak&zD0o&wL_o0*;$u@#p8i5Xou?!p?4P z9t`Q!I1N{XkfaGQG7xjmZFQjofWO4WM;u31OIl+)m zQv5C8E&Lq?8JIZM!4#ntWI@)3y(~2^XB?wEDh!|o;KuAEcda9@A6}Ci%9&cNclM^( z+ML}r&D;Lf%Tz0^@_26C?2meUIF`;3&?=ZzKwi$sABg>!JY7<%>$wePfI;gPC#tHN zH1M!Pe#i?#(m6|CKRo&3(`@{X|49AM%+N}*)4bqZX|h53wBhR^tx%kYoHQNof26om zxl1x1^1t_dC>I3<)l#e0IdtZy z6%8fumAx_oiQ_2lRB@r0NoJfpZ(>~xP~f|d(%8d7@K?2myw95QUap~_&>Zfe3<_%s z`d1kN<|B2NH6;r`AM;%=F2TIz``C{HBX{GFJ&o$iYLl}`)55~QC1>5m>QOgCtd(PY zBgszR4dekDbQJB*Ofk62O6zDc?fCj@i`rb#4;0;;>|&@aAO02wSr-Bwz7la6Ng||q zkM=0!=42k2iM+KoD=h&>i!qBV+zvF4CAu6KWIEMO3~o?zUa7mFI2Of)WmJ*=b>}}K z2|z2)W0^QOb2`IZJsyp}Ov9+0GGxoNN4r8maFwz2+U4XyflQI-8nfX)X_BTWd0@jRP*H=d6@B=9PX*~ z#dd)+eNKEGsx~}8rFV0Ddc0hgV(SD(BYKTfb{)>&zj-$W*R0~{>9AP0_Hcs8X?5z_ znplo%P!J1dn^OZ;fzJ?I-zeUIgf}>)!rlQq(a{!EI(!YOC`RV!W)4$soTFl`%N?8F-W^t1j*k^ zAp`dgGl@879O%sRA>C~WlW%}-^VLC!NWx#ZS3VtU22&B`d9v-}9~~$F=I^z&hxb>4 zA|8Fvm~2T+LhI^rH-S7=rEwZOlpm#~ED3Z0q8^_nVzB@uih1LoIa+xnK=So|iuaUB zc>LoeT2jz$ktosocHnO;5sL}RHQaFGK(gJt6*qHlk(>5-!6Pm?sgKenz*fYjq^0Gb zTAUc$Pd3<>G5=WdQhb5;yAODWAl=TqJl-*E92mYeI~z=+KSdNfg+&7jlHH?S^E+qQxuy*pQ#t)haS61n!JlS7Yl$w~n3EyG06umNlSAFo_d+-jdrQiMq~ zv22f!M)M5f0C#(8gjc#vQfySt*~EfbIEzE*tAkl8U|!ib?Rbiz^xpLXjTN<-FK|!_#kj2{SO6jo%Cc?M-WB|x=k$4=wyfc zNwQ#O&O>$A)gDgAyG-YR>=&3H+EMb{soXkA?(kB4aalE$9=1wflJD1@1*Q;Z1me_`oiO@F2yqmqscEIslyKG=MTh`U7|b zCAX<}49Qab%0z+|N~Tr?-(@(?i5p1uWYd~NIteBdnDuCqfYc=Ef%mzj(3k$CzDMg) zeN?toq4vWzdrh$ET>|>R_M+Sm|AIR|#tpyDNe{mbzp`Is9jeo&HybQq+raQ|yevVH z!}R1Lf%Q7An_2W(C12yobgO3$BS(D`QmeosFk<%8R}iak`jG?fCW?D%A>5G2>wTP6l@uy`zQ z`t7u>$wlL;NhAtCOpC!`S8gJ1_y+v>^S+iyE!FC#-%Jju+9LcfEmwH2>V&pYKzRWC zMW)R{#RP^U^-Y^^%IcA|k!)M8_9IUE)1YFdWB&b1^`mo^PX8=;C7hC0qsiVh1-ryQ zg8bM1Bf=xy@~LJ(lYNqJzr`sF6$Z0>Af~oNH3?jU@W)tu@C?4g9iRo(gx9upxTzaN z?mtq%4ppD(@6#gsts8p+#VJWF$twF93IJiGyRA_k9^;>vSa*lQz~6H55VR27fqf+m zaubv^u8qVEE|VaeHTy`xyVa$DlPeK53VAQ;>brnPb!Fn_?TY{+PTgD>y*Upq3f6hl zy@tRD*WCr!!+CIl2AN|SpRr{F7%}VSx3`I8W^lp88Q0!?KQSztrDmqw6%H`<_H$>cq%$_B%r{8(i z|G1?(t_u#>3%3K6OQXY{(u_xqfW-?E*VU@0w!_I2E9@rB0lwRK$K9@T;&PJ83T;c= zs|)8+9!>-Ohluh)W=~I#g09533|NyeL%=VWCr1#teR!d85H?A(^r*QSL~I$6u;L`6 z`oxW2bdNT-)FZ_8zygut+#{NGhZ5Ne|9uc8A~|l}N^4!_F%kVdA^_;q@B(JTEJ-9h zQ{!cQRZc>Ncugop#g9X`D`9uUbfYWnt?Y?>3b4{vpp-hy`;=888Eh80;-aFziD8%W z3ruB?&fEiehM>AdWe+QQFQnZBR53M3oFfV2G6+^1>&S7VOiwZ-i|3^ar^NhQhqUNI z2(QWEs16SbSW^*eMZoe-twMAC)Hv^fD~EG zSX52Uh@~22n@2tZ6U&Z`h>@;%-oGp2K4i1M;Ku00gZp3@M|>DD5|pzDl69@A&nXTn zA!F~UX3P4j>sAxOJc0YozeF=PUMWnQmEI!#iN@BNAx2G8n$lK7tgdCRxEVJLSK6{q z=Qv}={3V6yr3721D|g}fD5&O&7b>y~?<9;siTqO-Ab%HMGkBuB#=R3vdv$4!>$ z+)O$+*TfqUGH}-{HooLX_dec*`rEw7xZOy(RiKxtN(aV)al`7H2oX}ZO%;E{70HKf zsS4TJb=LpwGc;M|HconvHu|{VJ)Vn+;kQ_eD*nodEpxwB5GPpx^Vf7)8-?4a>k-qGn|C#4Yh3wT%@J9ZVx&#am36=;D zK_YPfQnJvGqAfnb;bH1>w}fTIkJYDC?zXwH;cJ_B^`r<%aR}=UfJUUW{QKyDTy(lz zeI2HTgRpoyYDYcIhsKnlv~geb;N2i{G!!rx{IbtjzcSeUFta)OdcV=^y7so8&J^kD z>a?7+2k#wTOI*oy4erLWdz1&NB!(BY9iByhJ5Lu7+)FGDG_0l-VtaD_@?}!c!PNVD zxc$(!lpdwQjjx(@YQ#!i!OxZu5Qb6!k5(IT3^ke~7opxA0g#9jhZg8VOQgj3i5I<8 z+oSuARstTpW~?3Qb*!$m-7S>+3tk4MeCbZDanGudsnTtyXM2q6L-8*X4C5P9jghmb z*yL+U@Up)Q$!_)`Ng^3%!TaabcUI=LVCHH5>C2H?F#g?fajD|df`5;$K-9;R_w6Lc z33nl;?*3&4Yr&@O)q{^BYEhI{iO+{jJhr+DdzOk)ykYi-hLiX3qI5zmH_!;bC2)75 zz|FabhgtJ0;^HO<`xU?Dy3cO=P4RMGx9Xi&p2II>if^GF(3Ye2kO0nO1=U-gyB!nI zR;4F+#KBUmU9R!+8u-aoDuNfS*um*`V9}RviGp})#<*p&(((ssFcs|F5BUD;BOkAq z|1VNlOc5jrPiX2>kE4D-XDbP!)+>CxNYZlp-j>yA-?lS z^VN(`{_|ZoEqc4^^Vb<}3NFQ5w#70gqr0+KcH#>LFPZ18UKSjGf|O$vzyx!!`C6o}RQc^Rp0l4*#P)I#m} zr-mjf-&t`ya=V?m%*3%-clOrbfSU63{?jQ7zcO**C2?DlK|<<*WufNj8nv7AM$yKdULZtH-67Urk(H1jg8c#-A5N30+S7V19?9IOrWW9 zR|t}^l1Ar%13Q@~PW^42u)Q-+Iz|UAFrd)aR>Qe`z^q)^ZZ-o`=Yo9-@e4b9nvl4pji$9#^CVcBYVz1H|6A9yjiw6kz zVgg$zFquRw@hvrleZJH<;&~xqyzSN-IAt{y`dO9`Ja5!$XU}^KriIw*W^bmc&)4S> z;rMLZPXB7llQg(Y)w>Ip`W)Jsc_`ni)`e=UBE98O}J{@a3$fx|NGz<3H0)>6o zz|La`Yag;Ud~TfO^l&n@W zwqDb{E8Uq0?%E)>+2U4w@;?hftQKmURH*L4Mqm5LM&XzG_^^{bY208P-rGOUUi2*T zMoaCA@ld~5kn6~GWkxe=d@8^w$9avQ`TA9C!^5w`b!6d)l}1kJo@&ULN?^!L=j)Uc(zdZQ z@8}5JzEjIdon`qHhTp4|F8i^9i`3z@%q<(f??PqT>8uh#rdD^4lYzRy#c@)x8$`L^ z!q$6=N$zPPQBVW_*yI6h%lebKJ7V8C;6#2alUy5b zO0!YLhNc7Un1I`qYLSlj^;gc&$}O8c>v0Nu{+{X-e;@&^^H=JAB6mKo#Q?u!o*QVNY7D=UXb1se)r=%qy`SKd!iaern*ro9O}xNAo``y~SZH zpNClp!xg;C(ZIJ4{7+o~dt!+><{oeiG5BSfia6xD;zx$i=FjhzV-zX9?vLnDMqTS@ zZ3#i)PJ;(4OlC)o@K3ern%@?$39|^Lrm*6jDJ@U_ZLiJ*Ex2cF$I&wxoSOS>>tF#( z@pXgP0=b+T_{(wN={l|-w7AR?C70`!Y;0Khaw|~cvQe}TR9-k}lChIqw&#N_OO76}UulS5jhpdbW@tQwx`i7?o} zSi!=Z;s-5*^YyAF%QSws`N-3i$!|)hXGqZen>71&N=_koFLB??d`nP|64t;3u=L2@ z@LpYi&ME-{x%Pk6t)ni1c2x`bmBVNT3irtL7p2=#{|awT8U=`jg&}7#PN>Bly++c8 z|BF16eLB?4eEw9OGY!hq!5Qv0;#`FIzg@D*Z!no&H1 zUnxF4%Kq7AU1^$@B!B-?=Nx7$B>7kID5$##KYtfd>U1!#^oRcOy81<7g>;7eigWj8 zQ)m=LRtaW8XjB$R2G7$6575$t@T9}w3TSIP5OT(0eGI0Y%r`HPog_q?ih38tpan}h z`d302KOohu*k(g4OjDMGAOHEZOOKrSv^g%$++=7>litDD_|g}=N@p`aQ{J``2@`|a zSN+5!yqj(JrQSntn=rrDRO!~nGImTsZDJXJa2=X=z^pI>R*5f)bvDPec)>fkz>$U@ z`@89bsr(ubgE;kk*9Ahgg3q_Y&+k)NM4O$gd;6oPsEi6E zuvo@FC(!*AKI}kTJZUUoh<9mETWyAMKsB6$W8ZJNt{VKo3Ly2eOgk@Ft4FWoOxjp> z4*<-};NZKn@NpUXjFAm8iJ9}2UE`HE@<^5HdwrzULbrNu+0H3!5@7IPBZ)iIFY*ul zju3R-dnAxtI~?8^1+5B2TypflnIY6Gq`Dz3-jL5of`k+F@tQnmYJ8F2drqIjXn^d40U ziaziik&3jQR%gi;q6n{B_kAgRwOjHv^ZMS|<0Em(w7Xk>>Y@Gsl3N5RiBC<)ttLMK zkBZ%@-J_9(x}fh3c0iWbMauI^=^vT5T$-zS%$88p%^Mq_RT#j>7DaF$saDaXYchzW z#2y~@&i-IY{5tPA;m?nbJNc=v>`q^P{nYEaY@Xd#vl2zXTnQ!cL_58?`xP!HP#b{< z{_--CJob|1uG`Ufl&=2w?$83gt@u`{e_aaL-UQa;oA7$UTYf*eo4ZvzE85 z_)QMc`zdfce*%9a-h?a{EKN{3;Em(OrP_^*k$4TE$d-%?P;Dr#H-M{qP5>>wE@FRXEh(&scc8p`Z9Lv|3 zz^%gqNthv1$2#zrtxtIscap^YI1!f;4z1Q^<+F5R7OBiJYhma z%75c8RSMtgoo>!hX|G#YgD!#K0~0J>1RCDD_09EgDWDSo4Tk(9K8MDs(bcK{5!g;K zj_1SoXX!WuzL7*}6yG!fXx_ge78r05MYN#0LK<-m2Ppajk@TU-sZTcx<9K?rPLk@! zM$n0?I5#3U=$G93O6gg(9l2gNKvI32e)N~OGMh!#0RZ6Byz-&SHe=l_zcQw)*u>y9ItJ>z&L5e@) ztK6(DnHx|+nRYw8i=ng-sJ~6xk7ld1AiICxJk)L`i*vmN|FV6O{xP;Tf=?p|BI~9I zmnK90rKstDKCM#rC~Mku#Bdlgc3TPT`C6;*rJ9zofA$c85kAKR6pcD@%0OAG0AVhN zYwAg}limdHGV{1Ge;TY(N>C9H+K&G$GFI<)2kWWVio#_lQ>usGF^dum8eMYv{1T#h}q_ zT&{{e+{vOZY}Mt9*Y;zGg`hboB+$@qt&=N!J(2lVXi3AnWH2T8$N)xI6%%-s$e@# zf9JhrNi8n%buKq{SDKSV*JGc>Gy;`ylp;~XUBq;{lLTDPnt)>2_@HJbHzHt!3?nSr zQ{9SZ&7aJI5`&K29r;GPsm*R9jxh`MXPa!xM6HA8d)#9>rH+p)Y|o0I_67i7egfpo zmY=pdXCuON0tEVTC~TWfHe@AOGSo%R^Y58f9N2A}~Z(p&H|0-1&(5vSbMhiR2qJf^qxw(uw3s z3VNU)7uH7|jc?I@U?&H@zy+k*ZGsL~(`lGtjQ6t0Ivlkuw(JLJ(>A!7c?o`gW$@9` zc@Yd6TEXKpSwfVQ`Gk( znULf2S0$m$`5B4VjlS5i(3p^>i0-8DXhT_Su zn3LxQxVW@0ug6na9Z+y63g}(N7bWD|8j%m2sI0p;u9Li9gg@Xj^$CddnRtIZD8$PO zv7mOMz^-0LU1Wmp^-mq7g=+38lKbOxB)PR-faWZk?i`~712bk9UhU|U7K76sEFj*| z?m7W7u3T%WdC!A{)CdUIqjnDx;x(+r8D5Z;_sskZHg*!*71D9}Gji;n7uDP5Ot!$RL6o z8Na&a>JQ=p^EC#5;`ajs@{zm8`z$}__e!1Dm6(Zp+~++zY$;1)>TNKIVZn!5wuR@pQ!k`EKs(eTbh1Zf?qqup2WZAdhn zZNBG~SU|yR7IH^+6Q>CkhPP$k&1KYYkXw=6As}e%`C#il=ZjBHS(BXNY5N2Mq#}6P zVM2B@U}Wlgm7u!UogD^as#rz%p|Ve4t~r3Q+VOScqmT&u5s$K z(|NfTF2913LsP`K-Wu8cU$cf5$n%ecnhA?C*LFtSj&E<*lk6yW`PKX<+Z1J_OGYG# zC>?NwktlRHAH(Y9_GpD&kbr0jHY#_I?@Pg|(q6f(if_N`YvMuClA=D&)(*MXIQ7$K zNQIOMQ3>a5qsU!HH0P=q1~-PAQF^^{>_AJ})HpvWzKpH_rKnu8lyh>ed3R(I``Z^jw1QRd{$d){#w*@~J z5ep~J)=-GHCH*gIKDaLMmQQhUlpZ%1%%X$2(_=Tfgg?q*GAm!Qd9*2Q-M4?2-kSfn z;_f)fnFGIi3=s#7G$mNV@YTht59TYN@gFfs^27IKb7xM{CB0>+RMSDA>RvEaS0n*} z=cmO))Wtvk_#b{6edHZhb4V_4m1>1f*O2MTUw{tlhTMKaJnp0G-WGE&P#(INW-PBc zREel%A=R(iXg6WUz><@~`lLc8=Y!OmKIlM-7fV;67WeDU4C6pNg;PX!XSqAWi!bfAWpgGAP$@T1J!A|(gnjHMew*mzTWwT^NaPtji6P{7aPS6D z?0|F!IF6ye0>r<|yypIZDA!({_2_vaf1+99jL(BBgyS7AAjs^Y{}9+?qr_=~}zrd-n=}A5S*SNvMs`nISbZ z{#Ty};wnXVij7~$Ex+mA!rq+1+ei!M-}}%cR^@;+lxj>|vEpf&pQyq6lG!fP z7&T$P(D!XFS=CRT9S|nN1Y#vKM#8mMIGtRz{`SP$a;SM26wm7`ce23NB-_iJ;SFED zPTB3n_Qqt8_A)vzjvpw|1YMUBf)&bM7R3*}KJZV=tR~_CsSpCeRS+x6=tPqMe4amn z9?#aJU8=vIC7s!`5=g53C@IMulhF7 zCw>vpBpl*G6xusv5{qE}0c?k8M}3FxbjSpA9xF(dDoDle0!ShMl9w?7heQ7I7q!zWIM_{c6Sl47xiYDN^=RvG2zvjt_zXMrv%!$5wy15PvfaG|Mn*}3{|I6$trh#58u6! z_Ueu%<206FAx6UNI}A)FS)nj}^q8Q=d$;mAuKDw3nS#5;)DZ`veAvQlE=mn+meYs% z>Ia(S$wUJ}wvyUz2lGFSVbggMR}k=SlLjmkPl3+TSB?%EApT-3AzM>KQq3|94 zi0brIT9NR&hP6ZW!(6XlD;g1nRbsgRM^2f_H^Jyie(!3M4cUdDC%5I(0f}Iz1(m#7 z%9cU;u4I3G6xdmIKZ9^kOVL?-&kw4td|yVD%rN9SmN!Df0Y&P523CfuwBdeAX`*H%rp%0XPq4scHo`dMUMhT3;+Q9r~kfyub@o(@8|#1AL7Q~CAFx8 zYe~nQS`3E539GY3rs39#lvKPd_FdjL(?{~~QYj07`S6nbQPSKP_aTO~e656$mzklew%aP6`d~_Og$K@1GHg}2=rOdck3qcyKeHx1PS(^;*LzlqKs(BjmBaE>2xvLDI|!tj50q(CI3(`uDeS+L5H zv^QNcdGB2hhL{zk7au^JtE-cUY$`4HPv#nv2=_aIlWTHg5Re&*3NLT*q;RoysJ_G+BO9X0u`b1`F|KRZ{ z`dIbrSl`sU*7Ul?hsD{A{~!;xmxmOTuBItYJlFWcHJAHBrk_^S3rDrCE;t4n{(~=G z1_iKR{Wmxu{OP~l2LE70PQCJT&ac&BQZVfxu&z{O(^3`w8yZpWJW@{~&u6^JMRh|H z_EK&uBHVw3L{o_d9~noL3hYP^Yt|Pc*;(AQKCHN( zlZoomI^Bwx1H?EoL|F0N*GS{!(>3zDynn1q##~aSe&T*0%$;WzFX4`hpUkTa7&6mT znB%036%fqipuYC{vBAwuYOSbgjRj5;9!9v6(0hzz*`n;Wd&)ytqT%_9m4?w?xxyvB zev4lI`c^L*^-*KHARqMDI<(js-bFm!`7-?Gkf_%L{l1*X0($%0Z?#02!0WGwBR)Ah9B|$_*4Vf%R?De(w`*hv$&=o*!`uWtr z?c%vx&Kbi!G1z(s{8)Q$pqSkD(%Ta8&JJI?v;C$DdR6F4^v7MdiU;G;s(M;Fel@SHvbeP-DE_9`LkMN(U zS~fa8c=9xpj;1;|4tCRNO(CrBhIgx1oorveco?}bszSt6y!DHpJBmgLamdhcF65<f?b8W@Nf)VKKBr{vqkLe+-+@jns=`GZ%^VLLgC#87R; ziCfW)oD*NAtHv|X@9TM{$H~+R)#b+61AW4m>!pqQ<)>siH3Kl16|x{R6aQwRK?eP% z8Ot&Dr=>M3y1ixw=AxbEJ^G@bl?}sLEW*RS>SG)^7cbDVTlcZI%4l436s>BgaGx*L zO`cecX}~9Xo*ii}j{L56EBb1w&-NLC!T(M5yKI&!S^>O}XfvPJHHG|F7PI=LCEq3| zb1&jWo76_hSf8ASCvCoeII{gN(l6@$EPVZ}Hu2z*$3hn^$Zqv7XwUkO6y7^OF<*zj zJuXC3rQ3wMN)c%zpu$_eVxIa?-_&CM+^{M&OsQA%g|g^{Qe$smedaihrmZzq#-w$b zJ$TwhM(pl0dj29pzpo^(XwRL5uAw3;}*So~Etrr0TAz5gNJa8bsJ=Gk_uXu8GK ze4OBBHbP@&=FAOc2FR3^DtRFU`06su+F z&1E9qtG&Q`ovkX0usEt^9{Oz{X(gJiwP1C=;wJCX_Bn3cQRiO=*QAK`P^Y)bq{oOl zPnJ#nbD9dg!0_D{BCKS$H2XOZXQ6^RqU!8-xXlvuP7;q(Q8)CuP_~>S^#UDMyd1%M zGKsMsnP*2-jOiPUh1{AK1{PN9lz$hHe(23%Jz4zg*iCaO5moEOLv~e?^m6~EbXo=_0@mkN!ba+F0JZOZ0aM?d=iTtPDd1$(-1Q^fr zua+Uf+remV{Y5 zqXg@Z88>EOdE{PfV@#w&E1(k-2$945-`Rp!qFAv<$X(8&eWUuLSs@Qoi-YGjI9GdbEYf(SIzvIjdf z_tr7PH%e&Pdtm4MO~~S=w{OnN6X%??MJxKZ8(DwC)$+g1daN|j+B0VSejlg3;s*My zny>2xn;5t@cCD*g3gl`b$(?1BmmDV7sk^kMtCN2>o#HLvxDLFDMzq!PY8?oJpHEdK z6rJNfrEE!C{6}68r56Juj21w*6+1q3CbWHs1A3NhL>!sRVs}AWn&x8s$bbHyTPv&n zC)A&EoE+S<@bK1wN$Aw~e`F~!l|GK$ls8579KG6?97UizBQ4=ULUgLP1odg;_9U8u z&8}k*N4$XAxa+x-pbb^0hN@4^LW|`?QskqI5c4@YKt5|7)fpgo97! zL1*7x`oZr|$KGJv0>rTgyhBg6&W=ML_EqlX;HKY@*?;$~EttJg0s{|jMi)02i!Uq1 z<<+!M5%?(DwP4*DL6d7ZcO zXy^CXUyv3J6UJoA9vry}BI7wtzP+4S?i4~9F!*DAz5L27kl`*N!Tp!mNo zP9H7W9~_U6L0=`CjS{7}$IGTDR6+M*PQ zvOW5_E6Ex4?&r#rK=whh$TU9IzXd6``F}b;)G!SpY91ygk=$qP)`=~3Fb;q1(9#(I z5jfbOZpN6%wbWGfxQCqjpY<_Md*KNAt;%1m?x$S$j@D(ks`<_gLF2hSClU?VPAt}* z>daz3{x$rMEYz<61u&|o%-&o6S=2uGfwDc*%M}`dW1ZEke ze4JV-GBhT7L!W+xQ%ou%d1h5^EmjcsC(9|{fXMcDT>Q`#+OyfT7~~Jj;;EdLz1u!} z$F-jj{5akV}2_kMg@aTz@kycoKB`FBEToGdlPDahM!zMM%?Ej3?28|hgTz|@)aT4r*O zBun5MD6h{md)KM6XKi&%<=sc8k1<)3@f-Qy%ti%yh%qun{^2c)owt3bz|DeoZv)+X zQxyJ3Z(?2JOxb)arvJ7TiV675Ix^jL5_E=&IZf9_2b{Qan{nDJ7oOm~|1J2=tAU$P z@=ys3_b9WI?V}6*u#;rRC5LdU zj3(8I$9EWM4AMG}slQ_t4mQ@;d5Y z7U)L~AeA4fd+d%2id1y;b8FK?EpySt=X0f1Kf5#UMDGc9QqX*t1LFT$@GP$u+v z;XO(4T>+;{9BH_ZMd&QjUd8g7M?qlh3#Y?=TDJ-xvu221^*|A$09`oM%)FiRe3m$W znqj9Bi&;z&`|XQ56^TjD@m41@Z?ajhKh)_66Uje2kb-SN$JM}(qio^lmnWM8{&T*~ zcDP=~5N`NxX|N>_sm#<8Nr3YHG>eEsiUjSt@~`=1{-M{4=XmmA84aO0!*DbpEcv5CCc^fF7J$NAF2Abr|6tAlzbv) zhL_Z{;(lhpRI!bAP?Mice)~RseG5jf>ufq?pMl8ZWzv6{&~b!pU_}NLFPz+XB?)1D z{x&d*mT0TJLMgEn(6&^KZ5aPSSygX!+LH-Nqjm%0{J9|U(6!bVF!)uU*@jT?n~a8) z*s?2!QVj*M=Xmrs{3s|q@&c95lvq#$!kgGXg7m?Lbf3N_qI_WfOtks~*}dngu;zdI zF6$c2b{qWP^>ks>ySGZh7gd-=>7N@NF+mBoPpuJO#wnPvvv@~zmbN$M*SbMy9abV? zZKU$=*a~8OHYqE1^)e`U93<<0li8G)_f54b_YWKMA^<*{-B92XHI>pJcx z^ESjm^MX*V6gCg1#~U&Ax5F>xiPHEC7aD{YPJf9s^fM(QT=iVf!{`esi*qPdHJ^~) zh*+?``ywW<=KAsfJeD9Ji0PbXUaroo^}@)d6S)a~)4g&^YK!6KBBTl)u_B~RVG&tw zFJ~Jdcq?8mz7ArIn`O$LjPdro;0dbL%*!&177&~}L&3`dVNetmc?k|(!bW@yYR#;d z+oB&IWc;CUv->f*YN=F2ha;8FaysjBMLrA%LTgzvLRP+twP@^gP?9M=w`oL5ZJeqg zfXoEGSB?S*?2P0&L$oLM7^IbapJp+KsVh3m8)x2s$OxlX!gIK(zvsyeI>|lgY#jjF*`QjVh z@`umRF7FLf&5X;351KydrG>JMGXyu7+ORo&*{T^uu0h6d!1yUx0gu%28 zh1#d?ncJS~yCodN|0v=GQp7L2|Iq!woMZMZyIJ$%*zwM)p+Km+0F257<9dF)WiaF5 zdRTFjT4P`ox0oo#wfyl&IdybnO12oEZCD-NV!Lv88kBTK$mV`Nd}es<`}4n<3Lya- z(F%mTcQeF|VG(T*KVae$2{0d%oq=gak$4}*lyAJ(a@&XwTA5rrDv2%V z>LR2sp?48pW-Rj>Az!4w{bNxPTA4=w+uMIjCV1RBbjm{#uUtvb{8 zo+fEu!Yg|J(CF&s(bc>NhIwHC^CK8~)Io=HJ)yVc$SG#q&a@kM5kUN8Rxv0O;br7@ zw?vV#n$Z=onewxh-35Fw04?1#W)Q!WGGszm!_Q;DYhwyYxm%XIhf_(}CpC<6$rm z-izofr6LTKjPk^X-3| zjR5kutYtWZYu)}uyUB^`3T{0HFGcR1#MAHk9ZwHxHJ9RgmpU%zY#~oG^}HjKUt;WH zwegnw?QF_cY$~2M``pegY-NXPWlGS3sCDb!;Ht0eNq4&eHPK^u-owWY{5Ra3Vka7= zi`D;EdtVtcL`ht0ZHj@=?h46X$9%<(w$P5 zl5UuD`MgaVX-c(bMAA_K70RmoPFZyGwCEG6q-!opHAatf8Vp=EjKyZ z!?QU&y7i38eRiRE%{#JTh9u=GJn?+((#D5h{tr6H`wpgK8(=DMlG|pmCrmSgGAlvl zJRu8K1zHP!{fL5=6$S3TFnMhhf}~(4$@wKZF$>@2%R{`SsjGa8-(q^%1adn}n@U}3 z^8%&M8I~r+5=>M@>G~#S%&Kn2bcnpgiX4I&9 z+1ZF798D~XdcNJnaP7|`fj=W~A3TqH%{n-CWtL&){MM!O&dxQe7=FCHRKJ zC@^A$2XFN{!@=VI35Vw8zqEbbaATZFkF)46A|Zt(jowrZY#v>x9IoyO9L>e|*RBRE z&A~q89ZZ2)Y)p}p*Ir2ojsCU?xY&wwVr&(EBIn&YkI)S=xmf<7$HsBhVZtRBXDe1> z^=9}L|NdzW#*deE>qZru-|e(=yGAvWBsk7f{zULB)j!I6_M@He!5ogIN-+Idw16Pz zZvA1zO6B@sY0=Aj=DnqB{DKT%CkZ%Egk`I3=|XXqd^Au5xBv8Nr8-o5B8Mx;MvCeh zFWO)`9XPV>-y_$~Vxi9LZ*s7<(aNHGzr@KM^%@Aox$Xc=CbyWCI8qdXmRY~dg9DRB z4(V=H3^6T*%wFfNUy*nt!JDVC<{|RiHU$+^9gdT-sgu-JxrL(K!yk>!j{E*-7q1yF z-+q*wO&iJmXf7Vbk-Aj*SK7ndn1PtBS^rzQ(w)z=Xkwy6YD+(Yo&9#YM|;rMul|g# z*NbIU1(^~-p{gN?>TAz+J%-{RRQ{E%oeshFyJz$@_sypDYh3MkIhaWcan=GETds;J zLBcCbgSR*gdT1d$YW-!l%_f+&_#hywppjaF|G@~nv_#wZWJ%1mrclp*i}VLI(M@5D zZ-gN4f5sT~=8OG@YeLm(Uzz6rRrzq_ZU7xD6rg&Rh(YX}#)Tw&*qHh=M4kgP`H;@gN0XLxv+tJPs zQlB(XDr5TF%meQa4zf<8hDWYW(kg0Z#2$TzE7rL?hAGF5F)qI#9IrjRy!nXR6gB&i zn`z&{ z)_e3}i18qlU)$q!I)VG3JC5?qouwhwc zN2m@1m9$@9uUZAl6t}TD-K4#BtO)c={2r2cnJf@H^n_X*PXc@gf_=Z`2>GIP%4*Sq=S_~#NhNGh|?yyHgB~xb48q$`8^jcH8`9KB4{;mImqbfVyQSY zT+2wmvLIYHT>KL!_I2g|Sb6(jmg4?@0h)W_60apm!CzVtt9U;z#W96|2B%ip01@)CJ{OVnZ-PKVrnZjL3#>6Z$NiILp zOZ>zvGD##=FOf`legzl$gKBV|l=)#UUn@^ed|QWwB6v|5O(CD1q>pE$bm3Ec60#hn zLDa!(clZ6G>W{<28?bE?9<^XnN_wS%yw^iNeIu%84|)08a9mjYA33g|Ond%lvA&j; zb{eF=nr}g3khTFMR4a)d%kxKnGcJ0W^~;&Zx^7Iyzo)!!WsGSpc4gZ9+ixP_N1RE^ zV{boYdkS?$OM~kz7{e*YSF1ZL9zvEqA*jt)iFa#loa|_WlU??=rUWUHWd%Ulp#Y2_`v&DK0tHM{85=j8Oxa z*)Q;|GkA=dh~9TmFskR=R#14^ZEQOxsem~yXGg+de@~Ih6fD*TFVfB2o7}m#9b9kT zTgtVe%DV_H+QfcM4T6*Ls(}M1a!aLa_o=ZvTf*gc#geCkamkws_8#iVdJ1j( z{czb|V4)NN?=j%)NV`n`Nz3u}8_I6g=dir-hs@be!pK;T{^+It5R=l?RQI;j*c{Kl z5kLO~oDRg@7IOJZ$KbQvFOwT(Ti2_t`t9HJwo8D9a`Z!j8X~Z1gq9B>>-2)HZC+hQ zGuNPEa1Nzt=S=Y~#lQ2B-Wj;)|9iSHGK~|N_Q$p7{KRL7601|$HfS(uL0J7J9FaQo zSf#9mLAK#>g5to!Z}OF%OELRFC8q5ru_~Ksslcr4>@^a$mtv-eS6{L5tw8Y`-OZcE zj9RfI9DTy3jh9~$j%@t1$-5Jl#YDbLLKJp)g>Pa)JhpI6u_+5G8{RvW;%&+|=+JswBgC`?L6yUCE5!*?(CYWbqN* z?h`7?AcfIfFfy@ie)gd(W1Y1RI9N`4;h``O?yS-ESVM?qsw9ccQk+KXjw4~g=>Wu`K)zscTbp2d4Dj6Bq~T4jf_da}g~ zMk;V|HHsy9ftB=ZtUb%w`Ada^{(mR^b?ZtjE`?%4d#_i!q8IwJXX|d?mSu$SggMzJ zPsOI3Xu~V97R+=wKIdg8uv@qt4kZ43-)KfxieW zcrT8!<#eV|-VjY5Q@7kI>O&(9LJ02# z@Jv#zs)ZSa69XDXYjAlgGah*f(SkzGI=ra$&NFi{4LryU0 zEk5Gs{kAY>1y2n%*%)JZfV>!#_YmB0K~%Nl?R8fCeWdHJdW?MX8ZKm=0UDBAaO~C{ zjveXx4oVwjdm~#Dy`evJfX#D;{*$*Ex}$Wu)CCW<&uwge;zNnDfUM;1?HIvAHnt@u zwxv#A2F1)p;D)8p6i%v0U`Cb7XnF)0wQ;yE&iOY6+qw^W# z%^{X}NEQ%eGOC21Hx6CCPpw3pf(;Rg5CeX3h{*O3P{MnYidu?fkXjIg35HC|zW^k^O%3#w=bjlj!CisaVwjq!aTaW^2sbn>6+Kne~{(iV#{g|oeCG*Gh> zpjaHM?QCCGGaG!{h|~vaz72>I9~L~+vWUw8l;G5Fps*lS2*_rrV{_o4zEPnUzjb;< z=s5=T5=|PoUN6BiE@!x;@`V2F5+S5Y4ZiyN(OjIemApXB*o;;Rogh9$8;frCN2h`A z^lnAV_IHFJqL~yTvIk=zGD+`r``3TCSg7t51wX`qq;7&1ZalB-R@?KCv6POaE3GD6 z=t~xm$sMFQ-61De=l`p~L2y98lVlx)yzQ?l+uey&aaUZOg_mV{u}0H8T=@1;#W5eO z+kG1a>6sdsz6EpTmXAn7b(qlcW4*tBSA~T3q;kOvP6mo2s+PQ*Gu6d~XTSzKvwvaT z&MfML^$A<3_Zeaf!9C)R5DWE8}M7;_97Do@=`#ri1aII*QoKgogqk3%w| z+PX~3l&wmG?qEW<@Q?rMHuV?lCLSK(dN$fE1@^e$>;!=!nkwE|E70qK>lzXsyJ_4Y zDu0*K)_b)4@T2GXmGH!7y_2L96`nZI`_Ao%I~=9kUR;ABzv#uF1<9jF-32rx`n#JE zR~M?)Ma?XNcQQeXv@?~_47NVcJZpZ_0qUf524&VetvmHEPnyMgw|U>zaMJ1lA+(lc zki7UE6&AZojFf=H$(m#}FLFbSEUjkUYR-Z+Lj%Kn&EE4fLOUHo?AzyFBaw(W&NEy{ zKC-QrAZfc-$6424n``Dys8>A}M7srK{^pY16Fx}`y3A8Bbsi`yG`YA4$GKW+q;|9r zccPNIg$;>Qtrhaeh{X4XvOYnTc$g4?9P)zd6SeAN!1PGPkhr%APh|6Tx=h5El?- zwdM3 zkddIjPaRFS^`oi(eGU7ZFrR%_TyZC?dory<&)ym-?yKcQ0tNDVSKF-o+E|+9*velN zc+&#xn4zqt^gtw{oUkE<9UC!53O&xQX171#{Vhalaxx`6ZgCHg#2QaF6bDX=CTOmr zID`5yp;C_3xFe3Px2K!qo1SAjTArXKo)Uu;=gy__aG!6?fa|Ss^KTYWX~Z^X8ex;a zqtz6Stug-m*b%+(7uw^ycxGz|J4?iZ5jg+@mhEU-{f6$V@@G}RNiz+jeCN)oao`9t zeIfCBN)ck!Y|PA72KN|T)Skh&?K@xq&#y= zRS0NSsQFBC*@vx-HnpTxS_tNd5Dc~S)>gfYZ{cpkkw8gf0P`KyQmb^dY*WMSlbLoO z+^(zbu&H)dr`fYL){P^a^3@4<5a`wu8zVC~cclavfF%{^i zau2ceP{p_eQEIneAptln-tl?&QkuNGNvyCbLZ~7S9~*)d91JJL>Z^a8D{_)I6kvkP zRA#yM0308FoTR=T6!y8v@F5!%a||3V|3O9au=3 zH_Okj5pA=T!3Gi@=52ChS-@=%^JsC$MuoqD2BbGO-FEIc6lu{ zHmT5OyP;=h=5x@k6_DWB+{C8w`g^<^ms_s(+b|-{YXm&^!!*1c&YIgOdbNt2VkuEf z*5fpmRgbCa`s-Q}KU~2i?L-7rIdg#RN&G4hS}`7$wINNfa8ik|FE)r1PimnR?}>3w3CbS zLA8(NySkB`W-J3+?eq;ZW*&ho<9jL)bXP%MO5%02s-RfYIafe`Z-H_IfaL4e?T9L| zh=#3^&z+A*7%`u(r?@`jK>QV+gjNrBDQ!P|+zB?=AW9#UXVEkfqFQl}hxlt#j?c-F?>ccp?PSE{s#n~2j_?}_<|3Ba27drp2a=!#Kv-=!8H zZD`3-pIg^u99yM_GIaL^;TL=n0;;<(V$p`{db)>qH_cwRTQe42!5Z98KI#BU$ zcJA)o-?3%qlTA-i2N^i(E<6W&N+{8fLx`QNch$jbW)(a4acSdC6=y!CId=OZX6#cV zpyYr-f!`JU3=AReJJDH6?#;Q4Tg1T+LS+O*M21YkQGvmrHnqvR*#zfPT2r4)=8?+~ z$V|b<*ZkzjWH|00J0}c3akrvckEZQGwmLP{sn($%5&N54B1vUg=7EJRHK^K5U)xD( z+^32{`Lo^w>MvT}tbV$)9eYZI`Puo;5mXwTd2f&nSP;l&|elBn~@fn|El zYpbl~!drI%L4Ez(Bi3HeUAr-JZJ*(AxT)R*mXh%%&i9n2{s}wN#`Xs<&c-mH$8%!^ zLk~tt!YcyogZcf!fQO=>4{&>R21B`F~AvZ@|ZB5ZLd8cn=AjT!K2LS@G z<->2>e+s?Bg#Nily|{^ffMem#&Mgp=2EtD(M`F-zQcB}u01fA)-F_FZ>!F$Inyi3H z+`^X*J$^bD#q1O3WyfV|1A>cz3Z&p4tMjeGT(#i(%<@)D5HJzlz3+QUXv6#g3{|yN z+}C~l$fVM0B@)60%EVaGgseGFTsRaFR>Fda7wy+Zr`7D{*y>hk(eZiukEo?iz$R*e z-1=_v|P#d2g&T+C?P#Os0i$IPU`)PlnU5lB&Wpn=k`8^%^De^2nsvD@kk@^q} zPXe)PE_-4W2Ge)Cw+X_i_{J<2>!)HI|7vP(gYZ@j*y389JrSdKekAvR7}xM{vZsAI zrcUi_n%m3SK<^`)OoiTWg*^l%oQMcO19(aOY__MyTaB8}&?1vShXWFi@nYKQW z)wItXYh`SYQJo3|YopRLw1RK?fErK zC|v}2KANf5bJX)18?4?W5WC4}dBTIMs3+J0u9(oDf<7;KmJ$|<^?4EuQ@~je8Pa+k zTgo03S^?brssDtt*F{4@P;LJ-2|Y1Nj+waMM|nLakeeGV=ntn@o>IY6ni- z+ZEuMzJU@L5UqUVUzDQTm8`TcLk-U@*e@B`7OnEnaunqAIW?x(SPtVC2rxU5#=Q#z z+Cd>|{MOkEX;=6PV4JIBn=}RTao*3a8h)%V#i}OySIL|xxY-5x!t^5NO!E@@90sZ~pJNKTlVqh+4>Nl)i(2#;r zLhnJ$RzUhpDmM`50#U9E1wh(TM?o;Xmx|-M)Rxy-b-# zjQ*fQ)@lXK-SW?$WB_PE{HVi8YkS^bKN3WTl!yjEFpP4Q)bmu3=#P*>KOKmTQBr?- zcplb3f|MWzGa18g{cI&y(%zcRtitlZlHLPB@+RBKbO7(wh#+7O?@!ok7yYgLluoqa zAhN7CI*oM4#_FADtO3rWHF3AyA(0QvB|#E`PwCFnk9O)Ag$gazBfunb86P<&$j&I- zPr(H9IKBzvFOnIBDF3JAffiC`PoZ z#kPphkGk2Pre`~OH_oyM${qtYvYJgtygn2!rZe9a21`p6!`cz`OBqw|Rh_~(fzJ1? z=kwauJQ16_mOuqBymHfxS;r{|ZHTQY&H1CJ-pwV=m4eHi$Z$z>Y2H}@nch~UfC!SP zMq+wc;#WuiDSrTS8dy+nSrT7%pz>YjTTr@&Xm$N%)%nl+^w(_klyKkkLg?~-&2;qh z!-UM#I2od+S%7F(fE-+}9;rJ8;b%fnBG*i=zNbQ?tKudLKDe&rpfI6* zByZnB1dN3MJ(k0dnz=ZKBoMeWl+N0-bzG~cEmVRD_ZLP!_hexIt$A%f zP;rz7Dcj@R#~07W6wTi+7OuJg7LCu!b8KAQk|aek5>R5HA-uMl_wpV92*lX3jVuCW z)~lRfd<1nA!$oo;=V7|&zS6%V2mJ~5_Ac~(a)Bz8(=c7ESnLz@=v4JID~-==Z!~oQ z0XdVe58KvobIiq{9Vm3y{)5&;gmFG57NGsX&ExN(u1ldu1zWM7K*mI3lE&4EhQvjJ z6B|;ENa_FJ`o?jd0|35n8yVBrpF~FNcwH6Trndg6dH{`Z2no{qHjVuJWJLe60hgI? z^H5f^&xJJsphZ+Y?DS0S54jx8hM)!aw!gq7dA7R?1Jj~^*br@BCM?jUAAJ*-cBU#I z#kZFp$pmQF?=(JIx|0@!x}+fnxiDWJW#D4JDg)A6?ZQ|2XJS^4q_$bqASlAaqBo|= zIS^eXg%XIYSzKG~#RaV3w28A>6q(usxRL{x=J8v<{Mt7Pkn_vRvsGZqgc!})yk8>$ zDM$nvuxx?$Zt~iLGKFXai?a!{gGp$XJr_vj5v3pHnYXKtX96xl&TIKfQwYul?i)af zty5p=u1&jp6NZbTaqSyCm{cuwXb9!didV#lEtA<8jQW5IZynO-y1F+bDAO<@so|Y_ zmR|A&*S$1MwValnIr@H*3su2@GEI-8=d;=!uHj`d`YNX4s&XHX0E{b>ZZ58CtNS1X zbuBxJA1es>R?D(nh$5)_upOMB%S(c08jSD8$K4$LW%st|CId!b-2D4ctzI0a*Sx9t zP%~U};S;Sd44!)2w>e;qmX>PcPb>&F=3=E`(Sq}3o9ZQ~+X6(qOOd&5kv`^1vv1Y> zsVg>+T?Cl?WqTJXUE@w;>+Zz}wlX&EcYr3IncS;?O=Cy*K`bbzW4t?`ZT>P;2DD{1 z`J}J&C*DjaArRcxu2~bpN-|<{kT{ZhIUO_cRbq(tvCIU) z0~M2Cv`7ux*RV*WZ3qF;(WJLu>QFm*wAZWwFsiBz?`eU^otp~SC&ukLL(ldZx@+jb zZ@Mi3wqDGT6G&n}#C$^*V;2Fe*Hu;^JFVFi$rW`M03`CWg!yEP@+@6AD=;riXL_9* zg=v?Pv^h+$NRZDhJ;nuydfm)m>(wE!33i}mP^tFF1sJSwZ=7R6>SGn1^9MM)raz)JadfcB!SjkoG)@d(oQvj?xm~EQ&I026BH$R3`2;FWN1gO&%{q*mdj7hQ zqGy3UL%sQ4r9po^&V?_tS(CNR8wWKG``N;TObgpy@_ntYsi&dF~*PS5_$ zAc#rqatd3Tn>@4=1Bpz=_h*ZD!h-Cul9X(H5=@i_ZJa(Wbt?V{U zU@-FWi*`7&_}`2x7Ld}&Vf#ZH&Aj`bw}4rk4(JWIq&ke@2b$DHv8{l2VIoBgy-&pS zUTcwc{7$Rc4=Q%T>fU%P6s=@MTz{Bt-E-3?M z9e?@&O)#)?rr+=sxOQl;U~&tt;d=X@6rmaf%U8OMLTm%%|4F{|aGWlya1@mTqpey&C>{msU6c4@)0=%zWH&)T3>%7< v^l4b@@LYLg3V5m%Po KotlinProject + diff --git a/web/webpack.config.d/sqljs-config.js b/web/webpack.config.d/sqljs-config.js new file mode 100644 index 00000000..27e581eb --- /dev/null +++ b/web/webpack.config.d/sqljs-config.js @@ -0,0 +1,16 @@ +config.resolve = { + fallback: { + fs: false, + path: false, + crypto: false, + } +}; + +const CopyWebpackPlugin = require('copy-webpack-plugin'); +config.plugins.push( + new CopyWebpackPlugin({ + patterns: [ + '../../node_modules/sql.js/dist/sql-wasm.wasm' + ] + }) +); \ No newline at end of file From 44c15f1f6e100775fa726a94f0eaa9219bce06b8 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 12 Feb 2026 11:14:11 -0500 Subject: [PATCH 19/56] Revert "Moving ViewModels to factory" This reverts commit 0c78ac88597524192c91604c14ffcd400d921916. --- kotlin-js-store/yarn.lock | 15 --- .../co.touchlab.droidcon/ui/UiModule.kt | 115 ++++++++---------- .../viewmodel/ApplicationViewModel.kt | 48 ++++++-- .../viewmodel/WaitForLoadedContextModel.kt | 3 +- .../viewmodel/settings/SettingsViewModel.kt | 10 +- 5 files changed, 103 insertions(+), 88 deletions(-) diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 1f91e1a2..f8802357 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2,11 +2,6 @@ # yarn lockfile v1 -"@cashapp/sqldelight-sqljs-worker@2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@cashapp/sqldelight-sqljs-worker/-/sqldelight-sqljs-worker-2.2.1.tgz#c71776a9dddfc435d4f1e64317a7039d447ea024" - integrity sha512-cj/llgS1T94t7rz63fI7pbi+jJx+vQofCT58KyMZb9XVRuoxb4taB5wbbBa4e/iljiuN5XIGGPFx+5PvtVh3LQ== - "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -2113,11 +2108,6 @@ parseurl@~1.3.2, parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -path-browserify@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -2581,11 +2571,6 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" -sql.js@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-1.8.0.tgz#cb45d957e17a2239662fe2f614c9b678990867a6" - integrity sha512-3HD8pSkZL+5YvYUI8nlvNILs61ALqq34xgmF+BHpqxe68yZIJ1H+sIVIODvni25+CcxHUxDyrTJUL0lE/m7afw== - statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt index b5988997..734612ed 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt @@ -22,51 +22,34 @@ import org.koin.core.parameter.parametersOf import org.koin.dsl.module val uiModule = module { - single { SessionDetailScrollStateStorage() } - - // Factories for parameterized ViewModels (used by the ViewModels below) - single { SessionBlockViewModel.Factory(sessionListItemFactory = get(), dateFormatter = get()) } + // MARK: View model factories. single { - SessionDayViewModel.Factory( - sessionBlockFactory = get(), - dateFormatter = get(), - dateTimeService = get(), - conferenceConfigProvider = get(), - sessionDetailScrollStateStorage = get(), - ) - } - single { SessionListItemViewModel.Factory(dateTimeService = get()) } - single { - SessionDetailViewModel.Factory( - sessionGateway = get(), - speakerListItemFactory = get(), - speakerDetailFactory = get(), - dateFormatter = get(), - dateTimeService = get(), - parseUrlViewService = get(), - settingsGateway = get(), + ApplicationViewModel.Factory( + scheduleFactory = get(), + agendaFactory = get(), + sponsorsFactory = get(), + settingsFactory = get(), feedbackDialogFactory = get(), - feedbackService = get(), - conferenceConfigProvider = get(), + syncService = get(), + notificationSchedulingService = get(), notificationService = get(), + feedbackService = get(), + settingsGateway = get(), + conferenceRepository = get(), ) } - single { SpeakerListItemViewModel.Factory() } - single { SpeakerDetailViewModel.Factory(parseUrlViewService = get()) } - single { SponsorGroupViewModel.Factory(sponsorGroupItemFactory = get()) } - single { SponsorGroupItemViewModel.Factory() } + single { - SponsorDetailViewModel.Factory( - sponsorGateway = get(), - speakerListItemFactory = get(), - speakerDetailFactory = get(), + WaitForLoadedContextModel( + conferenceConfigProvider = get(), + applicationViewModelFactory = get(), + syncService = get(), + settingsGateway = get(), ) } - single { FeedbackDialogViewModel.Factory(sessionGateway = get(), get(parameters = { parametersOf("FeedbackDialogViewModel") })) } - // ViewModels provided by Koin single { - ScheduleViewModel( + ScheduleViewModel.Factory( sessionGateway = get(), sessionDayFactory = get(), sessionDetailFactory = get(), @@ -76,7 +59,7 @@ val uiModule = module { ) } single { - AgendaViewModel( + AgendaViewModel.Factory( sessionGateway = get(), sessionDayFactory = get(), sessionDetailFactory = get(), @@ -85,42 +68,46 @@ val uiModule = module { conferenceConfigProvider = get(), ) } + single { SessionBlockViewModel.Factory(sessionListItemFactory = get(), dateFormatter = get()) } single { - SponsorListViewModel( - sponsorGateway = get(), - sponsorGroupFactory = get(), - sponsorDetailFactory = get(), + SessionDayViewModel.Factory( + sessionBlockFactory = get(), + dateFormatter = get(), + dateTimeService = get(), + conferenceConfigProvider = get(), + sessionDetailScrollStateStorage = get(), ) } - single { AboutViewModel(aboutRepository = get(), parseUrlViewService = get()) } + single { SessionListItemViewModel.Factory(dateTimeService = get()) } + single { - SettingsViewModel( + SessionDetailViewModel.Factory( + sessionGateway = get(), + speakerListItemFactory = get(), + speakerDetailFactory = get(), + dateFormatter = get(), + dateTimeService = get(), + parseUrlViewService = get(), settingsGateway = get(), - about = get(), - conferenceRepository = get(), - ) - } - single { - ApplicationViewModel( - schedule = get(), - agenda = get(), - sponsors = get(), - settings = get(), feedbackDialogFactory = get(), - syncService = get(), - notificationSchedulingService = get(), - notificationService = get(), feedbackService = get(), - settingsGateway = get(), - conferenceRepository = get(), - ) - } - single { - WaitForLoadedContextModel( conferenceConfigProvider = get(), - applicationViewModel = get(), - syncService = get(), - settingsGateway = get(), + notificationService = get(), ) } + single { SpeakerListItemViewModel.Factory() } + + single { SpeakerDetailViewModel.Factory(parseUrlViewService = get()) } + + single { SponsorListViewModel.Factory(sponsorGateway = get(), sponsorGroupFactory = get(), sponsorDetailFactory = get()) } + single { SponsorGroupViewModel.Factory(sponsorGroupItemFactory = get()) } + single { SponsorGroupItemViewModel.Factory() } + single { SponsorDetailViewModel.Factory(sponsorGateway = get(), speakerListItemFactory = get(), speakerDetailFactory = get()) } + + single { SettingsViewModel.Factory(settingsGateway = get(), aboutFactory = get(), conferenceRepository = get()) } + single { AboutViewModel.Factory(aboutRepository = get(), parseUrlViewService = get()) } + + single { FeedbackDialogViewModel.Factory(sessionGateway = get(), get(parameters = { parametersOf("FeedbackDialogViewModel") })) } + + single { SessionDetailScrollStateStorage() } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt index 3fe4b1fd..9ea81320 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt @@ -20,10 +20,10 @@ import org.brightify.hyperdrive.multiplatformx.property.MutableObservablePropert import org.brightify.hyperdrive.multiplatformx.property.ObservableProperty class ApplicationViewModel( - val schedule: ScheduleViewModel, - val agenda: AgendaViewModel, - val sponsors: SponsorListViewModel, - val settings: SettingsViewModel, + scheduleFactory: ScheduleViewModel.Factory, + agendaFactory: AgendaViewModel.Factory, + sponsorsFactory: SponsorListViewModel.Factory, + settingsFactory: SettingsViewModel.Factory, private val feedbackDialogFactory: FeedbackDialogViewModel.Factory, private val syncService: SyncService, private val notificationSchedulingService: NotificationSchedulingService, @@ -34,11 +34,45 @@ class ApplicationViewModel( ) : BaseViewModel(), DeepLinkNotificationHandler { + class Factory( + private val scheduleFactory: ScheduleViewModel.Factory, + private val agendaFactory: AgendaViewModel.Factory, + private val sponsorsFactory: SponsorListViewModel.Factory, + private val settingsFactory: SettingsViewModel.Factory, + private val feedbackDialogFactory: FeedbackDialogViewModel.Factory, + private val syncService: SyncService, + private val notificationSchedulingService: NotificationSchedulingService, + private val notificationService: NotificationService, + private val feedbackService: FeedbackService, + private val settingsGateway: SettingsGateway, + private val conferenceRepository: ConferenceRepository, + ) { + + fun create(): ApplicationViewModel { + val applicationViewModel = ApplicationViewModel( + scheduleFactory = scheduleFactory, + agendaFactory = agendaFactory, + sponsorsFactory = sponsorsFactory, + settingsFactory = settingsFactory, + feedbackDialogFactory = feedbackDialogFactory, + syncService = syncService, + notificationSchedulingService = notificationSchedulingService, + notificationService = notificationService, + feedbackService = feedbackService, + settingsGateway = settingsGateway, + conferenceRepository = conferenceRepository, + ) + notificationService.setHandler(applicationViewModel) + return applicationViewModel + } + } + private val log = Logger.withTag("ApplicationViewModel") - init { - notificationService.setHandler(this) - } + val schedule by managed(scheduleFactory.create()) + val agenda by managed(agendaFactory.create()) + val sponsors by managed(sponsorsFactory.create()) + val settings by managed(settingsFactory.create()) var presentedFeedback: FeedbackDialogViewModel? by managed(null) val observePresentedFeedback by observe(::presentedFeedback) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt index fd92a2de..75913d56 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.withContext import org.brightify.hyperdrive.multiplatformx.BaseViewModel class WaitForLoadedContextModel( private val conferenceConfigProvider: ConferenceConfigProvider, - val applicationViewModel: ApplicationViewModel, + applicationViewModelFactory: ApplicationViewModel.Factory, private val syncService: SyncService, private val settingsGateway: SettingsGateway, ) : BaseViewModel() { @@ -25,6 +25,7 @@ class WaitForLoadedContextModel( private val _state = MutableStateFlow(State.Loading) val state: StateFlow = _state + val applicationViewModel by managed(applicationViewModelFactory.create()) private val log = Logger.withTag("WaitForLoadedContextModel") diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt index d5f663a6..2a4d1c59 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt @@ -10,7 +10,7 @@ import org.brightify.hyperdrive.multiplatformx.property.ObservableProperty class SettingsViewModel( settingsGateway: SettingsGateway, - val about: AboutViewModel, + private val aboutFactory: AboutViewModel.Factory, private val conferenceRepository: ConferenceRepository, ) : BaseViewModel() { private val log = Logger.withTag("SettingsViewModel") @@ -47,6 +47,7 @@ class SettingsViewModel( private val _selectedConference = MutableObservableProperty(null) val selectedConference: ObservableProperty = _selectedConference + val about by managed(aboutFactory.create()) override suspend fun whileAttached() { // Load conferences @@ -80,4 +81,11 @@ class SettingsViewModel( } } + class Factory( + private val settingsGateway: SettingsGateway, + private val aboutFactory: AboutViewModel.Factory, + private val conferenceRepository: ConferenceRepository, + ) { + fun create() = SettingsViewModel(settingsGateway, aboutFactory, conferenceRepository) + } } From 4e89e9bdfe87d7e962604240b12c6097bfacac2e Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 12 Feb 2026 11:19:08 -0500 Subject: [PATCH 20/56] Adding changes for --- web/webpack.config.d/sqljs-config.js | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/web/webpack.config.d/sqljs-config.js b/web/webpack.config.d/sqljs-config.js index 27e581eb..7fe5c973 100644 --- a/web/webpack.config.d/sqljs-config.js +++ b/web/webpack.config.d/sqljs-config.js @@ -1,16 +1,16 @@ -config.resolve = { - fallback: { - fs: false, - path: false, - crypto: false, - } -}; +config.resolve = config.resolve || {}; +config.resolve.fallback = Object.assign(config.resolve.fallback || {}, { + fs: false, + path: false, + crypto: false, + os: false, +}); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -config.plugins.push( - new CopyWebpackPlugin({ - patterns: [ - '../../node_modules/sql.js/dist/sql-wasm.wasm' - ] - }) -); \ No newline at end of file +// Serve sql.js WASM from node_modules (avoids needing copy-webpack-plugin) +const path = require('path'); +if (config.devServer && config.devServer.static) { + config.devServer.static.push({ + directory: path.resolve(__dirname, '../../node_modules/sql.js/dist'), + watch: false + }); +} \ No newline at end of file From dc5d18708b1d2ef726597d86a8d3772901182510 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 12 Feb 2026 11:35:19 -0500 Subject: [PATCH 21/56] fixing issues --- .../impl/SqlDelightDriverFactory.android.kt | 10 +++++++- .../gateway/impl/DefaultSessionGateway.kt | 25 ++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt index 143c4574..b7047457 100644 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt +++ b/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt @@ -1,10 +1,18 @@ package co.touchlab.droidcon.domain.repository.impl import android.content.Context +import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.db.SqlSchema import app.cash.sqldelight.driver.android.AndroidSqliteDriver import co.touchlab.droidcon.db.DroidconDatabase actual class SqlDelightDriverFactory(private val context: Context) { - actual fun createDriver(): SqlDriver = AndroidSqliteDriver(DroidconDatabase.Schema, context, "droidcon.db") + + @Suppress("UNCHECKED_CAST") + actual fun createDriver(): SqlDriver = AndroidSqliteDriver( + schema = DroidconDatabase.Schema as SqlSchema>, + context = context, + name = "droidcon.db", + ) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt index ca78baab..2b5d3bab 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt @@ -55,15 +55,22 @@ class DefaultSessionGateway( } } - private suspend fun scheduleItemForSession(session: Session): ScheduleItem { - val confId = conferenceId ?: throw IllegalStateException("Conference ID is not available") - return ScheduleItem( - session, - scheduleService.isInConflict(session), - session.room?.let { roomRepository.find(it, confId) }, - profileRepository.getSpeakersBySession(session.id, confId), - ) - } + private suspend fun scheduleItemForSession(session: Session): ScheduleItem = + if (conferenceId != null) + ScheduleItem( + session, + scheduleService.isInConflict(session), + session.room?.let { roomRepository.find(it, conferenceId!!) }, + profileRepository.getSpeakersBySession(session.id, conferenceId!!), + ) + + else + ScheduleItem( + session, + scheduleService.isInConflict(session), + null, + emptyList(), + ) override suspend fun setAttending(session: Session, attending: Boolean) { val confId = conferenceId ?: throw IllegalStateException("Conference ID is not available") From 615d65d18ea38aaf113047c0cc0cc0f2b6545726 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 12 Feb 2026 14:18:22 -0500 Subject: [PATCH 22/56] Moving factories into another file --- .../co.touchlab.droidcon/ui/UiModule.kt | 49 ++-- .../viewmodel/ApplicationViewModel.kt | 43 +--- .../viewmodel/FeedbackDialogViewModel.kt | 10 - .../viewmodel/ViewModelFactory.kt | 236 ++++++++++++++++++ .../viewmodel/session/AgendaViewModel.kt | 23 +- .../session/BaseSessionListViewModel.kt | 6 +- .../viewmodel/session/ScheduleViewModel.kt | 24 +- .../session/SessionBlockViewModel.kt | 8 +- .../viewmodel/session/SessionDayViewModel.kt | 25 +- .../session/SessionDetailViewModel.kt | 37 +-- .../session/SessionListItemViewModel.kt | 4 - .../session/SpeakerDetailViewModel.kt | 5 - .../session/SpeakerListItemViewModel.kt | 4 - .../viewmodel/settings/AboutViewModel.kt | 5 - .../viewmodel/settings/SettingsViewModel.kt | 11 +- .../sponsor/SponsorDetailViewModel.kt | 15 +- .../sponsor/SponsorGroupItemViewModel.kt | 5 - .../sponsor/SponsorGroupViewModel.kt | 8 +- .../viewmodel/sponsor/SponsorListViewModel.kt | 14 +- 19 files changed, 286 insertions(+), 246 deletions(-) create mode 100644 shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ViewModelFactory.kt diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt index 734612ed..ead950e9 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt @@ -1,30 +1,15 @@ package co.touchlab.droidcon.ui -import co.touchlab.droidcon.viewmodel.ApplicationViewModel -import co.touchlab.droidcon.viewmodel.FeedbackDialogViewModel +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel -import co.touchlab.droidcon.viewmodel.session.AgendaViewModel -import co.touchlab.droidcon.viewmodel.session.ScheduleViewModel -import co.touchlab.droidcon.viewmodel.session.SessionBlockViewModel -import co.touchlab.droidcon.viewmodel.session.SessionDayViewModel import co.touchlab.droidcon.viewmodel.session.SessionDetailScrollStateStorage -import co.touchlab.droidcon.viewmodel.session.SessionDetailViewModel -import co.touchlab.droidcon.viewmodel.session.SessionListItemViewModel -import co.touchlab.droidcon.viewmodel.session.SpeakerDetailViewModel -import co.touchlab.droidcon.viewmodel.session.SpeakerListItemViewModel -import co.touchlab.droidcon.viewmodel.settings.AboutViewModel -import co.touchlab.droidcon.viewmodel.settings.SettingsViewModel -import co.touchlab.droidcon.viewmodel.sponsor.SponsorDetailViewModel -import co.touchlab.droidcon.viewmodel.sponsor.SponsorGroupItemViewModel -import co.touchlab.droidcon.viewmodel.sponsor.SponsorGroupViewModel -import co.touchlab.droidcon.viewmodel.sponsor.SponsorListViewModel import org.koin.core.parameter.parametersOf import org.koin.dsl.module val uiModule = module { // MARK: View model factories. single { - ApplicationViewModel.Factory( + ViewModelFactory.ApplicationViewModelFactory( scheduleFactory = get(), agendaFactory = get(), sponsorsFactory = get(), @@ -49,7 +34,7 @@ val uiModule = module { } single { - ScheduleViewModel.Factory( + ViewModelFactory.ScheduleViewModelFactory( sessionGateway = get(), sessionDayFactory = get(), sessionDetailFactory = get(), @@ -59,7 +44,7 @@ val uiModule = module { ) } single { - AgendaViewModel.Factory( + ViewModelFactory.AgendaViewModelFactory( sessionGateway = get(), sessionDayFactory = get(), sessionDetailFactory = get(), @@ -68,9 +53,9 @@ val uiModule = module { conferenceConfigProvider = get(), ) } - single { SessionBlockViewModel.Factory(sessionListItemFactory = get(), dateFormatter = get()) } + single { ViewModelFactory.SessionBlockViewModelFactory(sessionListItemFactory = get(), dateFormatter = get()) } single { - SessionDayViewModel.Factory( + ViewModelFactory.SessionDayViewModelFactory( sessionBlockFactory = get(), dateFormatter = get(), dateTimeService = get(), @@ -78,10 +63,10 @@ val uiModule = module { sessionDetailScrollStateStorage = get(), ) } - single { SessionListItemViewModel.Factory(dateTimeService = get()) } + single { ViewModelFactory.SessionListItemViewModelFactory(dateTimeService = get()) } single { - SessionDetailViewModel.Factory( + ViewModelFactory.SessionDetailViewModelFactory( sessionGateway = get(), speakerListItemFactory = get(), speakerDetailFactory = get(), @@ -95,19 +80,19 @@ val uiModule = module { notificationService = get(), ) } - single { SpeakerListItemViewModel.Factory() } + single { ViewModelFactory.SpeakerListItemViewModelFactory() } - single { SpeakerDetailViewModel.Factory(parseUrlViewService = get()) } + single { ViewModelFactory.SpeakerDetailViewModelFactory(parseUrlViewService = get()) } - single { SponsorListViewModel.Factory(sponsorGateway = get(), sponsorGroupFactory = get(), sponsorDetailFactory = get()) } - single { SponsorGroupViewModel.Factory(sponsorGroupItemFactory = get()) } - single { SponsorGroupItemViewModel.Factory() } - single { SponsorDetailViewModel.Factory(sponsorGateway = get(), speakerListItemFactory = get(), speakerDetailFactory = get()) } + single { ViewModelFactory.SponsorListViewModelFactory(sponsorGateway = get(), sponsorGroupFactory = get(), sponsorDetailFactory = get()) } + single { ViewModelFactory.SponsorGroupViewModelFactory(sponsorGroupItemFactory = get()) } + single { ViewModelFactory.SponsorGroupItemViewModelFactory() } + single { ViewModelFactory.SponsorDetailViewModelFactory(sponsorGateway = get(), speakerListItemFactory = get(), speakerDetailFactory = get()) } - single { SettingsViewModel.Factory(settingsGateway = get(), aboutFactory = get(), conferenceRepository = get()) } - single { AboutViewModel.Factory(aboutRepository = get(), parseUrlViewService = get()) } + single { ViewModelFactory.SettingsViewModelFactory(settingsGateway = get(), aboutFactory = get(), conferenceRepository = get()) } + single { ViewModelFactory.AboutViewModelFactory(aboutRepository = get(), parseUrlViewService = get()) } - single { FeedbackDialogViewModel.Factory(sessionGateway = get(), get(parameters = { parametersOf("FeedbackDialogViewModel") })) } + single { ViewModelFactory.FeedbackDialogViewModelFactory(sessionGateway = get(), get(parameters = { parametersOf("FeedbackDialogViewModel") })) } single { SessionDetailScrollStateStorage() } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt index 9ea81320..3dc39645 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt @@ -20,11 +20,11 @@ import org.brightify.hyperdrive.multiplatformx.property.MutableObservablePropert import org.brightify.hyperdrive.multiplatformx.property.ObservableProperty class ApplicationViewModel( - scheduleFactory: ScheduleViewModel.Factory, - agendaFactory: AgendaViewModel.Factory, - sponsorsFactory: SponsorListViewModel.Factory, - settingsFactory: SettingsViewModel.Factory, - private val feedbackDialogFactory: FeedbackDialogViewModel.Factory, + scheduleFactory: ViewModelFactory.ScheduleViewModelFactory, + agendaFactory: ViewModelFactory.AgendaViewModelFactory, + sponsorsFactory: ViewModelFactory.SponsorListViewModelFactory, + settingsFactory: ViewModelFactory.SettingsViewModelFactory, + private val feedbackDialogFactory: ViewModelFactory.FeedbackDialogViewModelFactory, private val syncService: SyncService, private val notificationSchedulingService: NotificationSchedulingService, private val notificationService: NotificationService, @@ -34,39 +34,6 @@ class ApplicationViewModel( ) : BaseViewModel(), DeepLinkNotificationHandler { - class Factory( - private val scheduleFactory: ScheduleViewModel.Factory, - private val agendaFactory: AgendaViewModel.Factory, - private val sponsorsFactory: SponsorListViewModel.Factory, - private val settingsFactory: SettingsViewModel.Factory, - private val feedbackDialogFactory: FeedbackDialogViewModel.Factory, - private val syncService: SyncService, - private val notificationSchedulingService: NotificationSchedulingService, - private val notificationService: NotificationService, - private val feedbackService: FeedbackService, - private val settingsGateway: SettingsGateway, - private val conferenceRepository: ConferenceRepository, - ) { - - fun create(): ApplicationViewModel { - val applicationViewModel = ApplicationViewModel( - scheduleFactory = scheduleFactory, - agendaFactory = agendaFactory, - sponsorsFactory = sponsorsFactory, - settingsFactory = settingsFactory, - feedbackDialogFactory = feedbackDialogFactory, - syncService = syncService, - notificationSchedulingService = notificationSchedulingService, - notificationService = notificationService, - feedbackService = feedbackService, - settingsGateway = settingsGateway, - conferenceRepository = conferenceRepository, - ) - notificationService.setHandler(applicationViewModel) - return applicationViewModel - } - } - private val log = Logger.withTag("ApplicationViewModel") val schedule by managed(scheduleFactory.create()) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/FeedbackDialogViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/FeedbackDialogViewModel.kt index 4254cc1d..f191fa7e 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/FeedbackDialogViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/FeedbackDialogViewModel.kt @@ -61,14 +61,4 @@ class FeedbackDialogViewModel( Satisfied -> Session.Feedback.Rating.SATISFIED } } - - class Factory(private val sessionGateway: SessionGateway, private val log: Logger) { - - fun create( - session: Session, - submit: suspend (Session.Feedback) -> Unit, - closeAndDisable: (suspend () -> Unit)?, - skip: suspend () -> Unit, - ) = FeedbackDialogViewModel(sessionGateway, session, log, submit, closeAndDisable, skip) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ViewModelFactory.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ViewModelFactory.kt new file mode 100644 index 00000000..7d56e42d --- /dev/null +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ViewModelFactory.kt @@ -0,0 +1,236 @@ +package co.touchlab.droidcon.viewmodel + +import co.touchlab.droidcon.domain.entity.Profile +import co.touchlab.droidcon.application.gateway.SettingsGateway +import co.touchlab.droidcon.application.repository.AboutRepository +import co.touchlab.droidcon.application.service.NotificationSchedulingService +import co.touchlab.droidcon.application.service.NotificationService +import co.touchlab.droidcon.domain.composite.ScheduleItem +import co.touchlab.droidcon.domain.composite.SponsorGroupWithSponsors +import co.touchlab.droidcon.domain.entity.Session +import co.touchlab.droidcon.domain.entity.Sponsor +import co.touchlab.droidcon.domain.gateway.SessionGateway +import co.touchlab.droidcon.domain.gateway.SponsorGateway +import co.touchlab.droidcon.domain.repository.ConferenceRepository +import co.touchlab.droidcon.domain.service.ConferenceConfigProvider +import co.touchlab.droidcon.domain.service.DateTimeService +import co.touchlab.droidcon.domain.service.FeedbackService +import co.touchlab.droidcon.domain.service.SyncService +import co.touchlab.droidcon.service.ParseUrlViewService +import co.touchlab.droidcon.viewmodel.session.AgendaViewModel +import co.touchlab.droidcon.viewmodel.session.ScheduleViewModel +import co.touchlab.droidcon.viewmodel.session.SessionBlockViewModel +import co.touchlab.droidcon.viewmodel.session.SessionDayViewModel +import co.touchlab.droidcon.viewmodel.session.SessionDetailScrollStateStorage +import co.touchlab.droidcon.viewmodel.session.SessionDetailViewModel +import co.touchlab.droidcon.viewmodel.session.SessionListItemViewModel +import co.touchlab.droidcon.viewmodel.session.SpeakerDetailViewModel +import co.touchlab.droidcon.viewmodel.session.SpeakerListItemViewModel +import co.touchlab.droidcon.viewmodel.settings.AboutViewModel +import co.touchlab.droidcon.viewmodel.settings.SettingsViewModel +import co.touchlab.droidcon.viewmodel.sponsor.SponsorDetailViewModel +import co.touchlab.droidcon.viewmodel.sponsor.SponsorGroupItemViewModel +import co.touchlab.droidcon.viewmodel.sponsor.SponsorGroupViewModel +import co.touchlab.droidcon.viewmodel.sponsor.SponsorListViewModel +import co.touchlab.kermit.Logger +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import co.touchlab.droidcon.util.formatter.DateFormatter +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport + +@OptIn(ExperimentalJsExport::class) +@JsExport +object ViewModelFactory { + + class AboutViewModelFactory( + private val aboutRepository: AboutRepository, + private val parseUrlViewService: ParseUrlViewService, + ) { + fun create() = AboutViewModel(aboutRepository, parseUrlViewService) + } + + class SessionListItemViewModelFactory(private val dateTimeService: DateTimeService) { + fun create(item: ScheduleItem, selected: () -> Unit) = + SessionListItemViewModel(dateTimeService, item, selected) + } + + class SpeakerListItemViewModelFactory { + fun create(profile: Profile, selected: () -> Unit) = + SpeakerListItemViewModel(profile, selected) + } + + class SpeakerDetailViewModelFactory(private val parseUrlViewService: ParseUrlViewService) { + fun create(profile: Profile) = SpeakerDetailViewModel(parseUrlViewService, profile) + } + + class SessionBlockViewModelFactory( + private val sessionListItemFactory: SessionListItemViewModelFactory, + private val dateFormatter: DateFormatter, + ) { + fun create(startsAt: LocalDateTime, items: List, onScheduleItemSelected: (ScheduleItem) -> Unit) = + SessionBlockViewModel(sessionListItemFactory, dateFormatter, startsAt, items, onScheduleItemSelected) + } + + class SessionDayViewModelFactory( + private val sessionBlockFactory: SessionBlockViewModelFactory, + private val dateFormatter: DateFormatter, + private val dateTimeService: DateTimeService, + private val conferenceConfigProvider: ConferenceConfigProvider, + private val sessionDetailScrollStateStorage: SessionDetailScrollStateStorage, + ) { + fun create(date: LocalDate, attendingOnly: Boolean, items: List, onScheduleItemSelected: (ScheduleItem) -> Unit) = + SessionDayViewModel( + sessionBlockFactory, + dateFormatter, + dateTimeService, + conferenceConfigProvider, + date, + attendingOnly, + sessionDetailScrollStateStorage, + items, + onScheduleItemSelected, + ) + } + + class FeedbackDialogViewModelFactory(private val sessionGateway: SessionGateway, private val log: Logger) { + fun create( + session: Session, + submit: suspend (Session.Feedback) -> Unit, + closeAndDisable: (suspend () -> Unit)?, + skip: suspend () -> Unit, + ) = FeedbackDialogViewModel(sessionGateway, session, log, submit, closeAndDisable, skip) + } + + class SessionDetailViewModelFactory( + private val sessionGateway: SessionGateway, + private val settingsGateway: SettingsGateway, + private val conferenceConfigProvider: ConferenceConfigProvider, + private val speakerListItemFactory: SpeakerListItemViewModelFactory, + private val speakerDetailFactory: SpeakerDetailViewModelFactory, + private val feedbackDialogFactory: FeedbackDialogViewModelFactory, + private val dateFormatter: DateFormatter, + private val dateTimeService: DateTimeService, + private val parseUrlViewService: ParseUrlViewService, + private val feedbackService: FeedbackService, + private val notificationService: NotificationService, + ) { + fun create(item: ScheduleItem) = SessionDetailViewModel( + sessionGateway = sessionGateway, + settingsGateway = settingsGateway, + conferenceConfigProvider = conferenceConfigProvider, + speakerListItemFactory = speakerListItemFactory, + speakerDetailFactory = speakerDetailFactory, + feedbackDialogFactory = feedbackDialogFactory, + dateFormatter = dateFormatter, + dateTimeService = dateTimeService, + parseUrlViewService = parseUrlViewService, + feedbackService = feedbackService, + notificationService = notificationService, + initialItem = item, + ) + } + + class ScheduleViewModelFactory( + private val sessionGateway: SessionGateway, + private val sessionDayFactory: SessionDayViewModelFactory, + private val sessionDetailFactory: SessionDetailViewModelFactory, + private val sessionDetailScrollStateStorage: SessionDetailScrollStateStorage, + private val dateTimeService: DateTimeService, + private val conferenceConfigProvider: ConferenceConfigProvider, + ) { + fun create() = ScheduleViewModel( + sessionGateway, + sessionDayFactory, + sessionDetailFactory, + sessionDetailScrollStateStorage, + dateTimeService, + conferenceConfigProvider, + ) + } + + class AgendaViewModelFactory( + private val sessionGateway: SessionGateway, + private val sessionDayFactory: SessionDayViewModelFactory, + private val sessionDetailFactory: SessionDetailViewModelFactory, + private val sessionDetailScrollStateStorage: SessionDetailScrollStateStorage, + private val dateTimeService: DateTimeService, + private val conferenceConfigProvider: ConferenceConfigProvider, + ) { + fun create() = AgendaViewModel( + sessionGateway, + sessionDayFactory, + sessionDetailFactory, + sessionDetailScrollStateStorage, + dateTimeService, + conferenceConfigProvider, + ) + } + + class SponsorGroupItemViewModelFactory { + fun create(sponsor: Sponsor, selected: () -> Unit) = + SponsorGroupItemViewModel(sponsor, selected) + } + + class SponsorGroupViewModelFactory(private val sponsorGroupItemFactory: SponsorGroupItemViewModelFactory) { + fun create(sponsorGroup: SponsorGroupWithSponsors, onSponsorSelected: (Sponsor) -> Unit) = + SponsorGroupViewModel(sponsorGroupItemFactory, sponsorGroup, onSponsorSelected) + } + + class SponsorDetailViewModelFactory( + private val sponsorGateway: SponsorGateway, + private val speakerListItemFactory: SpeakerListItemViewModelFactory, + private val speakerDetailFactory: SpeakerDetailViewModelFactory, + ) { + fun create(sponsor: Sponsor, groupName: String) = + SponsorDetailViewModel(sponsorGateway, speakerListItemFactory, speakerDetailFactory, sponsor, groupName) + } + + class SponsorListViewModelFactory( + private val sponsorGateway: SponsorGateway, + private val sponsorGroupFactory: SponsorGroupViewModelFactory, + private val sponsorDetailFactory: SponsorDetailViewModelFactory, + ) { + fun create() = SponsorListViewModel(sponsorGateway, sponsorGroupFactory, sponsorDetailFactory) + } + + class SettingsViewModelFactory( + private val settingsGateway: SettingsGateway, + private val aboutFactory: AboutViewModelFactory, + private val conferenceRepository: ConferenceRepository, + ) { + fun create() = SettingsViewModel(settingsGateway, aboutFactory, conferenceRepository) + } + + class ApplicationViewModelFactory( + private val scheduleFactory: ScheduleViewModelFactory, + private val agendaFactory: AgendaViewModelFactory, + private val sponsorsFactory: SponsorListViewModelFactory, + private val settingsFactory: SettingsViewModelFactory, + private val feedbackDialogFactory: FeedbackDialogViewModelFactory, + private val syncService: SyncService, + private val notificationSchedulingService: NotificationSchedulingService, + private val notificationService: NotificationService, + private val feedbackService: FeedbackService, + private val settingsGateway: SettingsGateway, + private val conferenceRepository: ConferenceRepository, + ) { + fun create(): ApplicationViewModel { + val applicationViewModel = ApplicationViewModel( + scheduleFactory = scheduleFactory, + agendaFactory = agendaFactory, + sponsorsFactory = sponsorsFactory, + settingsFactory = settingsFactory, + feedbackDialogFactory = feedbackDialogFactory, + syncService = syncService, + notificationSchedulingService = notificationSchedulingService, + notificationService = notificationService, + feedbackService = feedbackService, + settingsGateway = settingsGateway, + conferenceRepository = conferenceRepository, + ) + notificationService.setHandler(applicationViewModel) + return applicationViewModel + } + } +} diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/AgendaViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/AgendaViewModel.kt index 90350aba..8b942d7d 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/AgendaViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/AgendaViewModel.kt @@ -1,13 +1,14 @@ package co.touchlab.droidcon.viewmodel.session +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.gateway.SessionGateway import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.DateTimeService class AgendaViewModel( sessionGateway: SessionGateway, - sessionDayFactory: SessionDayViewModel.Factory, - sessionDetailFactory: SessionDetailViewModel.Factory, + sessionDayFactory: ViewModelFactory.SessionDayViewModelFactory, + sessionDetailFactory: ViewModelFactory.SessionDetailViewModelFactory, sessionDetailScrollStateStorage: SessionDetailScrollStateStorage, dateTimeService: DateTimeService, conferenceConfigProvider: ConferenceConfigProvider, @@ -20,22 +21,4 @@ class AgendaViewModel( conferenceConfigProvider, attendingOnly = true, ) { - class Factory( - private val sessionGateway: SessionGateway, - private val sessionDayFactory: SessionDayViewModel.Factory, - private val sessionDetailFactory: SessionDetailViewModel.Factory, - private val sessionDetailScrollStateStorage: SessionDetailScrollStateStorage, - private val dateTimeService: DateTimeService, - private val conferenceConfigProvider: ConferenceConfigProvider, - ) { - - fun create() = AgendaViewModel( - sessionGateway, - sessionDayFactory, - sessionDetailFactory, - sessionDetailScrollStateStorage, - dateTimeService, - conferenceConfigProvider, - ) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt index 157c6d93..fe4f63c2 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.viewmodel.session +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.gateway.SessionGateway import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.DateTimeService @@ -8,8 +9,8 @@ import org.brightify.hyperdrive.multiplatformx.BaseViewModel abstract class BaseSessionListViewModel( private val sessionGateway: SessionGateway, - private val sessionDayFactory: SessionDayViewModel.Factory, - private val sessionDetailFactory: SessionDetailViewModel.Factory, + private val sessionDayFactory: ViewModelFactory.SessionDayViewModelFactory, + private val sessionDetailFactory: ViewModelFactory.SessionDetailViewModelFactory, private val sessionDetailScrollStateStorage: SessionDetailScrollStateStorage, private val dateTimeService: DateTimeService, private val conferenceConfigProvider: ConferenceConfigProvider, @@ -37,6 +38,7 @@ abstract class BaseSessionListViewModel( sessionGateway.observeSchedule() } + print("Collecting Item Flows") itemsFlow .collect { items -> items diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/ScheduleViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/ScheduleViewModel.kt index 8cb96e64..551a6e8b 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/ScheduleViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/ScheduleViewModel.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.viewmodel.session +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.entity.Session import co.touchlab.droidcon.domain.gateway.SessionGateway import co.touchlab.droidcon.domain.service.ConferenceConfigProvider @@ -7,8 +8,8 @@ import co.touchlab.droidcon.domain.service.DateTimeService class ScheduleViewModel( private val sessionGateway: SessionGateway, - sessionDayFactory: SessionDayViewModel.Factory, - private val sessionDetailFactory: SessionDetailViewModel.Factory, + sessionDayFactory: ViewModelFactory.SessionDayViewModelFactory, + private val sessionDetailFactory: ViewModelFactory.SessionDetailViewModelFactory, sessionDetailScrollStateStorage: SessionDetailScrollStateStorage, dateTimeService: DateTimeService, conferenceConfigProvider: ConferenceConfigProvider, @@ -28,23 +29,4 @@ class ScheduleViewModel( presentedSessionDetail = sessionDetailFactory.create(sessionItem) } } - - class Factory( - private val sessionGateway: SessionGateway, - private val sessionDayFactory: SessionDayViewModel.Factory, - private val sessionDetailFactory: SessionDetailViewModel.Factory, - private val sessionDetailScrollStateStorage: SessionDetailScrollStateStorage, - private val dateTimeService: DateTimeService, - private val conferenceConfigProvider: ConferenceConfigProvider, - ) { - - fun create() = ScheduleViewModel( - sessionGateway, - sessionDayFactory, - sessionDetailFactory, - sessionDetailScrollStateStorage, - dateTimeService, - conferenceConfigProvider, - ) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionBlockViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionBlockViewModel.kt index 21ba307a..a39ad78f 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionBlockViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionBlockViewModel.kt @@ -1,12 +1,13 @@ package co.touchlab.droidcon.viewmodel.session +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.composite.ScheduleItem import co.touchlab.droidcon.util.formatter.DateFormatter import kotlinx.datetime.LocalDateTime import org.brightify.hyperdrive.multiplatformx.BaseViewModel class SessionBlockViewModel( - sessionListItemFactory: SessionListItemViewModel.Factory, + sessionListItemFactory: ViewModelFactory.SessionListItemViewModelFactory, dateFormatter: DateFormatter, startsAt: LocalDateTime, items: List, @@ -24,9 +25,4 @@ class SessionBlockViewModel( }, ) val observeSessions by observe(::sessions) - - class Factory(private val sessionListItemFactory: SessionListItemViewModel.Factory, private val dateFormatter: DateFormatter) { - fun create(startsAt: LocalDateTime, items: List, onScheduleItemSelected: (ScheduleItem) -> Unit) = - SessionBlockViewModel(sessionListItemFactory, dateFormatter, startsAt, items, onScheduleItemSelected) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt index f7b05eed..54f6573c 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.viewmodel.session +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.composite.ScheduleItem import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.DateTimeService @@ -10,7 +11,7 @@ import kotlinx.datetime.LocalDate import org.brightify.hyperdrive.multiplatformx.BaseViewModel class SessionDayViewModel( - sessionBlockFactory: SessionBlockViewModel.Factory, + sessionBlockFactory: ViewModelFactory.SessionBlockViewModelFactory, dateFormatter: DateFormatter, dateTimeService: DateTimeService, private val conferenceConfigProvider: ConferenceConfigProvider, @@ -43,26 +44,4 @@ class SessionDayViewModel( } class ScrollState(val firstVisibleItemIndex: Int, val firstVisibleItemScrollOffset: Int) - - class Factory( - private val sessionBlockFactory: SessionBlockViewModel.Factory, - private val dateFormatter: DateFormatter, - private val dateTimeService: DateTimeService, - private val conferenceConfigProvider: ConferenceConfigProvider, - private val sessionDetailScrollStateStorage: SessionDetailScrollStateStorage, - ) { - - fun create(date: LocalDate, attendingOnly: Boolean, items: List, onScheduleItemSelected: (ScheduleItem) -> Unit) = - SessionDayViewModel( - sessionBlockFactory, - dateFormatter, - dateTimeService, - conferenceConfigProvider, - date, - attendingOnly, - sessionDetailScrollStateStorage, - items, - onScheduleItemSelected, - ) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt index 97681885..dbf4be1e 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.viewmodel.session +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.application.gateway.SettingsGateway import co.touchlab.droidcon.application.service.NotificationService import co.touchlab.droidcon.domain.composite.ScheduleItem @@ -26,9 +27,9 @@ class SessionDetailViewModel( private val sessionGateway: SessionGateway, private val settingsGateway: SettingsGateway, private val conferenceConfigProvider: ConferenceConfigProvider, - private val speakerListItemFactory: SpeakerListItemViewModel.Factory, - private val speakerDetailFactory: SpeakerDetailViewModel.Factory, - private val feedbackDialogFactory: FeedbackDialogViewModel.Factory, + private val speakerListItemFactory: ViewModelFactory.SpeakerListItemViewModelFactory, + private val speakerDetailFactory: ViewModelFactory.SpeakerDetailViewModelFactory, + private val feedbackDialogFactory: ViewModelFactory.FeedbackDialogViewModelFactory, private val dateFormatter: DateFormatter, private val dateTimeService: DateTimeService, private val parseUrlViewService: ParseUrlViewService, @@ -152,34 +153,4 @@ class SessionDetailViewModel( InProgress, Ended, } - - class Factory( - private val sessionGateway: SessionGateway, - private val settingsGateway: SettingsGateway, - private val conferenceConfigProvider: ConferenceConfigProvider, - private val speakerListItemFactory: SpeakerListItemViewModel.Factory, - private val speakerDetailFactory: SpeakerDetailViewModel.Factory, - private val feedbackDialogFactory: FeedbackDialogViewModel.Factory, - private val dateFormatter: DateFormatter, - private val dateTimeService: DateTimeService, - private val parseUrlViewService: ParseUrlViewService, - private val feedbackService: FeedbackService, - private val notificationService: NotificationService, - ) { - - fun create(item: ScheduleItem) = SessionDetailViewModel( - sessionGateway = sessionGateway, - settingsGateway = settingsGateway, - conferenceConfigProvider = conferenceConfigProvider, - speakerListItemFactory = speakerListItemFactory, - speakerDetailFactory = speakerDetailFactory, - feedbackDialogFactory = feedbackDialogFactory, - dateFormatter = dateFormatter, - dateTimeService = dateTimeService, - parseUrlViewService = parseUrlViewService, - feedbackService = feedbackService, - notificationService = notificationService, - initialItem = item, - ) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionListItemViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionListItemViewModel.kt index dd074975..971f3d4d 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionListItemViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionListItemViewModel.kt @@ -25,8 +25,4 @@ class SessionListItemViewModel(dateTimeService: DateTimeService, item: ScheduleI }, ) val observeIsInPast by observe(::isInPast) - - class Factory(private val dateTimeService: DateTimeService) { - fun create(item: ScheduleItem, selected: () -> Unit) = SessionListItemViewModel(dateTimeService, item, selected) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SpeakerDetailViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SpeakerDetailViewModel.kt index e571ddb4..484e7009 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SpeakerDetailViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SpeakerDetailViewModel.kt @@ -30,9 +30,4 @@ class SpeakerDetailViewModel(private val parseUrlViewService: ParseUrlViewServic linkedIn, ).isEmpty() } - - class Factory(private val parseUrlViewService: ParseUrlViewService) { - - fun create(profile: Profile) = SpeakerDetailViewModel(parseUrlViewService, profile) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SpeakerListItemViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SpeakerListItemViewModel.kt index 3c555229..7371dafc 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SpeakerListItemViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SpeakerListItemViewModel.kt @@ -7,8 +7,4 @@ class SpeakerListItemViewModel(profile: Profile, val selected: () -> Unit) : Bas val avatarUrl = profile.profilePicture val info = listOfNotNull(profile.fullName, profile.tagLine).joinToString() val bio = profile.bio - - class Factory { - fun create(profile: Profile, selected: () -> Unit) = SpeakerListItemViewModel(profile, selected) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/AboutViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/AboutViewModel.kt index ffc99102..2f93b8b3 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/AboutViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/AboutViewModel.kt @@ -20,9 +20,4 @@ class AboutViewModel(private val aboutRepository: AboutRepository, private val p AboutItemViewModel(it.title, it.detail, links, it.icon) } } - - class Factory(private val aboutRepository: AboutRepository, private val parseUrlViewService: ParseUrlViewService) { - - fun create() = AboutViewModel(aboutRepository, parseUrlViewService) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt index 2a4d1c59..9615f3bd 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.viewmodel.settings +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.application.gateway.SettingsGateway import co.touchlab.droidcon.domain.entity.Conference import co.touchlab.droidcon.domain.repository.ConferenceRepository @@ -10,7 +11,7 @@ import org.brightify.hyperdrive.multiplatformx.property.ObservableProperty class SettingsViewModel( settingsGateway: SettingsGateway, - private val aboutFactory: AboutViewModel.Factory, + private val aboutFactory: ViewModelFactory.AboutViewModelFactory, private val conferenceRepository: ConferenceRepository, ) : BaseViewModel() { private val log = Logger.withTag("SettingsViewModel") @@ -80,12 +81,4 @@ class SettingsViewModel( } } } - - class Factory( - private val settingsGateway: SettingsGateway, - private val aboutFactory: AboutViewModel.Factory, - private val conferenceRepository: ConferenceRepository, - ) { - fun create() = SettingsViewModel(settingsGateway, aboutFactory, conferenceRepository) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorDetailViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorDetailViewModel.kt index ac547f4f..e85b6d96 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorDetailViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorDetailViewModel.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.viewmodel.sponsor +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.entity.Sponsor import co.touchlab.droidcon.domain.gateway.SponsorGateway import co.touchlab.droidcon.viewmodel.session.SpeakerDetailViewModel @@ -8,8 +9,8 @@ import org.brightify.hyperdrive.multiplatformx.BaseViewModel class SponsorDetailViewModel( private val sponsorGateway: SponsorGateway, - private val speakerListItemFactory: SpeakerListItemViewModel.Factory, - private val speakerDetailFactory: SpeakerDetailViewModel.Factory, + private val speakerListItemFactory: ViewModelFactory.SpeakerListItemViewModelFactory, + private val speakerDetailFactory: ViewModelFactory.SpeakerDetailViewModelFactory, private val sponsor: Sponsor, val groupName: String, ) : BaseViewModel() { @@ -35,14 +36,4 @@ class SponsorDetailViewModel( ) } } - - class Factory( - private val sponsorGateway: SponsorGateway, - private val speakerListItemFactory: SpeakerListItemViewModel.Factory, - private val speakerDetailFactory: SpeakerDetailViewModel.Factory, - ) { - - fun create(sponsor: Sponsor, groupName: String) = - SponsorDetailViewModel(sponsorGateway, speakerListItemFactory, speakerDetailFactory, sponsor, groupName) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupItemViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupItemViewModel.kt index f5514181..81588728 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupItemViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupItemViewModel.kt @@ -16,9 +16,4 @@ class SponsorGroupItemViewModel(private val sponsor: Sponsor, val selected: () - } catch (e: URLParserException) { null } - - class Factory { - - fun create(sponsor: Sponsor, selected: () -> Unit) = SponsorGroupItemViewModel(sponsor, selected) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupViewModel.kt index 2131db07..4a20390b 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupViewModel.kt @@ -1,11 +1,12 @@ package co.touchlab.droidcon.viewmodel.sponsor +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.composite.SponsorGroupWithSponsors import co.touchlab.droidcon.domain.entity.Sponsor import org.brightify.hyperdrive.multiplatformx.BaseViewModel class SponsorGroupViewModel( - sponsorGroupItemFactory: SponsorGroupItemViewModel.Factory, + sponsorGroupItemFactory: ViewModelFactory.SponsorGroupItemViewModelFactory, sponsorGroup: SponsorGroupWithSponsors, onSponsorSelected: (Sponsor) -> Unit, ) : BaseViewModel() { @@ -17,9 +18,4 @@ class SponsorGroupViewModel( }, ) val observeSponsors by observe(::sponsors) - - class Factory(private val sponsorGroupItemFactory: SponsorGroupItemViewModel.Factory) { - fun create(sponsorGroup: SponsorGroupWithSponsors, onSponsorSelected: (Sponsor) -> Unit) = - SponsorGroupViewModel(sponsorGroupItemFactory, sponsorGroup, onSponsorSelected) - } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorListViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorListViewModel.kt index a64996bd..af0cb34e 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorListViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorListViewModel.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.viewmodel.sponsor +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.composite.Url import co.touchlab.droidcon.domain.gateway.SponsorGateway import kotlinx.coroutines.flow.map @@ -7,8 +8,8 @@ import org.brightify.hyperdrive.multiplatformx.BaseViewModel class SponsorListViewModel( private val sponsorGateway: SponsorGateway, - private val sponsorGroupFactory: SponsorGroupViewModel.Factory, - private val sponsorDetailFactory: SponsorDetailViewModel.Factory, + private val sponsorGroupFactory: ViewModelFactory.SponsorGroupViewModelFactory, + private val sponsorDetailFactory: ViewModelFactory.SponsorDetailViewModelFactory, ) : BaseViewModel() { val sponsorGroups: List by managedList( emptyList(), @@ -37,13 +38,4 @@ class SponsorListViewModel( var presentedUrl: Url? by published(null) val observePresentedUrl by observe(::presentedUrl) - - class Factory( - private val sponsorGateway: SponsorGateway, - private val sponsorGroupFactory: SponsorGroupViewModel.Factory, - private val sponsorDetailFactory: SponsorDetailViewModel.Factory, - ) { - - fun create() = SponsorListViewModel(sponsorGateway, sponsorGroupFactory, sponsorDetailFactory) - } } From 5c8dc69288f5af721f3d2e834546698ec0711dcf Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 13 Feb 2026 14:08:22 -0500 Subject: [PATCH 23/56] Fixing issues with images and fixing SQLDelight async issues --- gradle/libs.versions.toml | 2 + kotlin-js-store/wasm/yarn.lock | 8 +++ kotlin-js-store/yarn.lock | 15 +++++ shared-ui/build.gradle.kts | 3 + .../ui/CircularProgressIndicator.kt | 29 ++++++++ .../ui/MainComposeView.kt | 19 +----- .../ui/session/SessionDetailView.kt | 2 +- .../ui/session/SpeakerDetailView.kt | 2 +- .../ui/sponsors/SponsorDetailView.kt | 4 +- .../ui/sponsors/SponsorsView.kt | 2 +- .../co.touchlab.droidcon/ui/util/Image.kt | 18 +---- .../viewmodel/WaitForLoadedContextModel.kt | 23 ++++--- .../viewmodel/sponsor/SponsorListViewModel.kt | 3 +- .../co.touchlab.droidcon/ui/util/Image.js.kt | 25 +++++++ .../ui/util/TimeZoneInit.kt | 5 ++ .../touchlab/droidcon/ui/util/Image.mobile.kt | 25 +++++++ shared/build.gradle.kts | 10 +-- .../kotlin/co/touchlab/droidcon/Koin.kt | 5 +- .../DefaultNotificationSchedulingService.kt | 3 +- .../gateway/impl/DefaultSessionGateway.kt | 6 ++ .../gateway/impl/DefaultSponsorGateway.kt | 33 +++++++++- .../domain/repository/ProfileRepository.kt | 2 +- .../droidcon/domain/repository/Repository.kt | 2 +- .../domain/repository/RoomRepository.kt | 2 +- .../domain/repository/SessionRepository.kt | 4 +- .../repository/SponsorGroupRepository.kt | 2 +- .../domain/repository/SponsorRepository.kt | 2 +- .../impl/SqlDelightConferenceRepository.kt | 5 +- .../impl/SqlDelightProfileRepository.kt | 12 ++-- .../impl/SqlDelightRoomRepository.kt | 6 +- .../impl/SqlDelightSessionRepository.kt | 28 ++++++-- .../impl/SqlDelightSponsorGroupRepository.kt | 10 +-- .../impl/SqlDelightSponsorRepository.kt | 10 +-- .../service/impl/DefaultApiDataSource.kt | 8 +++ .../impl/DefaultConferenceConfigProvider.kt | 11 +++- .../service/impl/DefaultFeedbackService.kt | 3 +- .../kotlin/co/touchlab/droidcon/Koin.js.kt | 3 +- web/build.gradle.kts | 2 + .../droidcon/web/DependencyInjection.kt | 66 ++++++++++++++----- .../kotlin/co/touchlab/droidcon/web/Main2.kt | 30 ++++++++- 40 files changed, 340 insertions(+), 110 deletions(-) create mode 100644 kotlin-js-store/wasm/yarn.lock create mode 100644 shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/CircularProgressIndicator.kt create mode 100644 shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt create mode 100644 shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/TimeZoneInit.kt create mode 100644 shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/util/Image.mobile.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2ece09c..5ad93319 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] ## SDK Versions +kamelImageDefault = "1.0.9" minSdk = "23" targetSdk = "36" compileSdk = "36" @@ -57,6 +58,7 @@ skie = "0.10.8" [libraries] coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } +kamel-image-default = { module = "media.kamel:kamel-image-default", version.ref = "kamelImageDefault" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } multiplatform-settings-make-observable = { module = "com.russhwolf:multiplatform-settings-make-observable", version.ref = "multiplatformSettings" } sqliter = { module = "co.touchlab:sqliter-driver", version.ref = "sqliter" } diff --git a/kotlin-js-store/wasm/yarn.lock b/kotlin-js-store/wasm/yarn.lock new file mode 100644 index 00000000..5f4567d6 --- /dev/null +++ b/kotlin-js-store/wasm/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index f8802357..3f4f8ada 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@cashapp/sqldelight-sqljs-worker@2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@cashapp/sqldelight-sqljs-worker/-/sqldelight-sqljs-worker-2.2.1.tgz#c71776a9dddfc435d4f1e64317a7039d447ea024" + integrity sha512-cj/llgS1T94t7rz63fI7pbi+jJx+vQofCT58KyMZb9XVRuoxb4taB5wbbBa4e/iljiuN5XIGGPFx+5PvtVh3LQ== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -63,6 +68,11 @@ resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== +"@js-joda/timezone@2.22.0": + version "2.22.0" + resolved "https://registry.yarnpkg.com/@js-joda/timezone/-/timezone-2.22.0.tgz#dc0eea2b33c6bac4404240ce91095573543f5d3e" + integrity sha512-9UNXxEztbcofD6XvV7xPrbzB2nE/AWaHr/XfugRZgVqg2vCZOVPnD8QI7GW164EFIWMw0c97Gs6STJ5dh0J99Q== + "@jsonjoy.com/base64@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" @@ -2571,6 +2581,11 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +sql.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-1.8.0.tgz#cb45d957e17a2239662fe2f614c9b678990867a6" + integrity sha512-3HD8pSkZL+5YvYUI8nlvNILs61ALqq34xgmF+BHpqxe68yZIJ1H+sIVIODvni25+CcxHUxDyrTJUL0lE/m7afw== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" diff --git a/shared-ui/build.gradle.kts b/shared-ui/build.gradle.kts index 9d1eec8c..4e86b24b 100644 --- a/shared-ui/build.gradle.kts +++ b/shared-ui/build.gradle.kts @@ -129,6 +129,9 @@ kotlin { val iosX64Main by getting { dependsOn(iosMain.get()) } + jsMain.dependencies { + implementation(libs.kamel.image.default) + } all { languageSettings.apply { diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/CircularProgressIndicator.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/CircularProgressIndicator.kt new file mode 100644 index 00000000..0694d7a8 --- /dev/null +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/CircularProgressIndicator.kt @@ -0,0 +1,29 @@ +package co.touchlab.droidcon.ui + +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.height +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun CircularProgressIndicator(text: String) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(16.dp)) + Text(text) + } + } +} diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt index d8dae372..7c095024 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt @@ -42,28 +42,12 @@ internal fun MainComposeView(waitForLoadedContextModel: WaitForLoadedContextMode DroidconTheme { when (val state = loadingState) { - WaitForLoadedContextModel.State.Loading -> LoadingScreen() + WaitForLoadedContextModel.State.Loading -> CircularProgressIndicator("Updating Droidcon Events!") is WaitForLoadedContextModel.State.Ready -> MainAppBody(waitForLoadedContextModel, state.conference, modifier) } } } -@Composable -private fun LoadingScreen() { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CircularProgressIndicator() - Spacer(modifier = Modifier.height(16.dp)) - Text("Updating Droidcon Events!") - } - } -} - @Composable private fun MainAppBody(waitForLoadedContextModel: WaitForLoadedContextModel, selectedConference: Conference, modifier: Modifier) { LaunchedEffect(selectedConference) { @@ -85,6 +69,7 @@ private fun MainAppBody(waitForLoadedContextModel: WaitForLoadedContextModel, se viewModel.selectedTab = ApplicationViewModel.Tab.Schedule } + print("isFirstRun") if (conferences.size == 1) { LaunchedEffect(conferences) { onConferenceSelected(conferences.first()) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt index ca7be9c6..cbd4d415 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt @@ -274,7 +274,7 @@ private fun SpeakerView(speaker: SpeakerListItemViewModel) { if (imageUrl != null) { DcAsyncImage( logTag = LOG_TAG, - model = imageUrl, + url = imageUrl, contentDescription = speaker.info, modifier = Modifier.width(80.dp) .padding( diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SpeakerDetailView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SpeakerDetailView.kt index 8f5589ef..4cab2a3b 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SpeakerDetailView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SpeakerDetailView.kt @@ -98,7 +98,7 @@ private fun HeaderView(name: String, tagLine: String, imageUrl: Url?) { if (imageUrl != null) { DcAsyncImage( logTag = LOG_TAG, - model = imageUrl.string, + url = imageUrl.string, contentDescription = name, modifier = Modifier .width(100.dp) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorDetailView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorDetailView.kt index 416f58f5..48ab2ae1 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorDetailView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorDetailView.kt @@ -132,7 +132,7 @@ private fun HeaderView(name: String, groupTitle: String, imageUrl: Url?) { if (imageUrl != null) { DcAsyncImage( logTag = LOG_TAG, - model = imageUrl.string, + url = imageUrl.string, modifier = Modifier .width(120.dp) .padding(horizontal = Dimensions.Padding.default) @@ -204,7 +204,7 @@ private fun RepresentativeInfoView(profile: SpeakerListItemViewModel) { } else { DcAsyncImage( logTag = LOG_TAG, - model = imageUrl.string, + url = imageUrl.string, contentDescription = profile.info, modifier = Modifier .width(80.dp) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt index e1755a5d..28091c11 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt @@ -138,7 +138,7 @@ private fun SponsorGroupView(sponsorGroup: SponsorGroupViewModel) { if (imageUrl != null) { DcAsyncImage( logTag = LOG_TAG, - model = imageUrl, + url = imageUrl, contentDescription = sponsor.name, ) } else { diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Image.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Image.kt index 197c04ec..d458ab04 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Image.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Image.kt @@ -2,28 +2,13 @@ package co.touchlab.droidcon.ui.util import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import co.touchlab.kermit.Logger import coil3.ImageLoader import coil3.PlatformContext -import coil3.compose.AsyncImage import coil3.request.crossfade import coil3.util.DebugLogger @Composable -fun DcAsyncImage(logTag: String, model: Any?, contentDescription: String?, modifier: Modifier = Modifier) { - AsyncImage( - modifier = modifier, - model = model, - contentDescription = contentDescription, - onError = { - Logger.e( - messageString = logTag, - throwable = it.result.throwable, - tag = "AsyncImage OnError Request = ${it.result.request}\n", - ) - }, - ) -} +expect fun DcAsyncImage(logTag: String, url: String?, contentDescription: String?, modifier: Modifier = Modifier) fun dcImageLoader(context: PlatformContext, debug: Boolean = false): ImageLoader = ImageLoader.Builder(context) .crossfade(true) @@ -33,3 +18,4 @@ fun dcImageLoader(context: PlatformContext, debug: Boolean = false): ImageLoader } } .build() + diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt index 75913d56..c72ae61d 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.withContext import org.brightify.hyperdrive.multiplatformx.BaseViewModel class WaitForLoadedContextModel( private val conferenceConfigProvider: ConferenceConfigProvider, - applicationViewModelFactory: ApplicationViewModel.Factory, + applicationViewModelFactory: ViewModelFactory.ApplicationViewModelFactory, private val syncService: SyncService, private val settingsGateway: SettingsGateway, ) : BaseViewModel() { @@ -34,19 +34,24 @@ class WaitForLoadedContextModel( } suspend fun watchConferenceChanges() { + log.i { "watchConferenceChanges" } lifecycle.whileAttached { - withContext(IODispatcher) { - try { - syncService.syncConferences() - } catch (e: Exception) { - log.e(e) { "Failed to sync conferences" } - } - } - + log.i { "Starting to Sync Conferences" } launch { + log.i { "Observing conferenceConfigProvider" } conferenceConfigProvider.observeChanges().collect { conference -> if (conference != null) { + log.i { "WaitForLoadedContextModel: Emitting Conference!" } _state.emit(State.Ready(conference)) + + withContext(IODispatcher) { + try { + log.i { "syncConferences" } + syncService.syncConferences() + } catch (e: Exception) { + log.e(e) { "Failed to sync conferences" } + } + } } } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorListViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorListViewModel.kt index af0cb34e..deaf637d 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorListViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorListViewModel.kt @@ -1,8 +1,8 @@ package co.touchlab.droidcon.viewmodel.sponsor -import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.composite.Url import co.touchlab.droidcon.domain.gateway.SponsorGateway +import co.touchlab.droidcon.viewmodel.ViewModelFactory import kotlinx.coroutines.flow.map import org.brightify.hyperdrive.multiplatformx.BaseViewModel @@ -11,6 +11,7 @@ class SponsorListViewModel( private val sponsorGroupFactory: ViewModelFactory.SponsorGroupViewModelFactory, private val sponsorDetailFactory: ViewModelFactory.SponsorDetailViewModelFactory, ) : BaseViewModel() { + val sponsorGroups: List by managedList( emptyList(), sponsorGateway.observeSponsors() diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt new file mode 100644 index 00000000..64eb2f83 --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt @@ -0,0 +1,25 @@ +package co.touchlab.droidcon.ui.util + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import co.touchlab.kermit.Logger +import io.kamel.image.KamelImage +import io.kamel.image.asyncPainterResource + +@Composable +actual fun DcAsyncImage(logTag: String, url: String?, contentDescription: String?, modifier: Modifier) { + KamelImage( + resource = { + asyncPainterResource(data = "url") + }, + contentDescription = "Profile", + modifier = Modifier, + onFailure = { + Logger.e( + tag = logTag, + throwable = it, + messageString = "AsyncImage OnError Request = ${it}\n", + ) + }, + ) +} diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/TimeZoneInit.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/TimeZoneInit.kt new file mode 100644 index 00000000..cc10d305 --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/TimeZoneInit.kt @@ -0,0 +1,5 @@ +@file:JsModule("@js-joda/timezone") +@file:JsNonModule +package co.touchlab.droidcon.ui.util + +external object TimezoneInit diff --git a/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/util/Image.mobile.kt b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/util/Image.mobile.kt new file mode 100644 index 00000000..7e98bca3 --- /dev/null +++ b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/util/Image.mobile.kt @@ -0,0 +1,25 @@ +package co.touchlab.droidcon.ui.util + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import co.touchlab.kermit.Logger +import coil3.compose.AsyncImage + +@Composable +actual fun DcAsyncImage(logTag: String, url: String?, contentDescription: String?, modifier: Modifier) { + AsyncImage( + modifier = modifier, + model = url, + contentDescription = contentDescription, + onError = { + Logger.e( + logTag, + throwable = it.result.throwable, + message = { + "AsyncImage OnError Request = ${it.result.request}\n" + }, + ) + }, + ) +} + diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 1a0203b8..0a290087 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -122,6 +122,7 @@ kotlin { //implementation(devNpm("copy-webpack-plugin", "9.1.0")) implementation(npm("sql.js", "1.8.0")) implementation(npm("@cashapp/sqldelight-sqljs-worker", "2.2.1")) + implementation(npm("@js-joda/timezone", "2.22.0")) /* implementation(npm("path-browserify", "1.0.1")) implementation(npm("crypto-browserify", "3.12.0")) @@ -146,9 +147,10 @@ kotlin { } sqldelight { - databases.create("DroidconDatabase") { - packageName.set("co.touchlab.droidcon.db") - generateAsync.set(true) - + databases{ + create("DroidconDatabase") { + packageName.set("co.touchlab.droidcon.db") + generateAsync = true + } } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index 41b8e22f..d40b9cb3 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -63,9 +63,9 @@ import org.koin.dsl.module fun initKoin(additionalModules: List): KoinApplication { val koinApplication = startKoin { modules( - additionalModules + platformModule + - coreModule, + coreModule + + additionalModules, ) } @@ -135,7 +135,6 @@ private val coreModule = module { single { DefaultConferenceConfigProvider( conferenceRepository = get(), - initialConference = null, ) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt index 6d35cff6..0e39de9e 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt @@ -41,7 +41,8 @@ class DefaultNotificationSchedulingService( private var scheduledNotifications: List get() = settings.getStringOrNull(SCHEDULED_NOTIFICATIONS_KEY)?.let { serializedList -> - json.decodeFromString>(serializedList).map { Session.Id(it) } + if(serializedList.isBlank()) emptyList() + else json.decodeFromString>(serializedList).map { Session.Id(it) } } ?: emptyList() set(value) { settings[SCHEDULED_NOTIFICATIONS_KEY] = json.encodeToString(value.map { it.value }) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt index 2b5d3bab..8aefd04c 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt @@ -8,6 +8,7 @@ import co.touchlab.droidcon.domain.repository.RoomRepository import co.touchlab.droidcon.domain.repository.SessionRepository import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.ScheduleService +import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -21,16 +22,21 @@ class DefaultSessionGateway( private val conferenceConfigProvider: ConferenceConfigProvider, ) : SessionGateway { + private val log = Logger.withTag("DefaultSessionGateway") + private val conferenceId: Long? get() = conferenceConfigProvider.getConferenceId() override fun observeSchedule(): Flow> = conferenceConfigProvider.observeChanges().flatMapLatest { conf -> + log.i { "observeSchedule: Conference: ${conf?.id}" } if (conf == null) { flowOf(emptyList()) } else { sessionRepository.observeAll(conf.id) } }.map { sessions -> + log.i { "observeSchedule: Map: $sessions" } + sessions.map { session -> scheduleItemForSession(session) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt index 927483fa..85297064 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt @@ -8,7 +8,13 @@ import co.touchlab.droidcon.domain.repository.ProfileRepository import co.touchlab.droidcon.domain.repository.SponsorGroupRepository import co.touchlab.droidcon.domain.repository.SponsorRepository import co.touchlab.droidcon.domain.service.ConferenceConfigProvider +import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map class DefaultSponsorGateway( @@ -18,17 +24,40 @@ class DefaultSponsorGateway( private val conferenceConfigProvider: ConferenceConfigProvider, ) : SponsorGateway { + private val log = Logger.withTag("DefaultSponsorGateway") + override fun observeSponsors(): Flow> { + log.i { "observeSponsors" } + + return conferenceConfigProvider.observeChanges() + .filterNotNull() + .map { conference -> conference.id } + .distinctUntilChanged() + .flatMapLatest { id -> + sponsorGroupRepository.observeAll(id).map { groups -> + groups.map { group -> + log.i { "Found a Group of Sponsors" } + SponsorGroupWithSponsors( + group, + sponsorRepository.allByGroupName(group.name, id), + ) + } + } + } +/* val conferenceId = conferenceConfigProvider.getConferenceId() - ?: return kotlinx.coroutines.flow.flowOf(emptyList()) + + log.i { "Got the Conference ID" } return sponsorGroupRepository.observeAll(conferenceId).map { groups -> groups.map { group -> + log.i { "Found a Group of Sponsors" } + SponsorGroupWithSponsors( group, sponsorRepository.allByGroupName(group.name, conferenceId), ) } - } + }*/ } override fun observeSponsorById(id: Sponsor.Id): Flow { diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepository.kt index 148bea82..4f1a0d22 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepository.kt @@ -14,5 +14,5 @@ interface ProfileRepository : Repository { suspend fun getSponsorRepresentatives(sponsorId: Sponsor.Id, conferenceId: Long): List - fun allSync(conferenceId: Long): List + suspend fun allSync(conferenceId: Long): List } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/Repository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/Repository.kt index e9075b09..a3d4ad08 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/Repository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/Repository.kt @@ -28,5 +28,5 @@ interface Repository> { suspend fun addOrUpdate(entity: ENTITY, conferenceId: Long) - fun contains(id: ID, conferenceId: Long): Boolean + suspend fun contains(id: ID, conferenceId: Long): Boolean } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/RoomRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/RoomRepository.kt index 10d42741..8e7244a2 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/RoomRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/RoomRepository.kt @@ -3,5 +3,5 @@ package co.touchlab.droidcon.domain.repository import co.touchlab.droidcon.domain.entity.Room interface RoomRepository : Repository { - fun allSync(conferenceId: Long): List + suspend fun allSync(conferenceId: Long): List } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SessionRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SessionRepository.kt index 23b19b4a..b4d471bc 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SessionRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SessionRepository.kt @@ -17,7 +17,7 @@ interface SessionRepository : Repository { suspend fun setFeedbackSent(sessionId: Session.Id, isSent: Boolean, conferenceId: Long) - fun allSync(conferenceId: Long): List + suspend fun allSync(conferenceId: Long): List - fun findSync(id: Session.Id, conferenceId: Long): Session? + suspend fun findSync(id: Session.Id, conferenceId: Long): Session? } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SponsorGroupRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SponsorGroupRepository.kt index 79b09b03..b1a06a84 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SponsorGroupRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SponsorGroupRepository.kt @@ -3,5 +3,5 @@ package co.touchlab.droidcon.domain.repository import co.touchlab.droidcon.domain.entity.SponsorGroup interface SponsorGroupRepository : Repository { - fun allSync(conferenceId: Long): List + suspend fun allSync(conferenceId: Long): List } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SponsorRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SponsorRepository.kt index 3f647de8..f5342265 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SponsorRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/SponsorRepository.kt @@ -5,5 +5,5 @@ import co.touchlab.droidcon.domain.entity.Sponsor interface SponsorRepository : Repository { suspend fun allByGroupName(group: String, conferenceId: Long): List - fun allSync(conferenceId: Long): List + suspend fun allSync(conferenceId: Long): List } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt index de9913bc..cb7a457b 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOne @@ -21,7 +22,7 @@ class SqlDelightConferenceRepository( override fun observeSelected(): Flow = conferenceQueries.selectSelected(::conferenceFactory).asFlow().mapToOne(Dispatchers.Main) - override suspend fun getSelected(): Conference = conferenceQueries.selectSelected(::conferenceFactory).executeAsOne() + override suspend fun getSelected(): Conference = conferenceQueries.selectSelected(::conferenceFactory).awaitAsOne() override suspend fun select(conferenceId: Long): Boolean { try { @@ -46,7 +47,7 @@ class SqlDelightConferenceRepository( venueMap = conference.venueMap, ) // Return the last inserted ID - return conferenceQueries.lastInsertRowId().executeAsOne() + return conferenceQueries.lastInsertRowId().awaitAsOne() } override suspend fun update(conference: Conference): Boolean { diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt index a375bc2e..b57890ce 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt @@ -1,5 +1,7 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOne @@ -23,7 +25,7 @@ class SqlDelightProfileRepository( ProfileRepository { override suspend fun getSpeakersBySession(id: Session.Id, conferenceId: Long): List = - profileQueries.selectBySession(id.value, conferenceId, ::profileFactory).executeAsList() + profileQueries.selectBySession(id.value, conferenceId, ::profileFactory).awaitAsList() override suspend fun setSessionSpeakers(session: Session, speakers: List, conferenceId: Long) { speakerQueries.deleteBySessionId(session.id.value, conferenceId) @@ -62,8 +64,8 @@ class SqlDelightProfileRepository( mapper = ::profileFactory, ).executeAsList() - override fun allSync(conferenceId: Long): List = - profileQueries.selectAll(conferenceId, mapper = ::profileFactory).executeAsList() + override suspend fun allSync(conferenceId: Long): List = + profileQueries.selectAll(conferenceId, mapper = ::profileFactory).awaitAsList() override fun observe(id: Profile.Id, conferenceId: Long): Flow = profileQueries.selectById(id.value, conferenceId, ::profileFactory).asFlow().mapToOne(Dispatchers.Main) @@ -92,8 +94,8 @@ class SqlDelightProfileRepository( profileQueries.delete(id.value, conferenceId) } - override fun contains(id: Profile.Id, conferenceId: Long): Boolean = - profileQueries.existsById(id.value, conferenceId).executeAsOne().toBoolean() + override suspend fun contains(id: Profile.Id, conferenceId: Long): Boolean = + profileQueries.existsById(id.value, conferenceId).awaitAsOne().toBoolean() private fun profileFactory( id: String, diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt index e21719c4..7f29b1cf 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt @@ -1,5 +1,7 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOne @@ -14,7 +16,7 @@ class SqlDelightRoomRepository(private val roomQueries: RoomQueries) : BaseRepository(), RoomRepository { - override fun allSync(conferenceId: Long): List = roomQueries.selectAll(conferenceId, ::roomFactory).executeAsList() + override suspend fun allSync(conferenceId: Long): List = roomQueries.selectAll(conferenceId, ::roomFactory).awaitAsList() override fun observe(id: Room.Id, conferenceId: Long): Flow = roomQueries.selectById(id.value, conferenceId, ::roomFactory).asFlow().mapToOne(Dispatchers.Main) @@ -37,7 +39,7 @@ class SqlDelightRoomRepository(private val roomQueries: RoomQueries) : roomQueries.deleteById(id.value, conferenceId) } - override fun contains(id: Room.Id, conferenceId: Long): Boolean = roomQueries.existsById(id.value, conferenceId).executeAsOne() != 0L + override suspend fun contains(id: Room.Id, conferenceId: Long): Boolean = roomQueries.existsById(id.value, conferenceId).awaitAsOne() != 0L private fun roomFactory(id: Long, conferenceId: Long, name: String) = Room(id = Room.Id(id), name = name) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt index 4ea6407e..6196badd 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt @@ -1,5 +1,8 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOne @@ -9,19 +12,30 @@ import co.touchlab.droidcon.domain.entity.Room import co.touchlab.droidcon.domain.entity.Session import co.touchlab.droidcon.domain.repository.SessionRepository import co.touchlab.droidcon.domain.service.DateTimeService +import co.touchlab.kermit.Logger import kotlin.time.Instant +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, private val sessionQueries: SessionQueries) : BaseRepository(), SessionRepository { + + private val log = Logger.withTag("SqlDelightSessionRepository") + val lifecycleScope = CoroutineScope(SupervisorJob()) + Dispatchers.Main + + + override fun observe(id: Session.Id, conferenceId: Long): Flow = sessionQueries.sessionById(id.value, conferenceId, ::sessionFactory).asFlow().mapToOne(Dispatchers.Main) - fun sessionById(id: Session.Id, conferenceId: Long): Session? = - sessionQueries.sessionById(id.value, conferenceId, ::sessionFactory).executeAsOneOrNull() + suspend fun sessionById(id: Session.Id, conferenceId: Long): Session? = + sessionQueries.sessionById(id.value, conferenceId, ::sessionFactory).awaitAsOneOrNull() override fun observeOrNull(id: Session.Id, conferenceId: Long): Flow = sessionQueries.sessionById(id.value, conferenceId, ::sessionFactory).asFlow().mapToOneOrNull(Dispatchers.Main) @@ -47,10 +61,10 @@ class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, sessionQueries.updateFeedBackSent(if (isSent) 1 else 0, sessionId.value, conferenceId) } - override fun allSync(conferenceId: Long): List = sessionQueries.allSessions(conferenceId, ::sessionFactory).executeAsList() + override suspend fun allSync(conferenceId: Long): List = sessionQueries.allSessions(conferenceId, ::sessionFactory).awaitAsList() - override fun findSync(id: Session.Id, conferenceId: Long): Session? = - sessionQueries.sessionById(id.value, conferenceId, mapper = ::sessionFactory).executeAsOneOrNull() + override suspend fun findSync(id: Session.Id, conferenceId: Long): Session? = + sessionQueries.sessionById(id.value, conferenceId, mapper = ::sessionFactory).awaitAsOneOrNull() override fun observeAll(conferenceId: Long): Flow> = sessionQueries.allSessions(conferenceId, ::sessionFactory).asFlow().mapToList(Dispatchers.Main) @@ -77,8 +91,8 @@ class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, sessionQueries.deleteById(id.value, conferenceId) } - override fun contains(id: Session.Id, conferenceId: Long): Boolean = - sessionQueries.existsById(id.value, conferenceId).executeAsOne().toBoolean() + override suspend fun contains(id: Session.Id, conferenceId: Long): Boolean = + sessionQueries.existsById(id.value, conferenceId).awaitAsOne().toBoolean() private fun sessionFactory( id: String, diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorGroupRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorGroupRepository.kt index 41770949..52207734 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorGroupRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorGroupRepository.kt @@ -1,5 +1,7 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOne @@ -14,8 +16,8 @@ class SqlDelightSponsorGroupRepository(private val sponsorGroupQueries: SponsorG BaseRepository(), SponsorGroupRepository { - override fun allSync(conferenceId: Long): List = - sponsorGroupQueries.selectAll(conferenceId, ::sponsorGroupFactory).executeAsList() + override suspend fun allSync(conferenceId: Long): List = + sponsorGroupQueries.selectAll(conferenceId, ::sponsorGroupFactory).awaitAsList() override fun observe(id: SponsorGroup.Id, conferenceId: Long): Flow = sponsorGroupQueries.sponsorGroupByName(id.value, conferenceId, ::sponsorGroupFactory) @@ -29,8 +31,8 @@ class SqlDelightSponsorGroupRepository(private val sponsorGroupQueries: SponsorG sponsorGroupQueries.selectAll(conferenceId, ::sponsorGroupFactory) .asFlow().mapToList(Dispatchers.Main) - override fun contains(id: SponsorGroup.Id, conferenceId: Long): Boolean = - sponsorGroupQueries.existsByName(id.value, conferenceId).executeAsOne().toBoolean() + override suspend fun contains(id: SponsorGroup.Id, conferenceId: Long): Boolean = + sponsorGroupQueries.existsByName(id.value, conferenceId).awaitAsOne().toBoolean() override suspend fun doUpsert(entity: SponsorGroup, conferenceId: Long) { sponsorGroupQueries.upsert( diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorRepository.kt index 1c0584e5..9a1062a2 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSponsorRepository.kt @@ -1,5 +1,7 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOne @@ -26,13 +28,13 @@ class SqlDelightSponsorRepository(private val sponsorQueries: SponsorQueries) : override fun observeAll(conferenceId: Long): Flow> = sponsorQueries.selectAll(conferenceId, ::sponsorFactory).asFlow().mapToList(Dispatchers.Main) - override fun contains(id: Sponsor.Id, conferenceId: Long): Boolean = - sponsorQueries.existsById(id.name, id.group, conferenceId).executeAsOne().toBoolean() + override suspend fun contains(id: Sponsor.Id, conferenceId: Long): Boolean = + sponsorQueries.existsById(id.name, id.group, conferenceId).awaitAsOne().toBoolean() override suspend fun allByGroupName(group: String, conferenceId: Long): List = - sponsorQueries.sponsorsByGroup(group, conferenceId, ::sponsorFactory).executeAsList() + sponsorQueries.sponsorsByGroup(group, conferenceId, ::sponsorFactory).awaitAsList() - override fun allSync(conferenceId: Long): List = sponsorQueries.selectAll(conferenceId, ::sponsorFactory).executeAsList() + override suspend fun allSync(conferenceId: Long): List = sponsorQueries.selectAll(conferenceId, ::sponsorFactory).awaitAsList() override suspend fun doUpsert(entity: Sponsor, conferenceId: Long) { sponsorQueries.upsert( diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt index c15420c3..e663f1fb 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt @@ -6,6 +6,7 @@ import co.touchlab.droidcon.domain.service.impl.dto.ScheduleDto import co.touchlab.droidcon.domain.service.impl.dto.SpeakersDto import co.touchlab.droidcon.domain.service.impl.dto.SponsorSessionsDto import co.touchlab.droidcon.domain.service.impl.dto.SponsorsDto +import co.touchlab.kermit.Logger import io.ktor.client.HttpClient import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.get @@ -20,6 +21,9 @@ class DefaultApiDataSource( private val json: Json, private val conferenceConfigProvider: ConferenceConfigProvider, ) : DefaultSyncService.DataSource { + + private val log = Logger.withTag("DefaultApiDataSource") + override suspend fun getSpeakers(): List? { val scheduleId = conferenceConfigProvider.getScheduleId() ?: return null val jsonString = client.get { @@ -46,6 +50,7 @@ class DefaultApiDataSource( } override suspend fun getSponsors(): SponsorsDto.SponsorCollectionDto? { + log.i { "gettingSponsors" } val projectId = conferenceConfigProvider.getProjectId() ?: return null val collectionName = conferenceConfigProvider.getCollectionName() ?: return null val apiKey = conferenceConfigProvider.getApiKey() ?: return null @@ -58,8 +63,11 @@ class DefaultApiDataSource( } suspend fun getConferences(): ConferencesDto.ConferenceCollectionDto? { + log.i { "getConferences" } val projectId = conferenceConfigProvider.getProjectId() ?: return null + log.i { "Have Project ID: $projectId" } val apiKey = conferenceConfigProvider.getApiKey() ?: return null + log.i { "Have API Key" } val databaseName = "(default)" val conferenceListCollection = "conferenceListMobile" diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt index 10df081a..bdbc4431 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt @@ -22,7 +22,9 @@ class DefaultConferenceConfigProvider( private val currentConference: Conference? get() = currentConferenceState.value - override fun getConferenceId(): Long? = currentConference?.id + override fun getConferenceId(): Long? { + return currentConference?.id + } override fun getConferenceTimeZone(): TimeZone? = currentConference?.timeZone @@ -30,7 +32,10 @@ class DefaultConferenceConfigProvider( override fun getCollectionName(): String? = currentConference?.collectionName - override fun getApiKey(): String? = currentConference?.apiKey + override fun getApiKey(): String? { + log.i { "Getting API Key $currentConference" } + return currentConference?.apiKey + } override fun getScheduleId(): String? = currentConference?.scheduleId @@ -51,6 +56,7 @@ class DefaultConferenceConfigProvider( // Implementation of the interface method to load the conference asynchronously // Also sets up continuous observation of conference changes override suspend fun loadSelectedConference() { + log.i { "DefaultConferenceConfigProvider: loadSelectedConference" } conferenceRepository.observeSelected() .map { it } .catch { e -> @@ -58,6 +64,7 @@ class DefaultConferenceConfigProvider( emit(null) } .collect { conference -> + log.i { "loadSelectedConference: Emitting Conference! $conference" } _currentConferenceState.value = conference } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt index 620b5e6b..5a561265 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt @@ -22,7 +22,8 @@ class DefaultFeedbackService( } private var completedSessionIds: Set = settings.getStringOrNull(COMPLETED_SESSION_FEEDBACKS_KEY)?.let { - json.decodeFromString(it) + if(it.isBlank()) emptySet() + else json.decodeFromString(it) } ?: emptySet() override suspend fun next(): Session? = sessionGateway.observeAgenda().first() diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt index 00502571..45c33e24 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt @@ -11,6 +11,7 @@ import co.touchlab.kermit.Logger import co.touchlab.kermit.StaticConfig import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.cio.CIO +import io.ktor.client.engine.js.Js import org.koin.dsl.module actual val platformModule: org.koin.core.module.Module = module { @@ -19,7 +20,7 @@ actual val platformModule: org.koin.core.module.Module = module { } single { - CIO.create() + Js.create() } single { diff --git a/web/build.gradle.kts b/web/build.gradle.kts index 0a3fadfa..bfbc683a 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -31,6 +31,8 @@ kotlin { implementation(libs.multiplatformSettings.core) implementation(libs.multiplatform.settings.make.observable) implementation(libs.koin.compose) + implementation(libs.hyperdrive.multiplatformx.api) + } } val jsTest by getting { diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt index 09160bef..57fc1e47 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt @@ -1,11 +1,20 @@ package co.touchlab.droidcon.web +import app.cash.sqldelight.db.SqlDriver import co.touchlab.droidcon.application.service.NotificationSchedulingService +import co.touchlab.droidcon.db.ConferenceTable +import co.touchlab.droidcon.db.DroidconDatabase +import co.touchlab.droidcon.db.SessionTable +import co.touchlab.droidcon.db.SponsorGroupTable +import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory +import co.touchlab.droidcon.domain.repository.impl.adapter.InstantSqlDelightAdapter import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin +import co.touchlab.droidcon.intToLongAdapter import co.touchlab.droidcon.service.JsNotificationService import co.touchlab.droidcon.service.ParseUrlViewService +import co.touchlab.droidcon.timeZoneAdapter import co.touchlab.droidcon.ui.uiModule import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel @@ -20,29 +29,54 @@ import com.russhwolf.settings.StorageSettings import org.koin.core.KoinApplication import org.koin.dsl.module import com.russhwolf.settings.observable.makeObservable +import kotlin.time.ExperimentalTime -@OptIn(ExperimentalSettingsApi::class) -fun startKoin(): KoinApplication = - initKoin( - module { - single { - val storageSettings: Settings = StorageSettings() - storageSettings.makeObservable() - } - single { WebResourceReader() } +@OptIn(ExperimentalSettingsApi::class, ExperimentalTime::class) +suspend fun startKoin(): KoinApplication { - single { WebDateFormatter() } + val driver = SqlDelightDriverFactory().createDriver() + DroidconDatabase.Schema.create(driver).await() - single { NotificationLocalizedStringFactory() } + val db = DroidconDatabase.invoke( + driver = driver, + sessionTableAdapter = SessionTable.Adapter( + startsAtAdapter = InstantSqlDelightAdapter, + endsAtAdapter = InstantSqlDelightAdapter, + feedbackRatingAdapter = intToLongAdapter, + ), + sponsorGroupTableAdapter = SponsorGroupTable.Adapter( + intToLongAdapter, + ), + conferenceTableAdapter = ConferenceTable.Adapter( + conferenceTimeZoneAdapter = timeZoneAdapter, + // Note: selectedAdapter will be added when the adapter is regenerated + ), + ) + + return initKoin( + module { + single { + val storageSettings: Settings = StorageSettings() + storageSettings.makeObservable() + } + single { WebResourceReader() } + + single { WebDateFormatter() } + + single { NotificationLocalizedStringFactory() } + + single { WebAnalyticsService() } - single { WebAnalyticsService() } + single { DefaultParseUrlViewService() } - single { DefaultParseUrlViewService() } + // Provide JsNotificationService which is required by the JS platform module + single { JsNotificationService() } - // Provide JsNotificationService which is required by the JS platform module - single { JsNotificationService() } - } + uiModule + single { driver } + single { db } + } + uiModule ) +} @OptIn(ExperimentalWasmJsInterop::class) @Suppress("unused") diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt index f1d8117f..9206c724 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt @@ -3,15 +3,43 @@ package co.touchlab.droidcon.web import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport import co.touchlab.droidcon.ui.MainView +import co.touchlab.droidcon.ui.util.TimezoneInit import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus +import org.brightify.hyperdrive.multiplatformx.LifecycleGraph import org.koin.compose.koinInject @OptIn(ExperimentalComposeUiApi::class) -fun main() { +suspend fun main() { val koinApplication = startKoin() + val lifecycleScope = CoroutineScope(SupervisorJob()) + Dispatchers.Main + + @Suppress("UNUSED_VARIABLE") + val tz = TimezoneInit + + // Now set up view model lifecycle + val root = LifecycleGraph.Root("…") + + lifecycleScope.launch { + val cancelAttach = root.attach(lifecycleScope) + try { + awaitCancellation() + } finally { + cancelAttach.cancel() + } + } + ComposeViewport { val viewModel:WaitForLoadedContextModel = koinInject() // Works + viewModel.lifecycle.removeFromParent() + root.addChild(viewModel.lifecycle) + MainView(viewModel) } From 3d40d6bdb8117260cfe366618aa29462226b95b1 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 13 Feb 2026 15:54:15 -0500 Subject: [PATCH 24/56] Fixing issues with Sponsor images --- gradle/libs.versions.toml | 7 ++- kotlin-js-store/yarn.lock | 49 ++++++++++--------- shared-ui/build.gradle.kts | 17 +++++-- .../ui/MainComposeView.kt | 18 +------ .../co.touchlab.droidcon/ui/util/Image.kt | 15 +----- .../ui/venue/VenueView.kt | 38 +------------- .../co.touchlab.droidcon/ui/util/Image.js.kt | 35 +++++++------ .../ui/venue/VenueView.kt | 42 ++++++++++++++++ .../touchlab/droidcon/ui/util/Image.mobile.kt | 22 +++++++++ .../touchlab/droidcon/ui/venue/VenueView.kt | 41 ++++++++++++++++ 10 files changed, 177 insertions(+), 107 deletions(-) create mode 100644 shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt create mode 100644 shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ad93319..5bfd79ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,13 @@ [versions] ## SDK Versions kamelImageDefault = "1.0.9" +kotlinxBrowser = "0.5.0" minSdk = "23" targetSdk = "36" compileSdk = "36" # Dependencies -kotlin = "2.2.21" +kotlin = "2.3.10" ## Gradle Plugin version must be compatible with Multiplatform ## https://kotlinlang.org/docs/multiplatform-compatibility-guide.html#version-compatibility @@ -53,12 +54,13 @@ uuid = "0.8.4" ktlint = "14.0.1" coil = "3.3.0" zoomimage = "1.4.0" -skie = "0.10.8" +skie = "0.10.10" [libraries] coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } kamel-image-default = { module = "media.kamel:kamel-image-default", version.ref = "kamelImageDefault" } +kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinxBrowser" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } multiplatform-settings-make-observable = { module = "com.russhwolf:multiplatform-settings-make-observable", version.ref = "multiplatformSettings" } sqliter = { module = "co.touchlab:sqliter-driver", version.ref = "sqliter" } @@ -123,6 +125,7 @@ koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } zoomimage-composeResources = { module = "io.github.panpf.zoomimage:zoomimage-compose-resources", version.ref = "zoomimage" } +zoomimage-compose = { module = "io.github.panpf.zoomimage:zoomimage-compose", version.ref = "zoomimage" } [plugins] crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics-gradle" } diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 3f4f8ada..1bdf7642 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -1034,13 +1034,13 @@ engine.io@~6.6.0: engine.io-parser "~5.2.1" ws "~8.17.1" -enhanced-resolve@^5.17.2: - version "5.18.3" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz#9b5f4c5c076b8787c78fe540392ce76a88855b44" - integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww== +enhanced-resolve@^5.17.3: + version "5.19.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz#6687446a15e969eaa63c2fa2694510e17ae6d97c" + integrity sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg== dependencies: graceful-fs "^4.2.4" - tapable "^2.2.0" + tapable "^2.3.0" ent@~2.2.0: version "2.2.2" @@ -1597,6 +1597,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" @@ -1728,10 +1733,9 @@ karma-webpack@5.0.1: minimatch "^9.0.3" webpack-merge "^4.1.5" -karma@6.4.4: +"karma@github:Kotlin/karma#6.4.5": version "6.4.4" - resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.4.tgz#dfa5a426cf5a8b53b43cd54ef0d0d09742351492" - integrity sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w== + resolved "https://codeload.github.com/Kotlin/karma/tar.gz/239a8fc984584f0d96b1dd750e7a5e2c79da93a6" dependencies: "@colors/colors" "1.5.0" body-parser "^1.19.0" @@ -1763,10 +1767,10 @@ kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -kotlin-web-helpers@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-2.1.0.tgz#6cd4b0f0dc3baea163929c8638155b8d19c55a74" - integrity sha512-NAJhiNB84tnvJ5EQx7iER3GWw7rsTZkX9HVHZpe7E3dDBD/dhTzqgSwNU3MfQjniy2rB04bP24WM9Z32ntUWRg== +kotlin-web-helpers@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-3.0.0.tgz#3ed6b48f694f74bb60a737a9d7e2c0e3b29abdb9" + integrity sha512-kdQO4AJQkUPvpLh9aglkXDRyN+CfXO7pKq+GESEnxooBFkQpytLrqZis3ABvmFN1cGw/ZQ/K38u5sRGW+NfBnw== dependencies: format-util "^1.0.5" @@ -1941,10 +1945,10 @@ mkdirp@^0.5.5: dependencies: minimist "^1.2.6" -mocha@11.7.1: - version "11.7.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.7.1.tgz#91948fecd624fb4bd154ed260b7e1ad3910d7c7a" - integrity sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A== +mocha@11.7.5: + version "11.7.5" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.7.5.tgz#58f5bbfa5e0211ce7e5ee6128107cefc2515a627" + integrity sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig== dependencies: browser-stdout "^1.3.1" chokidar "^4.0.1" @@ -1954,6 +1958,7 @@ mocha@11.7.1: find-up "^5.0.0" glob "^10.4.5" he "^1.2.0" + is-path-inside "^3.0.3" js-yaml "^4.1.0" log-symbols "^4.1.0" minimatch "^9.0.5" @@ -2675,7 +2680,7 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tapable@^2.1.1, tapable@^2.2.0: +tapable@^2.1.1, tapable@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== @@ -2900,10 +2905,10 @@ webpack-sources@^3.3.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== -webpack@5.100.2: - version "5.100.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.100.2.tgz#e2341facf9f7de1d702147c91bcb65b693adf9e8" - integrity sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw== +webpack@5.101.3: + version "5.101.3" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.3.tgz#3633b2375bb29ea4b06ffb1902734d977bc44346" + integrity sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A== dependencies: "@types/eslint-scope" "^3.7.7" "@types/estree" "^1.0.8" @@ -2915,7 +2920,7 @@ webpack@5.100.2: acorn-import-phases "^1.0.3" browserslist "^4.24.0" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.17.2" + enhanced-resolve "^5.17.3" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" diff --git a/shared-ui/build.gradle.kts b/shared-ui/build.gradle.kts index 4e86b24b..18901db9 100644 --- a/shared-ui/build.gradle.kts +++ b/shared-ui/build.gradle.kts @@ -9,6 +9,11 @@ plugins { alias(libs.plugins.composeCompiler) } +// Force Kotlin stdlib for JS so compiler sees stdlib (fixes "Symbol for Any not found") +configurations.all { + resolutionStrategy.force("org.jetbrains.kotlin:kotlin-stdlib-js:${libs.versions.kotlin.get()}") +} + android { namespace = "co.touchlab.droidcon.sharedui" compileSdk = libs.versions.compileSdk.get().toInt() @@ -82,8 +87,6 @@ kotlin { api(libs.kotlinx.datetime) api(libs.multiplatformSettings.core) api(libs.uuid) - implementation(libs.coil.compose) - implementation(libs.coil.network) implementation(libs.bundles.ktor.common) implementation(libs.bundles.sqldelight.common) @@ -103,11 +106,15 @@ kotlin { implementation(libs.zoomimage.composeResources) implementation(libs.hyperdrive.multiplatformx.api) + implementation(libs.kamel.image.default) } val mobileMain by creating { dependsOn(commonMain.get()) dependencies { api(libs.kermit.crashlytics) + implementation(libs.coil.compose) + implementation(libs.coil.network) + implementation(compose.components.resources) } } @@ -130,7 +137,11 @@ kotlin { dependsOn(iosMain.get()) } jsMain.dependencies { - implementation(libs.kamel.image.default) + implementation(kotlin("stdlib-js")) + implementation(libs.kotlinx.browser) + implementation(libs.zoomimage.compose) + implementation(libs.kamel.image.default) // add this + } all { diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt index 7c095024..0d2d758a 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt @@ -1,34 +1,20 @@ package co.touchlab.droidcon.ui -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.height -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import co.touchlab.droidcon.domain.entity.Conference import co.touchlab.droidcon.ui.theme.DroidconTheme -import co.touchlab.droidcon.ui.util.dcImageLoader +import co.touchlab.droidcon.ui.util.InitImageLoader import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.viewmodel.ApplicationViewModel import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel -import coil3.annotation.ExperimentalCoilApi -import coil3.compose.setSingletonImageLoaderFactory -@OptIn(ExperimentalCoilApi::class) @Composable internal fun MainComposeView(waitForLoadedContextModel: WaitForLoadedContextModel, modifier: Modifier = Modifier) { - setSingletonImageLoaderFactory { context -> - dcImageLoader(context, true) - } + InitImageLoader() LaunchedEffect(Unit) { waitForLoadedContextModel.watchConferenceChanges() diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Image.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Image.kt index d458ab04..f762002f 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Image.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Image.kt @@ -2,20 +2,9 @@ package co.touchlab.droidcon.ui.util import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import coil3.ImageLoader -import coil3.PlatformContext -import coil3.request.crossfade -import coil3.util.DebugLogger @Composable expect fun DcAsyncImage(logTag: String, url: String?, contentDescription: String?, modifier: Modifier = Modifier) -fun dcImageLoader(context: PlatformContext, debug: Boolean = false): ImageLoader = ImageLoader.Builder(context) - .crossfade(true) - .apply { - if (debug) { - logger(DebugLogger()) - } - } - .build() - +@Composable +expect fun InitImageLoader() diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt index 4df998c6..86d17ebf 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt @@ -1,19 +1,9 @@ package co.touchlab.droidcon.ui.venue -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import coil3.compose.AsyncImagePainter -import coil3.compose.rememberAsyncImagePainter -import com.github.panpf.zoomimage.ZoomImage @Composable fun VenueView(venueMapUrl: String?) { @@ -26,30 +16,4 @@ fun VenueView(venueMapUrl: String?) { } @Composable -fun VenueBodyView(modifier: Modifier = Modifier, venueMapUrl: String?) { - val painter = rememberAsyncImagePainter(venueMapUrl) - val state by painter.state.collectAsState() - - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - when (state) { - is AsyncImagePainter.State.Empty, - is AsyncImagePainter.State.Loading, - -> { - CircularProgressIndicator() - } - is AsyncImagePainter.State.Error -> { - Text("Error loading venue map.") - } - is AsyncImagePainter.State.Success -> { - ZoomImage( - painter = painter, - contentDescription = null, - modifier = modifier.fillMaxSize(), - ) - } - } - } -} +expect fun VenueBodyView(modifier: Modifier = Modifier, venueMapUrl: String?) diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt index 64eb2f83..5b966bc7 100644 --- a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt @@ -8,18 +8,25 @@ import io.kamel.image.asyncPainterResource @Composable actual fun DcAsyncImage(logTag: String, url: String?, contentDescription: String?, modifier: Modifier) { - KamelImage( - resource = { - asyncPainterResource(data = "url") - }, - contentDescription = "Profile", - modifier = Modifier, - onFailure = { - Logger.e( - tag = logTag, - throwable = it, - messageString = "AsyncImage OnError Request = ${it}\n", - ) - }, - ) + if(url != null) { + KamelImage( + resource = { asyncPainterResource(data = url) }, + modifier = modifier, + contentDescription = "Profile", + + onFailure = { exception -> + Logger.e( + logTag, + throwable = exception, + message = { + "AsyncImage OnError Request = ${exception}\n" + }, + ) + }, + ) + } +} + +@Composable +actual fun InitImageLoader() { } diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt new file mode 100644 index 00000000..af3708f3 --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt @@ -0,0 +1,42 @@ +package co.touchlab.droidcon.ui.venue + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import com.github.panpf.zoomimage.ZoomImage +import io.kamel.core.Resource +import io.kamel.image.asyncPainterResource + +@Composable +actual fun VenueBodyView(modifier: Modifier, venueMapUrl: String?){ + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + if (venueMapUrl != null) { + when (val resource = asyncPainterResource(venueMapUrl)) { + is Resource.Loading -> { + CircularProgressIndicator() + } + is Resource.Success -> { + val painter: Painter = resource.value + ZoomImage( + painter = painter, + contentDescription = null, + modifier = modifier.fillMaxSize(), + ) + } + is Resource.Failure -> { + Text("Error loading venue map.") + } + } + } else { + CircularProgressIndicator() + } + } +} diff --git a/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/util/Image.mobile.kt b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/util/Image.mobile.kt index 7e98bca3..e39fe88d 100644 --- a/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/util/Image.mobile.kt +++ b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/util/Image.mobile.kt @@ -3,7 +3,13 @@ package co.touchlab.droidcon.ui.util import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import co.touchlab.kermit.Logger +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.annotation.ExperimentalCoilApi import coil3.compose.AsyncImage +import coil3.compose.setSingletonImageLoaderFactory +import coil3.request.crossfade +import coil3.util.DebugLogger @Composable actual fun DcAsyncImage(logTag: String, url: String?, contentDescription: String?, modifier: Modifier) { @@ -23,3 +29,19 @@ actual fun DcAsyncImage(logTag: String, url: String?, contentDescription: String ) } +@OptIn(ExperimentalCoilApi::class) +@Composable +actual fun InitImageLoader() { + setSingletonImageLoaderFactory { context -> + dcImageLoader(context, true) + } +} + +private fun dcImageLoader(context: PlatformContext, debug: Boolean = false): ImageLoader = ImageLoader.Builder(context) + .crossfade(true) + .apply { + if (debug) { + logger(DebugLogger()) + } + } + .build() diff --git a/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView.kt b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView.kt new file mode 100644 index 00000000..b72d695b --- /dev/null +++ b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView.kt @@ -0,0 +1,41 @@ +package co.touchlab.droidcon.ui.venue + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import coil3.compose.AsyncImagePainter +import coil3.compose.rememberAsyncImagePainter +import com.github.panpf.zoomimage.ZoomImage + +@Composable +actual fun VenueBodyView(modifier: Modifier, venueMapUrl: String?){ + val painter = rememberAsyncImagePainter(venueMapUrl) + val state by painter.state.collectAsState() + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + when (state) { + is AsyncImagePainter.State.Empty, + is AsyncImagePainter.State.Loading, + -> { CircularProgressIndicator() } + is AsyncImagePainter.State.Error -> { + Text("Error loading venue map.") + } + is AsyncImagePainter.State.Success -> { + ZoomImage( + painter = painter, + contentDescription = null, + modifier = modifier.fillMaxSize(), + ) + } + } + } +} From b8b42027c9ba18b093f5b1bee722e4d6ad676f6f Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Tue, 13 Feb 2024 16:47:42 -0500 Subject: [PATCH 25/56] Making ConferenceConfigProvider suspendable --- .../gateway/impl/DefaultSessionGateway.kt | 56 +++++++++---------- .../gateway/impl/DefaultSponsorGateway.kt | 10 ++-- .../service/ConferenceConfigProvider.kt | 14 ++--- .../impl/DefaultConferenceConfigProvider.kt | 38 ++++++------- 4 files changed, 58 insertions(+), 60 deletions(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt index 8aefd04c..afa7eb12 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt @@ -1,6 +1,7 @@ package co.touchlab.droidcon.domain.gateway.impl import co.touchlab.droidcon.domain.composite.ScheduleItem +import co.touchlab.droidcon.domain.composite.SponsorGroupWithSponsors import co.touchlab.droidcon.domain.entity.Session import co.touchlab.droidcon.domain.gateway.SessionGateway import co.touchlab.droidcon.domain.repository.ProfileRepository @@ -9,7 +10,10 @@ import co.touchlab.droidcon.domain.repository.SessionRepository import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.ScheduleService import co.touchlab.kermit.Logger +import kotlin.collections.map import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -24,9 +28,6 @@ class DefaultSessionGateway( private val log = Logger.withTag("DefaultSessionGateway") - private val conferenceId: Long? - get() = conferenceConfigProvider.getConferenceId() - override fun observeSchedule(): Flow> = conferenceConfigProvider.observeChanges().flatMapLatest { conf -> log.i { "observeSchedule: Conference: ${conf?.id}" } if (conf == null) { @@ -55,41 +56,40 @@ class DefaultSessionGateway( } override fun observeScheduleItem(id: Session.Id): Flow { - val confId = conferenceId ?: throw IllegalStateException("Conference ID is not available") - return sessionRepository.observe(id, confId).map { session -> - scheduleItemForSession(session) - } + return conferenceConfigProvider.observeChanges() + .filterNotNull() + .map { conference -> conference.id } + .distinctUntilChanged() + .flatMapLatest { confId -> + sessionRepository.observe(id, confId).map { session -> + scheduleItemForSession(session) + } + } } - private suspend fun scheduleItemForSession(session: Session): ScheduleItem = - if (conferenceId != null) - ScheduleItem( - session, - scheduleService.isInConflict(session), - session.room?.let { roomRepository.find(it, conferenceId!!) }, - profileRepository.getSpeakersBySession(session.id, conferenceId!!), - ) + private suspend fun scheduleItemForSession(session: Session): ScheduleItem { + val conferenceId = conferenceConfigProvider.getConferenceId() - else - ScheduleItem( - session, - scheduleService.isInConflict(session), - null, - emptyList(), - ) + return ScheduleItem( + session, + scheduleService.isInConflict(session), + session.room?.let { roomRepository.find(it, conferenceId) }, + profileRepository.getSpeakersBySession(session.id, conferenceId), + ) + } override suspend fun setAttending(session: Session, attending: Boolean) { - val confId = conferenceId ?: throw IllegalStateException("Conference ID is not available") - sessionRepository.setRsvp(session.id, Session.RSVP(attending, false), confId) + val conferenceId = conferenceConfigProvider.getConferenceId() + sessionRepository.setRsvp(session.id, Session.RSVP(attending, false), conferenceId) } override suspend fun setFeedback(session: Session, feedback: Session.Feedback) { - val confId = conferenceId ?: throw IllegalStateException("Conference ID is not available") - sessionRepository.setFeedback(session.id, feedback, confId) + val conferenceId = conferenceConfigProvider.getConferenceId() + sessionRepository.setFeedback(session.id, feedback, conferenceId) } override suspend fun getScheduleItem(id: Session.Id): ScheduleItem? { - val confId = conferenceId ?: return null - return sessionRepository.find(id, confId)?.let { scheduleItemForSession(it) } + val conferenceId = conferenceConfigProvider.getConferenceId() + return sessionRepository.find(id, conferenceId)?.let { scheduleItemForSession(it) } } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt index 85297064..10f4f64a 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull class DefaultSponsorGateway( private val sponsorRepository: SponsorRepository, @@ -61,14 +62,15 @@ class DefaultSponsorGateway( } override fun observeSponsorById(id: Sponsor.Id): Flow { - val conferenceId = conferenceConfigProvider.getConferenceId() - ?: throw IllegalStateException("Conference ID is not available") - return sponsorRepository.observe(id, conferenceId) + return conferenceConfigProvider.observeChanges() + .filterNotNull() + .flatMapLatest { conference -> + sponsorRepository.observe(id, conference.id) + } } override suspend fun getRepresentatives(sponsorId: Sponsor.Id): List { val conferenceId = conferenceConfigProvider.getConferenceId() - ?: throw IllegalStateException("Conference ID is not available") return profileRepository.getSponsorRepresentatives(sponsorId, conferenceId) } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt index ff0dce13..15f3fe0a 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt @@ -5,19 +5,19 @@ import kotlinx.coroutines.flow.Flow import kotlinx.datetime.TimeZone interface ConferenceConfigProvider { - fun getConferenceId(): Long? + suspend fun getConferenceId(): Long fun getConferenceTimeZone(): TimeZone? - fun getProjectId(): String? - fun getCollectionName(): String? - fun getApiKey(): String? - fun getScheduleId(): String? - fun showVenueMap(): Boolean? + fun getProjectId(): String + suspend fun getCollectionName(): String + suspend fun getApiKey(): String + suspend fun getScheduleId(): String + suspend fun showVenueMap(): Boolean fun observeChanges(): Flow /** * Get the currently selected conference */ - suspend fun getSelectedConference(): Conference? + suspend fun getSelectedConference(): Conference /** * Initiates conference observation. diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt index bdbc4431..2d66931a 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update import kotlinx.datetime.TimeZone class DefaultConferenceConfigProvider( @@ -16,42 +17,30 @@ class DefaultConferenceConfigProvider( initialConference: Conference? = null ) : ConferenceConfigProvider { private val log = Logger.withTag("DefaultConferenceConfigProvider") - private val _currentConferenceState = MutableStateFlow(initialConference) + private val _currentConferenceState = MutableStateFlow(initialConference) val currentConferenceState: StateFlow = _currentConferenceState private val currentConference: Conference? get() = currentConferenceState.value - override fun getConferenceId(): Long? { - return currentConference?.id - } + override suspend fun getConferenceId(): Long = getConference().id override fun getConferenceTimeZone(): TimeZone? = currentConference?.timeZone - override fun getProjectId(): String? = "droidcon-148cc" + override fun getProjectId(): String = "droidcon-148cc" - override fun getCollectionName(): String? = currentConference?.collectionName + override suspend fun getCollectionName(): String = getConference().collectionName - override fun getApiKey(): String? { - log.i { "Getting API Key $currentConference" } - return currentConference?.apiKey - } + override suspend fun getApiKey(): String = getConference().apiKey - override fun getScheduleId(): String? = currentConference?.scheduleId + override suspend fun getScheduleId(): String = getConference().scheduleId - override fun showVenueMap(): Boolean? = true // Default to true, will be configurable per conference later + override suspend fun showVenueMap(): Boolean = true // Default to true, will be configurable per conference later - override fun observeChanges(): Flow = conferenceRepository.observeSelected() - .map { it } - .catch { emit(null) } + override fun observeChanges(): Flow = conferenceRepository.observeSelected() // Implementation of the interface method to get the currently selected conference - override suspend fun getSelectedConference(): Conference? = try { - conferenceRepository.getSelected() - } catch (e: Exception) { - log.w { "No conference selected: ${e.message}" } - null - } + override suspend fun getSelectedConference(): Conference = conferenceRepository.getSelected() // Implementation of the interface method to load the conference asynchronously // Also sets up continuous observation of conference changes @@ -68,4 +57,11 @@ class DefaultConferenceConfigProvider( _currentConferenceState.value = conference } } + + private suspend fun getConference(): Conference { + if(currentConference != null) return currentConference!! + val conference = conferenceRepository.getSelected() + _currentConferenceState.update { conference } + return conference + } } From 876e38b0e80a75a5726d67e4dd2171b9f626f6c5 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 15 Apr 2026 15:01:02 -0400 Subject: [PATCH 26/56] Adding adaptive layout features --- gradle/libs.versions.toml | 6 + shared-ui/build.gradle.kts | 5 + .../ui/BottomNavigationView.kt | 187 +++++++++++++++++- .../ui/session/SessionListView.kt | 5 +- .../viewmodel/ApplicationViewModel.kt | 1 + .../ui/util/LocalImage.js.kt | 6 +- 6 files changed, 200 insertions(+), 10 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5bfd79ed..de7be3cc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,9 @@ [versions] ## SDK Versions +adaptive = "1.3.0-alpha06" kamelImageDefault = "1.0.9" kotlinxBrowser = "0.5.0" +material3AdaptiveNavigationSuiteVersion = "1.9.0" minSdk = "23" targetSdk = "36" compileSdk = "36" @@ -57,11 +59,15 @@ zoomimage = "1.4.0" skie = "0.10.10" [libraries] +adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "adaptive" } +adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "adaptive" } +adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "adaptive" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } kamel-image-default = { module = "media.kamel:kamel-image-default", version.ref = "kamelImageDefault" } kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinxBrowser" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +material3-adaptive-navigation-suite = { module = "org.jetbrains.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavigationSuiteVersion" } multiplatform-settings-make-observable = { module = "com.russhwolf:multiplatform-settings-make-observable", version.ref = "multiplatformSettings" } sqliter = { module = "co.touchlab:sqliter-driver", version.ref = "sqliter" } compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose-compiler" } diff --git a/shared-ui/build.gradle.kts b/shared-ui/build.gradle.kts index 18901db9..9ae5c64e 100644 --- a/shared-ui/build.gradle.kts +++ b/shared-ui/build.gradle.kts @@ -107,6 +107,11 @@ kotlin { implementation(libs.hyperdrive.multiplatformx.api) implementation(libs.kamel.image.default) + implementation(libs.adaptive) + implementation(libs.adaptive.layout) + implementation(libs.adaptive.navigation) + implementation(libs.material3.adaptive.navigation.suite) + } val mobileMain by creating { dependsOn(commonMain.get()) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt index 4f117661..40655bea 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.ui +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -12,12 +13,27 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemColors import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.NavigationDrawerItemColors +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.NavigationRailItemColors import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective +import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldLayout import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.window.core.layout.WindowSizeClass import co.touchlab.droidcon.domain.entity.Conference import co.touchlab.droidcon.ui.session.SessionListView import co.touchlab.droidcon.ui.settings.SettingsView @@ -26,10 +42,172 @@ import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.ui.venue.VenueView import co.touchlab.droidcon.viewmodel.ApplicationViewModel +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable -internal fun BottomNavigationView(viewModel: ApplicationViewModel, currentConference: Conference, modifier: Modifier = Modifier) { +internal fun BottomNavigationView( + viewModel: ApplicationViewModel, + currentConference: Conference, + modifier: Modifier = Modifier, +) { val selectedTab by viewModel.observeSelectedTab.observeAsState() + val navigator = rememberListDetailPaneScaffoldNavigator() + val iconColor = MaterialTheme.colorScheme.onPrimary + val textColor = MaterialTheme.colorScheme.primary + val indicatorColor = MaterialTheme.colorScheme.primary + val railColors = NavigationRailItemColors( + selectedIconColor = iconColor, + selectedTextColor = textColor, + selectedIndicatorColor = indicatorColor, + unselectedIconColor = iconColor, + unselectedTextColor = textColor, + disabledIconColor = iconColor, + disabledTextColor = textColor, + ) + val barColors = NavigationBarItemColors( + selectedIconColor = iconColor, + selectedTextColor = textColor, + selectedIndicatorColor = indicatorColor, + unselectedIconColor = iconColor, + unselectedTextColor = textColor, + disabledIconColor = iconColor, + disabledTextColor = textColor, + ) + val drawerColors = NavigationDrawerItemDefaults.colors( + selectedIconColor = iconColor, + selectedTextColor = MaterialTheme.colorScheme.primary, + selectedContainerColor = MaterialTheme.colorScheme.primary, + ) + ListDetailPaneScaffold( + modifier = modifier, + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + Box(Modifier.background(Color.Red)) { + NavigationSuiteScaffold( + navigationSuiteItems = { + viewModel.listTabs(currentConference).forEach { tab -> + val (title, icon) = when (tab) { + ApplicationViewModel.Tab.Schedule -> "Schedule" to Icons.Filled.CalendarMonth + // FIXME: Was originally "My agenda" but then it doesn't seem to fit. + ApplicationViewModel.Tab.MyAgenda -> "Agenda" to Icons.Filled.Schedule + ApplicationViewModel.Tab.Venue -> "Venue" to Icons.Filled.Map + ApplicationViewModel.Tab.Sponsors -> "Sponsors" to Icons.Filled.LocalFireDepartment + ApplicationViewModel.Tab.Settings -> "Settings" to Icons.Filled.Settings + } + item( + icon = { Icon(imageVector = icon, contentDescription = null) }, + label = { Text(text = title) }, + selected = selectedTab == tab, + onClick = { + viewModel.selectedTab = tab + }, + colors = NavigationSuiteItemColors( + navigationRailItemColors = railColors, + navigationBarItemColors = barColors, + navigationDrawerItemColors = drawerColors, + ) + ) + } + }, + content = { + Box { + when (selectedTab) { + ApplicationViewModel.Tab.Schedule -> SessionListView( + viewModel = viewModel.schedule, + title = currentConference.name, + emptyText = "Sessions could not be loaded.", + ) + + ApplicationViewModel.Tab.MyAgenda -> SessionListView( + viewModel = viewModel.agenda, + title = "Agenda", + emptyText = "Add sessions to your agenda from session detail in schedule.", + ) + + ApplicationViewModel.Tab.Venue -> VenueView(currentConference.venueMap) + ApplicationViewModel.Tab.Sponsors -> SponsorsView(viewModel.sponsors) + ApplicationViewModel.Tab.Settings -> SettingsView(viewModel.settings) + } + } + } + ) + } + }, + detailPane = {}, + ) + + val feedback by viewModel.observePresentedFeedback.observeAsState() + feedback?.let { + FeedbackDialog(it) + } +}/* + ListDetailPaneScaffold( + modifier = modifier, + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + NavigationSuiteScaffold( + navigationSuiteItems = { + viewModel.listTabs(currentConference).forEach { tab -> + val (title, icon) = when (tab) { + ApplicationViewModel.Tab.Schedule -> "Schedule" to Icons.Filled.CalendarMonth + // FIXME: Was originally "My agenda" but then it doesn't seem to fit. + ApplicationViewModel.Tab.MyAgenda -> "Agenda" to Icons.Filled.Schedule + ApplicationViewModel.Tab.Venue -> "Venue" to Icons.Filled.Map + ApplicationViewModel.Tab.Sponsors -> "Sponsors" to Icons.Filled.LocalFireDepartment + ApplicationViewModel.Tab.Settings -> "Settings" to Icons.Filled.Settings + } + item( + icon = { Icon(imageVector = icon, contentDescription = null) }, + label = { Text(text = title) }, + selected = selectedTab == tab, + onClick = { + viewModel.selectedTab = tab + }, + colors = NavigationBarItemDefaults.colors( + indicatorColor = MaterialTheme.colorScheme.primary, + selectedIconColor = MaterialTheme.colorScheme.onPrimary, + selectedTextColor = MaterialTheme.colorScheme.primary, + ), + ) + } + } + ) + }) + { + innerPadding -> + Box(modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding())) { + when (selectedTab) { + ApplicationViewModel.Tab.Schedule -> SessionListView( + viewModel = viewModel.schedule, + title = currentConference.name, + emptyText = "Sessions could not be loaded.", + ) + + ApplicationViewModel.Tab.MyAgenda -> SessionListView( + viewModel = viewModel.agenda, + title = "Agenda", + emptyText = "Add sessions to your agenda from session detail in schedule.", + ) + + ApplicationViewModel.Tab.Venue -> VenueView(currentConference.venueMap) + ApplicationViewModel.Tab.Sponsors -> SponsorsView(viewModel.sponsors) + ApplicationViewModel.Tab.Settings -> SettingsView(viewModel.settings) + } + } + } + }, + detailPane = { + Text("World") + + }, + extraPane = { + Text("Etc") + + }, + )*/ +/* Scaffold( modifier = modifier, bottomBar = { @@ -80,9 +258,4 @@ internal fun BottomNavigationView(viewModel: ApplicationViewModel, currentConfer } } } - - val feedback by viewModel.observePresentedFeedback.observeAsState() - feedback?.let { - FeedbackDialog(it) - } -} +*/ diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt index 1007e1f3..b1c8571d 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import co.touchlab.droidcon.ui.CircularProgressIndicator import co.touchlab.droidcon.ui.theme.Dimensions import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.util.NavigationStack @@ -86,7 +87,9 @@ internal fun SessionListView(viewModel: BaseSessionListViewModel, title: String, .padding(top = innerPadding.calculateTopPadding()), ) { val days by viewModel.observeDays.observeAsState() - if (days?.isEmpty() != false) { + if(days == null){ + CircularProgressIndicator("Updating Droidcon Events!") + } else if (days?.isEmpty() != false) { EmptyView(emptyText) } else { val selectedDay by viewModel.observeSelectedDay.observeAsState() diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt index 3dc39645..e2867c29 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt @@ -79,6 +79,7 @@ class ApplicationViewModel( lifecycle.whileAttached { try { syncService.runSynchronization(conference = conference) + syncService.forceSynchronize(conference) } catch (e: Exception) { log.e(e) { "Error starting sync service" } } diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt index 4abbd124..7e720b48 100644 --- a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.layout.ContentScale import co.touchlab.droidcon.ui.theme.Dimensions +import kotlin.js.js import kotlinx.browser.window import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -33,9 +34,10 @@ import org.khronos.webgl.Int8Array internal actual fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?) { val painter: Painter? by produceState(null, imageResourceName) { value = try { - val fetched = window.fetch("drawable/$imageResourceName").await() + val fetched = window.fetch("drawable/$imageResourceName", js("{}")).await() if (fetched.ok) { - val bytes = Int8Array(fetched.arrayBuffer().await()).unsafeCast() + val buffer = fetched.arrayBuffer().await() + val bytes = Int8Array(buffer).unsafeCast() makeFromEncoded(bytes).toComposeImageBitmap().let(::BitmapPainter) } else { null From 790c44b8b3a41e459224b37d49ff96a53c4158a8 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 22 Apr 2026 08:56:02 -0400 Subject: [PATCH 27/56] Updating date and main view --- .../droidcon/ios/DependencyInjection.kt | 4 - .../ios/util/formatter/IOSDateFormatter.kt | 52 ---- .../ui/BottomNavigationView.kt | 219 +++------------ .../ui/session/SessionBlockView.kt | 3 +- .../ui/session/SessionDetailView.kt | 253 +++++++++++------- .../session/SessionListDetailPaneScaffold.kt | 79 ++++++ .../ui/session/SessionListView.kt | 232 ++++++++-------- .../{VenueView.kt => VenueView_Mobile.kt} | 0 .../co/touchlab/droidcon/Koin.android.kt | 9 - .../util/formatter/AndroidDateFormatter.kt | 40 --- .../kotlin/co/touchlab/droidcon/Koin.kt | 6 + .../util/formatter/KotlinXDateFormatter.kt | 33 +++ .../kotlin/co/touchlab/droidcon/Koin.js.kt | 6 - .../util/formatter/JsDateFormatter.kt | 13 - .../co/touchlab/droidcon/web/KoinTest.kt | 4 +- .../droidcon/web/DependencyInjection.kt | 4 - .../web/util/formatter/WebDateFormatter.kt | 14 - 17 files changed, 432 insertions(+), 539 deletions(-) delete mode 100644 ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/util/formatter/IOSDateFormatter.kt create mode 100644 shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt rename shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/{VenueView.kt => VenueView_Mobile.kt} (100%) delete mode 100644 shared/src/androidMain/kotlin/co/touchlab/droidcon/util/formatter/AndroidDateFormatter.kt create mode 100644 shared/src/commonMain/kotlin/co/touchlab/droidcon/util/formatter/KotlinXDateFormatter.kt delete mode 100644 shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt delete mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/util/formatter/WebDateFormatter.kt diff --git a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt index 3b658b17..20efce88 100644 --- a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt +++ b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt @@ -6,11 +6,9 @@ import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.ios.service.DefaultParseUrlViewService import co.touchlab.droidcon.ios.util.NotificationLocalizedStringFactory -import co.touchlab.droidcon.ios.util.formatter.IOSDateFormatter import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule import co.touchlab.droidcon.util.BundleResourceReader -import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.NSUserDefaultsSettings @@ -28,8 +26,6 @@ fun initKoinIos(userDefaults: NSUserDefaults, analyticsService: AnalyticsService single { NSUserDefaultsSettings(delegate = userDefaults) } single { BundleResourceReader(bundle = get().bundle) } - single { IOSDateFormatter() } - single { NotificationLocalizedStringFactory(bundle = get().bundle) } diff --git a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/util/formatter/IOSDateFormatter.kt b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/util/formatter/IOSDateFormatter.kt deleted file mode 100644 index 807c0676..00000000 --- a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/util/formatter/IOSDateFormatter.kt +++ /dev/null @@ -1,52 +0,0 @@ -package co.touchlab.droidcon.ios.util.formatter - -import co.touchlab.droidcon.util.formatter.DateFormatter -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.toNSDateComponents -import platform.Foundation.NSCalendar -import platform.Foundation.NSDateFormatter -import platform.Foundation.NSDateFormatterNoStyle -import platform.Foundation.NSDateFormatterShortStyle -import platform.Foundation.NSLocale -import platform.Foundation.currentLocale - -class IOSDateFormatter : DateFormatter { - - private val monthWithDay: NSDateFormatter by lazy { - NSDateFormatter().also { - val dateTemplate = "MMM d" - it.dateFormat = NSDateFormatter.dateFormatFromTemplate(dateTemplate, 0.toULong(), NSLocale.currentLocale)!! - } - } - - private val timeOnly: NSDateFormatter by lazy { - NSDateFormatter().also { - it.dateStyle = NSDateFormatterNoStyle - it.timeStyle = NSDateFormatterShortStyle - } - } - - private val timeOnlyNoPeriod: NSDateFormatter by lazy { - NSDateFormatter().also { - val dateTemplate = "hh:mm" - it.dateFormat = NSDateFormatter.dateFormatFromTemplate(dateTemplate, 0.toULong(), NSLocale.currentLocale)!! - } - } - - override fun monthWithDay(date: LocalDate) = date.date()?.let { monthWithDay.stringFromDate(it) } - - override fun timeOnly(dateTime: LocalDateTime) = dateTime.date()?.let { timeOnly.stringFromDate(it) } - - override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime) = interval( - fromDateTime.date()?.let { timeOnlyNoPeriod.stringFromDate(it) }, - toDateTime.date()?.let { timeOnly.stringFromDate(it) }, - ) - - private fun LocalDate.date() = NSCalendar.currentCalendar.dateFromComponents(toNSDateComponents()) - - // This uses the device time zone, which is appropriate for local date/time formatting - private fun LocalDateTime.date() = NSCalendar.currentCalendar.dateFromComponents(toNSDateComponents()) - - private fun interval(from: String?, to: String?) = listOfNotNull(from, to).joinToString(" – ") -} diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt index 40655bea..b1d4afdd 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt @@ -1,8 +1,5 @@ package co.touchlab.droidcon.ui -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.LocalFireDepartment @@ -11,36 +8,25 @@ import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemColors -import androidx.compose.material3.NavigationBarItemDefaults -import androidx.compose.material3.NavigationDrawerItemColors import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.NavigationRailItemColors -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold -import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective -import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldLayout import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.window.core.layout.WindowSizeClass import co.touchlab.droidcon.domain.entity.Conference -import co.touchlab.droidcon.ui.session.SessionListView +import co.touchlab.droidcon.ui.session.SessionListDetailPaneScaffold import co.touchlab.droidcon.ui.settings.SettingsView import co.touchlab.droidcon.ui.sponsors.SponsorsView import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.ui.venue.VenueView import co.touchlab.droidcon.viewmodel.ApplicationViewModel +import co.touchlab.droidcon.viewmodel.session.BaseSessionListViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable @@ -50,8 +36,6 @@ internal fun BottomNavigationView( modifier: Modifier = Modifier, ) { val selectedTab by viewModel.observeSelectedTab.observeAsState() - val navigator = rememberListDetailPaneScaffoldNavigator() - val iconColor = MaterialTheme.colorScheme.onPrimary val textColor = MaterialTheme.colorScheme.primary val indicatorColor = MaterialTheme.colorScheme.primary @@ -78,175 +62,43 @@ internal fun BottomNavigationView( selectedTextColor = MaterialTheme.colorScheme.primary, selectedContainerColor = MaterialTheme.colorScheme.primary, ) - ListDetailPaneScaffold( - modifier = modifier, - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, - listPane = { - Box(Modifier.background(Color.Red)) { - NavigationSuiteScaffold( - navigationSuiteItems = { - viewModel.listTabs(currentConference).forEach { tab -> - val (title, icon) = when (tab) { - ApplicationViewModel.Tab.Schedule -> "Schedule" to Icons.Filled.CalendarMonth - // FIXME: Was originally "My agenda" but then it doesn't seem to fit. - ApplicationViewModel.Tab.MyAgenda -> "Agenda" to Icons.Filled.Schedule - ApplicationViewModel.Tab.Venue -> "Venue" to Icons.Filled.Map - ApplicationViewModel.Tab.Sponsors -> "Sponsors" to Icons.Filled.LocalFireDepartment - ApplicationViewModel.Tab.Settings -> "Settings" to Icons.Filled.Settings - } - item( - icon = { Icon(imageVector = icon, contentDescription = null) }, - label = { Text(text = title) }, - selected = selectedTab == tab, - onClick = { - viewModel.selectedTab = tab - }, - colors = NavigationSuiteItemColors( - navigationRailItemColors = railColors, - navigationBarItemColors = barColors, - navigationDrawerItemColors = drawerColors, - ) - ) - } - }, - content = { - Box { - when (selectedTab) { - ApplicationViewModel.Tab.Schedule -> SessionListView( - viewModel = viewModel.schedule, - title = currentConference.name, - emptyText = "Sessions could not be loaded.", - ) - - ApplicationViewModel.Tab.MyAgenda -> SessionListView( - viewModel = viewModel.agenda, - title = "Agenda", - emptyText = "Add sessions to your agenda from session detail in schedule.", - ) - - ApplicationViewModel.Tab.Venue -> VenueView(currentConference.venueMap) - ApplicationViewModel.Tab.Sponsors -> SponsorsView(viewModel.sponsors) - ApplicationViewModel.Tab.Settings -> SettingsView(viewModel.settings) - } - } - } - ) - } - }, - detailPane = {}, - ) - - val feedback by viewModel.observePresentedFeedback.observeAsState() - feedback?.let { - FeedbackDialog(it) - } -}/* - ListDetailPaneScaffold( - modifier = modifier, - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, - listPane = { - NavigationSuiteScaffold( - navigationSuiteItems = { - viewModel.listTabs(currentConference).forEach { tab -> - val (title, icon) = when (tab) { - ApplicationViewModel.Tab.Schedule -> "Schedule" to Icons.Filled.CalendarMonth - // FIXME: Was originally "My agenda" but then it doesn't seem to fit. - ApplicationViewModel.Tab.MyAgenda -> "Agenda" to Icons.Filled.Schedule - ApplicationViewModel.Tab.Venue -> "Venue" to Icons.Filled.Map - ApplicationViewModel.Tab.Sponsors -> "Sponsors" to Icons.Filled.LocalFireDepartment - ApplicationViewModel.Tab.Settings -> "Settings" to Icons.Filled.Settings - } - item( - icon = { Icon(imageVector = icon, contentDescription = null) }, - label = { Text(text = title) }, - selected = selectedTab == tab, - onClick = { - viewModel.selectedTab = tab - }, - colors = NavigationBarItemDefaults.colors( - indicatorColor = MaterialTheme.colorScheme.primary, - selectedIconColor = MaterialTheme.colorScheme.onPrimary, - selectedTextColor = MaterialTheme.colorScheme.primary, - ), - ) - } - } - ) - }) - { - innerPadding -> - Box(modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding())) { - when (selectedTab) { - ApplicationViewModel.Tab.Schedule -> SessionListView( - viewModel = viewModel.schedule, - title = currentConference.name, - emptyText = "Sessions could not be loaded.", - ) - - ApplicationViewModel.Tab.MyAgenda -> SessionListView( - viewModel = viewModel.agenda, - title = "Agenda", - emptyText = "Add sessions to your agenda from session detail in schedule.", - ) - - ApplicationViewModel.Tab.Venue -> VenueView(currentConference.venueMap) - ApplicationViewModel.Tab.Sponsors -> SponsorsView(viewModel.sponsors) - ApplicationViewModel.Tab.Settings -> SettingsView(viewModel.settings) - } - } - } - }, - detailPane = { - Text("World") - - }, - extraPane = { - Text("Etc") - }, - )*/ -/* - Scaffold( + NavigationSuiteScaffold( modifier = modifier, - bottomBar = { - NavigationBar { - viewModel.listTabs(currentConference).forEach { tab -> - val (title, icon) = when (tab) { - ApplicationViewModel.Tab.Schedule -> "Schedule" to Icons.Filled.CalendarMonth - // FIXME: Was originally "My agenda" but then it doesn't seem to fit. - ApplicationViewModel.Tab.MyAgenda -> "Agenda" to Icons.Filled.Schedule - ApplicationViewModel.Tab.Venue -> "Venue" to Icons.Filled.Map - ApplicationViewModel.Tab.Sponsors -> "Sponsors" to Icons.Filled.LocalFireDepartment - ApplicationViewModel.Tab.Settings -> "Settings" to Icons.Filled.Settings - } - NavigationBarItem( - icon = { Icon(imageVector = icon, contentDescription = null) }, - label = { Text(text = title) }, - selected = selectedTab == tab, - onClick = { - viewModel.selectedTab = tab - }, - colors = NavigationBarItemDefaults.colors( - indicatorColor = MaterialTheme.colorScheme.primary, - selectedIconColor = MaterialTheme.colorScheme.onPrimary, - selectedTextColor = MaterialTheme.colorScheme.primary, - ), - ) + navigationSuiteItems = { + viewModel.listTabs(currentConference).forEach { tab -> + val (title, icon) = when (tab) { + ApplicationViewModel.Tab.Schedule -> "Schedule" to Icons.Filled.CalendarMonth + // FIXME: Was originally "My agenda" but then it doesn't seem to fit. + ApplicationViewModel.Tab.MyAgenda -> "Agenda" to Icons.Filled.Schedule + ApplicationViewModel.Tab.Venue -> "Venue" to Icons.Filled.Map + ApplicationViewModel.Tab.Sponsors -> "Sponsors" to Icons.Filled.LocalFireDepartment + ApplicationViewModel.Tab.Settings -> "Settings" to Icons.Filled.Settings } + item( + icon = { Icon(imageVector = icon, contentDescription = null) }, + label = { Text(text = title) }, + selected = selectedTab == tab, + onClick = { + viewModel.selectedTab = tab + }, + colors = NavigationSuiteItemColors( + navigationRailItemColors = railColors, + navigationBarItemColors = barColors, + navigationDrawerItemColors = drawerColors, + ), + ) } }, - ) { innerPadding -> - Box(modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding())) { + content = { when (selectedTab) { - ApplicationViewModel.Tab.Schedule -> SessionListView( + ApplicationViewModel.Tab.Schedule -> SessionListDetailPaneScaffold( viewModel = viewModel.schedule, title = currentConference.name, emptyText = "Sessions could not be loaded.", ) - ApplicationViewModel.Tab.MyAgenda -> SessionListView( + ApplicationViewModel.Tab.MyAgenda -> SessionListDetailPaneScaffold( viewModel = viewModel.agenda, title = "Agenda", emptyText = "Add sessions to your agenda from session detail in schedule.", @@ -256,6 +108,15 @@ internal fun BottomNavigationView( ApplicationViewModel.Tab.Sponsors -> SponsorsView(viewModel.sponsors) ApplicationViewModel.Tab.Settings -> SettingsView(viewModel.settings) } - } + }, + ) + + + + + + val feedback by viewModel.observePresentedFeedback.observeAsState() + feedback?.let { + FeedbackDialog(it) } -*/ +} diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionBlockView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionBlockView.kt index df0dcd47..f4d0d39f 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionBlockView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionBlockView.kt @@ -29,7 +29,7 @@ import co.touchlab.droidcon.viewmodel.session.SessionBlockViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun SessionBlockView(sessionsBlock: SessionBlockViewModel) { +internal fun SessionBlockView(sessionsBlock: SessionBlockViewModel, onClick: () -> Unit) { Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.TopStart) { Text( text = sessionsBlock.time, @@ -56,6 +56,7 @@ internal fun SessionBlockView(sessionsBlock: SessionBlockViewModel) { modifier = Modifier.weight(1f), onClick = { session.selected() + onClick() }, enabled = isClickable, ) { diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt index cbd4d415..897061e7 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt @@ -51,138 +51,199 @@ import co.touchlab.droidcon.ui.util.WebLinkText import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.util.NavigationController import co.touchlab.droidcon.util.NavigationStack +import co.touchlab.droidcon.viewmodel.FeedbackDialogViewModel +import co.touchlab.droidcon.viewmodel.session.SessionDayViewModel import co.touchlab.droidcon.viewmodel.session.SessionDetailViewModel +import co.touchlab.droidcon.viewmodel.session.SessionDetailViewModel.SessionState import co.touchlab.droidcon.viewmodel.session.SpeakerListItemViewModel private const val LOG_TAG = "SessionDetailView" @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun SessionDetailView(viewModel: SessionDetailViewModel) { - NavigationStack( - key = viewModel, - links = { - navigationLink(viewModel.observePresentedSpeakerDetail) { - SpeakerDetailView(viewModel = it) - } - }, - ) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopAppBar( - title = { Text("Session") }, - navigationIcon = { - IconButton(onClick = { NavigationController.root.handleBackPress() }) { +internal fun SessionDetailView( + scrollStateValue: Int, + state: SessionState?, + title: String, + description: String?, + descriptionLinks: List, + locationInfo: String, + isAttending: Boolean, + showFeedbackOption: Boolean, + feedbackAlreadyWritten: Boolean, + showBackButton: Boolean, + speakers: List, + feedback: FeedbackDialogViewModel?, + attendingTapped:()-> Unit, + writeFeedbackTapped: () -> Unit, + onScrollStateChanged:(Int)-> Unit, + onBack: (() -> Unit)? = null, +) { + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = { Text("Session") }, + navigationIcon = { + if(showBackButton) { + IconButton( + onClick = { + if (onBack != null) { + onBack() + } else { + NavigationController.root.handleBackPress() + } + }, + ) { Icon( imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back", ) } - }, - scrollBehavior = scrollBehavior, - ) - }, - ) { paddingValues -> - val scrollState = rememberScrollState(viewModel.scrollState) - - if (viewModel.scrollState != scrollState.value) { - viewModel.scrollState = scrollState.value - } - - Column( - modifier = Modifier - .verticalScroll(scrollState) - .padding(top = paddingValues.calculateTopPadding()), - ) { - val state by viewModel.observeState.observeAsState() - Box(contentAlignment = Alignment.BottomStart) { - Column(modifier = Modifier.padding(bottom = 22.dp)) { - val title by viewModel.observeTitle.observeAsState() - val locationInfo by viewModel.observeInfo.observeAsState() - HeaderView(title, locationInfo) - HorizontalDivider() - } - if (state != SessionDetailViewModel.SessionState.Ended) { - val isAttending by viewModel.observeIsAttending.observeAsState() - FloatingActionButton( - onClick = viewModel::attendingTapped, - modifier = Modifier - .padding(start = Dimensions.Padding.default) - .size(44.dp), - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ) { - val icon = - if (isAttending) Icons.Default.BookmarkAdded else Icons.Outlined.BookmarkAdd - val description = if (isAttending) { - "Do not attend" - } else { - "Attend" - } - Icon(imageVector = icon, contentDescription = description) - } } - } + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { paddingValues -> + val scrollState = rememberScrollState(scrollStateValue) - val status = when (state) { - SessionDetailViewModel.SessionState.InConflict -> "This session is in conflict with another session in your schedule." - SessionDetailViewModel.SessionState.InProgress -> "This session is happening now." - SessionDetailViewModel.SessionState.Ended -> "This session has already ended." - null -> "This session hasn't started yet." - } - InfoView(status) + if (scrollStateValue != scrollState.value) { + onScrollStateChanged(scrollState.value) + } - val showFeedbackOption by viewModel.observeShowFeedbackOption.observeAsState() - if (showFeedbackOption) { - Button( - onClick = viewModel::writeFeedbackTapped, + Column( + modifier = Modifier + .verticalScroll(scrollState) + .padding(top = paddingValues.calculateTopPadding()), + ) { + Box(contentAlignment = Alignment.BottomStart) { + Column(modifier = Modifier.padding(bottom = 22.dp)) { + HeaderView(title, locationInfo) + HorizontalDivider() + } + if (state != SessionDetailViewModel.SessionState.Ended) { + FloatingActionButton( + onClick = attendingTapped, modifier = Modifier - .padding(Dimensions.Padding.default) - .align(Alignment.CenterHorizontally), + .padding(start = Dimensions.Padding.default) + .size(44.dp), + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, ) { - val feedbackAlreadyWritten by viewModel.observeFeedbackAlreadyWritten.observeAsState() - val text = if (feedbackAlreadyWritten) { - "Change your feedback" + val icon = + if (isAttending) Icons.Default.BookmarkAdded else Icons.Outlined.BookmarkAdd + val description = if (isAttending) { + "Do not attend" } else { - "Add feedback" + "Attend" } - Text(text = text) + Icon(imageVector = icon, contentDescription = description) } } + } - val description by viewModel.observeAbstract.observeAsState() - val descriptionLinks by viewModel.observeAbstractLinks.observeAsState() - description?.let { - DescriptionView(it, descriptionLinks) + val status = when (state) { + SessionState.InConflict -> "This session is in conflict with another session in your schedule." + SessionState.InProgress -> "This session is happening now." + SessionState.Ended -> "This session has already ended." + null -> "This session hasn't started yet." + } + InfoView(status) + + if (showFeedbackOption) { + Button( + onClick = writeFeedbackTapped, + modifier = Modifier + .padding(Dimensions.Padding.default) + .align(Alignment.CenterHorizontally), + ) { + val text = if (feedbackAlreadyWritten) { + "Change your feedback" + } else { + "Add feedback" + } + Text(text = text) } + } - val speakers by viewModel.observeSpeakers.observeAsState() - if (speakers.isNotEmpty()) { - Text( - text = "Speakers", - modifier = Modifier.fillMaxWidth().padding(Dimensions.Padding.default), - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center, - ) - HorizontalDivider() + description?.let { + DescriptionView(it, descriptionLinks) + } - speakers.forEach { speaker -> - SpeakerView(speaker) - } + if (speakers.isNotEmpty()) { + Text( + text = "Speakers", + modifier = Modifier.fillMaxWidth().padding(Dimensions.Padding.default), + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + ) + + HorizontalDivider() + + speakers.forEach { speaker -> + SpeakerView(speaker) } } } } - val feedback by viewModel.observePresentedFeedback.observeAsState() feedback?.let { FeedbackDialog(it) } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SessionDetailView( + viewModel: SessionDetailViewModel, + showBackButton: Boolean, + onBack: (() -> Unit)?, +) { + NavigationStack( + key = viewModel, + links = { + navigationLink(viewModel.observePresentedSpeakerDetail) { + SpeakerDetailView(viewModel = it) + } + }, + ) { + val scrollStateValue = viewModel.scrollState + val state by viewModel.observeState.observeAsState() + val title by viewModel.observeTitle.observeAsState() + val description by viewModel.observeAbstract.observeAsState() + val descriptionLinks by viewModel.observeAbstractLinks.observeAsState() + val locationInfo by viewModel.observeInfo.observeAsState() + val isAttending by viewModel.observeIsAttending.observeAsState() + val showFeedbackOption by viewModel.observeShowFeedbackOption.observeAsState() + val feedbackAlreadyWritten by viewModel.observeFeedbackAlreadyWritten.observeAsState() + val speakers by viewModel.observeSpeakers.observeAsState() + val feedback by viewModel.observePresentedFeedback.observeAsState() + + SessionDetailView( + scrollStateValue = scrollStateValue, + state = state, + title = title, + description = description, + descriptionLinks = descriptionLinks, + locationInfo = locationInfo, + isAttending = isAttending, + showFeedbackOption = showFeedbackOption, + feedbackAlreadyWritten = feedbackAlreadyWritten, + showBackButton = showBackButton, + speakers = speakers, + feedback = feedback, + attendingTapped = viewModel::attendingTapped, + writeFeedbackTapped = viewModel::writeFeedbackTapped, + onScrollStateChanged = { viewModel.scrollState = it }, + onBack = onBack, + ) + } +} + @Composable private fun HeaderView(title: String, locationInfo: String) { Column( diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt new file mode 100644 index 00000000..25834d25 --- /dev/null +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt @@ -0,0 +1,79 @@ +package co.touchlab.droidcon.ui.session + +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import co.touchlab.droidcon.ui.util.observeAsState +import co.touchlab.droidcon.viewmodel.session.BaseSessionListViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun SessionListDetailPaneScaffold(viewModel: BaseSessionListViewModel, title: String, emptyText: String, modifier: Modifier = Modifier) { + val scope = rememberCoroutineScope() + val navigator = rememberListDetailPaneScaffoldNavigator() + val presentedSessionDetail by viewModel.observePresentedSessionDetail.observeAsState() + + ListDetailPaneScaffold( + modifier = modifier, + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + AnimatedPane( + enterTransition = slideInHorizontally { -it }, + exitTransition = slideOutHorizontally { -it }, + ) { + SessionListView( + viewModel = viewModel, + title = title, + emptyText = emptyText, + ) { + scope.launch { + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail) + } + } + } + }, + detailPane = { + AnimatedPane( + enterTransition = slideInHorizontally { it }, + exitTransition = slideOutHorizontally { it }, + ) { + val listPaneAdapted = navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] + val fullWidth = listPaneAdapted == PaneAdaptedValue.Hidden + val detailViewModel = presentedSessionDetail + if (detailViewModel != null) { + SessionDetailView( + viewModel = detailViewModel, + showBackButton = fullWidth, + onBack = { + scope.launch { + navigator.navigateBack() + if (!fullWidth) { + delay(1000) + } + viewModel.presentedSessionDetail = null + } + }, + ) + } else { + Column { + Text("TODO") + } + } + } + }, + ) +} diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt index b1c8571d..31cefe4d 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt @@ -24,11 +24,11 @@ import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Scaffold import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow +import androidx.compose.material3.TabIndicatorScope import androidx.compose.material3.TabRowDefaults -import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -53,7 +53,6 @@ import androidx.compose.ui.unit.dp import co.touchlab.droidcon.ui.CircularProgressIndicator import co.touchlab.droidcon.ui.theme.Dimensions import co.touchlab.droidcon.ui.util.observeAsState -import co.touchlab.droidcon.util.NavigationStack import co.touchlab.droidcon.viewmodel.session.BaseSessionListViewModel import co.touchlab.droidcon.viewmodel.session.SessionDayViewModel import co.touchlab.kermit.Logger @@ -61,123 +60,107 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable -internal fun SessionListView(viewModel: BaseSessionListViewModel, title: String, emptyText: String) { - NavigationStack( - key = viewModel, - links = { - navigationLink(viewModel.observePresentedSessionDetail) { - SessionDetailView(viewModel = it) - } +internal fun SessionListView(viewModel: BaseSessionListViewModel, title: String, emptyText: String, onClick: () -> Unit) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + CenterAlignedTopAppBar( + title = { Text(title) }, + scrollBehavior = scrollBehavior, + ) }, - ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - CenterAlignedTopAppBar( - title = { Text(title) }, - scrollBehavior = scrollBehavior, - ) - }, - ) { innerPadding -> - var size by remember { mutableStateOf(IntSize(0, 0)) } - Column( - modifier = Modifier - .onSizeChanged { size = it } - .padding(top = innerPadding.calculateTopPadding()), - ) { - val days by viewModel.observeDays.observeAsState() - if(days == null){ - CircularProgressIndicator("Updating Droidcon Events!") - } else if (days?.isEmpty() != false) { - EmptyView(emptyText) - } else { - val selectedDay by viewModel.observeSelectedDay.observeAsState() - val selectedTabIndex = viewModel.days?.indexOf(selectedDay) ?: 0 - val coroutineScope = rememberCoroutineScope() + ) { innerPadding -> + var size by remember { mutableStateOf(IntSize(0, 0)) } + Column( + modifier = Modifier + .onSizeChanged { size = it } + .padding(top = innerPadding.calculateTopPadding()), + ) { + val days by viewModel.observeDays.observeAsState() + if (days == null) { + CircularProgressIndicator("Updating Droidcon Events!") + } else if (days?.isEmpty() != false) { + EmptyView(emptyText) + } else { + val selectedDay by viewModel.observeSelectedDay.observeAsState() + val selectedTabIndex = viewModel.days?.indexOf(selectedDay) ?: 0 + val coroutineScope = rememberCoroutineScope() - val pagerState = rememberPagerState( - initialPage = selectedTabIndex, - pageCount = { - days?.size ?: 0 - }, - ) + val pagerState = rememberPagerState( + initialPage = selectedTabIndex, + pageCount = { + days?.size ?: 0 + }, + ) - TabRow( - selectedTabIndex = pagerState.currentPage, - indicator = { tabPositions -> - if (tabPositions.indices.contains(pagerState.currentPage)) { - TabIndicator( - Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), - ) - } else { - Logger.w( - "SessionList TabRow requested an indicator for selectedTabIndex: " + - "${pagerState.currentPage}, but only got ${tabPositions.count()} tabs.", - ) - TabRowDefaults.SecondaryIndicator() - } - }, - ) { - days?.forEachIndexed { index, daySchedule -> - Tab( - selected = pagerState.currentPage == index, - onClick = { - viewModel.selectedDay = daySchedule + PrimaryTabRow( + selectedTabIndex = pagerState.currentPage, + indicator = { + SessionDayTabIndicator( + selectedTabIndex = pagerState.currentPage, + tabCount = days?.size ?: 0, + ) + }, + ) { + days?.forEachIndexed { index, daySchedule -> + Tab( + selected = pagerState.currentPage == index, + onClick = { + viewModel.selectedDay = daySchedule - coroutineScope.launch { - pagerState.animateScrollToPage(index) - } - }, - unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ) { - Text( - text = daySchedule.day, - modifier = Modifier.padding(Dimensions.Padding.default), - fontWeight = FontWeight.Bold, - ) - } + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) { + Text( + text = daySchedule.day, + modifier = Modifier.padding(Dimensions.Padding.default), + fontWeight = FontWeight.Bold, + ) } } + } - LaunchedEffect(pagerState) { - snapshotFlow { pagerState.currentPage }.collect { page -> - viewModel.selectedDay = viewModel.days?.get(page) - } + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.currentPage }.collect { page -> + viewModel.selectedDay = viewModel.days?.get(page) } - HorizontalPager( - state = pagerState, - ) { page -> - val day = days?.get(page) ?: return@HorizontalPager - val scrollState = rememberLazyListState( - day.scrollState.firstVisibleItemIndex, - day.scrollState.firstVisibleItemScrollOffset, + } + HorizontalPager( + state = pagerState, + ) { page -> + val day = days?.get(page) ?: return@HorizontalPager + val scrollState = rememberLazyListState( + day.scrollState.firstVisibleItemIndex, + day.scrollState.firstVisibleItemScrollOffset, + ) + if ( + day.scrollState.firstVisibleItemIndex != scrollState.firstVisibleItemIndex || + day.scrollState.firstVisibleItemScrollOffset != scrollState.firstVisibleItemScrollOffset + ) { + day.scrollState = SessionDayViewModel.ScrollState( + scrollState.firstVisibleItemIndex, + scrollState.firstVisibleItemScrollOffset, ) - if ( - day.scrollState.firstVisibleItemIndex != scrollState.firstVisibleItemIndex || - day.scrollState.firstVisibleItemScrollOffset != scrollState.firstVisibleItemScrollOffset - ) { - day.scrollState = SessionDayViewModel.ScrollState( - scrollState.firstVisibleItemIndex, - scrollState.firstVisibleItemScrollOffset, - ) - } + } - val density = LocalDensity.current - LazyColumn( - state = scrollState, - contentPadding = PaddingValues(vertical = Dimensions.Padding.quarter), - modifier = Modifier.width(with(density) { size.width.toDp() }), - ) { - items(day.blocks) { hourBlock -> - Box( - modifier = Modifier.padding( - vertical = Dimensions.Padding.quarter, - horizontal = Dimensions.Padding.half, - ), - ) { - SessionBlockView(hourBlock) - } + val density = LocalDensity.current + LazyColumn( + state = scrollState, + contentPadding = PaddingValues(vertical = Dimensions.Padding.quarter), + modifier = Modifier.width(with(density) { size.width.toDp() }), + ) { + items(day.blocks) { hourBlock -> + Box( + modifier = Modifier.padding( + vertical = Dimensions.Padding.quarter, + horizontal = Dimensions.Padding.half, + ), + ) { + SessionBlockView(hourBlock, onClick = onClick) } } } @@ -188,15 +171,26 @@ internal fun SessionListView(viewModel: BaseSessionListViewModel, title: String, } @Composable -private fun TabIndicator(modifier: Modifier = Modifier) { - Box( - modifier - .fillMaxWidth() - .padding(horizontal = 64.dp) - .height(2.dp) - .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) - .background(MaterialTheme.colorScheme.primary), - ) +private fun TabIndicatorScope.SessionDayTabIndicator(selectedTabIndex: Int, tabCount: Int) { + if (tabCount > 0 && selectedTabIndex in 0 until tabCount) { + Box( + Modifier + .tabIndicatorOffset(selectedTabIndex, matchContentSize = true) + .fillMaxWidth() + .padding(horizontal = 64.dp) + .height(2.dp) + .clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)) + .background(MaterialTheme.colorScheme.primary), + ) + } else { + Logger.w( + "SessionList PrimaryTabRow requested an indicator for selectedTabIndex: " + + "$selectedTabIndex, but only got $tabCount tabs.", + ) + TabRowDefaults.PrimaryIndicator( + modifier = Modifier.tabIndicatorOffset(0, matchContentSize = true), + ) + } } @Composable diff --git a/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView.kt b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView_Mobile.kt similarity index 100% rename from shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView.kt rename to shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView_Mobile.kt diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/Koin.android.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/Koin.android.kt index 34fb8216..10ffab02 100644 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/Koin.android.kt +++ b/shared/src/androidMain/kotlin/co/touchlab/droidcon/Koin.android.kt @@ -4,8 +4,6 @@ import app.cash.sqldelight.db.SqlDriver import co.touchlab.droidcon.application.service.NotificationService import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory import co.touchlab.droidcon.service.AndroidNotificationService -import co.touchlab.droidcon.util.formatter.AndroidDateFormatter -import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.kermit.ExperimentalKermitApi import co.touchlab.kermit.LogcatWriter import co.touchlab.kermit.Logger @@ -42,13 +40,6 @@ actual val platformModule: Module = module { get() } - single { - AndroidDateFormatter( - dateTimeService = get(), - conferenceConfigProvider = get(), - ) - } - val baseKermit = Logger(config = StaticConfig(logWriterList = listOf(LogcatWriter(), CrashlyticsLogWriter())), tag = "Droidcon") factory { (tag: String?) -> if (tag != null) baseKermit.withTag(tag) else baseKermit } } diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/formatter/AndroidDateFormatter.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/formatter/AndroidDateFormatter.kt deleted file mode 100644 index 9a7418df..00000000 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/formatter/AndroidDateFormatter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package co.touchlab.droidcon.util.formatter - -import co.touchlab.droidcon.domain.service.ConferenceConfigProvider -import co.touchlab.droidcon.domain.service.DateTimeService -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.atTime - -class AndroidDateFormatter(private val dateTimeService: DateTimeService, private val conferenceConfigProvider: ConferenceConfigProvider) : - DateFormatter { - - // Get timezone from ConferenceConfigProvider - private val conferenceTimeZone: kotlinx.datetime.TimeZone? - get() = conferenceConfigProvider.getConferenceTimeZone() - - // Create formatters as properties to ensure they use the current conference timezone - private val shortDateFormat - get() = SimpleDateFormat("MMM d", Locale.getDefault()).apply { - timeZone = java.util.TimeZone.getTimeZone(conferenceTimeZone?.id ?: "UTC") - } - - private val minuteHourTimeFormat - get() = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()) - .apply { timeZone = java.util.TimeZone.getTimeZone(conferenceTimeZone?.id ?: "UTC") } - - override fun monthWithDay(date: LocalDate): String = shortDateFormat.format( - Date(with(dateTimeService) { date.atTime(0, 0).fromConferenceDateTime(conferenceTimeZone ?: kotlinx.datetime.TimeZone.UTC) }.toEpochMilliseconds()), - ).uppercase() - - override fun timeOnly(dateTime: LocalDateTime): String? = minuteHourTimeFormat.format( - Date(with(dateTimeService) { dateTime.fromConferenceDateTime(conferenceTimeZone ?: kotlinx.datetime.TimeZone.UTC) }.toEpochMilliseconds()), - ) - - override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime): String = - timeOnly(fromDateTime) + " - " + timeOnly(toDateTime) -} diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index d40b9cb3..7ce75622 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -45,6 +45,8 @@ import co.touchlab.droidcon.domain.service.impl.DefaultScheduleService import co.touchlab.droidcon.domain.service.impl.DefaultServerApi import co.touchlab.droidcon.domain.service.impl.DefaultSyncService import co.touchlab.droidcon.domain.service.impl.DefaultUserIdProvider +import co.touchlab.droidcon.util.formatter.DateFormatter +import co.touchlab.droidcon.util.formatter.KotlinXDateFormatter import co.touchlab.droidcon.domain.service.impl.json.AboutJsonResourceDataSource import co.touchlab.droidcon.domain.service.impl.json.JsonResourceReader import io.ktor.client.HttpClient @@ -144,6 +146,10 @@ private val coreModule = module { ) } + single { + KotlinXDateFormatter() + } + single { co.touchlab.droidcon.util.AppChecker( conferenceConfigProvider = get(), diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/util/formatter/KotlinXDateFormatter.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/util/formatter/KotlinXDateFormatter.kt new file mode 100644 index 00000000..2b16db1d --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/util/formatter/KotlinXDateFormatter.kt @@ -0,0 +1,33 @@ +package co.touchlab.droidcon.util.formatter + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime + +/** + * Shared [DateFormatter] for all platforms using kotlinx-datetime only (no java.text / NSDateFormatter / JS stubs). + * Schedule data uses conference-local [LocalDate] / [LocalDateTime]; we format those fields directly. + */ +class KotlinXDateFormatter : DateFormatter { + + override fun monthWithDay(date: LocalDate): String { + val m = date.month.name.take(3).uppercase() + return "$m ${date.day}" + } + + override fun timeOnly(dateTime: LocalDateTime): String = formatTime12h(dateTime) + + override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime): String = + "${timeOnly(fromDateTime)} - ${timeOnly(toDateTime)}" + + private fun formatTime12h(dateTime: LocalDateTime): String { + val h = dateTime.hour + val min = dateTime.minute + val h12 = when { + h == 0 -> 12 + h > 12 -> h - 12 + else -> h + } + val suffix = if (h < 12) "AM" else "PM" + return "$h12:${min.toString().padStart(2, '0')} $suffix" + } +} diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt index 45c33e24..1cd3c74b 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt @@ -4,8 +4,6 @@ import app.cash.sqldelight.db.SqlDriver import co.touchlab.droidcon.application.service.NotificationService import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory import co.touchlab.droidcon.service.JsNotificationService -import co.touchlab.droidcon.util.formatter.DateFormatter -import co.touchlab.droidcon.util.formatter.JsDateFormatter import co.touchlab.kermit.CommonWriter import co.touchlab.kermit.Logger import co.touchlab.kermit.StaticConfig @@ -27,10 +25,6 @@ actual val platformModule: org.koin.core.module.Module = module { get() } - single { - JsDateFormatter() - } - val baseKermit = Logger(config = StaticConfig(logWriterList = listOf(CommonWriter())), tag = "Droidcon") factory { (tag: String?) -> if (tag != null) baseKermit.withTag(tag) else baseKermit } } diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt deleted file mode 100644 index 2244fff7..00000000 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/formatter/JsDateFormatter.kt +++ /dev/null @@ -1,13 +0,0 @@ -package co.touchlab.droidcon.util.formatter - -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime - -class JsDateFormatter : DateFormatter { - - override fun monthWithDay(date: LocalDate): String? = null - - override fun timeOnly(dateTime: LocalDateTime): String? = null - - override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime): String = "" -} diff --git a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt index 2677c0a1..cc5d3707 100644 --- a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt +++ b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt @@ -7,10 +7,10 @@ import co.touchlab.droidcon.service.JsNotificationService import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule import co.touchlab.droidcon.util.formatter.DateFormatter +import co.touchlab.droidcon.util.formatter.KotlinXDateFormatter import co.touchlab.droidcon.web.service.DefaultParseUrlViewService import co.touchlab.droidcon.web.util.NotificationLocalizedStringFactory import co.touchlab.droidcon.web.util.WebResourceReader -import co.touchlab.droidcon.web.util.formatter.WebDateFormatter import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ObservableSettings import com.russhwolf.settings.Settings @@ -34,7 +34,7 @@ class MyTest : KoinTest { } single { WebResourceReader() } - single { WebDateFormatter() } + single { KotlinXDateFormatter() } single { NotificationLocalizedStringFactory() } diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt index 57fc1e47..85f95481 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt @@ -16,12 +16,10 @@ import co.touchlab.droidcon.service.JsNotificationService import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.timeZoneAdapter import co.touchlab.droidcon.ui.uiModule -import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel import co.touchlab.droidcon.web.service.DefaultParseUrlViewService import co.touchlab.droidcon.web.util.NotificationLocalizedStringFactory import co.touchlab.droidcon.web.util.WebResourceReader -import co.touchlab.droidcon.web.util.formatter.WebDateFormatter import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ObservableSettings import com.russhwolf.settings.Settings @@ -61,8 +59,6 @@ suspend fun startKoin(): KoinApplication { } single { WebResourceReader() } - single { WebDateFormatter() } - single { NotificationLocalizedStringFactory() } single { WebAnalyticsService() } diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/formatter/WebDateFormatter.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/formatter/WebDateFormatter.kt deleted file mode 100644 index a211e6fc..00000000 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/formatter/WebDateFormatter.kt +++ /dev/null @@ -1,14 +0,0 @@ -package co.touchlab.droidcon.web.util.formatter - -import co.touchlab.droidcon.util.formatter.DateFormatter -import kotlin.getValue -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime - -class WebDateFormatter : DateFormatter { - override fun monthWithDay(date: LocalDate): String? = null - - override fun timeOnly(dateTime: LocalDateTime): String? = null - - override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime): String = "" -} From df3f59611b795ae88df27a33b67a1bbf84afa295 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 22 Apr 2026 11:01:50 -0400 Subject: [PATCH 28/56] Updating sponsors view and fixing animations --- .../session/SessionListDetailPaneScaffold.kt | 18 ++++--- .../ui/sponsors/SponsorsView.kt | 54 ++++++++++++------- .../util/NavigationController.kt | 9 +++- 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt index 25834d25..4dfca94b 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt @@ -2,6 +2,8 @@ package co.touchlab.droidcon.ui.session import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @@ -25,6 +27,12 @@ fun SessionListDetailPaneScaffold(viewModel: BaseSessionListViewModel, title: St val scope = rememberCoroutineScope() val navigator = rememberListDetailPaneScaffoldNavigator() val presentedSessionDetail by viewModel.observePresentedSessionDetail.observeAsState() + val listPaneAdapted = navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] + val fullWidth = listPaneAdapted == PaneAdaptedValue.Hidden + val listEnterTransition = if (fullWidth) slideInHorizontally { -it } else EnterTransition.None + val listExitTransition = if (fullWidth) slideOutHorizontally { -it } else ExitTransition.None + val detailEnterTransition = if (fullWidth) slideInHorizontally { it } else EnterTransition.None + val detailExitTransition = if (fullWidth) slideOutHorizontally { it } else ExitTransition.None ListDetailPaneScaffold( modifier = modifier, @@ -32,8 +40,8 @@ fun SessionListDetailPaneScaffold(viewModel: BaseSessionListViewModel, title: St value = navigator.scaffoldValue, listPane = { AnimatedPane( - enterTransition = slideInHorizontally { -it }, - exitTransition = slideOutHorizontally { -it }, + enterTransition = listEnterTransition, + exitTransition = listExitTransition, ) { SessionListView( viewModel = viewModel, @@ -48,11 +56,9 @@ fun SessionListDetailPaneScaffold(viewModel: BaseSessionListViewModel, title: St }, detailPane = { AnimatedPane( - enterTransition = slideInHorizontally { it }, - exitTransition = slideOutHorizontally { it }, + enterTransition = detailEnterTransition, + exitTransition = detailExitTransition, ) { - val listPaneAdapted = navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] - val fullWidth = listPaneAdapted == PaneAdaptedValue.Hidden val detailViewModel = presentedSessionDetail if (detailViewModel != null) { SessionDetailView( diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt index 28091c11..77b59347 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt @@ -4,9 +4,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize @@ -14,7 +16,11 @@ 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.sizeIn import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons @@ -27,6 +33,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -39,6 +46,9 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min +import androidx.window.core.layout.WindowSizeClass +import androidx.window.core.layout.WindowWidthSizeClass import co.touchlab.droidcon.ui.theme.Dimensions import co.touchlab.droidcon.ui.util.DcAsyncImage import co.touchlab.droidcon.ui.util.observeAsState @@ -113,25 +123,36 @@ private fun SponsorGroupView(sponsorGroup: SponsorGroupViewModel) { ), style = MaterialTheme.typography.headlineLarge, ) - val columnCount = if (sponsorGroup.isProminent) 3 else 4 - val sponsors by sponsorGroup.observeSponsors.observeAsState() + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + + val maxItemsInEeachRow = if (sponsorGroup.isProminent) 3 else 4 + val sizingModifier = when { + windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND) -> + Modifier + .sizeIn(maxWidth = 200.dp, maxHeight = 200.dp) + .aspectRatio(1f, matchHeightConstraintsFirst = true) + + else -> + Modifier + .fillMaxWidth(1f / maxItemsInEeachRow) + .aspectRatio(1f) + } - repeat(sponsors.size / columnCount + if (sponsors.size % columnCount == 0) 0 else 1) { rowIndex -> - Row(modifier = Modifier.padding(horizontal = Dimensions.Padding.half)) { - val startIndex = rowIndex * columnCount - val endIndex = min(startIndex + columnCount, sponsors.size) - sponsors.subList(startIndex, endIndex).forEach { sponsor -> + val sponsors by sponsorGroup.observeSponsors.observeAsState() + if (sponsors.isNotEmpty()) { + FlowRow( + modifier = Modifier.fillMaxWidth().padding(horizontal = Dimensions.Padding.half), + horizontalArrangement = Arrangement.spacedBy(Dimensions.Padding.half), + verticalArrangement = Arrangement.spacedBy(Dimensions.Padding.half), + maxItemsInEachRow = maxItemsInEeachRow, + ) { + sponsors.forEach { sponsor -> Box( - modifier = Modifier - .weight(1f) - .aspectRatio(1f) - .padding(Dimensions.Padding.quarter) + modifier = sizingModifier .clip(CircleShape) .background(Color.White) - .clickable { - sponsor.selected() - }, + .clickable { sponsor.selected() }, contentAlignment = Alignment.Center, ) { val imageUrl = sponsor.validImageUrl @@ -152,9 +173,6 @@ private fun SponsorGroupView(sponsorGroup: SponsorGroupViewModel) { } } } - repeat(columnCount - endIndex + startIndex) { - Spacer(modifier = Modifier.weight(1f)) - } } } Spacer(modifier = Modifier.height(Dimensions.Padding.default)) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt index 4068c74b..b13b6d2d 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt @@ -1,7 +1,9 @@ package co.touchlab.droidcon.util import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.ExitTransition import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith @@ -259,7 +261,12 @@ internal fun NavigationStack(key: Any?, links: NavigationStackScope.() -> Unit, AnimatedContent( targetState = activeLinkComposables, transitionSpec = { - if (initialState.indexOfLast { it.body != null } < targetState.indexOfLast { it.body != null }) { + val initialDepth = initialState.indexOfLast { it.body != null } + val targetDepth = targetState.indexOfLast { it.body != null } + + if (initialDepth == -1 && targetDepth == -1) { + EnterTransition.None.togetherWith(ExitTransition.None) + } else if (initialDepth < targetDepth) { slideInHorizontally(initialOffsetX = { it }).togetherWith(slideOutHorizontally(targetOffsetX = { -it })) } else { slideInHorizontally(initialOffsetX = { -it }).togetherWith(slideOutHorizontally(targetOffsetX = { it })) From 39fbf1b1e9a4e46f9a6ef84384fa7778feac33f7 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 22 Apr 2026 13:11:38 -0400 Subject: [PATCH 29/56] Adjusting Images --- .../co/touchlab/droidcon/android/MainApp.kt | 4 +- .../impl/DefaultParseUrlViewService.kt | 5 +- .../Droidcon.xcodeproj/project.pbxproj | 2 +- .../droidcon/ios/DependencyInjection.kt | 3 +- .../droidcon/ui/util/LocalImage.jvm.kt | 59 -------------- .../drawable-dark}/about_droidcon.xml | 0 .../drawable-dark}/about_kotlin.xml | 0 .../drawable/about_droidcon.xml | 0 .../drawable/about_kotlin.xml | 0 .../drawable}/about_touchlab.png | Bin .../composeResources}/drawable/linkedin.xml | 0 .../composeResources}/drawable/twitter.xml | 0 .../session/SessionListDetailPaneScaffold.kt | 13 ++- .../ui/settings/AboutView.kt | 17 +++- .../ui/util/LocalImage.kt | 64 +++++++++++++-- .../viewmodel/WaitForLoadedContextModel.kt | 6 +- .../touchlab/droidcon/ui/util/LocalImage.kt | 56 ------------- .../ui/util/LocalImage.js.kt | 76 ------------------ .../droidcon/util/AssetResourceReader.kt | 16 ---- .../droidcon/util/ClasspathResourceReader.kt | 15 ---- .../files}/about.json | 0 .../domain/service/impl/ResourceReader.kt | 12 ++- .../impl/json/AboutJsonResourceDataSource.kt | 4 +- .../service/impl/json/JsonResourceReader.kt | 2 +- .../co/touchlab/droidcon/web/KoinTest.kt | 3 +- .../droidcon/web/DependencyInjection.kt | 3 +- .../droidcon/web/util/WebResourceReader.kt | 24 +++++- 27 files changed, 135 insertions(+), 249 deletions(-) delete mode 100644 shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.jvm.kt rename {android/src/main/res/drawable-night => shared-ui/src/commonMain/composeResources/drawable-dark}/about_droidcon.xml (100%) rename {android/src/main/res/drawable-night => shared-ui/src/commonMain/composeResources/drawable-dark}/about_kotlin.xml (100%) rename {android/src/main/res => shared-ui/src/commonMain/composeResources}/drawable/about_droidcon.xml (100%) rename {android/src/main/res => shared-ui/src/commonMain/composeResources}/drawable/about_kotlin.xml (100%) rename {android/src/main/res/drawable-nodpi => shared-ui/src/commonMain/composeResources/drawable}/about_touchlab.png (100%) rename {android/src/main/res => shared-ui/src/commonMain/composeResources}/drawable/linkedin.xml (100%) rename {android/src/main/res => shared-ui/src/commonMain/composeResources}/drawable/twitter.xml (100%) delete mode 100644 shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.kt delete mode 100644 shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt delete mode 100644 shared/src/androidMain/kotlin/co/touchlab/droidcon/util/AssetResourceReader.kt delete mode 100644 shared/src/androidMain/kotlin/co/touchlab/droidcon/util/ClasspathResourceReader.kt rename shared/src/commonMain/{resources => composeResources/files}/about.json (100%) diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt index 9c1cec2f..590bcbca 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt @@ -9,11 +9,11 @@ import co.touchlab.droidcon.android.service.impl.DefaultParseUrlViewService import co.touchlab.droidcon.android.util.NotificationLocalizedStringFactory import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule -import co.touchlab.droidcon.util.ClasspathResourceReader import com.google.firebase.Firebase import com.google.firebase.analytics.analytics import com.russhwolf.settings.ExperimentalSettingsApi @@ -47,7 +47,7 @@ class MainApp : } single { - ClasspathResourceReader() + ComposeResourceReader() } single { diff --git a/android/src/main/java/co/touchlab/droidcon/android/service/impl/DefaultParseUrlViewService.kt b/android/src/main/java/co/touchlab/droidcon/android/service/impl/DefaultParseUrlViewService.kt index 0824b44f..21180d0c 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/service/impl/DefaultParseUrlViewService.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/service/impl/DefaultParseUrlViewService.kt @@ -8,5 +8,8 @@ class DefaultParseUrlViewService : ParseUrlViewService { private val urlRegex = Patterns.WEB_URL.toRegex() - override fun parse(text: String): List = urlRegex.findAll(text).map { WebLink(it.range, it.value) }.toList() + override fun parse(text: String): List = urlRegex + .findAll(text) + .map { WebLink(it.range, it.value) } + .toList() } diff --git a/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj index 159ad0ce..eece57a1 100644 --- a/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj +++ b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 1821427726B5418D0047DB71 /* about.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = about.json; path = ../../../shared/src/commonMain/resources/about.json; sourceTree = ""; }; + 1821427726B5418D0047DB71 /* about.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = about.json; path = ../../../shared/src/commonMain/composeResources/files/about.json; sourceTree = ""; }; 18240FB62C6FED770099E416 /* Droidcon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Droidcon.entitlements; sourceTree = ""; }; 1833220F26B0CF5600D79482 /* DroidconApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DroidconApp.swift; sourceTree = ""; }; 18E89B48283E5D2C00C08C9B /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; diff --git a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt index 20efce88..b3413c22 100644 --- a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt +++ b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt @@ -2,6 +2,7 @@ package co.touchlab.droidcon.ios import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.ios.service.DefaultParseUrlViewService @@ -24,7 +25,7 @@ fun initKoinIos(userDefaults: NSUserDefaults, analyticsService: AnalyticsService module { single { BundleProvider(bundle = NSBundle.mainBundle) } single { NSUserDefaultsSettings(delegate = userDefaults) } - single { BundleResourceReader(bundle = get().bundle) } + single { ComposeResourceReader() /*BundleResourceReader(bundle = get().bundle)*/ } single { NotificationLocalizedStringFactory(bundle = get().bundle) diff --git a/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.jvm.kt b/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.jvm.kt deleted file mode 100644 index 761ff22e..00000000 --- a/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.jvm.kt +++ /dev/null @@ -1,59 +0,0 @@ -@file:Suppress("ktlint:standard:filename") - -package co.touchlab.droidcon.ui.util - -import android.annotation.SuppressLint -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Warning -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.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import co.touchlab.droidcon.ui.theme.Dimensions -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers - -// Use of the function getIdentifier is discouraged, but we need to use it since the drawable names are defined in the common code for both -// platforms and on each platform we need to get the drawable according to provided name. -@SuppressLint("ComposableNaming", "DiscouragedApi") -@Composable -internal actual fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?) { - val context = LocalContext.current - val imageRes = context.resources.getIdentifier(imageResourceName, "drawable", context.packageName).takeIf { it != 0 } - if (imageRes != null) { - androidx.compose.foundation.Image( - modifier = modifier, - painter = painterResource(id = imageRes), - contentDescription = contentDescription, - contentScale = ContentScale.FillWidth, - ) - } else { - Row( - modifier = modifier.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(Dimensions.Padding.half)), - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = Modifier.weight(1f)) - Icon( - imageVector = Icons.Default.Warning, - contentDescription = contentDescription, - modifier = Modifier.padding(Dimensions.Padding.half), - tint = Color.White, - ) - Text("Image not supported", modifier = Modifier.padding(Dimensions.Padding.default), color = Color.White) - Spacer(modifier = Modifier.weight(1f)) - } - } -} - -actual val IODispatcher: CoroutineDispatcher = Dispatchers.IO diff --git a/android/src/main/res/drawable-night/about_droidcon.xml b/shared-ui/src/commonMain/composeResources/drawable-dark/about_droidcon.xml similarity index 100% rename from android/src/main/res/drawable-night/about_droidcon.xml rename to shared-ui/src/commonMain/composeResources/drawable-dark/about_droidcon.xml diff --git a/android/src/main/res/drawable-night/about_kotlin.xml b/shared-ui/src/commonMain/composeResources/drawable-dark/about_kotlin.xml similarity index 100% rename from android/src/main/res/drawable-night/about_kotlin.xml rename to shared-ui/src/commonMain/composeResources/drawable-dark/about_kotlin.xml diff --git a/android/src/main/res/drawable/about_droidcon.xml b/shared-ui/src/commonMain/composeResources/drawable/about_droidcon.xml similarity index 100% rename from android/src/main/res/drawable/about_droidcon.xml rename to shared-ui/src/commonMain/composeResources/drawable/about_droidcon.xml diff --git a/android/src/main/res/drawable/about_kotlin.xml b/shared-ui/src/commonMain/composeResources/drawable/about_kotlin.xml similarity index 100% rename from android/src/main/res/drawable/about_kotlin.xml rename to shared-ui/src/commonMain/composeResources/drawable/about_kotlin.xml diff --git a/android/src/main/res/drawable-nodpi/about_touchlab.png b/shared-ui/src/commonMain/composeResources/drawable/about_touchlab.png similarity index 100% rename from android/src/main/res/drawable-nodpi/about_touchlab.png rename to shared-ui/src/commonMain/composeResources/drawable/about_touchlab.png diff --git a/android/src/main/res/drawable/linkedin.xml b/shared-ui/src/commonMain/composeResources/drawable/linkedin.xml similarity index 100% rename from android/src/main/res/drawable/linkedin.xml rename to shared-ui/src/commonMain/composeResources/drawable/linkedin.xml diff --git a/android/src/main/res/drawable/twitter.xml b/shared-ui/src/commonMain/composeResources/drawable/twitter.xml similarity index 100% rename from android/src/main/res/drawable/twitter.xml rename to shared-ui/src/commonMain/composeResources/drawable/twitter.xml diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt index 4dfca94b..98bf5aca 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt @@ -7,6 +7,7 @@ import androidx.compose.animation.ExitTransition import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.layout.AnimatedPane import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole @@ -16,6 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.window.core.layout.WindowSizeClass import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.viewmodel.session.BaseSessionListViewModel import kotlinx.coroutines.delay @@ -29,10 +31,13 @@ fun SessionListDetailPaneScaffold(viewModel: BaseSessionListViewModel, title: St val presentedSessionDetail by viewModel.observePresentedSessionDetail.observeAsState() val listPaneAdapted = navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] val fullWidth = listPaneAdapted == PaneAdaptedValue.Hidden - val listEnterTransition = if (fullWidth) slideInHorizontally { -it } else EnterTransition.None - val listExitTransition = if (fullWidth) slideOutHorizontally { -it } else ExitTransition.None - val detailEnterTransition = if (fullWidth) slideInHorizontally { it } else EnterTransition.None - val detailExitTransition = if (fullWidth) slideOutHorizontally { it } else ExitTransition.None + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val isCompactWidth = !windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) + val usePaneSlideAnimation = fullWidth && isCompactWidth + val listEnterTransition = if (usePaneSlideAnimation) slideInHorizontally { -it } else EnterTransition.None + val listExitTransition = if (usePaneSlideAnimation) slideOutHorizontally { -it } else ExitTransition.None + val detailEnterTransition = if (usePaneSlideAnimation) slideInHorizontally { it } else EnterTransition.None + val detailExitTransition = if (usePaneSlideAnimation) slideOutHorizontally { it } else ExitTransition.None ListDetailPaneScaffold( modifier = modifier, diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/AboutView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/AboutView.kt index 42c403df..5d8d23aa 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/AboutView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/AboutView.kt @@ -2,17 +2,23 @@ package co.touchlab.droidcon.ui.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowSizeClass import co.touchlab.droidcon.ui.theme.Dimensions import co.touchlab.droidcon.ui.util.LocalImage import co.touchlab.droidcon.ui.util.WebLinkText @@ -53,15 +59,22 @@ private fun AboutItemView(viewModel: AboutItemViewModel) { modifier = Modifier.padding(end = Dimensions.Padding.default), ) + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val sizingModifier = when { + windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND) -> + Modifier.sizeIn(maxHeight = 150.dp) + else -> Modifier.fillMaxWidth() + } + LocalImage( imageResourceName = viewModel.icon, - modifier = Modifier - .fillMaxWidth() + modifier = sizingModifier .padding( end = Dimensions.Padding.double, top = Dimensions.Padding.default, bottom = Dimensions.Padding.default, ), + contentScale = ContentScale.Fit ) } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt index 3fbfe73e..3d0d5648 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt @@ -1,14 +1,66 @@ package co.touchlab.droidcon.ui.util +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +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.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.ContentScale.Companion +import co.touchlab.droidcon.ui.theme.Dimensions +import droidcon.shared_ui.generated.resources.Res +import droidcon.shared_ui.generated.resources.about_droidcon +import droidcon.shared_ui.generated.resources.about_kotlin +import droidcon.shared_ui.generated.resources.about_touchlab +import droidcon.shared_ui.generated.resources.linkedin +import droidcon.shared_ui.generated.resources.twitter +import droidcon.shared_ui.generated.resources.venue_map_1 import kotlinx.coroutines.CoroutineDispatcher -@Composable -internal expect fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?) - -expect val IODispatcher: CoroutineDispatcher +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource @Composable -internal fun LocalImage(imageResourceName: String, modifier: Modifier = Modifier, contentDescription: String? = null) { - __LocalImage(imageResourceName, modifier, contentDescription) +internal fun LocalImage(imageResourceName: String, modifier: Modifier = Modifier, contentDescription: String? = null, contentScale: ContentScale = ContentScale.FillWidth) { + val imageRes = when(imageResourceName.lowercase()){ + "about_droidcon" -> Res.drawable.about_droidcon + "about_touchlab" -> Res.drawable.about_touchlab + "about_kotlin" -> Res.drawable.about_kotlin + "linkedin" -> Res.drawable.linkedin + "twitter" -> Res.drawable.twitter + "venue-map-1" -> Res.drawable.venue_map_1 + else -> null + } + if (imageRes != null) { + Image( + modifier = modifier, + painter = painterResource(imageRes), + contentDescription = contentDescription, + contentScale = contentScale, + ) + } else { + Row( + modifier = modifier.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(Dimensions.Padding.half)), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.Warning, + contentDescription = contentDescription, + modifier = Modifier.padding(Dimensions.Padding.half), + tint = Color.White, + ) + Text("Image not supported", modifier = Modifier.padding(Dimensions.Padding.default), color = Color.White) + Spacer(modifier = Modifier.weight(1f)) + } + } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt index c72ae61d..0dd5ca5d 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt @@ -4,14 +4,14 @@ import co.touchlab.droidcon.application.gateway.SettingsGateway import co.touchlab.droidcon.domain.entity.Conference import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.SyncService -import co.touchlab.droidcon.ui.util.IODispatcher import co.touchlab.kermit.Logger -import kotlin.js.JsExport +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.brightify.hyperdrive.multiplatformx.BaseViewModel + class WaitForLoadedContextModel( private val conferenceConfigProvider: ConferenceConfigProvider, applicationViewModelFactory: ViewModelFactory.ApplicationViewModelFactory, @@ -44,7 +44,7 @@ class WaitForLoadedContextModel( log.i { "WaitForLoadedContextModel: Emitting Conference!" } _state.emit(State.Ready(conference)) - withContext(IODispatcher) { + withContext(Dispatchers.Default) { try { log.i { "syncConferences" } syncService.syncConferences() diff --git a/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.kt b/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.kt deleted file mode 100644 index 734999a1..00000000 --- a/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.kt +++ /dev/null @@ -1,56 +0,0 @@ -package co.touchlab.droidcon.ui.util - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.graphics.toComposeImageBitmap -import androidx.compose.ui.layout.ContentScale -import co.touchlab.droidcon.ui.theme.Dimensions -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import platform.UIKit.UIImage - -@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) -@Composable -internal actual fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?) { - val painter = remember { UIImage.imageNamed(imageResourceName)?.toSkiaImage()?.toComposeImageBitmap()?.let(::BitmapPainter) } - if (painter != null) { - androidx.compose.foundation.Image( - modifier = modifier, - painter = painter, - contentDescription = contentDescription, - contentScale = ContentScale.FillWidth, - ) - } else { - Row( - modifier = modifier.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(Dimensions.Padding.half)), - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = Modifier.weight(1f)) - Icon( - imageVector = Icons.Default.Warning, - contentDescription = contentDescription, - modifier = Modifier.padding(Dimensions.Padding.half), - tint = Color.White, - ) - Text("Image not supported", modifier = Modifier.padding(Dimensions.Padding.default), color = Color.White) - Spacer(modifier = Modifier.weight(1f)) - } - } -} - -actual val IODispatcher: CoroutineDispatcher = Dispatchers.IO diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt deleted file mode 100644 index 7e720b48..00000000 --- a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.js.kt +++ /dev/null @@ -1,76 +0,0 @@ -package co.touchlab.droidcon.ui.util - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.toComposeImageBitmap -import androidx.compose.ui.layout.ContentScale -import co.touchlab.droidcon.ui.theme.Dimensions -import kotlin.js.js -import kotlinx.browser.window -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.await -import org.jetbrains.skia.Image.Companion.makeFromEncoded -import org.khronos.webgl.Int8Array - -@Composable -internal actual fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?) { - val painter: Painter? by produceState(null, imageResourceName) { - value = try { - val fetched = window.fetch("drawable/$imageResourceName", js("{}")).await() - if (fetched.ok) { - val buffer = fetched.arrayBuffer().await() - val bytes = Int8Array(buffer).unsafeCast() - makeFromEncoded(bytes).toComposeImageBitmap().let(::BitmapPainter) - } else { - null - } - } catch (e: Exception) { - null - } - } - - val currentPainter = painter - if (currentPainter != null) { - Image( - painter = currentPainter, - contentDescription = contentDescription, - modifier = modifier, - contentScale = ContentScale.FillWidth, - ) - } else { - Row( - modifier = modifier.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(Dimensions.Padding.half)), - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = Modifier.weight(1f)) - Icon( - imageVector = Icons.Default.Warning, - contentDescription = contentDescription, - modifier = Modifier.padding(Dimensions.Padding.half), - tint = Color.White, - ) - Text("Image not supported", modifier = Modifier.padding(Dimensions.Padding.default), color = Color.White) - Spacer(modifier = Modifier.weight(1f)) - } - } -} - -actual val IODispatcher: CoroutineDispatcher = Dispatchers.Default diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/AssetResourceReader.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/AssetResourceReader.kt deleted file mode 100644 index cd4ed601..00000000 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/AssetResourceReader.kt +++ /dev/null @@ -1,16 +0,0 @@ -package co.touchlab.droidcon.util - -import android.content.Context -import co.touchlab.droidcon.domain.service.impl.ResourceReader -import java.io.InputStreamReader - -class AssetResourceReader(private val context: Context) : ResourceReader { - override fun readResource(name: String): String { - // TODO: Catch Android-only exceptions and map them to common ones. - return context.assets.open(name).use { stream -> - InputStreamReader(stream).use { reader -> - reader.readText() - } - } - } -} diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/ClasspathResourceReader.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/ClasspathResourceReader.kt deleted file mode 100644 index 44bc8c96..00000000 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/ClasspathResourceReader.kt +++ /dev/null @@ -1,15 +0,0 @@ -package co.touchlab.droidcon.util - -import co.touchlab.droidcon.domain.service.impl.ResourceReader -import java.io.InputStreamReader - -class ClasspathResourceReader : ResourceReader { - override fun readResource(name: String): String { - // TODO: Catch Android-only exceptions and map them to common ones. - return javaClass.classLoader?.getResourceAsStream(name).use { stream -> - InputStreamReader(stream).use { reader -> - reader.readText() - } - } - } -} diff --git a/shared/src/commonMain/resources/about.json b/shared/src/commonMain/composeResources/files/about.json similarity index 100% rename from shared/src/commonMain/resources/about.json rename to shared/src/commonMain/composeResources/files/about.json diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/ResourceReader.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/ResourceReader.kt index aa295d15..24eef313 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/ResourceReader.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/ResourceReader.kt @@ -1,5 +1,15 @@ package co.touchlab.droidcon.domain.service.impl +import droidcon.shared.generated.resources.Res + interface ResourceReader { - fun readResource(name: String): String + suspend fun readResource(name: String): String +} + +class ComposeResourceReader : ResourceReader { + override suspend fun readResource(name: String): String { + val bytes = Res.readBytes(name) + val jsonString = bytes.decodeToString() + return jsonString + } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/json/AboutJsonResourceDataSource.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/json/AboutJsonResourceDataSource.kt index 140e090f..48e6557b 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/json/AboutJsonResourceDataSource.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/json/AboutJsonResourceDataSource.kt @@ -5,6 +5,6 @@ import kotlinx.serialization.builtins.ListSerializer class AboutJsonResourceDataSource(private val jsonResourceReader: JsonResourceReader) { - fun getAboutItems(): List = - jsonResourceReader.readAndDecodeResource("about.json", ListSerializer(AboutDto.AboutItemDto.serializer())) + suspend fun getAboutItems(): List = + jsonResourceReader.readAndDecodeResource("files/about.json", ListSerializer(AboutDto.AboutItemDto.serializer())) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/json/JsonResourceReader.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/json/JsonResourceReader.kt index dab005b6..f79e056c 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/json/JsonResourceReader.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/json/JsonResourceReader.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.json.Json class JsonResourceReader(private val resourceReader: ResourceReader, private val json: Json) { - internal fun readAndDecodeResource(name: String, strategy: DeserializationStrategy): T { + internal suspend fun readAndDecodeResource(name: String, strategy: DeserializationStrategy): T { val text = resourceReader.readResource(name) return json.decodeFromString(strategy, text) } diff --git a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt index cc5d3707..9fe91c4d 100644 --- a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt +++ b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt @@ -2,6 +2,7 @@ package co.touchlab.droidcon.web import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.service.JsNotificationService import co.touchlab.droidcon.service.ParseUrlViewService @@ -32,7 +33,7 @@ class MyTest : KoinTest { val storageSettings: Settings = StorageSettings() storageSettings.makeObservable() } - single { WebResourceReader() } + single { ComposeResourceReader() } single { KotlinXDateFormatter() } diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt index 85f95481..a8389702 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt @@ -9,6 +9,7 @@ import co.touchlab.droidcon.db.SponsorGroupTable import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory import co.touchlab.droidcon.domain.repository.impl.adapter.InstantSqlDelightAdapter import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.intToLongAdapter @@ -57,7 +58,7 @@ suspend fun startKoin(): KoinApplication { val storageSettings: Settings = StorageSettings() storageSettings.makeObservable() } - single { WebResourceReader() } + single { ComposeResourceReader() } single { NotificationLocalizedStringFactory() } diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt index f8d80617..f2cc8f43 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt @@ -1,7 +1,29 @@ package co.touchlab.droidcon.web.util import co.touchlab.droidcon.domain.service.impl.ResourceReader +import org.w3c.xhr.XMLHttpRequest class WebResourceReader : ResourceReader { - override fun readResource(name: String): String = "" + override suspend fun readResource(name: String): String { + val candidatePaths = listOf( + "shared/src/commonMain/resources/$name", + "/shared/src/commonMain/resources/$name", + name, + "/$name", + "resources/$name", + "/resources/$name", + ) + + candidatePaths.forEach { path -> + val request = XMLHttpRequest() + request.open("GET", path, false) + request.send() + + if (request.status.toInt() in 200..299 && request.responseText.isNotBlank()) { + return request.responseText + } + } + + error("Unable to load web resource: $name") + } } From 9b0d4d867d142ad6145dc8994d1381e01c74efd4 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 22 Apr 2026 13:49:23 -0400 Subject: [PATCH 30/56] updating empty view --- .../composeResources/drawable/event_note.xml | 9 ++ .../ui/session/SessionDetailView.kt | 6 +- .../session/SessionListDetailPaneScaffold.kt | 28 +++- .../ui/session/SessionListView.kt | 139 +++++++++--------- .../ui/venue/VenueView.kt | 1 + 5 files changed, 112 insertions(+), 71 deletions(-) create mode 100644 shared-ui/src/commonMain/composeResources/drawable/event_note.xml diff --git a/shared-ui/src/commonMain/composeResources/drawable/event_note.xml b/shared-ui/src/commonMain/composeResources/drawable/event_note.xml new file mode 100644 index 00000000..208b9ddc --- /dev/null +++ b/shared-ui/src/commonMain/composeResources/drawable/event_note.xml @@ -0,0 +1,9 @@ + + + diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt index 897061e7..f72c103f 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt @@ -202,6 +202,7 @@ internal fun SessionDetailView( viewModel: SessionDetailViewModel, showBackButton: Boolean, onBack: (() -> Unit)?, + attendingTapped: (Boolean) -> Unit, ) { NavigationStack( key = viewModel, @@ -236,7 +237,10 @@ internal fun SessionDetailView( showBackButton = showBackButton, speakers = speakers, feedback = feedback, - attendingTapped = viewModel::attendingTapped, + attendingTapped = { + attendingTapped(!viewModel.isAttending) + viewModel.attendingTapped() + }, writeFeedbackTapped = viewModel::writeFeedbackTapped, onScrollStateChanged = { viewModel.scrollState = it }, onBack = onBack, diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt index 98bf5aca..2c0a4455 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt @@ -4,7 +4,11 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -16,12 +20,16 @@ import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaf import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.window.core.layout.WindowSizeClass import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.viewmodel.session.BaseSessionListViewModel +import droidcon.shared_ui.generated.resources.Res +import droidcon.shared_ui.generated.resources.event_note import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.painterResource @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable @@ -78,10 +86,26 @@ fun SessionListDetailPaneScaffold(viewModel: BaseSessionListViewModel, title: St viewModel.presentedSessionDetail = null } }, + attendingTapped = { attending -> + if (!attending) { + scope.launch { + viewModel.presentedSessionDetail = null + navigator.navigateBack() + if (!fullWidth) { + delay(1000) + } + } + } + }, ) } else { - Column { - Text("TODO") + Column( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text("No session selected. Please select a session for more details", style = MaterialTheme.typography.titleLarge) + Icon(modifier = Modifier.fillMaxSize(0.33f), painter = painterResource(Res.drawable.event_note), contentDescription = "No Session Selected") } } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt index 31cefe4d..2fb484ed 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListView.kt @@ -35,6 +35,7 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -84,83 +85,85 @@ internal fun SessionListView(viewModel: BaseSessionListViewModel, title: String, EmptyView(emptyText) } else { val selectedDay by viewModel.observeSelectedDay.observeAsState() - val selectedTabIndex = viewModel.days?.indexOf(selectedDay) ?: 0 - val coroutineScope = rememberCoroutineScope() + key(days) { + val selectedTabIndex = (viewModel.days?.indexOf(selectedDay) ?: 0).coerceAtLeast(0) + val coroutineScope = rememberCoroutineScope() - val pagerState = rememberPagerState( - initialPage = selectedTabIndex, - pageCount = { - days?.size ?: 0 - }, - ) - - PrimaryTabRow( - selectedTabIndex = pagerState.currentPage, - indicator = { - SessionDayTabIndicator( - selectedTabIndex = pagerState.currentPage, - tabCount = days?.size ?: 0, - ) - }, - ) { - days?.forEachIndexed { index, daySchedule -> - Tab( - selected = pagerState.currentPage == index, - onClick = { - viewModel.selectedDay = daySchedule + val pagerState = rememberPagerState( + initialPage = selectedTabIndex, + pageCount = { + days?.size ?: 0 + }, + ) - coroutineScope.launch { - pagerState.animateScrollToPage(index) - } - }, - unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ) { - Text( - text = daySchedule.day, - modifier = Modifier.padding(Dimensions.Padding.default), - fontWeight = FontWeight.Bold, + PrimaryTabRow( + selectedTabIndex = pagerState.currentPage, + indicator = { + SessionDayTabIndicator( + selectedTabIndex = pagerState.currentPage, + tabCount = days?.size ?: 0, ) + }, + ) { + days?.forEachIndexed { index, daySchedule -> + Tab( + selected = pagerState.currentPage == index, + onClick = { + viewModel.selectedDay = daySchedule + + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) { + Text( + text = daySchedule.day, + modifier = Modifier.padding(Dimensions.Padding.default), + fontWeight = FontWeight.Bold, + ) + } } } - } - LaunchedEffect(pagerState) { - snapshotFlow { pagerState.currentPage }.collect { page -> - viewModel.selectedDay = viewModel.days?.get(page) + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.currentPage }.collect { page -> + viewModel.selectedDay = viewModel.days?.get(page) + } } - } - HorizontalPager( - state = pagerState, - ) { page -> - val day = days?.get(page) ?: return@HorizontalPager - val scrollState = rememberLazyListState( - day.scrollState.firstVisibleItemIndex, - day.scrollState.firstVisibleItemScrollOffset, - ) - if ( - day.scrollState.firstVisibleItemIndex != scrollState.firstVisibleItemIndex || - day.scrollState.firstVisibleItemScrollOffset != scrollState.firstVisibleItemScrollOffset - ) { - day.scrollState = SessionDayViewModel.ScrollState( - scrollState.firstVisibleItemIndex, - scrollState.firstVisibleItemScrollOffset, + HorizontalPager( + state = pagerState, + ) { page -> + val day = days?.get(page) ?: return@HorizontalPager + val scrollState = rememberLazyListState( + day.scrollState.firstVisibleItemIndex, + day.scrollState.firstVisibleItemScrollOffset, ) - } + if ( + day.scrollState.firstVisibleItemIndex != scrollState.firstVisibleItemIndex || + day.scrollState.firstVisibleItemScrollOffset != scrollState.firstVisibleItemScrollOffset + ) { + day.scrollState = SessionDayViewModel.ScrollState( + scrollState.firstVisibleItemIndex, + scrollState.firstVisibleItemScrollOffset, + ) + } - val density = LocalDensity.current - LazyColumn( - state = scrollState, - contentPadding = PaddingValues(vertical = Dimensions.Padding.quarter), - modifier = Modifier.width(with(density) { size.width.toDp() }), - ) { - items(day.blocks) { hourBlock -> - Box( - modifier = Modifier.padding( - vertical = Dimensions.Padding.quarter, - horizontal = Dimensions.Padding.half, - ), - ) { - SessionBlockView(hourBlock, onClick = onClick) + val density = LocalDensity.current + LazyColumn( + state = scrollState, + contentPadding = PaddingValues(vertical = Dimensions.Padding.quarter), + modifier = Modifier.width(with(density) { size.width.toDp() }), + ) { + items(day.blocks) { hourBlock -> + Box( + modifier = Modifier.padding( + vertical = Dimensions.Padding.quarter, + horizontal = Dimensions.Padding.half, + ), + ) { + SessionBlockView(hourBlock, onClick = onClick) + } } } } diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt index af3708f3..96dcd7dd 100644 --- a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt @@ -33,6 +33,7 @@ actual fun VenueBodyView(modifier: Modifier, venueMapUrl: String?){ } is Resource.Failure -> { Text("Error loading venue map.") + print(resource.exception.message) } } } else { From 07d6119c39d783e624f07cb3b332d08a12b59cc5 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 22 Apr 2026 14:41:55 -0400 Subject: [PATCH 31/56] formatting --- .github/workflows/build.yml | 2 +- gradle/libs.versions.toml | 1 + gradle/wrapper/gradle-wrapper.properties | 2 +- .../droidcon/ios/DependencyInjection.kt | 5 +++-- shared-ui/build.gradle.kts | 4 +--- .../ui/BottomNavigationView.kt | 12 +--------- .../co.touchlab.droidcon/ui/UiModule.kt | 17 +++++++++++--- .../ui/session/SessionDetailView.kt | 9 +++----- .../session/SessionListDetailPaneScaffold.kt | 10 ++++++--- .../ui/settings/AboutView.kt | 3 +-- .../ui/sponsors/SponsorsView.kt | 7 ------ .../ui/util/LocalImage.kt | 12 +++++----- .../util/NavigationController.kt | 2 +- .../viewmodel/ApplicationViewModel.kt | 4 ---- .../viewmodel/ViewModelFactory.kt | 22 +++++++------------ .../viewmodel/session/AgendaViewModel.kt | 5 ++--- .../session/BaseSessionListViewModel.kt | 2 +- .../viewmodel/session/ScheduleViewModel.kt | 2 +- .../session/SessionBlockViewModel.kt | 2 +- .../viewmodel/session/SessionDayViewModel.kt | 2 +- .../session/SessionDetailViewModel.kt | 2 +- .../viewmodel/settings/SettingsViewModel.kt | 2 +- .../sponsor/SponsorDetailViewModel.kt | 2 +- .../sponsor/SponsorGroupViewModel.kt | 2 +- .../co.touchlab.droidcon/ui/MainView.kt | 1 - .../co.touchlab.droidcon/ui/util/Image.js.kt | 2 +- .../ui/util/TimeZoneInit.kt | 4 ++++ .../ui/venue/VenueView.kt | 2 +- .../{VenueView_Mobile.kt => VenueBodyView.kt} | 6 +++-- shared/build.gradle.kts | 19 +++++++--------- .../impl/SqlDelightDriverFactory.android.kt | 8 +++---- .../kotlin/co/touchlab/droidcon/Koin.kt | 8 +++---- .../DefaultNotificationSchedulingService.kt | 7 ++++-- .../gateway/impl/DefaultSessionGateway.kt | 19 +++++++--------- .../gateway/impl/DefaultSponsorGateway.kt | 11 +++------- .../impl/SqlDelightRoomRepository.kt | 3 ++- .../impl/SqlDelightSessionRepository.kt | 6 ++--- .../impl/DefaultConferenceConfigProvider.kt | 8 +++---- .../service/impl/DefaultFeedbackService.kt | 7 ++++-- .../impl/SqlDelightDriverFactory.ios.kt | 3 ++- .../droidcon/util/BundleResourceReader.kt | 2 +- .../kotlin/co/touchlab/droidcon/Koin.js.kt | 1 - .../impl/SqlDelightDriverFactory.js.kt | 8 +++---- web/build.gradle.kts | 5 +---- .../web/{KoinTest.kt => WebKoinTest.kt} | 6 ++--- .../droidcon/web/DependencyInjection.kt | 8 +++---- .../kotlin/co/touchlab/droidcon/web/Main2.kt | 3 +-- .../droidcon/web/WebAnalyticsService.kt | 2 +- .../web/service/DefaultParseUrlViewService.kt | 2 +- .../NotificationLocalizedStringFactory.kt | 2 +- 50 files changed, 128 insertions(+), 158 deletions(-) rename shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/{VenueView_Mobile.kt => VenueBodyView.kt} (94%) rename web/src/jsTest/kotlin/co/touchlab/droidcon/web/{KoinTest.kt => WebKoinTest.kt} (94%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb803b22..48bd055c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,4 +25,4 @@ jobs: uses: android-actions/setup-android@v3 - name: Check, Assemble Android and compile iOS - run: ./gradlew ktlintCheck assembleDebug compileKotlinIosX64 --no-daemon \ No newline at end of file + run: ./gradlew ktlintCheck assembleDebug compileKotlinIosSimulatorArm64 --no-daemon \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index de7be3cc..37c97e9a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -105,6 +105,7 @@ sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", ver sqldelight-driver-ios = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" } sqldelight-driver-js = { module = "app.cash.sqldelight:web-worker-driver", version.ref = "sqlDelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqlDelight" } +sqldelight-async-extensions = { module = "app.cash.sqldelight:async-extensions", version.ref = "sqlDelight" } sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da4..aaaabb3c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt index b3413c22..1408f510 100644 --- a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt +++ b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt @@ -9,7 +9,6 @@ import co.touchlab.droidcon.ios.service.DefaultParseUrlViewService import co.touchlab.droidcon.ios.util.NotificationLocalizedStringFactory import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule -import co.touchlab.droidcon.util.BundleResourceReader import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.NSUserDefaultsSettings @@ -25,7 +24,9 @@ fun initKoinIos(userDefaults: NSUserDefaults, analyticsService: AnalyticsService module { single { BundleProvider(bundle = NSBundle.mainBundle) } single { NSUserDefaultsSettings(delegate = userDefaults) } - single { ComposeResourceReader() /*BundleResourceReader(bundle = get().bundle)*/ } + single { + ComposeResourceReader() + } single { NotificationLocalizedStringFactory(bundle = get().bundle) diff --git a/shared-ui/build.gradle.kts b/shared-ui/build.gradle.kts index 9ae5c64e..e0619228 100644 --- a/shared-ui/build.gradle.kts +++ b/shared-ui/build.gradle.kts @@ -111,7 +111,6 @@ kotlin { implementation(libs.adaptive.layout) implementation(libs.adaptive.navigation) implementation(libs.material3.adaptive.navigation.suite) - } val mobileMain by creating { dependsOn(commonMain.get()) @@ -145,8 +144,7 @@ kotlin { implementation(kotlin("stdlib-js")) implementation(libs.kotlinx.browser) implementation(libs.zoomimage.compose) - implementation(libs.kamel.image.default) // add this - + implementation(libs.kamel.image.default) // add this } all { diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt index b1d4afdd..8d27cc9b 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt @@ -17,7 +17,6 @@ import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemCo import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import co.touchlab.droidcon.domain.entity.Conference import co.touchlab.droidcon.ui.session.SessionListDetailPaneScaffold @@ -26,15 +25,10 @@ import co.touchlab.droidcon.ui.sponsors.SponsorsView import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.ui.venue.VenueView import co.touchlab.droidcon.viewmodel.ApplicationViewModel -import co.touchlab.droidcon.viewmodel.session.BaseSessionListViewModel @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable -internal fun BottomNavigationView( - viewModel: ApplicationViewModel, - currentConference: Conference, - modifier: Modifier = Modifier, -) { +internal fun BottomNavigationView(viewModel: ApplicationViewModel, currentConference: Conference, modifier: Modifier = Modifier) { val selectedTab by viewModel.observeSelectedTab.observeAsState() val iconColor = MaterialTheme.colorScheme.onPrimary val textColor = MaterialTheme.colorScheme.primary @@ -111,10 +105,6 @@ internal fun BottomNavigationView( }, ) - - - - val feedback by viewModel.observePresentedFeedback.observeAsState() feedback?.let { FeedbackDialog(it) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt index ead950e9..8a5a72f2 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt @@ -84,15 +84,26 @@ val uiModule = module { single { ViewModelFactory.SpeakerDetailViewModelFactory(parseUrlViewService = get()) } - single { ViewModelFactory.SponsorListViewModelFactory(sponsorGateway = get(), sponsorGroupFactory = get(), sponsorDetailFactory = get()) } + single { + ViewModelFactory.SponsorListViewModelFactory(sponsorGateway = get(), sponsorGroupFactory = get(), sponsorDetailFactory = get()) + } single { ViewModelFactory.SponsorGroupViewModelFactory(sponsorGroupItemFactory = get()) } single { ViewModelFactory.SponsorGroupItemViewModelFactory() } - single { ViewModelFactory.SponsorDetailViewModelFactory(sponsorGateway = get(), speakerListItemFactory = get(), speakerDetailFactory = get()) } + single { + ViewModelFactory.SponsorDetailViewModelFactory(sponsorGateway = get(), speakerListItemFactory = get(), speakerDetailFactory = get()) + } single { ViewModelFactory.SettingsViewModelFactory(settingsGateway = get(), aboutFactory = get(), conferenceRepository = get()) } single { ViewModelFactory.AboutViewModelFactory(aboutRepository = get(), parseUrlViewService = get()) } - single { ViewModelFactory.FeedbackDialogViewModelFactory(sessionGateway = get(), get(parameters = { parametersOf("FeedbackDialogViewModel") })) } + single { + ViewModelFactory.FeedbackDialogViewModelFactory( + sessionGateway = get(), + get(parameters = { + parametersOf("FeedbackDialogViewModel") + }), + ) + } single { SessionDetailScrollStateStorage() } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt index f72c103f..4a37cf92 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionDetailView.kt @@ -52,7 +52,6 @@ import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.util.NavigationController import co.touchlab.droidcon.util.NavigationStack import co.touchlab.droidcon.viewmodel.FeedbackDialogViewModel -import co.touchlab.droidcon.viewmodel.session.SessionDayViewModel import co.touchlab.droidcon.viewmodel.session.SessionDetailViewModel import co.touchlab.droidcon.viewmodel.session.SessionDetailViewModel.SessionState import co.touchlab.droidcon.viewmodel.session.SpeakerListItemViewModel @@ -74,12 +73,11 @@ internal fun SessionDetailView( showBackButton: Boolean, speakers: List, feedback: FeedbackDialogViewModel?, - attendingTapped:()-> Unit, + attendingTapped: () -> Unit, writeFeedbackTapped: () -> Unit, - onScrollStateChanged:(Int)-> Unit, + onScrollStateChanged: (Int) -> Unit, onBack: (() -> Unit)? = null, ) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -87,7 +85,7 @@ internal fun SessionDetailView( TopAppBar( title = { Text("Session") }, navigationIcon = { - if(showBackButton) { + if (showBackButton) { IconButton( onClick = { if (onBack != null) { @@ -169,7 +167,6 @@ internal fun SessionDetailView( } } - description?.let { DescriptionView(it, descriptionLinks) } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt index 2c0a4455..5bc52b68 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt @@ -1,9 +1,9 @@ package co.touchlab.droidcon.ui.session -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -105,7 +105,11 @@ fun SessionListDetailPaneScaffold(viewModel: BaseSessionListViewModel, title: St verticalArrangement = Arrangement.Center, ) { Text("No session selected. Please select a session for more details", style = MaterialTheme.typography.titleLarge) - Icon(modifier = Modifier.fillMaxSize(0.33f), painter = painterResource(Res.drawable.event_note), contentDescription = "No Session Selected") + Icon( + modifier = Modifier.fillMaxSize(0.25f), + painter = painterResource(Res.drawable.event_note), + contentDescription = "No Session Selected", + ) } } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/AboutView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/AboutView.kt index 5d8d23aa..8966909c 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/AboutView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/AboutView.kt @@ -2,7 +2,6 @@ package co.touchlab.droidcon.ui.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn @@ -74,7 +73,7 @@ private fun AboutItemView(viewModel: AboutItemViewModel) { top = Dimensions.Padding.default, bottom = Dimensions.Padding.default, ), - contentScale = ContentScale.Fit + contentScale = ContentScale.Fit, ) } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt index 77b59347..2bf76e5e 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorsView.kt @@ -4,10 +4,8 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio @@ -18,8 +16,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape @@ -46,16 +42,13 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min import androidx.window.core.layout.WindowSizeClass -import androidx.window.core.layout.WindowWidthSizeClass import co.touchlab.droidcon.ui.theme.Dimensions import co.touchlab.droidcon.ui.util.DcAsyncImage import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.util.NavigationStack import co.touchlab.droidcon.viewmodel.sponsor.SponsorGroupViewModel import co.touchlab.droidcon.viewmodel.sponsor.SponsorListViewModel -import kotlin.math.min private const val LOG_TAG = "SponsorsView" diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt index 3d0d5648..43de68f3 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/LocalImage.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.ContentScale.Companion import co.touchlab.droidcon.ui.theme.Dimensions import droidcon.shared_ui.generated.resources.Res import droidcon.shared_ui.generated.resources.about_droidcon @@ -25,13 +24,16 @@ import droidcon.shared_ui.generated.resources.about_touchlab import droidcon.shared_ui.generated.resources.linkedin import droidcon.shared_ui.generated.resources.twitter import droidcon.shared_ui.generated.resources.venue_map_1 -import kotlinx.coroutines.CoroutineDispatcher -import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource @Composable -internal fun LocalImage(imageResourceName: String, modifier: Modifier = Modifier, contentDescription: String? = null, contentScale: ContentScale = ContentScale.FillWidth) { - val imageRes = when(imageResourceName.lowercase()){ +internal fun LocalImage( + imageResourceName: String, + modifier: Modifier = Modifier, + contentDescription: String? = null, + contentScale: ContentScale = ContentScale.FillWidth, +) { + val imageRes = when (imageResourceName.lowercase()) { "about_droidcon" -> Res.drawable.about_droidcon "about_touchlab" -> Res.drawable.about_touchlab "about_kotlin" -> Res.drawable.about_kotlin diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt index b13b6d2d..eaa6325a 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt @@ -2,8 +2,8 @@ package co.touchlab.droidcon.util import androidx.compose.animation.AnimatedContent import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt index e2867c29..904351c5 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt @@ -9,10 +9,6 @@ import co.touchlab.droidcon.domain.repository.ConferenceRepository import co.touchlab.droidcon.domain.service.FeedbackService import co.touchlab.droidcon.domain.service.SyncService import co.touchlab.droidcon.service.DeepLinkNotificationHandler -import co.touchlab.droidcon.viewmodel.session.AgendaViewModel -import co.touchlab.droidcon.viewmodel.session.ScheduleViewModel -import co.touchlab.droidcon.viewmodel.settings.SettingsViewModel -import co.touchlab.droidcon.viewmodel.sponsor.SponsorListViewModel import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.MutableStateFlow import org.brightify.hyperdrive.multiplatformx.BaseViewModel diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ViewModelFactory.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ViewModelFactory.kt index 7d56e42d..2b7746d2 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ViewModelFactory.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ViewModelFactory.kt @@ -1,12 +1,12 @@ package co.touchlab.droidcon.viewmodel -import co.touchlab.droidcon.domain.entity.Profile import co.touchlab.droidcon.application.gateway.SettingsGateway import co.touchlab.droidcon.application.repository.AboutRepository import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.application.service.NotificationService import co.touchlab.droidcon.domain.composite.ScheduleItem import co.touchlab.droidcon.domain.composite.SponsorGroupWithSponsors +import co.touchlab.droidcon.domain.entity.Profile import co.touchlab.droidcon.domain.entity.Session import co.touchlab.droidcon.domain.entity.Sponsor import co.touchlab.droidcon.domain.gateway.SessionGateway @@ -17,6 +17,7 @@ import co.touchlab.droidcon.domain.service.DateTimeService import co.touchlab.droidcon.domain.service.FeedbackService import co.touchlab.droidcon.domain.service.SyncService import co.touchlab.droidcon.service.ParseUrlViewService +import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.droidcon.viewmodel.session.AgendaViewModel import co.touchlab.droidcon.viewmodel.session.ScheduleViewModel import co.touchlab.droidcon.viewmodel.session.SessionBlockViewModel @@ -33,31 +34,25 @@ import co.touchlab.droidcon.viewmodel.sponsor.SponsorGroupItemViewModel import co.touchlab.droidcon.viewmodel.sponsor.SponsorGroupViewModel import co.touchlab.droidcon.viewmodel.sponsor.SponsorListViewModel import co.touchlab.kermit.Logger -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime -import co.touchlab.droidcon.util.formatter.DateFormatter import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime @OptIn(ExperimentalJsExport::class) @JsExport object ViewModelFactory { - class AboutViewModelFactory( - private val aboutRepository: AboutRepository, - private val parseUrlViewService: ParseUrlViewService, - ) { + class AboutViewModelFactory(private val aboutRepository: AboutRepository, private val parseUrlViewService: ParseUrlViewService) { fun create() = AboutViewModel(aboutRepository, parseUrlViewService) } class SessionListItemViewModelFactory(private val dateTimeService: DateTimeService) { - fun create(item: ScheduleItem, selected: () -> Unit) = - SessionListItemViewModel(dateTimeService, item, selected) + fun create(item: ScheduleItem, selected: () -> Unit) = SessionListItemViewModel(dateTimeService, item, selected) } class SpeakerListItemViewModelFactory { - fun create(profile: Profile, selected: () -> Unit) = - SpeakerListItemViewModel(profile, selected) + fun create(profile: Profile, selected: () -> Unit) = SpeakerListItemViewModel(profile, selected) } class SpeakerDetailViewModelFactory(private val parseUrlViewService: ParseUrlViewService) { @@ -168,8 +163,7 @@ object ViewModelFactory { } class SponsorGroupItemViewModelFactory { - fun create(sponsor: Sponsor, selected: () -> Unit) = - SponsorGroupItemViewModel(sponsor, selected) + fun create(sponsor: Sponsor, selected: () -> Unit) = SponsorGroupItemViewModel(sponsor, selected) } class SponsorGroupViewModelFactory(private val sponsorGroupItemFactory: SponsorGroupItemViewModelFactory) { diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/AgendaViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/AgendaViewModel.kt index 8b942d7d..fa13dda0 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/AgendaViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/AgendaViewModel.kt @@ -1,9 +1,9 @@ package co.touchlab.droidcon.viewmodel.session -import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.gateway.SessionGateway import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.DateTimeService +import co.touchlab.droidcon.viewmodel.ViewModelFactory class AgendaViewModel( sessionGateway: SessionGateway, @@ -20,5 +20,4 @@ class AgendaViewModel( dateTimeService, conferenceConfigProvider, attendingOnly = true, -) { -} +) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt index fe4f63c2..743fcfba 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt @@ -1,10 +1,10 @@ package co.touchlab.droidcon.viewmodel.session -import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.gateway.SessionGateway import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.DateTimeService import co.touchlab.droidcon.domain.service.toConferenceDateTime +import co.touchlab.droidcon.viewmodel.ViewModelFactory import org.brightify.hyperdrive.multiplatformx.BaseViewModel abstract class BaseSessionListViewModel( diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/ScheduleViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/ScheduleViewModel.kt index 551a6e8b..0aa8a67d 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/ScheduleViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/ScheduleViewModel.kt @@ -1,10 +1,10 @@ package co.touchlab.droidcon.viewmodel.session -import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.entity.Session import co.touchlab.droidcon.domain.gateway.SessionGateway import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.DateTimeService +import co.touchlab.droidcon.viewmodel.ViewModelFactory class ScheduleViewModel( private val sessionGateway: SessionGateway, diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionBlockViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionBlockViewModel.kt index a39ad78f..22aa08cd 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionBlockViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionBlockViewModel.kt @@ -1,8 +1,8 @@ package co.touchlab.droidcon.viewmodel.session -import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.composite.ScheduleItem import co.touchlab.droidcon.util.formatter.DateFormatter +import co.touchlab.droidcon.viewmodel.ViewModelFactory import kotlinx.datetime.LocalDateTime import org.brightify.hyperdrive.multiplatformx.BaseViewModel diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt index 54f6573c..8e6ee0c5 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt @@ -1,12 +1,12 @@ package co.touchlab.droidcon.viewmodel.session -import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.composite.ScheduleItem import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.DateTimeService import co.touchlab.droidcon.domain.service.toConferenceDateTime import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.droidcon.util.startOfMinute +import co.touchlab.droidcon.viewmodel.ViewModelFactory import kotlinx.datetime.LocalDate import org.brightify.hyperdrive.multiplatformx.BaseViewModel diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt index dbf4be1e..bbfa9404 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt @@ -1,6 +1,5 @@ package co.touchlab.droidcon.viewmodel.session -import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.application.gateway.SettingsGateway import co.touchlab.droidcon.application.service.NotificationService import co.touchlab.droidcon.domain.composite.ScheduleItem @@ -12,6 +11,7 @@ import co.touchlab.droidcon.dto.WebLink import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.droidcon.viewmodel.FeedbackDialogViewModel +import co.touchlab.droidcon.viewmodel.ViewModelFactory import kotlin.time.Instant import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt index 9615f3bd..2932164c 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt @@ -1,9 +1,9 @@ package co.touchlab.droidcon.viewmodel.settings -import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.application.gateway.SettingsGateway import co.touchlab.droidcon.domain.entity.Conference import co.touchlab.droidcon.domain.repository.ConferenceRepository +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.kermit.Logger import org.brightify.hyperdrive.multiplatformx.BaseViewModel import org.brightify.hyperdrive.multiplatformx.property.MutableObservableProperty diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorDetailViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorDetailViewModel.kt index e85b6d96..9a8d8930 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorDetailViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorDetailViewModel.kt @@ -1,8 +1,8 @@ package co.touchlab.droidcon.viewmodel.sponsor -import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.entity.Sponsor import co.touchlab.droidcon.domain.gateway.SponsorGateway +import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.viewmodel.session.SpeakerDetailViewModel import co.touchlab.droidcon.viewmodel.session.SpeakerListItemViewModel import org.brightify.hyperdrive.multiplatformx.BaseViewModel diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupViewModel.kt index 4a20390b..cb8b627a 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/sponsor/SponsorGroupViewModel.kt @@ -1,8 +1,8 @@ package co.touchlab.droidcon.viewmodel.sponsor -import co.touchlab.droidcon.viewmodel.ViewModelFactory import co.touchlab.droidcon.domain.composite.SponsorGroupWithSponsors import co.touchlab.droidcon.domain.entity.Sponsor +import co.touchlab.droidcon.viewmodel.ViewModelFactory import org.brightify.hyperdrive.multiplatformx.BaseViewModel class SponsorGroupViewModel( diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/MainView.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/MainView.kt index 265a4ab2..e6679a5d 100644 --- a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/MainView.kt +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/MainView.kt @@ -8,4 +8,3 @@ import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel fun MainView(waitForLoadedContextModel: WaitForLoadedContextModel) { MainComposeView(waitForLoadedContextModel = waitForLoadedContextModel, modifier = Modifier) } - diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt index 5b966bc7..6847cb67 100644 --- a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt @@ -8,7 +8,7 @@ import io.kamel.image.asyncPainterResource @Composable actual fun DcAsyncImage(logTag: String, url: String?, contentDescription: String?, modifier: Modifier) { - if(url != null) { + if (url != null) { KamelImage( resource = { asyncPainterResource(data = url) }, modifier = modifier, diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/TimeZoneInit.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/TimeZoneInit.kt index cc10d305..6ae49cd6 100644 --- a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/TimeZoneInit.kt +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/TimeZoneInit.kt @@ -1,5 +1,9 @@ + @file:JsModule("@js-joda/timezone") @file:JsNonModule + +@file:Suppress("ktlint:standard:filename") + package co.touchlab.droidcon.ui.util external object TimezoneInit diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt index 96dcd7dd..448d7fc1 100644 --- a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt @@ -13,7 +13,7 @@ import io.kamel.core.Resource import io.kamel.image.asyncPainterResource @Composable -actual fun VenueBodyView(modifier: Modifier, venueMapUrl: String?){ +actual fun VenueBodyView(modifier: Modifier, venueMapUrl: String?) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, diff --git a/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView_Mobile.kt b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueBodyView.kt similarity index 94% rename from shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView_Mobile.kt rename to shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueBodyView.kt index b72d695b..81919a9b 100644 --- a/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueView_Mobile.kt +++ b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueBodyView.kt @@ -14,7 +14,7 @@ import coil3.compose.rememberAsyncImagePainter import com.github.panpf.zoomimage.ZoomImage @Composable -actual fun VenueBodyView(modifier: Modifier, venueMapUrl: String?){ +actual fun VenueBodyView(modifier: Modifier, venueMapUrl: String?) { val painter = rememberAsyncImagePainter(venueMapUrl) val state by painter.state.collectAsState() @@ -25,7 +25,9 @@ actual fun VenueBodyView(modifier: Modifier, venueMapUrl: String?){ when (state) { is AsyncImagePainter.State.Empty, is AsyncImagePainter.State.Loading, - -> { CircularProgressIndicator() } + -> { + CircularProgressIndicator() + } is AsyncImagePainter.State.Error -> { Text("Error loading venue map.") } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 0a290087..e618167f 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -6,6 +6,9 @@ plugins { alias(libs.plugins.serialization) alias(libs.plugins.androidLibrary) alias(libs.plugins.sqlDelight) + // Compose used for Resources + alias(libs.plugins.composeCompiler) + alias(libs.plugins.jetbrainsCompose) } android { @@ -62,14 +65,15 @@ kotlin { js(IR) { browser() binaries.executable() - - } version = "1.0" sourceSets { commonMain.dependencies { + // Required when composeCompiler plugin is applied in this module. + implementation(compose.runtime) + implementation(compose.components.resources) api(libs.kermit) api(libs.kotlinx.coroutines.core) api(libs.kotlinx.datetime) @@ -78,6 +82,7 @@ kotlin { implementation(libs.bundles.ktor.common) implementation(libs.bundles.sqldelight.common) + implementation(libs.sqldelight.async.extensions) implementation(libs.stately.common) implementation(libs.koin.core) @@ -119,17 +124,9 @@ kotlin { jsMain.dependencies { implementation(libs.ktor.client.cio) implementation(libs.sqldelight.driver.js) - //implementation(devNpm("copy-webpack-plugin", "9.1.0")) implementation(npm("sql.js", "1.8.0")) implementation(npm("@cashapp/sqldelight-sqljs-worker", "2.2.1")) implementation(npm("@js-joda/timezone", "2.22.0")) -/* - implementation(npm("path-browserify", "1.0.1")) - implementation(npm("crypto-browserify", "3.12.0")) - implementation(npm("os-browserify", "0.3.0")) - implementation(npm("buffer", "6.0.3")) - implementation(npm("stream-browserify", "3.0.0")) - implementation(npm("vm-browserify", "1.1.2"))*/ } all { @@ -147,7 +144,7 @@ kotlin { } sqldelight { - databases{ + databases { create("DroidconDatabase") { packageName.set("co.touchlab.droidcon.db") generateAsync = true diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt index b7047457..2dcd18e0 100644 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt +++ b/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt @@ -1,17 +1,15 @@ package co.touchlab.droidcon.domain.repository.impl import android.content.Context -import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.db.SqlDriver -import app.cash.sqldelight.db.SqlSchema import app.cash.sqldelight.driver.android.AndroidSqliteDriver import co.touchlab.droidcon.db.DroidconDatabase actual class SqlDelightDriverFactory(private val context: Context) { - - @Suppress("UNCHECKED_CAST") + actual fun createDriver(): SqlDriver = AndroidSqliteDriver( - schema = DroidconDatabase.Schema as SqlSchema>, + schema = DroidconDatabase.Schema.synchronous(), context = context, name = "droidcon.db", ) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index 7ce75622..5c2e3b5f 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -45,10 +45,10 @@ import co.touchlab.droidcon.domain.service.impl.DefaultScheduleService import co.touchlab.droidcon.domain.service.impl.DefaultServerApi import co.touchlab.droidcon.domain.service.impl.DefaultSyncService import co.touchlab.droidcon.domain.service.impl.DefaultUserIdProvider -import co.touchlab.droidcon.util.formatter.DateFormatter -import co.touchlab.droidcon.util.formatter.KotlinXDateFormatter import co.touchlab.droidcon.domain.service.impl.json.AboutJsonResourceDataSource import co.touchlab.droidcon.domain.service.impl.json.JsonResourceReader +import co.touchlab.droidcon.util.formatter.DateFormatter +import co.touchlab.droidcon.util.formatter.KotlinXDateFormatter import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout import kotlin.time.Clock @@ -65,8 +65,8 @@ import org.koin.dsl.module fun initKoin(additionalModules: List): KoinApplication { val koinApplication = startKoin { modules( - platformModule + - coreModule + + platformModule + + coreModule + additionalModules, ) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt index 0e39de9e..0d37b75a 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt @@ -41,8 +41,11 @@ class DefaultNotificationSchedulingService( private var scheduledNotifications: List get() = settings.getStringOrNull(SCHEDULED_NOTIFICATIONS_KEY)?.let { serializedList -> - if(serializedList.isBlank()) emptyList() - else json.decodeFromString>(serializedList).map { Session.Id(it) } + if (serializedList.isBlank()) { + emptyList() + } else { + json.decodeFromString>(serializedList).map { Session.Id(it) } + } } ?: emptyList() set(value) { settings[SCHEDULED_NOTIFICATIONS_KEY] = json.encodeToString(value.map { it.value }) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt index afa7eb12..104b07ed 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt @@ -1,7 +1,6 @@ package co.touchlab.droidcon.domain.gateway.impl import co.touchlab.droidcon.domain.composite.ScheduleItem -import co.touchlab.droidcon.domain.composite.SponsorGroupWithSponsors import co.touchlab.droidcon.domain.entity.Session import co.touchlab.droidcon.domain.gateway.SessionGateway import co.touchlab.droidcon.domain.repository.ProfileRepository @@ -55,17 +54,15 @@ class DefaultSessionGateway( } } - override fun observeScheduleItem(id: Session.Id): Flow { - return conferenceConfigProvider.observeChanges() - .filterNotNull() - .map { conference -> conference.id } - .distinctUntilChanged() - .flatMapLatest { confId -> - sessionRepository.observe(id, confId).map { session -> - scheduleItemForSession(session) - } + override fun observeScheduleItem(id: Session.Id): Flow = conferenceConfigProvider.observeChanges() + .filterNotNull() + .map { conference -> conference.id } + .distinctUntilChanged() + .flatMapLatest { confId -> + sessionRepository.observe(id, confId).map { session -> + scheduleItemForSession(session) } - } + } private suspend fun scheduleItemForSession(session: Session): ScheduleItem { val conferenceId = conferenceConfigProvider.getConferenceId() diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt index 10f4f64a..6895677b 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt @@ -10,13 +10,10 @@ import co.touchlab.droidcon.domain.repository.SponsorRepository import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull class DefaultSponsorGateway( private val sponsorRepository: SponsorRepository, @@ -61,13 +58,11 @@ class DefaultSponsorGateway( }*/ } - override fun observeSponsorById(id: Sponsor.Id): Flow { - return conferenceConfigProvider.observeChanges() - .filterNotNull() - .flatMapLatest { conference -> + override fun observeSponsorById(id: Sponsor.Id): Flow = conferenceConfigProvider.observeChanges() + .filterNotNull() + .flatMapLatest { conference -> sponsorRepository.observe(id, conference.id) } - } override suspend fun getRepresentatives(sponsorId: Sponsor.Id): List { val conferenceId = conferenceConfigProvider.getConferenceId() diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt index 7f29b1cf..d0a8dac7 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt @@ -39,7 +39,8 @@ class SqlDelightRoomRepository(private val roomQueries: RoomQueries) : roomQueries.deleteById(id.value, conferenceId) } - override suspend fun contains(id: Room.Id, conferenceId: Long): Boolean = roomQueries.existsById(id.value, conferenceId).awaitAsOne() != 0L + override suspend fun contains(id: Room.Id, conferenceId: Long): Boolean = + roomQueries.existsById(id.value, conferenceId).awaitAsOne() != 0L private fun roomFactory(id: Long, conferenceId: Long, name: String) = Room(id = Room.Id(id), name = name) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt index 6196badd..5fb1ab30 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch import kotlinx.coroutines.plus class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, private val sessionQueries: SessionQueries) : @@ -29,8 +28,6 @@ class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, private val log = Logger.withTag("SqlDelightSessionRepository") val lifecycleScope = CoroutineScope(SupervisorJob()) + Dispatchers.Main - - override fun observe(id: Session.Id, conferenceId: Long): Flow = sessionQueries.sessionById(id.value, conferenceId, ::sessionFactory).asFlow().mapToOne(Dispatchers.Main) @@ -61,7 +58,8 @@ class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, sessionQueries.updateFeedBackSent(if (isSent) 1 else 0, sessionId.value, conferenceId) } - override suspend fun allSync(conferenceId: Long): List = sessionQueries.allSessions(conferenceId, ::sessionFactory).awaitAsList() + override suspend fun allSync(conferenceId: Long): List = + sessionQueries.allSessions(conferenceId, ::sessionFactory).awaitAsList() override suspend fun findSync(id: Session.Id, conferenceId: Long): Session? = sessionQueries.sessionById(id.value, conferenceId, mapper = ::sessionFactory).awaitAsOneOrNull() diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt index 2d66931a..33aa2095 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt @@ -12,10 +12,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.datetime.TimeZone -class DefaultConferenceConfigProvider( - private val conferenceRepository: ConferenceRepository, - initialConference: Conference? = null -) : ConferenceConfigProvider { +class DefaultConferenceConfigProvider(private val conferenceRepository: ConferenceRepository, initialConference: Conference? = null) : + ConferenceConfigProvider { private val log = Logger.withTag("DefaultConferenceConfigProvider") private val _currentConferenceState = MutableStateFlow(initialConference) val currentConferenceState: StateFlow = _currentConferenceState @@ -59,7 +57,7 @@ class DefaultConferenceConfigProvider( } private suspend fun getConference(): Conference { - if(currentConference != null) return currentConference!! + if (currentConference != null) return currentConference!! val conference = conferenceRepository.getSelected() _currentConferenceState.update { conference } return conference diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt index 5a561265..b96b27be 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultFeedbackService.kt @@ -22,8 +22,11 @@ class DefaultFeedbackService( } private var completedSessionIds: Set = settings.getStringOrNull(COMPLETED_SESSION_FEEDBACKS_KEY)?.let { - if(it.isBlank()) emptySet() - else json.decodeFromString(it) + if (it.isBlank()) { + emptySet() + } else { + json.decodeFromString(it) + } } ?: emptySet() override suspend fun next(): Session? = sessionGateway.observeAgenda().first() diff --git a/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt b/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt index b20c448c..96a700ba 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.ios.kt @@ -1,9 +1,10 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver import co.touchlab.droidcon.db.DroidconDatabase actual class SqlDelightDriverFactory { - actual fun createDriver(): SqlDriver = NativeSqliteDriver(DroidconDatabase.Schema, "droidcon.db") + actual fun createDriver(): SqlDriver = NativeSqliteDriver(DroidconDatabase.Schema.synchronous(), "droidcon.db") } diff --git a/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/BundleResourceReader.kt b/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/BundleResourceReader.kt index 2b94136a..24f68c96 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/BundleResourceReader.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/BundleResourceReader.kt @@ -18,7 +18,7 @@ import platform.darwin.NSObjectMeta @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) @BetaInteropApi class BundleResourceReader(private val bundle: NSBundle = NSBundle.bundleForClass(BundleMarker)) : ResourceReader { - override fun readResource(name: String): String { + override suspend fun readResource(name: String): String { // TODO: Catch iOS-only exceptions and map them to common ones. val (filename, type) = when (val lastPeriodIndex = name.lastIndexOf('.')) { 0 -> { diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt index 1cd3c74b..7ddc285c 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt @@ -8,7 +8,6 @@ import co.touchlab.kermit.CommonWriter import co.touchlab.kermit.Logger import co.touchlab.kermit.StaticConfig import io.ktor.client.engine.HttpClientEngine -import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.js.Js import org.koin.dsl.module diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt index 137b43b6..d2d174a1 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt @@ -3,13 +3,11 @@ package co.touchlab.droidcon.domain.repository.impl import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.worker.WebWorkerDriver import app.cash.sqldelight.driver.worker.expected.Worker -import co.touchlab.droidcon.db.DroidconDatabase actual class SqlDelightDriverFactory { - actual fun createDriver(): SqlDriver = - WebWorkerDriver( + actual fun createDriver(): SqlDriver = WebWorkerDriver( Worker( - js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)""") - ) + js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)"""), + ), ) } diff --git a/web/build.gradle.kts b/web/build.gradle.kts index bfbc683a..ab6633f9 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl - plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.jetbrainsCompose) @@ -32,13 +30,12 @@ kotlin { implementation(libs.multiplatform.settings.make.observable) implementation(libs.koin.compose) implementation(libs.hyperdrive.multiplatformx.api) - } } val jsTest by getting { dependencies { implementation("org.jetbrains.kotlin:kotlin-test:2.2.21") - //implementation(libs.kotlin.test) + // implementation(libs.kotlin.test) implementation(libs.koin.test) } } diff --git a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt similarity index 94% rename from web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt rename to web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt index 9fe91c4d..0cfb5e0e 100644 --- a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/KoinTest.kt +++ b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt @@ -11,7 +11,6 @@ import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.droidcon.util.formatter.KotlinXDateFormatter import co.touchlab.droidcon.web.service.DefaultParseUrlViewService import co.touchlab.droidcon.web.util.NotificationLocalizedStringFactory -import co.touchlab.droidcon.web.util.WebResourceReader import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ObservableSettings import com.russhwolf.settings.Settings @@ -23,7 +22,7 @@ import org.koin.dsl.module import org.koin.test.KoinTest import org.koin.test.check.checkModules -class MyTest : KoinTest { +class WebKoinTest : KoinTest { @OptIn(ExperimentalSettingsApi::class) @Test @@ -48,9 +47,8 @@ class MyTest : KoinTest { } koinApplication { - modules(mainModule,uiModule) + modules(mainModule, uiModule) checkModules() } } } - diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt index a8389702..0c9984ea 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt @@ -20,19 +20,17 @@ import co.touchlab.droidcon.ui.uiModule import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel import co.touchlab.droidcon.web.service.DefaultParseUrlViewService import co.touchlab.droidcon.web.util.NotificationLocalizedStringFactory -import co.touchlab.droidcon.web.util.WebResourceReader import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ObservableSettings import com.russhwolf.settings.Settings import com.russhwolf.settings.StorageSettings -import org.koin.core.KoinApplication -import org.koin.dsl.module import com.russhwolf.settings.observable.makeObservable import kotlin.time.ExperimentalTime +import org.koin.core.KoinApplication +import org.koin.dsl.module @OptIn(ExperimentalSettingsApi::class, ExperimentalTime::class) suspend fun startKoin(): KoinApplication { - val driver = SqlDelightDriverFactory().createDriver() DroidconDatabase.Schema.create(driver).await() @@ -71,7 +69,7 @@ suspend fun startKoin(): KoinApplication { single { driver } single { db } - } + uiModule + } + uiModule, ) } diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt index 9206c724..bdb63a1e 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt @@ -36,11 +36,10 @@ suspend fun main() { } ComposeViewport { - val viewModel:WaitForLoadedContextModel = koinInject() // Works + val viewModel: WaitForLoadedContextModel = koinInject() // Works viewModel.lifecycle.removeFromParent() root.addChild(viewModel.lifecycle) MainView(viewModel) - } } diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt index 3961c1c5..497c1add 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt @@ -4,6 +4,6 @@ import co.touchlab.droidcon.domain.service.AnalyticsService class WebAnalyticsService : AnalyticsService { override fun logEvent(name: String, params: Map) { - //TODO("Not yet implemented") + // TODO("Not yet implemented") } } diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt index 415d6fef..fcde6fb5 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt @@ -11,7 +11,7 @@ class DefaultParseUrlViewService : ParseUrlViewService { override fun parse(text: String): List = urlRegex.findAll(text).map { WebLink( it.range, - it.value + it.value, ) }.toList() } diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt index 404d3444..d67f36cf 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt +++ b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt @@ -2,7 +2,7 @@ package co.touchlab.droidcon.web.util import co.touchlab.droidcon.application.service.NotificationSchedulingService -class NotificationLocalizedStringFactory() : NotificationSchedulingService.LocalizedStringFactory { +class NotificationLocalizedStringFactory : NotificationSchedulingService.LocalizedStringFactory { override fun reminderTitle(roomName: String?): String = "" override fun reminderBody(sessionTitle: String): String = "" From bdbb364bfba4dc24b3e47f2e9679b5f4a8a5f84a Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 23 Apr 2026 10:32:02 -0400 Subject: [PATCH 32/56] Update libs.versions.toml --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 37c97e9a..2a4a7790 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] ## SDK Versions -adaptive = "1.3.0-alpha06" +adaptive = "1.3.0-alpha02" kamelImageDefault = "1.0.9" kotlinxBrowser = "0.5.0" material3AdaptiveNavigationSuiteVersion = "1.9.0" From 4559a4d74c35192a8d3f85c6849035e7c103d607 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Tue, 28 Apr 2026 16:12:33 -0400 Subject: [PATCH 33/56] formatting --- shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt | 1 - .../domain/repository/impl/SqlDelightSessionRepository.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index f771fc30..5c2e3b5f 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -52,7 +52,6 @@ import co.touchlab.droidcon.util.formatter.KotlinXDateFormatter import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout import kotlin.time.Clock -import kotlinx.coroutines.runBlocking import kotlinx.datetime.TimeZone import kotlinx.serialization.json.Json import org.koin.core.KoinApplication diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt index 430f45a8..5fb1ab30 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt @@ -15,7 +15,6 @@ import co.touchlab.droidcon.domain.service.DateTimeService import co.touchlab.kermit.Logger import kotlin.time.Instant import kotlinx.coroutines.CoroutineScope -import kotlin.time.Instant import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow From 3dac4be4e3ba70cad3be376fce3461b077f48a0f Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 20 May 2026 15:07:31 -0400 Subject: [PATCH 34/56] Responding to comments --- build.gradle.kts | 5 ----- .../domain/gateway/impl/DefaultSponsorGateway.kt | 14 -------------- .../domain/service/ConferenceConfigProvider.kt | 1 - .../impl/DefaultConferenceConfigProvider.kt | 2 -- 4 files changed, 22 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 30a3cbfa..3d01dd44 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,8 +45,3 @@ subprojects { } } } -/* -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} -*/ diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt index 6895677b..0dcafedf 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSponsorGateway.kt @@ -42,20 +42,6 @@ class DefaultSponsorGateway( } } } -/* - val conferenceId = conferenceConfigProvider.getConferenceId() - - log.i { "Got the Conference ID" } - return sponsorGroupRepository.observeAll(conferenceId).map { groups -> - groups.map { group -> - log.i { "Found a Group of Sponsors" } - - SponsorGroupWithSponsors( - group, - sponsorRepository.allByGroupName(group.name, conferenceId), - ) - } - }*/ } override fun observeSponsorById(id: Sponsor.Id): Flow = conferenceConfigProvider.observeChanges() diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt index 15f3fe0a..7117087e 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt @@ -11,7 +11,6 @@ interface ConferenceConfigProvider { suspend fun getCollectionName(): String suspend fun getApiKey(): String suspend fun getScheduleId(): String - suspend fun showVenueMap(): Boolean fun observeChanges(): Flow /** diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt index 33aa2095..457ee165 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt @@ -33,8 +33,6 @@ class DefaultConferenceConfigProvider(private val conferenceRepository: Conferen override suspend fun getScheduleId(): String = getConference().scheduleId - override suspend fun showVenueMap(): Boolean = true // Default to true, will be configurable per conference later - override fun observeChanges(): Flow = conferenceRepository.observeSelected() // Implementation of the interface method to get the currently selected conference From ef845f9f6579e1f4cf9f044bda03c8f4253db018 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 15:33:19 -0400 Subject: [PATCH 35/56] Updating the platform --- .github/workflows/build.yml | 9 +- gradle/libs.versions.toml | 1 + .../ui/util/NavigationBackPressWrapper.js.kt | 2 +- shared/build.gradle.kts | 2 +- .../kotlin/co/touchlab/droidcon/Koin.js.kt | 6 -- web/build.gradle.kts | 3 +- .../drawable/compose-multiplatform.xml | 0 .../droidcon/web/DependencyInjection.kt | 60 +++++++++++++ .../kotlin/co/touchlab/droidcon/web/Main.kt} | 5 +- .../droidcon/web/WebAnalyticsService.kt | 0 .../web/service/DefaultParseUrlViewService.kt | 0 .../NotificationLocalizedStringFactory.kt | 0 .../{webMain => jsMain}/resources/favicon.png | Bin .../{webMain => jsMain}/resources/index.html | 2 +- .../{webMain => jsMain}/resources/styles.css | 0 .../co/touchlab/droidcon/web/WebKoinTest.kt | 40 +-------- .../org/example/project/Platform.wasmJs.kt | 7 -- .../droidcon/web/DependencyInjection.kt | 79 ------------------ .../droidcon/web/util/WebResourceReader.kt | 29 ------- 19 files changed, 77 insertions(+), 168 deletions(-) rename web/src/{webMain => jsMain}/composeResources/drawable/compose-multiplatform.xml (100%) create mode 100644 web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt rename web/src/{webMain/kotlin/co/touchlab/droidcon/web/Main2.kt => jsMain/kotlin/co/touchlab/droidcon/web/Main.kt} (88%) rename web/src/{webMain => jsMain}/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt (100%) rename web/src/{webMain => jsMain}/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt (100%) rename web/src/{webMain => jsMain}/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt (100%) rename web/src/{webMain => jsMain}/resources/favicon.png (100%) rename web/src/{webMain => jsMain}/resources/index.html (96%) rename web/src/{webMain => jsMain}/resources/styles.css (100%) delete mode 100644 web/src/wasmJsMain/kotlin/org/example/project/Platform.wasmJs.kt delete mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt delete mode 100644 web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48bd055c..f7bbac6b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,5 +24,10 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v3 - - name: Check, Assemble Android and compile iOS - run: ./gradlew ktlintCheck assembleDebug compileKotlinIosSimulatorArm64 --no-daemon \ No newline at end of file + - name: Check, assemble Android, and compile iOS and JS + run: > + ./gradlew ktlintCheck assembleDebug + compileKotlinIosSimulatorArm64 + compileKotlinJs + :web:jsTest + --no-daemon \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a4a7790..b1fbeedc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -93,6 +93,7 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version. ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-ios = { module = "io.ktor:ktor-client-ios", version.ref = "ktor" } +ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/NavigationBackPressWrapper.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/NavigationBackPressWrapper.js.kt index dc494370..27fc3427 100644 --- a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/NavigationBackPressWrapper.js.kt +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/NavigationBackPressWrapper.js.kt @@ -4,6 +4,6 @@ import androidx.compose.runtime.Composable @Composable internal actual fun NavigationBackPressWrapper(content: @Composable (() -> Unit)) { - // For now no back press wrapping is needed on Android. + // No back-press handling needed on web. content() } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index e618167f..f19b80ff 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -122,7 +122,7 @@ kotlin { iosArm64Main.dependsOn(iosMain.get()) iosSimulatorArm64Main.dependsOn(iosMain.get()) jsMain.dependencies { - implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.js) implementation(libs.sqldelight.driver.js) implementation(npm("sql.js", "1.8.0")) implementation(npm("@cashapp/sqldelight-sqljs-worker", "2.2.1")) diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt index 7ddc285c..ba10a3c3 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt @@ -1,8 +1,6 @@ package co.touchlab.droidcon -import app.cash.sqldelight.db.SqlDriver import co.touchlab.droidcon.application.service.NotificationService -import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory import co.touchlab.droidcon.service.JsNotificationService import co.touchlab.kermit.CommonWriter import co.touchlab.kermit.Logger @@ -12,10 +10,6 @@ import io.ktor.client.engine.js.Js import org.koin.dsl.module actual val platformModule: org.koin.core.module.Module = module { - single { - SqlDelightDriverFactory().createDriver() - } - single { Js.create() } diff --git a/web/build.gradle.kts b/web/build.gradle.kts index ab6633f9..52beb6b8 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -34,8 +34,7 @@ kotlin { } val jsTest by getting { dependencies { - implementation("org.jetbrains.kotlin:kotlin-test:2.2.21") - // implementation(libs.kotlin.test) + implementation(kotlin("test")) implementation(libs.koin.test) } } diff --git a/web/src/webMain/composeResources/drawable/compose-multiplatform.xml b/web/src/jsMain/composeResources/drawable/compose-multiplatform.xml similarity index 100% rename from web/src/webMain/composeResources/drawable/compose-multiplatform.xml rename to web/src/jsMain/composeResources/drawable/compose-multiplatform.xml diff --git a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt new file mode 100644 index 00000000..a24b3a06 --- /dev/null +++ b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt @@ -0,0 +1,60 @@ +package co.touchlab.droidcon.web + +import app.cash.sqldelight.db.SqlDriver +import co.touchlab.droidcon.application.service.NotificationSchedulingService +import co.touchlab.droidcon.db.DroidconDatabase +import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory +import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader +import co.touchlab.droidcon.domain.service.impl.ResourceReader +import co.touchlab.droidcon.initKoin +import co.touchlab.droidcon.service.JsNotificationService +import co.touchlab.droidcon.service.ParseUrlViewService +import co.touchlab.droidcon.ui.uiModule +import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel +import co.touchlab.droidcon.web.service.DefaultParseUrlViewService +import co.touchlab.droidcon.web.util.NotificationLocalizedStringFactory +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ObservableSettings +import com.russhwolf.settings.Settings +import com.russhwolf.settings.StorageSettings +import com.russhwolf.settings.observable.makeObservable +import kotlin.time.ExperimentalTime +import org.koin.core.KoinApplication +import org.koin.core.module.Module +import org.koin.dsl.module + +@OptIn(ExperimentalSettingsApi::class) +fun webAppModule(driver: SqlDriver? = null): Module = module { + single { + val storageSettings: Settings = StorageSettings() + storageSettings.makeObservable() + } + single { ComposeResourceReader() } + + single { NotificationLocalizedStringFactory() } + + single { WebAnalyticsService() } + + single { DefaultParseUrlViewService() } + + single { JsNotificationService() } + + if (driver != null) { + single { driver } + } +} + +@OptIn(ExperimentalSettingsApi::class, ExperimentalTime::class) +suspend fun startKoin(): KoinApplication { + val driver = SqlDelightDriverFactory().createDriver() + DroidconDatabase.Schema.create(driver).await() + + return initKoin( + webAppModule(driver) + uiModule, + ) +} + +@Suppress("unused") +val KoinApplication.waitForLoadedContextModel: WaitForLoadedContextModel + get() = get() diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/Main.kt similarity index 88% rename from web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt rename to web/src/jsMain/kotlin/co/touchlab/droidcon/web/Main.kt index bdb63a1e..013eb2f3 100644 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/Main2.kt +++ b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/Main.kt @@ -16,14 +16,13 @@ import org.koin.compose.koinInject @OptIn(ExperimentalComposeUiApi::class) suspend fun main() { - val koinApplication = startKoin() + startKoin() val lifecycleScope = CoroutineScope(SupervisorJob()) + Dispatchers.Main @Suppress("UNUSED_VARIABLE") val tz = TimezoneInit - // Now set up view model lifecycle val root = LifecycleGraph.Root("…") lifecycleScope.launch { @@ -36,7 +35,7 @@ suspend fun main() { } ComposeViewport { - val viewModel: WaitForLoadedContextModel = koinInject() // Works + val viewModel: WaitForLoadedContextModel = koinInject() viewModel.lifecycle.removeFromParent() root.addChild(viewModel.lifecycle) diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt similarity index 100% rename from web/src/webMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt rename to web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt similarity index 100% rename from web/src/webMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt rename to web/src/jsMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt similarity index 100% rename from web/src/webMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt rename to web/src/jsMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt diff --git a/web/src/webMain/resources/favicon.png b/web/src/jsMain/resources/favicon.png similarity index 100% rename from web/src/webMain/resources/favicon.png rename to web/src/jsMain/resources/favicon.png diff --git a/web/src/webMain/resources/index.html b/web/src/jsMain/resources/index.html similarity index 96% rename from web/src/webMain/resources/index.html rename to web/src/jsMain/resources/index.html index 54d52f48..8259f765 100644 --- a/web/src/webMain/resources/index.html +++ b/web/src/jsMain/resources/index.html @@ -3,7 +3,7 @@ - KotlinProject + Droidcon diff --git a/web/src/webMain/resources/styles.css b/web/src/jsMain/resources/styles.css similarity index 100% rename from web/src/webMain/resources/styles.css rename to web/src/jsMain/resources/styles.css diff --git a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt index 0cfb5e0e..b48e845a 100644 --- a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt +++ b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt @@ -1,24 +1,9 @@ package co.touchlab.droidcon.web -import co.touchlab.droidcon.application.service.NotificationSchedulingService -import co.touchlab.droidcon.domain.service.AnalyticsService -import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader -import co.touchlab.droidcon.domain.service.impl.ResourceReader -import co.touchlab.droidcon.service.JsNotificationService -import co.touchlab.droidcon.service.ParseUrlViewService -import co.touchlab.droidcon.ui.uiModule -import co.touchlab.droidcon.util.formatter.DateFormatter -import co.touchlab.droidcon.util.formatter.KotlinXDateFormatter -import co.touchlab.droidcon.web.service.DefaultParseUrlViewService -import co.touchlab.droidcon.web.util.NotificationLocalizedStringFactory +import co.touchlab.droidcon.platformModule import com.russhwolf.settings.ExperimentalSettingsApi -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.Settings -import com.russhwolf.settings.StorageSettings -import com.russhwolf.settings.observable.makeObservable import kotlin.test.Test import org.koin.dsl.koinApplication -import org.koin.dsl.module import org.koin.test.KoinTest import org.koin.test.check.checkModules @@ -26,28 +11,9 @@ class WebKoinTest : KoinTest { @OptIn(ExperimentalSettingsApi::class) @Test - fun `should inject my components`() { - val mainModule = module { - single { - val storageSettings: Settings = StorageSettings() - storageSettings.makeObservable() - } - single { ComposeResourceReader() } - - single { KotlinXDateFormatter() } - - single { NotificationLocalizedStringFactory() } - - single { WebAnalyticsService() } - - single { DefaultParseUrlViewService() } - - // Provide JsNotificationService which is required by the JS platform module - single { JsNotificationService() } - } - + fun `should inject web app components`() { koinApplication { - modules(mainModule, uiModule) + modules(webAppModule(), platformModule) checkModules() } } diff --git a/web/src/wasmJsMain/kotlin/org/example/project/Platform.wasmJs.kt b/web/src/wasmJsMain/kotlin/org/example/project/Platform.wasmJs.kt deleted file mode 100644 index 7db26af4..00000000 --- a/web/src/wasmJsMain/kotlin/org/example/project/Platform.wasmJs.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.example.project - -class WasmPlatform : Platform { - override val name: String = "Web with Kotlin/Wasm" -} - -actual fun getPlatform(): Platform = WasmPlatform() diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt deleted file mode 100644 index 0c9984ea..00000000 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt +++ /dev/null @@ -1,79 +0,0 @@ -package co.touchlab.droidcon.web - -import app.cash.sqldelight.db.SqlDriver -import co.touchlab.droidcon.application.service.NotificationSchedulingService -import co.touchlab.droidcon.db.ConferenceTable -import co.touchlab.droidcon.db.DroidconDatabase -import co.touchlab.droidcon.db.SessionTable -import co.touchlab.droidcon.db.SponsorGroupTable -import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory -import co.touchlab.droidcon.domain.repository.impl.adapter.InstantSqlDelightAdapter -import co.touchlab.droidcon.domain.service.AnalyticsService -import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader -import co.touchlab.droidcon.domain.service.impl.ResourceReader -import co.touchlab.droidcon.initKoin -import co.touchlab.droidcon.intToLongAdapter -import co.touchlab.droidcon.service.JsNotificationService -import co.touchlab.droidcon.service.ParseUrlViewService -import co.touchlab.droidcon.timeZoneAdapter -import co.touchlab.droidcon.ui.uiModule -import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel -import co.touchlab.droidcon.web.service.DefaultParseUrlViewService -import co.touchlab.droidcon.web.util.NotificationLocalizedStringFactory -import com.russhwolf.settings.ExperimentalSettingsApi -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.Settings -import com.russhwolf.settings.StorageSettings -import com.russhwolf.settings.observable.makeObservable -import kotlin.time.ExperimentalTime -import org.koin.core.KoinApplication -import org.koin.dsl.module - -@OptIn(ExperimentalSettingsApi::class, ExperimentalTime::class) -suspend fun startKoin(): KoinApplication { - val driver = SqlDelightDriverFactory().createDriver() - DroidconDatabase.Schema.create(driver).await() - - val db = DroidconDatabase.invoke( - driver = driver, - sessionTableAdapter = SessionTable.Adapter( - startsAtAdapter = InstantSqlDelightAdapter, - endsAtAdapter = InstantSqlDelightAdapter, - feedbackRatingAdapter = intToLongAdapter, - ), - sponsorGroupTableAdapter = SponsorGroupTable.Adapter( - intToLongAdapter, - ), - conferenceTableAdapter = ConferenceTable.Adapter( - conferenceTimeZoneAdapter = timeZoneAdapter, - // Note: selectedAdapter will be added when the adapter is regenerated - ), - ) - - return initKoin( - module { - single { - val storageSettings: Settings = StorageSettings() - storageSettings.makeObservable() - } - single { ComposeResourceReader() } - - single { NotificationLocalizedStringFactory() } - - single { WebAnalyticsService() } - - single { DefaultParseUrlViewService() } - - // Provide JsNotificationService which is required by the JS platform module - single { JsNotificationService() } - - single { driver } - single { db } - } + uiModule, - ) -} - -@OptIn(ExperimentalWasmJsInterop::class) -@Suppress("unused") -val KoinApplication.waitForLoadedContextModel: WaitForLoadedContextModel - get() = get() diff --git a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt b/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt deleted file mode 100644 index f2cc8f43..00000000 --- a/web/src/webMain/kotlin/co/touchlab/droidcon/web/util/WebResourceReader.kt +++ /dev/null @@ -1,29 +0,0 @@ -package co.touchlab.droidcon.web.util - -import co.touchlab.droidcon.domain.service.impl.ResourceReader -import org.w3c.xhr.XMLHttpRequest - -class WebResourceReader : ResourceReader { - override suspend fun readResource(name: String): String { - val candidatePaths = listOf( - "shared/src/commonMain/resources/$name", - "/shared/src/commonMain/resources/$name", - name, - "/$name", - "resources/$name", - "/resources/$name", - ) - - candidatePaths.forEach { path -> - val request = XMLHttpRequest() - request.open("GET", path, false) - request.send() - - if (request.status.toInt() in 200..299 && request.responseText.isNotBlank()) { - return request.responseText - } - } - - error("Unable to load web resource: $name") - } -} From d2eec98848b641e0dc418df3c905f80deefd01a5 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 15:53:43 -0400 Subject: [PATCH 36/56] fixing minor issues --- .github/workflows/build.yml | 1 + .../co.touchlab.droidcon/ui/util/Dialog.js.kt | 23 ++++++++++++------- .../droidcon/web/WebAnalyticsService.kt | 12 +++++++++- .../NotificationLocalizedStringFactory.kt | 12 ++++++---- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7bbac6b..3c35f38b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,5 +29,6 @@ jobs: ./gradlew ktlintCheck assembleDebug compileKotlinIosSimulatorArm64 compileKotlinJs + :web:jsBrowserProductionWebpack :web:jsTest --no-daemon \ No newline at end of file diff --git a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Dialog.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Dialog.js.kt index 6e7cfb7d..6bcdc97a 100644 --- a/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Dialog.js.kt +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Dialog.js.kt @@ -12,13 +12,20 @@ import androidx.compose.ui.graphics.Color @Composable internal actual fun Dialog(dismiss: () -> Unit, content: @Composable (() -> Unit)) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.3f)) - .clickable(interactionSource = MutableInteractionSource(), indication = null) { }, - contentAlignment = Alignment.Center, - ) { - content() + Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.3f)) + .clickable(interactionSource = MutableInteractionSource(), indication = null) { + dismiss() + }, + ) + Box( + modifier = Modifier.align(Alignment.Center), + contentAlignment = Alignment.Center, + ) { + content() + } } } diff --git a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt index 497c1add..e743d6bf 100644 --- a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt +++ b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt @@ -1,9 +1,19 @@ package co.touchlab.droidcon.web import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.kermit.Logger + +// Firebase Analytics does not support Web KMP, so we're using Kermit for now. class WebAnalyticsService : AnalyticsService { + + private val log = Logger.withTag("WebAnalytics") + override fun logEvent(name: String, params: Map) { - // TODO("Not yet implemented") + if (params.isEmpty()) { + log.i { "Event: $name" } + } else { + log.i { "Event: $name params=$params" } + } } } diff --git a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt index d67f36cf..32e922cc 100644 --- a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt +++ b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt @@ -4,10 +4,14 @@ import co.touchlab.droidcon.application.service.NotificationSchedulingService class NotificationLocalizedStringFactory : NotificationSchedulingService.LocalizedStringFactory { - override fun reminderTitle(roomName: String?): String = "" - override fun reminderBody(sessionTitle: String): String = "" + override fun reminderTitle(roomName: String?): String { + val ending = roomName?.let { " in $it" } ?: "" + return "Upcoming event$ending" + } - override fun feedbackTitle(): String = "" + override fun reminderBody(sessionTitle: String): String = "$sessionTitle is starting soon." - override fun feedbackBody(): String = "" + override fun feedbackTitle(): String = "Feedback Time!" + + override fun feedbackBody(): String = "Your Feedback is Requested." } From 26c6f768c45a2d64e28f21b0138dac91dde0e47a Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 15:54:31 -0400 Subject: [PATCH 37/56] Update shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt Co-authored-by: Daniel Bertoldi --- shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt index ba10a3c3..1e38c1bd 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt @@ -9,7 +9,7 @@ import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.js.Js import org.koin.dsl.module -actual val platformModule: org.koin.core.module.Module = module { +actual val platformModule: Module = module { single { Js.create() } From fdc7789083a56cb20a955c3208ab4b9e454d3e69 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 15:54:43 -0400 Subject: [PATCH 38/56] Update shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt Co-authored-by: Daniel Bertoldi --- .../viewmodel/session/SessionDetailViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt index bbfa9404..1d165cb3 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt @@ -58,7 +58,7 @@ class SessionDetailViewModel( listOfNotNull( it.room?.name, with(dateTimeService) { - val timeZone = conferenceConfigProvider.getConferenceTimeZone() ?: kotlinx.datetime.TimeZone.UTC + val timeZone = conferenceConfigProvider.getConferenceTimeZone() ?: TimeZone.UTC dateFormatter.timeOnlyInterval( it.session.startsAt.toConferenceDateTime(timeZone), it.session.endsAt.toConferenceDateTime(timeZone), From c96a19efbe5c9fb314af3ae56ea43115b5951bdd Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 15:54:54 -0400 Subject: [PATCH 39/56] Update shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt Co-authored-by: Daniel Bertoldi --- .../viewmodel/session/SessionDayViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt index 8e6ee0c5..37cfa47f 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt @@ -28,7 +28,7 @@ class SessionDayViewModel( .groupBy { it.session.startsAt.toConferenceDateTime( dateTimeService, - conferenceTimeZone = conferenceConfigProvider.getConferenceTimeZone() ?: kotlinx.datetime.TimeZone.UTC, + conferenceTimeZone = conferenceConfigProvider.getConferenceTimeZone() ?: TimeZone.UTC, ).startOfMinute } .map { (startsAt, items) -> From 7bed0afa811358896fe53b6c78078196f41aed5d Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 16:01:47 -0400 Subject: [PATCH 40/56] Update shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt Co-authored-by: Daniel Bertoldi --- .../viewmodel/session/BaseSessionListViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt index 743fcfba..90b79deb 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt @@ -45,7 +45,7 @@ abstract class BaseSessionListViewModel( .groupBy { it.session.startsAt.toConferenceDateTime( dateTimeService, - conferenceConfigProvider.getConferenceTimeZone() ?: kotlinx.datetime.TimeZone.UTC, + conferenceConfigProvider.getConferenceTimeZone() ?: TimeZone.UTC, ).date } .map { (date, items) -> From b355c0a93dec267c0c8e465768871724b6028720 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 16:02:33 -0400 Subject: [PATCH 41/56] Update shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt Co-authored-by: Daniel Bertoldi --- .../commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt index 0d2d758a..ddd6f1f9 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt @@ -55,7 +55,6 @@ private fun MainAppBody(waitForLoadedContextModel: WaitForLoadedContextModel, se viewModel.selectedTab = ApplicationViewModel.Tab.Schedule } - print("isFirstRun") if (conferences.size == 1) { LaunchedEffect(conferences) { onConferenceSelected(conferences.first()) From d298ef9b8d9fab94193878ede6f7adc0a08f03fe Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 16:07:29 -0400 Subject: [PATCH 42/56] Update shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt Co-authored-by: Daniel Bertoldi --- .../droidcon/domain/service/impl/DefaultApiDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt index e663f1fb..c2e352f5 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt @@ -25,7 +25,7 @@ class DefaultApiDataSource( private val log = Logger.withTag("DefaultApiDataSource") override suspend fun getSpeakers(): List? { - val scheduleId = conferenceConfigProvider.getScheduleId() ?: return null + val scheduleId = conferenceConfigProvider.getScheduleId() val jsonString = client.get { // We want to use the same scheduleId for speakers and schedule sessionize("/api/v2/$scheduleId/view/speakers") From 3b5d3e7171a58cfeb8b1654d7ef6481438f8ca5d Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 16:07:51 -0400 Subject: [PATCH 43/56] Update shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt Co-authored-by: Daniel Bertoldi --- .../droidcon/domain/service/impl/DefaultApiDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt index c2e352f5..151d42ce 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt @@ -34,7 +34,7 @@ class DefaultApiDataSource( } override suspend fun getSchedule(): List? { - val scheduleId = conferenceConfigProvider.getScheduleId() ?: return null + val scheduleId = conferenceConfigProvider.getScheduleId() val jsonString = client.get { sessionize("/api/v2/$scheduleId/view/gridtable") }.bodyAsText() From 6a6b0de25533042182fe0117e369a3a60c8afb2f Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 16:08:12 -0400 Subject: [PATCH 44/56] Update shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt Co-authored-by: Daniel Bertoldi --- .../droidcon/domain/service/impl/DefaultApiDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt index 151d42ce..b5c69a46 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt @@ -42,7 +42,7 @@ class DefaultApiDataSource( } override suspend fun getSponsorSessions(): List? { - val scheduleId = conferenceConfigProvider.getScheduleId() ?: return null + val scheduleId = conferenceConfigProvider.getScheduleId() val jsonString = client.get { sessionize("/api/v2/$scheduleId/view/sessions") }.bodyAsText() From 8992cf72285a7f4ee79df4781eff11e7452f11b6 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 16:09:37 -0400 Subject: [PATCH 45/56] Update shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt Co-authored-by: Daniel Bertoldi --- .../droidcon/domain/service/impl/DefaultApiDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt index b5c69a46..fdf4cf13 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt @@ -66,7 +66,7 @@ class DefaultApiDataSource( log.i { "getConferences" } val projectId = conferenceConfigProvider.getProjectId() ?: return null log.i { "Have Project ID: $projectId" } - val apiKey = conferenceConfigProvider.getApiKey() ?: return null + val apiKey = conferenceConfigProvider.getApiKey() log.i { "Have API Key" } val databaseName = "(default)" val conferenceListCollection = "conferenceListMobile" From bd6ba7f42a9f33fcac0afc3fe60b0a5c834bd314 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 16:10:16 -0400 Subject: [PATCH 46/56] removing logs and responding to comments --- .../viewmodel/session/BaseSessionListViewModel.kt | 5 +++-- shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt | 6 +++--- .../droidcon/domain/service/impl/DefaultApiDataSource.kt | 3 --- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt index 90b79deb..8bd6bbc3 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/BaseSessionListViewModel.kt @@ -5,6 +5,7 @@ import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.DateTimeService import co.touchlab.droidcon.domain.service.toConferenceDateTime import co.touchlab.droidcon.viewmodel.ViewModelFactory +import kotlinx.datetime.TimeZone import org.brightify.hyperdrive.multiplatformx.BaseViewModel abstract class BaseSessionListViewModel( @@ -38,14 +39,14 @@ abstract class BaseSessionListViewModel( sessionGateway.observeSchedule() } - print("Collecting Item Flows") itemsFlow .collect { items -> items .groupBy { it.session.startsAt.toConferenceDateTime( dateTimeService, - conferenceConfigProvider.getConferenceTimeZone() ?: TimeZone.UTC, + // Defaulting to default Conference added in the database. This is mostly a race condition. + conferenceConfigProvider.getConferenceTimeZone() ?: TimeZone.of("America/New_York"), ).date } .map { (date, items) -> diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index 5c2e3b5f..d7cc6f1c 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -65,9 +65,9 @@ import org.koin.dsl.module fun initKoin(additionalModules: List): KoinApplication { val koinApplication = startKoin { modules( - platformModule + - coreModule + - additionalModules, + additionalModules + + platformModule + + coreModule, ) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt index fdf4cf13..ab35cba1 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt @@ -63,11 +63,8 @@ class DefaultApiDataSource( } suspend fun getConferences(): ConferencesDto.ConferenceCollectionDto? { - log.i { "getConferences" } val projectId = conferenceConfigProvider.getProjectId() ?: return null - log.i { "Have Project ID: $projectId" } val apiKey = conferenceConfigProvider.getApiKey() - log.i { "Have API Key" } val databaseName = "(default)" val conferenceListCollection = "conferenceListMobile" From a85510efa4189edf02dd34c82aa646e52161356c Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 16:16:22 -0400 Subject: [PATCH 47/56] Adding KotlinXDateFormatterTest --- .github/workflows/build.yml | 1 + shared/build.gradle.kts | 4 + .../formatter/KotlinXDateFormatterTest.kt | 75 +++++++++++++++++++ .../kotlin/co/touchlab/droidcon/Koin.js.kt | 1 + .../co/touchlab/droidcon/web/WebKoinTest.kt | 20 ----- 5 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/util/formatter/KotlinXDateFormatterTest.kt delete mode 100644 web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3c35f38b..1076cccb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,5 +30,6 @@ jobs: compileKotlinIosSimulatorArm64 compileKotlinJs :web:jsBrowserProductionWebpack + :shared:jsTest :web:jsTest --no-daemon \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index f19b80ff..e9cb612f 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -129,6 +129,10 @@ kotlin { implementation(npm("@js-joda/timezone", "2.22.0")) } + commonTest.dependencies { + implementation(kotlin("test")) + } + all { languageSettings.apply { optIn("kotlin.RequiresOptIn") diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/util/formatter/KotlinXDateFormatterTest.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/util/formatter/KotlinXDateFormatterTest.kt new file mode 100644 index 00000000..6b9c0e36 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/util/formatter/KotlinXDateFormatterTest.kt @@ -0,0 +1,75 @@ +package co.touchlab.droidcon.util.formatter + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.Month + +class KotlinXDateFormatterTest { + + private val formatter = KotlinXDateFormatter() + + @Test + fun monthWithDay_formatsThreeLetterUppercaseMonthAndDay() { + assertEquals( + "MAR 7", + formatter.monthWithDay(LocalDate(2024, Month.MARCH, 7)), + ) + assertEquals( + "DEC 31", + formatter.monthWithDay(LocalDate(2024, Month.DECEMBER, 31)), + ) + } + + @Test + fun timeOnly_formatsMidnightAs12Am() { + assertEquals( + "12:00 AM", + formatter.timeOnly(LocalDateTime(2024, Month.JUNE, 1, 0, 0)), + ) + } + + @Test + fun timeOnly_formatsNoonAs12Pm() { + assertEquals( + "12:00 PM", + formatter.timeOnly(LocalDateTime(2024, Month.JUNE, 1, 12, 0)), + ) + } + + @Test + fun timeOnly_formatsMorningAndAfternoonTimes() { + assertEquals( + "9:05 AM", + formatter.timeOnly(LocalDateTime(2024, Month.JUNE, 1, 9, 5)), + ) + assertEquals( + "1:30 PM", + formatter.timeOnly(LocalDateTime(2024, Month.JUNE, 1, 13, 30)), + ) + assertEquals( + "11:59 PM", + formatter.timeOnly(LocalDateTime(2024, Month.JUNE, 1, 23, 59)), + ) + } + + @Test + fun timeOnly_padsMinutesToTwoDigits() { + assertEquals( + "10:09 AM", + formatter.timeOnly(LocalDateTime(2024, Month.JUNE, 1, 10, 9)), + ) + } + + @Test + fun timeOnlyInterval_joinsFormattedStartAndEndTimes() { + val from = LocalDateTime(2024, Month.JUNE, 1, 9, 0) + val to = LocalDateTime(2024, Month.JUNE, 1, 10, 30) + + assertEquals( + "9:00 AM - 10:30 AM", + formatter.timeOnlyInterval(from, to), + ) + } +} diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt index 1e38c1bd..62710e31 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt @@ -7,6 +7,7 @@ import co.touchlab.kermit.Logger import co.touchlab.kermit.StaticConfig import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.js.Js +import org.koin.core.module.Module import org.koin.dsl.module actual val platformModule: Module = module { diff --git a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt b/web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt deleted file mode 100644 index b48e845a..00000000 --- a/web/src/jsTest/kotlin/co/touchlab/droidcon/web/WebKoinTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package co.touchlab.droidcon.web - -import co.touchlab.droidcon.platformModule -import com.russhwolf.settings.ExperimentalSettingsApi -import kotlin.test.Test -import org.koin.dsl.koinApplication -import org.koin.test.KoinTest -import org.koin.test.check.checkModules - -class WebKoinTest : KoinTest { - - @OptIn(ExperimentalSettingsApi::class) - @Test - fun `should inject web app components`() { - koinApplication { - modules(webAppModule(), platformModule) - checkModules() - } - } -} From 79db5a392cdd6fcf0380827892e34bef6269d43e Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 16:23:37 -0400 Subject: [PATCH 48/56] Apply suggestions from code review Co-authored-by: Daniel Bertoldi --- .../droidcon/domain/service/impl/DefaultApiDataSource.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt index ab35cba1..78a71535 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt @@ -51,9 +51,9 @@ class DefaultApiDataSource( override suspend fun getSponsors(): SponsorsDto.SponsorCollectionDto? { log.i { "gettingSponsors" } - val projectId = conferenceConfigProvider.getProjectId() ?: return null - val collectionName = conferenceConfigProvider.getCollectionName() ?: return null - val apiKey = conferenceConfigProvider.getApiKey() ?: return null + val projectId = conferenceConfigProvider.getProjectId() + val collectionName = conferenceConfigProvider.getCollectionName() + val apiKey = conferenceConfigProvider.getApiKey() val databaseName = "(default)" // This could be moved to ConferenceConfigProvider if needed val jsonString = client.get { @@ -63,7 +63,7 @@ class DefaultApiDataSource( } suspend fun getConferences(): ConferencesDto.ConferenceCollectionDto? { - val projectId = conferenceConfigProvider.getProjectId() ?: return null + val projectId = conferenceConfigProvider.getProjectId() val apiKey = conferenceConfigProvider.getApiKey() val databaseName = "(default)" val conferenceListCollection = "conferenceListMobile" From b5dd8ac40837b6cacda141fc53d80970393b051b Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 17:04:10 -0400 Subject: [PATCH 49/56] Fixing various issues and adding multiple tests --- .github/workflows/build.yml | 2 +- gradle/libs.versions.toml | 2 + .../viewmodel/ApplicationViewModel.kt | 1 - .../viewmodel/session/SessionDayViewModel.kt | 1 + .../session/SessionDetailViewModel.kt | 1 + shared/build.gradle.kts | 21 +++ .../droidcon/test/TestSqlDriver.android.kt | 12 ++ .../co/touchlab/droidcon/DatabaseFactory.kt | 23 +++ .../kotlin/co/touchlab/droidcon/Koin.kt | 20 +-- .../impl/SqlDelightConferenceRepository.kt | 3 +- .../impl/SqlDelightProfileRepository.kt | 3 +- .../impl/SqlDelightRoomRepository.kt | 3 +- .../impl/SqlDelightSessionRepository.kt | 3 +- .../domain/service/impl/DefaultServerApi.kt | 9 +- .../repository/ConferenceRepositoryTest.kt | 96 +++++++++++ .../repository/ProfileRepositoryTest.kt | 137 ++++++++++++++++ .../domain/repository/RoomRepositoryTest.kt | 84 ++++++++++ .../repository/SessionRepositoryTest.kt | 153 ++++++++++++++++++ .../repository/SponsorGroupRepositoryTest.kt | 89 ++++++++++ .../repository/SponsorRepositoryTest.kt | 105 ++++++++++++ .../domain/service/DefaultServerApiTest.kt | 132 +++++++++++++++ .../droidcon/test/FakeUserIdProvider.kt | 7 + .../droidcon/test/MainDispatcherRule.kt | 18 +++ .../co/touchlab/droidcon/test/TestDatabase.kt | 54 +++++++ .../droidcon/test/TestEntityFactory.kt | 82 ++++++++++ .../droidcon/test/TestSessionFactory.kt | 40 +++++ .../touchlab/droidcon/test/TestSqlDriver.kt | 5 + .../droidcon/test/TestSqlDriver.ios.kt | 11 ++ .../droidcon/test/TestSqlDriver.js.kt | 15 ++ .../droidcon/web/WebAnalyticsService.kt | 1 - 30 files changed, 1105 insertions(+), 28 deletions(-) create mode 100644 shared/src/androidUnitTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.android.kt create mode 100644 shared/src/commonMain/kotlin/co/touchlab/droidcon/DatabaseFactory.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/ConferenceRepositoryTest.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepositoryTest.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/RoomRepositoryTest.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SessionRepositoryTest.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SponsorGroupRepositoryTest.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SponsorRepositoryTest.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/service/DefaultServerApiTest.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/test/FakeUserIdProvider.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/test/MainDispatcherRule.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestDatabase.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestEntityFactory.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestSessionFactory.kt create mode 100644 shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.kt create mode 100644 shared/src/iosTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.ios.kt create mode 100644 shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1076cccb..44f6456a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,6 @@ jobs: compileKotlinIosSimulatorArm64 compileKotlinJs :web:jsBrowserProductionWebpack - :shared:jsTest + :shared:allTests :web:jsTest --no-daemon \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b1fbeedc..245772df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -98,6 +98,7 @@ ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "k ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-client-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } multiplatformSettings-core = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } multiplatformSettings-test = { module = "com.russhwolf:multiplatform-settings-test", version.ref = "multiplatformSettings" } @@ -105,6 +106,7 @@ multiplatformSettings-test = { module = "com.russhwolf:multiplatform-settings-te sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" } sqldelight-driver-ios = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" } sqldelight-driver-js = { module = "app.cash.sqldelight:web-worker-driver", version.ref = "sqlDelight" } +sqldelight-driver-sqlite = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqlDelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqlDelight" } sqldelight-async-extensions = { module = "app.cash.sqldelight:async-extensions", version.ref = "sqlDelight" } sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt index 904351c5..dc9fc889 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ApplicationViewModel.kt @@ -75,7 +75,6 @@ class ApplicationViewModel( lifecycle.whileAttached { try { syncService.runSynchronization(conference = conference) - syncService.forceSynchronize(conference) } catch (e: Exception) { log.e(e) { "Error starting sync service" } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt index 37cfa47f..48d5bd56 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDayViewModel.kt @@ -8,6 +8,7 @@ import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.droidcon.util.startOfMinute import co.touchlab.droidcon.viewmodel.ViewModelFactory import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone import org.brightify.hyperdrive.multiplatformx.BaseViewModel class SessionDayViewModel( diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt index 1d165cb3..c0f59ac5 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/session/SessionDetailViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.datetime.TimeZone import org.brightify.hyperdrive.multiplatformx.BaseViewModel import org.brightify.hyperdrive.multiplatformx.property.asFlow import org.brightify.hyperdrive.multiplatformx.property.flatMapLatest diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index e9cb612f..74fdc3b5 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -131,6 +131,27 @@ kotlin { commonTest.dependencies { implementation(kotlin("test")) + implementation(libs.test.coroutines) + implementation(libs.sqldelight.coroutines) + implementation(libs.sqldelight.async.extensions) + implementation(libs.ktor.client.mock) + } + + androidUnitTest.dependencies { + implementation(libs.sqldelight.driver.sqlite) + } + + val iosTest by creating { + dependsOn(commonTest.get()) + } + val iosX64Test by getting { + dependsOn(iosTest) + } + val iosArm64Test by getting { + dependsOn(iosTest) + } + val iosSimulatorArm64Test by getting { + dependsOn(iosTest) } all { diff --git a/shared/src/androidUnitTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.android.kt b/shared/src/androidUnitTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.android.kt new file mode 100644 index 00000000..f1df0583 --- /dev/null +++ b/shared/src/androidUnitTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.android.kt @@ -0,0 +1,12 @@ +package co.touchlab.droidcon.test + +import app.cash.sqldelight.async.coroutines.await +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import co.touchlab.droidcon.db.DroidconDatabase + +actual suspend fun createInMemoryDriver(): SqlDriver { + val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + DroidconDatabase.Schema.create(driver).await() + return driver +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/DatabaseFactory.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/DatabaseFactory.kt new file mode 100644 index 00000000..833edaec --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/DatabaseFactory.kt @@ -0,0 +1,23 @@ +package co.touchlab.droidcon + +import app.cash.sqldelight.db.SqlDriver +import co.touchlab.droidcon.db.ConferenceTable +import co.touchlab.droidcon.db.DroidconDatabase +import co.touchlab.droidcon.db.SessionTable +import co.touchlab.droidcon.db.SponsorGroupTable +import co.touchlab.droidcon.domain.repository.impl.adapter.InstantSqlDelightAdapter + +internal fun createDroidconDatabase(driver: SqlDriver): DroidconDatabase = DroidconDatabase( + driver = driver, + sessionTableAdapter = SessionTable.Adapter( + startsAtAdapter = InstantSqlDelightAdapter, + endsAtAdapter = InstantSqlDelightAdapter, + feedbackRatingAdapter = intToLongAdapter, + ), + sponsorGroupTableAdapter = SponsorGroupTable.Adapter( + intToLongAdapter, + ), + conferenceTableAdapter = ConferenceTable.Adapter( + conferenceTimeZoneAdapter = timeZoneAdapter, + ), +) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index d7cc6f1c..d3cdc98f 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -9,10 +9,7 @@ import co.touchlab.droidcon.application.repository.impl.DefaultAboutRepository import co.touchlab.droidcon.application.repository.impl.DefaultSettingsRepository import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.application.service.impl.DefaultNotificationSchedulingService -import co.touchlab.droidcon.db.ConferenceTable import co.touchlab.droidcon.db.DroidconDatabase -import co.touchlab.droidcon.db.SessionTable -import co.touchlab.droidcon.db.SponsorGroupTable import co.touchlab.droidcon.domain.gateway.SessionGateway import co.touchlab.droidcon.domain.gateway.SponsorGateway import co.touchlab.droidcon.domain.gateway.impl.DefaultSessionGateway @@ -29,7 +26,6 @@ import co.touchlab.droidcon.domain.repository.impl.SqlDelightRoomRepository import co.touchlab.droidcon.domain.repository.impl.SqlDelightSessionRepository import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorGroupRepository import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorRepository -import co.touchlab.droidcon.domain.repository.impl.adapter.InstantSqlDelightAdapter import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.DateTimeService import co.touchlab.droidcon.domain.service.FeedbackService @@ -94,21 +90,7 @@ val booleanAdapter = object : ColumnAdapter { private val coreModule = module { single { - DroidconDatabase.invoke( - driver = get(), - sessionTableAdapter = SessionTable.Adapter( - startsAtAdapter = InstantSqlDelightAdapter, - endsAtAdapter = InstantSqlDelightAdapter, - feedbackRatingAdapter = intToLongAdapter, - ), - sponsorGroupTableAdapter = SponsorGroupTable.Adapter( - intToLongAdapter, - ), - conferenceTableAdapter = ConferenceTable.Adapter( - conferenceTimeZoneAdapter = timeZoneAdapter, - // Note: selectedAdapter will be added when the adapter is regenerated - ), - ) + createDroidconDatabase(driver = get()) } single { Clock.System } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt index cb7a457b..f1197c8c 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList @@ -26,7 +27,7 @@ class SqlDelightConferenceRepository( override suspend fun select(conferenceId: Long): Boolean { try { - conferenceQueries.changeSelectedConference(conferenceId) + conferenceQueries.changeSelectedConference(conferenceId).await() return true } catch (e: Exception) { log.e(e) { "Error selecting conference" } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt index b57890ce..11965bd7 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.async.coroutines.awaitAsList import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.coroutines.asFlow @@ -91,7 +92,7 @@ class SqlDelightProfileRepository( } override suspend fun doDelete(id: Profile.Id, conferenceId: Long) { - profileQueries.delete(id.value, conferenceId) + profileQueries.delete(id.value, conferenceId).await() } override suspend fun contains(id: Profile.Id, conferenceId: Long): Boolean = diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt index d0a8dac7..5a411f80 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightRoomRepository.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.async.coroutines.awaitAsList import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.coroutines.asFlow @@ -36,7 +37,7 @@ class SqlDelightRoomRepository(private val roomQueries: RoomQueries) : } override suspend fun doDelete(id: Room.Id, conferenceId: Long) { - roomQueries.deleteById(id.value, conferenceId) + roomQueries.deleteById(id.value, conferenceId).await() } override suspend fun contains(id: Room.Id, conferenceId: Long): Boolean = diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt index 5fb1ab30..09df56a3 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightSessionRepository.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.domain.repository.impl +import app.cash.sqldelight.async.coroutines.await import app.cash.sqldelight.async.coroutines.awaitAsList import app.cash.sqldelight.async.coroutines.awaitAsOne import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull @@ -86,7 +87,7 @@ class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, } override suspend fun doDelete(id: Session.Id, conferenceId: Long) { - sessionQueries.deleteById(id.value, conferenceId) + sessionQueries.deleteById(id.value, conferenceId).await() } override suspend fun contains(id: Session.Id, conferenceId: Long): Boolean = diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultServerApi.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultServerApi.kt index 116d4712..0f4661bb 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultServerApi.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultServerApi.kt @@ -13,7 +13,12 @@ import io.ktor.http.isSuccess import io.ktor.http.takeFrom import kotlinx.serialization.json.Json -class DefaultServerApi(private val userIdProvider: UserIdProvider, private val client: HttpClient, private val json: Json) : ServerApi { +class DefaultServerApi( + private val userIdProvider: UserIdProvider, + private val client: HttpClient, + private val json: Json, + private val baseUrl: String = "https://droidcon-server.herokuapp.com", +) : ServerApi { override suspend fun setRsvp(sessionId: Session.Id, isAttending: Boolean): Boolean { val methodName = if (isAttending) { "sessionizeRsvpEvent" @@ -38,7 +43,7 @@ class DefaultServerApi(private val userIdProvider: UserIdProvider, private val c private fun HttpRequestBuilder.droidcon(path: String) { url { - takeFrom("https://droidcon-server.herokuapp.com") + takeFrom(baseUrl) encodedPath = path } } diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/ConferenceRepositoryTest.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/ConferenceRepositoryTest.kt new file mode 100644 index 00000000..3b7ec81f --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/ConferenceRepositoryTest.kt @@ -0,0 +1,96 @@ +package co.touchlab.droidcon.domain.repository + +import app.cash.sqldelight.async.coroutines.awaitAsList +import co.touchlab.droidcon.domain.repository.impl.SqlDelightConferenceRepository +import co.touchlab.droidcon.test.TestDatabase +import co.touchlab.droidcon.test.TestEntityFactory +import co.touchlab.droidcon.test.createTestDatabase +import co.touchlab.droidcon.test.runRepositoryTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class ConferenceRepositoryTest { + + @Test + fun add_persists_conference_and_returns_id() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val conference = TestEntityFactory.conference(name = "Added Conference") + + val id = repository.add(conference) + + assertTrue(id > 0) + val stored = testDb.database.conferenceQueries.selectById(id) { confId, name, _, _, _, _, _, _, _, _ -> + confId to name + }.awaitAsList().single() + assertEquals(id, stored.first) + assertEquals("Added Conference", stored.second) + } + + @Test + fun update_modifies_conference() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val conference = TestEntityFactory.conference( + id = testDb.conferenceId, + name = "Updated Conference", + projectId = "updated-project", + ) + + val updated = repository.update(conference) + + assertTrue(updated) + val storedName = testDb.database.conferenceQueries.selectById(testDb.conferenceId) { _, name, _, _, _, _, _, _, _, _ -> + name + }.awaitAsList().single() + assertEquals("Updated Conference", storedName) + } + + @Test + fun select_changes_selected_conference() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val newConferenceId = repository.add(TestEntityFactory.conference(name = "Selectable Conference")) + + val selected = repository.select(newConferenceId) + + assertTrue(selected) + val selectedConference = repository.getSelected() + assertEquals(newConferenceId, selectedConference.id) + assertEquals("Selectable Conference", selectedConference.name) + } + + @Test + fun delete_removes_conference() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val conferenceId = repository.add(TestEntityFactory.conference(name = "Deleted Conference")) + + val deleted = repository.delete(conferenceId) + + assertTrue(deleted) + assertTrue( + testDb.database.conferenceQueries.selectById(conferenceId) { id, _, _, _, _, _, _, _, _, _ -> + id + }.awaitAsList().isEmpty(), + ) + } + + @Test + fun getSelected_returns_test_conference_after_select() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + + repository.select(testDb.conferenceId) + + val selected = repository.getSelected() + assertEquals(testDb.conferenceId, selected.id) + assertEquals("Test Conference", selected.name) + assertNotEquals("Droidcon NYC 2025", selected.name) + } + + private fun createRepository(testDb: TestDatabase): SqlDelightConferenceRepository = + SqlDelightConferenceRepository(conferenceQueries = testDb.database.conferenceQueries) +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepositoryTest.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepositoryTest.kt new file mode 100644 index 00000000..ade47370 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/ProfileRepositoryTest.kt @@ -0,0 +1,137 @@ +package co.touchlab.droidcon.domain.repository + +import co.touchlab.droidcon.domain.repository.impl.SqlDelightProfileRepository +import co.touchlab.droidcon.domain.repository.impl.SqlDelightSessionRepository +import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorGroupRepository +import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorRepository +import co.touchlab.droidcon.test.TestDatabase +import co.touchlab.droidcon.test.TestEntityFactory +import co.touchlab.droidcon.test.TestSessionFactory +import co.touchlab.droidcon.test.createTestDatabase +import co.touchlab.droidcon.test.runRepositoryTest +import co.touchlab.droidcon.test.seedSecondConference +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ProfileRepositoryTest { + + @Test + fun addOrUpdate_and_allSync_returns_profile() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val profile = TestEntityFactory.profile(fullName = "Alex Developer") + + repository.addOrUpdate(profile, testDb.conferenceId) + + val all = repository.allSync(testDb.conferenceId) + assertEquals(1, all.size) + assertEquals("Alex Developer", all.first().fullName) + assertEquals(profile.bio, all.first().bio) + assertEquals(profile.twitter, all.first().twitter) + } + + @Test + fun setSessionSpeakers_and_getSpeakersBySession() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val sessionRepository = createSessionRepository(testDb) + val session = TestSessionFactory.session() + sessionRepository.addOrUpdate(session, testDb.conferenceId) + val speakerOne = TestEntityFactory.profile(id = "speaker-1", fullName = "First Speaker") + val speakerTwo = TestEntityFactory.profile(id = "speaker-2", fullName = "Second Speaker") + repository.addOrUpdate(speakerOne, testDb.conferenceId) + repository.addOrUpdate(speakerTwo, testDb.conferenceId) + + repository.setSessionSpeakers( + session, + listOf(speakerTwo.id, speakerOne.id), + testDb.conferenceId, + ) + + val speakers = repository.getSpeakersBySession(session.id, testDb.conferenceId) + assertEquals(listOf("Second Speaker", "First Speaker"), speakers.map { it.fullName }) + } + + @Test + fun setSponsorRepresentatives_and_getSponsorRepresentatives() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val groupRepository = SqlDelightSponsorGroupRepository(testDb.database.sponsorGroupQueries) + val sponsorRepository = SqlDelightSponsorRepository(testDb.database.sponsorQueries) + val group = TestEntityFactory.sponsorGroup() + groupRepository.addOrUpdate(group, testDb.conferenceId) + val sponsor = TestEntityFactory.sponsor() + sponsorRepository.addOrUpdate(sponsor, testDb.conferenceId) + val repOne = TestEntityFactory.profile(id = "rep-1", fullName = "Rep One") + val repTwo = TestEntityFactory.profile(id = "rep-2", fullName = "Rep Two") + repository.addOrUpdate(repOne, testDb.conferenceId) + repository.addOrUpdate(repTwo, testDb.conferenceId) + + repository.setSponsorRepresentatives( + sponsor, + listOf(repTwo.id, repOne.id), + testDb.conferenceId, + ) + + val representatives = repository.getSponsorRepresentatives(sponsor.id, testDb.conferenceId) + assertEquals(listOf("Rep Two", "Rep One"), representatives.map { it.fullName }) + } + + @Test + fun remove_deletes_profile() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val profile = TestEntityFactory.profile() + repository.addOrUpdate(profile, testDb.conferenceId) + + val removed = repository.remove(profile.id, testDb.conferenceId) + assertTrue(removed) + assertFalse(repository.contains(profile.id, testDb.conferenceId)) + assertTrue(repository.allSync(testDb.conferenceId).isEmpty()) + } + + @Test + fun conference_isolation() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val secondConferenceId = seedSecondConference(testDb.database) + assertNotEquals(testDb.conferenceId, secondConferenceId) + + val conferenceOneProfile = TestEntityFactory.profile(id = "profile-conf-one", fullName = "Conference One") + val conferenceTwoProfile = TestEntityFactory.profile(id = "profile-conf-two", fullName = "Conference Two") + repository.addOrUpdate(conferenceOneProfile, testDb.conferenceId) + repository.addOrUpdate(conferenceTwoProfile, secondConferenceId) + + assertEquals( + "Conference One", + findProfile(repository, conferenceOneProfile.id, testDb.conferenceId)?.fullName, + ) + assertEquals( + "Conference Two", + findProfile(repository, conferenceTwoProfile.id, secondConferenceId)?.fullName, + ) + assertNull(findProfile(repository, conferenceOneProfile.id, secondConferenceId)) + assertNull(findProfile(repository, conferenceTwoProfile.id, testDb.conferenceId)) + } + + private fun createRepository(testDb: TestDatabase): SqlDelightProfileRepository = SqlDelightProfileRepository( + profileQueries = testDb.database.profileQueries, + speakerQueries = testDb.database.sessionSpeakerQueries, + representativeQueries = testDb.database.sponsorRepresentativeQueries, + ) + + private fun createSessionRepository(testDb: TestDatabase): SqlDelightSessionRepository = SqlDelightSessionRepository( + dateTimeService = TestSessionFactory.dateTimeService, + sessionQueries = testDb.database.sessionQueries, + ) + + private suspend fun findProfile( + repository: SqlDelightProfileRepository, + id: co.touchlab.droidcon.domain.entity.Profile.Id, + conferenceId: Long, + ) = repository.allSync(conferenceId).find { it.id == id } +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/RoomRepositoryTest.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/RoomRepositoryTest.kt new file mode 100644 index 00000000..d471b205 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/RoomRepositoryTest.kt @@ -0,0 +1,84 @@ +package co.touchlab.droidcon.domain.repository + +import co.touchlab.droidcon.domain.repository.impl.SqlDelightRoomRepository +import co.touchlab.droidcon.test.TestDatabase +import co.touchlab.droidcon.test.TestEntityFactory +import co.touchlab.droidcon.test.createTestDatabase +import co.touchlab.droidcon.test.runRepositoryTest +import co.touchlab.droidcon.test.seedSecondConference +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class RoomRepositoryTest { + + @Test + fun addOrUpdate_and_allSync_returns_room() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val room = TestEntityFactory.room(name = "Auditorium") + + repository.addOrUpdate(room, testDb.conferenceId) + + val all = repository.allSync(testDb.conferenceId) + assertEquals(1, all.size) + assertEquals("Auditorium", all.first().name) + assertEquals(room.id, all.first().id) + } + + @Test + fun addOrUpdate_updates_existing_room() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val room = TestEntityFactory.room() + repository.addOrUpdate(room, testDb.conferenceId) + + repository.addOrUpdate(TestEntityFactory.room(id = room.id.value, name = "Updated Hall"), testDb.conferenceId) + + val found = repository.allSync(testDb.conferenceId).single() + assertEquals("Updated Hall", found.name) + } + + @Test + fun remove_deletes_room() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val room = TestEntityFactory.room() + repository.addOrUpdate(room, testDb.conferenceId) + + val removed = repository.remove(room.id, testDb.conferenceId) + assertTrue(removed) + assertFalse(repository.contains(room.id, testDb.conferenceId)) + assertTrue(repository.allSync(testDb.conferenceId).isEmpty()) + } + + @Test + fun conference_isolation() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val secondConferenceId = seedSecondConference(testDb.database) + assertNotEquals(testDb.conferenceId, secondConferenceId) + + val conferenceOneRoom = TestEntityFactory.room(id = 1L, name = "Room One") + val conferenceTwoRoom = TestEntityFactory.room(id = 2L, name = "Room Two") + repository.addOrUpdate(conferenceOneRoom, testDb.conferenceId) + repository.addOrUpdate(conferenceTwoRoom, secondConferenceId) + + assertEquals("Room One", findRoom(repository, conferenceOneRoom.id, testDb.conferenceId)?.name) + assertEquals("Room Two", findRoom(repository, conferenceTwoRoom.id, secondConferenceId)?.name) + assertNull(findRoom(repository, conferenceOneRoom.id, secondConferenceId)) + assertNull(findRoom(repository, conferenceTwoRoom.id, testDb.conferenceId)) + } + + private fun createRepository(testDb: TestDatabase): SqlDelightRoomRepository = + SqlDelightRoomRepository(roomQueries = testDb.database.roomQueries) + + private suspend fun findRoom( + repository: SqlDelightRoomRepository, + id: co.touchlab.droidcon.domain.entity.Room.Id, + conferenceId: Long, + ) = repository.allSync(conferenceId).find { it.id == id } +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SessionRepositoryTest.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SessionRepositoryTest.kt new file mode 100644 index 00000000..d1e1f559 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SessionRepositoryTest.kt @@ -0,0 +1,153 @@ +package co.touchlab.droidcon.domain.repository + +import co.touchlab.droidcon.domain.entity.Session +import co.touchlab.droidcon.domain.repository.impl.SqlDelightSessionRepository +import co.touchlab.droidcon.test.TestDatabase +import co.touchlab.droidcon.test.TestSessionFactory +import co.touchlab.droidcon.test.createTestDatabase +import co.touchlab.droidcon.test.runRepositoryTest +import co.touchlab.droidcon.test.seedSecondConference +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SessionRepositoryTest { + + @Test + fun addOrUpdate_and_find_returns_session() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val session = TestSessionFactory.session(title = "Kotlin Multiplatform") + + repository.addOrUpdate(session, testDb.conferenceId) + + val found = repository.findSync(session.id, testDb.conferenceId) + assertNotNull(found) + assertEquals("Kotlin Multiplatform", found.title) + assertEquals(session.description, found.description) + assertEquals(1, repository.allSync(testDb.conferenceId).size) + } + + @Test + fun setRsvp_updates_attending_and_observeAllAttending() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val session = TestSessionFactory.session() + repository.addOrUpdate(session, testDb.conferenceId) + + repository.setRsvp(session.id, Session.RSVP(isAttending = true, isSent = false), testDb.conferenceId) + + val attending = repository.allAttending(testDb.conferenceId) + assertEquals(1, attending.size) + assertTrue(attending.first().rsvp.isAttending) + + repository.setRsvp(session.id, Session.RSVP(isAttending = false, isSent = false), testDb.conferenceId) + assertTrue(repository.allAttending(testDb.conferenceId).isEmpty()) + } + + @Test + fun setRsvpSent_persists_sent_flag() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val session = TestSessionFactory.session() + repository.addOrUpdate(session, testDb.conferenceId) + + repository.setRsvp(session.id, Session.RSVP(isAttending = true, isSent = false), testDb.conferenceId) + repository.setRsvpSent(session.id, isSent = true, testDb.conferenceId) + + val found = repository.findSync(session.id, testDb.conferenceId) + assertNotNull(found) + assertTrue(found.rsvp.isSent) + } + + @Test + fun setFeedback_persists_rating_and_comment() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val session = TestSessionFactory.session() + repository.addOrUpdate(session, testDb.conferenceId) + + val feedback = Session.Feedback( + rating = Session.Feedback.Rating.SATISFIED, + comment = "Great talk", + isSent = true, + ) + repository.setFeedback(session.id, feedback, testDb.conferenceId) + + val found = repository.findSync(session.id, testDb.conferenceId) + assertNotNull(found?.feedback) + assertEquals(Session.Feedback.Rating.SATISFIED, found.feedback?.rating) + assertEquals("Great talk", found.feedback?.comment) + assertFalse(found.feedback!!.isSent) + } + + @Test + fun setFeedbackSent_persists() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val session = TestSessionFactory.session() + repository.addOrUpdate(session, testDb.conferenceId) + repository.setFeedback( + session.id, + Session.Feedback( + rating = Session.Feedback.Rating.NORMAL, + comment = "Good", + isSent = false, + ), + testDb.conferenceId, + ) + + repository.setFeedbackSent(session.id, isSent = true, testDb.conferenceId) + + val found = repository.findSync(session.id, testDb.conferenceId) + assertNotNull(found?.feedback) + assertTrue(found.feedback?.isSent == true) + } + + @Test + fun remove_deletes_session() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val session = TestSessionFactory.session() + repository.addOrUpdate(session, testDb.conferenceId) + + val removed = repository.remove(session.id, testDb.conferenceId) + assertTrue(removed) + assertFalse(repository.contains(session.id, testDb.conferenceId)) + assertNull(repository.findSync(session.id, testDb.conferenceId)) + } + + @Test + fun conference_isolation() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val secondConferenceId = seedSecondConference(testDb.database) + assertNotEquals(testDb.conferenceId, secondConferenceId) + + val conferenceOneSession = TestSessionFactory.session(id = "session-conf-one", title = "Conference One Session") + val conferenceTwoSession = TestSessionFactory.session(id = "session-conf-two", title = "Conference Two Session") + repository.addOrUpdate(conferenceOneSession, testDb.conferenceId) + repository.addOrUpdate(conferenceTwoSession, secondConferenceId) + + assertEquals( + "Conference One Session", + repository.findSync(conferenceOneSession.id, testDb.conferenceId)?.title, + ) + assertEquals( + "Conference Two Session", + repository.findSync(conferenceTwoSession.id, secondConferenceId)?.title, + ) + assertNull(repository.findSync(conferenceOneSession.id, secondConferenceId)) + assertNull(repository.findSync(conferenceTwoSession.id, testDb.conferenceId)) + } + + private fun createRepository(testDb: TestDatabase): SqlDelightSessionRepository = SqlDelightSessionRepository( + dateTimeService = TestSessionFactory.dateTimeService, + sessionQueries = testDb.database.sessionQueries, + ) + +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SponsorGroupRepositoryTest.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SponsorGroupRepositoryTest.kt new file mode 100644 index 00000000..7500cc05 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SponsorGroupRepositoryTest.kt @@ -0,0 +1,89 @@ +package co.touchlab.droidcon.domain.repository + +import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorGroupRepository +import co.touchlab.droidcon.test.TestDatabase +import co.touchlab.droidcon.test.TestEntityFactory +import co.touchlab.droidcon.test.createTestDatabase +import co.touchlab.droidcon.test.runRepositoryTest +import co.touchlab.droidcon.test.seedSecondConference +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SponsorGroupRepositoryTest { + + @Test + fun addOrUpdate_and_allSync_returns_sponsor_group() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val group = TestEntityFactory.sponsorGroup(name = "Platinum", displayPriority = 0) + + repository.addOrUpdate(group, testDb.conferenceId) + + val all = repository.allSync(testDb.conferenceId) + assertEquals(1, all.size) + assertEquals("Platinum", all.first().name) + assertEquals(0, all.first().displayPriority) + assertTrue(all.first().isProminent) + } + + @Test + fun addOrUpdate_updates_existing_group() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val group = TestEntityFactory.sponsorGroup(isProminent = false) + repository.addOrUpdate(group, testDb.conferenceId) + + repository.addOrUpdate( + TestEntityFactory.sponsorGroup(displayPriority = 5, isProminent = true), + testDb.conferenceId, + ) + + val found = repository.allSync(testDb.conferenceId).single() + assertEquals(5, found.displayPriority) + assertTrue(found.isProminent) + } + + @Test + fun remove_deletes_sponsor_group() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val group = TestEntityFactory.sponsorGroup() + repository.addOrUpdate(group, testDb.conferenceId) + + val removed = repository.remove(group.id, testDb.conferenceId) + assertTrue(removed) + assertFalse(repository.contains(group.id, testDb.conferenceId)) + assertTrue(repository.allSync(testDb.conferenceId).isEmpty()) + } + + @Test + fun conference_isolation() = runRepositoryTest { + val testDb = createTestDatabase() + val repository = createRepository(testDb) + val secondConferenceId = seedSecondConference(testDb.database) + assertNotEquals(testDb.conferenceId, secondConferenceId) + + val conferenceOneGroup = TestEntityFactory.sponsorGroup(name = "Gold", displayPriority = 1) + val conferenceTwoGroup = TestEntityFactory.sponsorGroup(name = "Silver", displayPriority = 99) + repository.addOrUpdate(conferenceOneGroup, testDb.conferenceId) + repository.addOrUpdate(conferenceTwoGroup, secondConferenceId) + + assertEquals(1, findGroup(repository, conferenceOneGroup.id, testDb.conferenceId)?.displayPriority) + assertEquals(99, findGroup(repository, conferenceTwoGroup.id, secondConferenceId)?.displayPriority) + assertNull(findGroup(repository, conferenceOneGroup.id, secondConferenceId)) + assertNull(findGroup(repository, conferenceTwoGroup.id, testDb.conferenceId)) + } + + private fun createRepository(testDb: TestDatabase): SqlDelightSponsorGroupRepository = + SqlDelightSponsorGroupRepository(sponsorGroupQueries = testDb.database.sponsorGroupQueries) + + private suspend fun findGroup( + repository: SqlDelightSponsorGroupRepository, + id: co.touchlab.droidcon.domain.entity.SponsorGroup.Id, + conferenceId: Long, + ) = repository.allSync(conferenceId).find { it.id == id } +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SponsorRepositoryTest.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SponsorRepositoryTest.kt new file mode 100644 index 00000000..38cfce41 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SponsorRepositoryTest.kt @@ -0,0 +1,105 @@ +package co.touchlab.droidcon.domain.repository + +import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorGroupRepository +import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorRepository +import co.touchlab.droidcon.test.TestDatabase +import co.touchlab.droidcon.test.TestEntityFactory +import co.touchlab.droidcon.test.createTestDatabase +import co.touchlab.droidcon.test.runRepositoryTest +import co.touchlab.droidcon.test.seedSecondConference +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SponsorRepositoryTest { + + @Test + fun addOrUpdate_and_allSync_returns_sponsor() = runRepositoryTest { + val testDb = createTestDatabase() + val groupRepository = createGroupRepository(testDb) + val repository = createRepository(testDb) + val group = TestEntityFactory.sponsorGroup() + groupRepository.addOrUpdate(group, testDb.conferenceId) + val sponsor = TestEntityFactory.sponsor(name = "Acme Corp") + + repository.addOrUpdate(sponsor, testDb.conferenceId) + + val all = repository.allSync(testDb.conferenceId) + assertEquals(1, all.size) + assertEquals("Acme Corp", all.first().name) + assertEquals("KMP specialists", all.first().description) + } + + @Test + fun allByGroupName_returns_sponsors_in_group() = runRepositoryTest { + val testDb = createTestDatabase() + val groupRepository = createGroupRepository(testDb) + val repository = createRepository(testDb) + groupRepository.addOrUpdate(TestEntityFactory.sponsorGroup(name = "Gold"), testDb.conferenceId) + groupRepository.addOrUpdate(TestEntityFactory.sponsorGroup(name = "Silver"), testDb.conferenceId) + repository.addOrUpdate(TestEntityFactory.sponsor(name = "Sponsor A", group = "Gold"), testDb.conferenceId) + repository.addOrUpdate(TestEntityFactory.sponsor(name = "Sponsor B", group = "Gold"), testDb.conferenceId) + repository.addOrUpdate(TestEntityFactory.sponsor(name = "Sponsor C", group = "Silver"), testDb.conferenceId) + + val goldSponsors = repository.allByGroupName("Gold", testDb.conferenceId) + assertEquals(2, goldSponsors.size) + assertEquals(setOf("Sponsor A", "Sponsor B"), goldSponsors.map { it.name }.toSet()) + } + + @Test + fun remove_deletes_sponsor() = runRepositoryTest { + val testDb = createTestDatabase() + val groupRepository = createGroupRepository(testDb) + val repository = createRepository(testDb) + groupRepository.addOrUpdate(TestEntityFactory.sponsorGroup(), testDb.conferenceId) + val sponsor = TestEntityFactory.sponsor() + repository.addOrUpdate(sponsor, testDb.conferenceId) + + val removed = repository.remove(sponsor.id, testDb.conferenceId) + assertTrue(removed) + assertFalse(repository.contains(sponsor.id, testDb.conferenceId)) + assertTrue(repository.allSync(testDb.conferenceId).isEmpty()) + } + + @Test + fun conference_isolation() = runRepositoryTest { + val testDb = createTestDatabase() + val groupRepository = createGroupRepository(testDb) + val repository = createRepository(testDb) + val secondConferenceId = seedSecondConference(testDb.database) + assertNotEquals(testDb.conferenceId, secondConferenceId) + + groupRepository.addOrUpdate(TestEntityFactory.sponsorGroup(), testDb.conferenceId) + groupRepository.addOrUpdate(TestEntityFactory.sponsorGroup(), secondConferenceId) + val conferenceOneSponsor = TestEntityFactory.sponsor(name = "Sponsor One", description = "Conference one") + val conferenceTwoSponsor = TestEntityFactory.sponsor(name = "Sponsor Two", description = "Conference two") + repository.addOrUpdate(conferenceOneSponsor, testDb.conferenceId) + repository.addOrUpdate(conferenceTwoSponsor, secondConferenceId) + + assertEquals( + "Conference one", + findSponsor(repository, conferenceOneSponsor.id, testDb.conferenceId)?.description, + ) + assertEquals( + "Conference two", + findSponsor(repository, conferenceTwoSponsor.id, secondConferenceId)?.description, + ) + assertNull(findSponsor(repository, conferenceOneSponsor.id, secondConferenceId)) + assertNull(findSponsor(repository, conferenceTwoSponsor.id, testDb.conferenceId)) + } + + private fun createRepository(testDb: TestDatabase): SqlDelightSponsorRepository = + SqlDelightSponsorRepository(sponsorQueries = testDb.database.sponsorQueries) + + private fun createGroupRepository(testDb: TestDatabase): SqlDelightSponsorGroupRepository = + SqlDelightSponsorGroupRepository(sponsorGroupQueries = testDb.database.sponsorGroupQueries) + + private suspend fun findSponsor( + repository: SqlDelightSponsorRepository, + id: co.touchlab.droidcon.domain.entity.Sponsor.Id, + conferenceId: Long, + ) = repository.allSync(conferenceId).find { it.id == id } +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/service/DefaultServerApiTest.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/service/DefaultServerApiTest.kt new file mode 100644 index 00000000..a0f65d15 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/service/DefaultServerApiTest.kt @@ -0,0 +1,132 @@ +package co.touchlab.droidcon.domain.service + +import co.touchlab.droidcon.domain.entity.Session +import co.touchlab.droidcon.domain.service.impl.DefaultServerApi +import co.touchlab.droidcon.test.FakeUserIdProvider +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.request.HttpRequestData +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.Parameters +import io.ktor.http.content.OutgoingContent +import io.ktor.http.content.TextContent +import io.ktor.http.parseUrlEncodedParameters +import io.ktor.utils.io.ByteChannel +import io.ktor.utils.io.readRemaining +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.coroutines.test.runTest +import kotlinx.io.readByteArray +import kotlinx.serialization.json.Json + +class DefaultServerApiTest { + + @Test + fun setRsvp_attending_posts_correct_path() = runTest { + val requests = mutableListOf() + val api = createApi(requests, HttpStatusCode.OK) + + val result = api.setRsvp(Session.Id("session-42"), isAttending = true) + + assertTrue(result) + assertEquals(1, requests.size) + val request = requests.single() + assertEquals(HttpMethod.Post, request.method) + assertEquals( + "/dataTest/sessionizeRsvpEvent/session-42/test-user", + request.url.encodedPath, + ) + } + + @Test + fun setRsvp_not_attending_posts_unrsvp_path() = runTest { + val requests = mutableListOf() + val api = createApi(requests, HttpStatusCode.OK) + + val result = api.setRsvp(Session.Id("session-42"), isAttending = false) + + assertTrue(result) + assertEquals( + "/dataTest/sessionizeUnrsvpEvent/session-42/test-user", + requests.single().url.encodedPath, + ) + } + + @Test + fun setFeedback_posts_form_fields() = runTest { + val requests = mutableListOf() + val api = createApi(requests, HttpStatusCode.OK) + + val result = api.setFeedback(Session.Id("session-99"), rating = 3, comment = "Excellent") + + assertTrue(result) + val request = requests.single() + assertEquals(HttpMethod.Post, request.method) + assertEquals( + "/dataTest/sessionizeFeedbackEvent/session-99/test-user", + request.url.encodedPath, + ) + val formParameters = request.readFormParameters() + assertEquals("3", formParameters["rating"]) + assertEquals("Excellent", formParameters["comment"]) + } + + @Test + fun non_success_status_returns_false() = runTest { + val api = createApi(mutableListOf(), HttpStatusCode.InternalServerError) + + val result = api.setRsvp(Session.Id("session-1"), isAttending = true) + + assertFalse(result) + } + + @Test + fun success_status_returns_true() = runTest { + val api = createApi(mutableListOf(), HttpStatusCode.OK) + + val result = api.setRsvp(Session.Id("session-1"), isAttending = true) + + assertTrue(result) + } + + private fun createApi(requests: MutableList, status: HttpStatusCode): DefaultServerApi { + val mockEngine = MockEngine { request -> + requests.add(request) + respond("OK", status) + } + val client = HttpClient(mockEngine) + return DefaultServerApi( + userIdProvider = FakeUserIdProvider("test-user"), + client = client, + json = Json { }, + baseUrl = "https://test.local", + ) + } + + private suspend fun HttpRequestData.readFormParameters(): Parameters { + val bodyText = readBodyText() + return if (bodyText.isEmpty()) { + Parameters.Empty + } else { + bodyText.parseUrlEncodedParameters() + } + } + + private suspend fun HttpRequestData.readBodyText(): String { + val content = body as OutgoingContent + return when (content) { + is TextContent -> content.text + is OutgoingContent.ByteArrayContent -> content.bytes().decodeToString() + is OutgoingContent.WriteChannelContent -> { + val channel = ByteChannel() + content.writeTo(channel) + channel.readRemaining().readByteArray().decodeToString() + } + else -> "" + } + } +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/FakeUserIdProvider.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/FakeUserIdProvider.kt new file mode 100644 index 00000000..0dd29e10 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/FakeUserIdProvider.kt @@ -0,0 +1,7 @@ +package co.touchlab.droidcon.test + +import co.touchlab.droidcon.domain.service.UserIdProvider + +class FakeUserIdProvider(private val userId: String) : UserIdProvider { + override suspend fun getId(): String = userId +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/MainDispatcherRule.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/MainDispatcherRule.kt new file mode 100644 index 00000000..fed9131d --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/MainDispatcherRule.kt @@ -0,0 +1,18 @@ +package co.touchlab.droidcon.test + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain + +fun runRepositoryTest(testBody: suspend TestScope.() -> Unit) = runTest { + val dispatcher = StandardTestDispatcher(testScheduler) + Dispatchers.setMain(dispatcher) + try { + testBody() + } finally { + Dispatchers.resetMain() + } +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestDatabase.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestDatabase.kt new file mode 100644 index 00000000..8949cf1d --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestDatabase.kt @@ -0,0 +1,54 @@ +package co.touchlab.droidcon.test + +import app.cash.sqldelight.async.coroutines.awaitAsList +import co.touchlab.droidcon.createDroidconDatabase +import co.touchlab.droidcon.db.DroidconDatabase +import kotlinx.datetime.TimeZone + +data class TestDatabase(val database: DroidconDatabase, val conferenceId: Long) + +suspend fun createTestDatabase(): TestDatabase { + val driver = createInMemoryDriver() + val database = createDroidconDatabase(driver) + val conferenceId = seedConference(database) + return TestDatabase(database = database, conferenceId = conferenceId) +} + +private suspend fun seedConference(database: DroidconDatabase): Long { + val conferenceName = "Test Conference" + database.conferenceQueries.insert( + conferenceName = conferenceName, + conferenceTimeZone = TimeZone.of("America/Los_Angeles"), + projectId = "test-project", + collectionName = "test-collection", + apiKey = "test-api-key", + scheduleId = "test-schedule", + selected = true, + active = true, + venueMap = null, + ) + return database.conferenceIdForName(conferenceName) +} + +internal suspend fun DroidconDatabase.conferenceIdForName(conferenceName: String): Long = + conferenceQueries.selectAll { id, name, _, _, _, _, _, _, _, _ -> + id to name + }.awaitAsList().first { (_, name) -> name == conferenceName }.first + +internal suspend fun seedSecondConference( + database: DroidconDatabase, + conferenceName: String = "Second Conference", +): Long { + database.conferenceQueries.insert( + conferenceName = conferenceName, + conferenceTimeZone = TimeZone.of("Europe/Berlin"), + projectId = "project-2", + collectionName = "collection-2", + apiKey = "api-key-2", + scheduleId = "schedule-2", + selected = false, + active = true, + venueMap = null, + ) + return database.conferenceIdForName(conferenceName) +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestEntityFactory.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestEntityFactory.kt new file mode 100644 index 00000000..ebb28326 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestEntityFactory.kt @@ -0,0 +1,82 @@ +package co.touchlab.droidcon.test + +import co.touchlab.droidcon.composite.Url +import co.touchlab.droidcon.domain.entity.Conference +import co.touchlab.droidcon.domain.entity.Profile +import co.touchlab.droidcon.domain.entity.Room +import co.touchlab.droidcon.domain.entity.Sponsor +import co.touchlab.droidcon.domain.entity.SponsorGroup +import kotlinx.datetime.TimeZone + +object TestEntityFactory { + fun room(id: Long = 1L, name: String = "Main Hall"): Room = Room(id = Room.Id(id), name = name) + + fun profile( + id: String = "profile-1", + fullName: String = "Jane Speaker", + bio: String? = "Speaker bio", + tagLine: String? = "Kotlin expert", + profilePicture: Url? = Url("https://example.com/photo.jpg"), + twitter: Url? = Url("https://twitter.com/jane"), + linkedIn: Url? = null, + website: Url? = Url("https://example.com"), + ): Profile = Profile( + id = Profile.Id(id), + fullName = fullName, + bio = bio, + tagLine = tagLine, + profilePicture = profilePicture, + twitter = twitter, + linkedIn = linkedIn, + website = website, + ) + + fun sponsorGroup( + name: String = "Gold", + displayPriority: Int = 1, + isProminent: Boolean = true, + ): SponsorGroup = SponsorGroup( + id = SponsorGroup.Id(name), + displayPriority = displayPriority, + isProminent = isProminent, + ) + + fun sponsor( + name: String = "Touchlab", + group: String = "Gold", + hasDetail: Boolean = true, + description: String? = "KMP specialists", + icon: Url = Url("https://example.com/icon.png"), + url: Url = Url("https://touchlab.co"), + ): Sponsor = Sponsor( + id = Sponsor.Id(name, group), + hasDetail = hasDetail, + description = description, + icon = icon, + url = url, + ) + + fun conference( + id: Long? = null, + name: String = "New Conference", + timeZone: TimeZone = TimeZone.of("America/Chicago"), + projectId: String = "new-project", + collectionName: String = "new-collection", + apiKey: String = "new-api-key", + scheduleId: String = "new-schedule", + selected: Boolean = false, + active: Boolean = true, + venueMap: String? = null, + ): Conference = Conference( + _id = id, + name = name, + timeZone = timeZone, + projectId = projectId, + collectionName = collectionName, + apiKey = apiKey, + scheduleId = scheduleId, + selected = selected, + active = active, + venueMap = venueMap, + ) +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestSessionFactory.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestSessionFactory.kt new file mode 100644 index 00000000..a78ef1fd --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestSessionFactory.kt @@ -0,0 +1,40 @@ +package co.touchlab.droidcon.test + +import co.touchlab.droidcon.domain.entity.Session +import co.touchlab.droidcon.domain.service.DateTimeService +import co.touchlab.droidcon.domain.service.impl.DefaultDateTimeService +import kotlin.time.Clock +import kotlin.time.Instant + +object TestSessionFactory { + private val testClock = object : Clock { + override fun now(): Instant = Instant.fromEpochMilliseconds(1_700_000_000_000) + } + + val dateTimeService: DateTimeService = DefaultDateTimeService(testClock) + + private val defaultStartsAt = Instant.fromEpochMilliseconds(1_700_000_000_000) + private val defaultEndsAt = Instant.fromEpochMilliseconds(1_700_003_600_000) + + fun session( + id: String = "session-1", + title: String = "Test Session", + description: String? = "Test description", + startsAt: Instant = defaultStartsAt, + endsAt: Instant = defaultEndsAt, + isServiceSession: Boolean = false, + rsvp: Session.RSVP = Session.RSVP(isAttending = false, isSent = false), + feedback: Session.Feedback? = null, + ): Session = Session( + dateTimeService = dateTimeService, + id = Session.Id(id), + title = title, + description = description, + startsAt = startsAt, + endsAt = endsAt, + isServiceSession = isServiceSession, + room = null, + rsvp = rsvp, + feedback = feedback, + ) +} diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.kt new file mode 100644 index 00000000..11ea449e --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.kt @@ -0,0 +1,5 @@ +package co.touchlab.droidcon.test + +import app.cash.sqldelight.db.SqlDriver + +expect suspend fun createInMemoryDriver(): SqlDriver diff --git a/shared/src/iosTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.ios.kt b/shared/src/iosTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.ios.kt new file mode 100644 index 00000000..9445fdf1 --- /dev/null +++ b/shared/src/iosTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.ios.kt @@ -0,0 +1,11 @@ +package co.touchlab.droidcon.test + +import app.cash.sqldelight.async.coroutines.synchronous +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.native.NativeSqliteDriver +import co.touchlab.droidcon.db.DroidconDatabase + +actual suspend fun createInMemoryDriver(): SqlDriver = NativeSqliteDriver( + schema = DroidconDatabase.Schema.synchronous(), + name = "test_${kotlin.random.Random.nextLong()}", +) diff --git a/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt b/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt new file mode 100644 index 00000000..81c5c2a7 --- /dev/null +++ b/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt @@ -0,0 +1,15 @@ +package co.touchlab.droidcon.test + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.worker.WebWorkerDriver +import co.touchlab.droidcon.db.DroidconDatabase +import app.cash.sqldelight.driver.worker.expected.Worker +actual suspend fun createInMemoryDriver(): SqlDriver { + val driver = WebWorkerDriver( + Worker( + js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)"""), + ), + ) + DroidconDatabase.Schema.create(driver).await() + return driver +} diff --git a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt index e743d6bf..c49c9d99 100644 --- a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt +++ b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt @@ -3,7 +3,6 @@ package co.touchlab.droidcon.web import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.kermit.Logger - // Firebase Analytics does not support Web KMP, so we're using Kermit for now. class WebAnalyticsService : AnalyticsService { From 7568a177ed39ee5baab341932b567a02855641ff Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 17:14:19 -0400 Subject: [PATCH 50/56] Moving localization --- .github/workflows/build.yml | 1 - .../co/touchlab/droidcon/android/MainApp.kt | 6 --- .../NotificationLocalizedStringFactory.kt | 19 --------- android/src/main/res/values/strings.xml | 6 --- .../Droidcon/en.lproj/Localizable.strings | 6 --- .../droidcon/ios/DependencyInjection.kt | 12 ------ .../NotificationLocalizedStringFactory.kt | 41 ------------------- .../composeResources/values/strings.xml | 7 ++++ .../kotlin/co/touchlab/droidcon/Koin.kt | 4 ++ .../service/NotificationSchedulingService.kt | 8 ++-- ...mposeNotificationLocalizedStringFactory.kt | 25 +++++++++++ .../DefaultNotificationSchedulingService.kt | 4 +- .../impl/DefaultConferenceConfigProvider.kt | 14 ++++--- .../droidcon/web/DependencyInjection.kt | 4 -- .../NotificationLocalizedStringFactory.kt | 17 -------- 15 files changed, 51 insertions(+), 123 deletions(-) delete mode 100644 android/src/main/java/co/touchlab/droidcon/android/util/NotificationLocalizedStringFactory.kt delete mode 100644 ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/util/NotificationLocalizedStringFactory.kt create mode 100644 shared/src/commonMain/composeResources/values/strings.xml create mode 100644 shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/ComposeNotificationLocalizedStringFactory.kt delete mode 100644 web/src/jsMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 44f6456a..9d238af3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,6 @@ jobs: ./gradlew ktlintCheck assembleDebug compileKotlinIosSimulatorArm64 compileKotlinJs - :web:jsBrowserProductionWebpack :shared:allTests :web:jsTest --no-daemon \ No newline at end of file diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt index 590bcbca..8db96d12 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt @@ -6,8 +6,6 @@ import android.content.Context import android.content.SharedPreferences import co.touchlab.droidcon.android.service.impl.AndroidAnalyticsService import co.touchlab.droidcon.android.service.impl.DefaultParseUrlViewService -import co.touchlab.droidcon.android.util.NotificationLocalizedStringFactory -import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader import co.touchlab.droidcon.domain.service.impl.ResourceReader @@ -50,10 +48,6 @@ class MainApp : ComposeResourceReader() } - single { - NotificationLocalizedStringFactory(context = get()) - } - single { AndroidAnalyticsService(firebaseAnalytics = Firebase.analytics) } diff --git a/android/src/main/java/co/touchlab/droidcon/android/util/NotificationLocalizedStringFactory.kt b/android/src/main/java/co/touchlab/droidcon/android/util/NotificationLocalizedStringFactory.kt deleted file mode 100644 index 01df3b82..00000000 --- a/android/src/main/java/co/touchlab/droidcon/android/util/NotificationLocalizedStringFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -package co.touchlab.droidcon.android.util - -import android.content.Context -import co.touchlab.droidcon.application.service.NotificationSchedulingService -import com.droidcon.app.R - -class NotificationLocalizedStringFactory(private val context: Context) : NotificationSchedulingService.LocalizedStringFactory { - - override fun reminderTitle(roomName: String?): String { - val ending = roomName?.let { context.getString(R.string.notification_reminder_title_in_room, it) } ?: "" - return context.getString(R.string.notification_reminder_title_base, ending) - } - - override fun reminderBody(sessionTitle: String): String = context.getString(R.string.notification_reminder_body, sessionTitle) - - override fun feedbackTitle(): String = context.getString(R.string.notification_feedback_title) - - override fun feedbackBody(): String = context.getString(R.string.notification_feedback_body) -} diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 3e3555f0..3fdc2cff 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -1,12 +1,6 @@ Droidcon - Upcoming event%s - " in %s" - %s is starting soon. - Feedback Time! - Your Feedback is Requested. - Droidcon diff --git a/ios/Droidcon/Droidcon/en.lproj/Localizable.strings b/ios/Droidcon/Droidcon/en.lproj/Localizable.strings index 0b70f5a2..2861ee20 100644 --- a/ios/Droidcon/Droidcon/en.lproj/Localizable.strings +++ b/ios/Droidcon/Droidcon/en.lproj/Localizable.strings @@ -1,11 +1,5 @@ "Shrug" = "¯\\_(ツ)_/¯"; -"Notification.Reminder.Title.Base" = "Upcoming event%@"; -"Notification.Reminder.Title.InRoom" = " in %@"; -"Notification.Reminder.Body" = "%@ is starting soon."; -"Notification.Feedback.Title" = "Feedback Time!"; -"Notification.Feedback.Body" = "Your Feedback is Requested."; - "Schedule.Title" = "Droidcon"; "Schedule.TabItem.Title" = "Schedule"; diff --git a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt index 1408f510..fbb8ba35 100644 --- a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt +++ b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt @@ -1,12 +1,10 @@ package co.touchlab.droidcon.ios -import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.ios.service.DefaultParseUrlViewService -import co.touchlab.droidcon.ios.util.NotificationLocalizedStringFactory import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel @@ -16,32 +14,22 @@ import com.russhwolf.settings.ObservableSettings import org.koin.core.Koin import org.koin.core.KoinApplication import org.koin.dsl.module -import platform.Foundation.NSBundle import platform.Foundation.NSUserDefaults @OptIn(ExperimentalSettingsApi::class) fun initKoinIos(userDefaults: NSUserDefaults, analyticsService: AnalyticsService): KoinApplication = initKoin( module { - single { BundleProvider(bundle = NSBundle.mainBundle) } single { NSUserDefaultsSettings(delegate = userDefaults) } single { ComposeResourceReader() } - single { - NotificationLocalizedStringFactory(bundle = get().bundle) - } - single { analyticsService } single { DefaultParseUrlViewService() } } + uiModule, ) -// Workaround class for injecting an `NSObject` class. -// When not used, an error "KClass of Objective-C classes is not supported." is thrown. -data class BundleProvider(val bundle: NSBundle) - @Suppress("unused") val Koin.waitForLoadedContextModel: WaitForLoadedContextModel get() = get() diff --git a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/util/NotificationLocalizedStringFactory.kt b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/util/NotificationLocalizedStringFactory.kt deleted file mode 100644 index 21ee3bf5..00000000 --- a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/util/NotificationLocalizedStringFactory.kt +++ /dev/null @@ -1,41 +0,0 @@ -package co.touchlab.droidcon.ios.util - -import co.touchlab.droidcon.application.service.NotificationSchedulingService -import kotlinx.cinterop.cstr -import platform.Foundation.NSBundle -import platform.Foundation.NSString -import platform.Foundation.stringWithFormat - -@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) -class NotificationLocalizedStringFactory(private val bundle: NSBundle) : NotificationSchedulingService.LocalizedStringFactory { - - override fun reminderTitle(roomName: String?): String { - val ending = roomName?.let { - NSString - .stringWithFormat( - bundle.localizedStringForKey("Notification.Reminder.Title.InRoom", null, null) - .convertParametersForPrintf(), - it.cstr, - ) - } ?: "" - return NSString - .stringWithFormat( - bundle.localizedStringForKey("Notification.Reminder.Title.Base", null, null) - .convertParametersForPrintf(), - ending.cstr, - ) - } - - override fun reminderBody(sessionTitle: String): String = NSString - .stringWithFormat( - bundle.localizedStringForKey("Notification.Reminder.Body", null, null) - .convertParametersForPrintf(), - sessionTitle.cstr, - ) - - override fun feedbackTitle(): String = bundle.localizedStringForKey("Notification.Feedback.Title", null, null) - - override fun feedbackBody(): String = bundle.localizedStringForKey("Notification.Feedback.Body", null, null) - - private fun String.convertParametersForPrintf(): String = replace("%@", "%s") -} diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 00000000..2a3f079a --- /dev/null +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,7 @@ + + Upcoming event%s + in %s + %s is starting soon. + Feedback Time! + Your Feedback is Requested. + diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index d3cdc98f..a359b555 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -8,6 +8,7 @@ import co.touchlab.droidcon.application.repository.SettingsRepository import co.touchlab.droidcon.application.repository.impl.DefaultAboutRepository import co.touchlab.droidcon.application.repository.impl.DefaultSettingsRepository import co.touchlab.droidcon.application.service.NotificationSchedulingService +import co.touchlab.droidcon.application.service.impl.ComposeNotificationLocalizedStringFactory import co.touchlab.droidcon.application.service.impl.DefaultNotificationSchedulingService import co.touchlab.droidcon.db.DroidconDatabase import co.touchlab.droidcon.domain.gateway.SessionGateway @@ -228,6 +229,9 @@ private val coreModule = module { aboutJsonResourceDataSource = get(), ) } + single { + ComposeNotificationLocalizedStringFactory() + } single { DefaultNotificationSchedulingService( sessionRepository = get(), diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationSchedulingService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationSchedulingService.kt index 15860c12..f3b4681a 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationSchedulingService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationSchedulingService.kt @@ -14,10 +14,10 @@ interface NotificationSchedulingService { interface LocalizedStringFactory { - fun reminderTitle(roomName: String?): String - fun reminderBody(sessionTitle: String): String + suspend fun reminderTitle(roomName: String?): String + suspend fun reminderBody(sessionTitle: String): String - fun feedbackTitle(): String - fun feedbackBody(): String + suspend fun feedbackTitle(): String + suspend fun feedbackBody(): String } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/ComposeNotificationLocalizedStringFactory.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/ComposeNotificationLocalizedStringFactory.kt new file mode 100644 index 00000000..752c4b4f --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/ComposeNotificationLocalizedStringFactory.kt @@ -0,0 +1,25 @@ +package co.touchlab.droidcon.application.service.impl + +import co.touchlab.droidcon.application.service.NotificationSchedulingService +import droidcon.shared.generated.resources.Res +import droidcon.shared.generated.resources.notification_feedback_body +import droidcon.shared.generated.resources.notification_feedback_title +import droidcon.shared.generated.resources.notification_reminder_body +import droidcon.shared.generated.resources.notification_reminder_title_base +import droidcon.shared.generated.resources.notification_reminder_title_in_room +import org.jetbrains.compose.resources.getString + +class ComposeNotificationLocalizedStringFactory : NotificationSchedulingService.LocalizedStringFactory { + + override suspend fun reminderTitle(roomName: String?): String { + val ending = roomName?.let { getString(Res.string.notification_reminder_title_in_room, it) } ?: "" + return getString(Res.string.notification_reminder_title_base, ending) + } + + override suspend fun reminderBody(sessionTitle: String): String = + getString(Res.string.notification_reminder_body, sessionTitle) + + override suspend fun feedbackTitle(): String = getString(Res.string.notification_feedback_title) + + override suspend fun feedbackBody(): String = getString(Res.string.notification_feedback_body) +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt index 0d37b75a..d0d97fa3 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/DefaultNotificationSchedulingService.kt @@ -110,8 +110,8 @@ class DefaultNotificationSchedulingService( notification = Notification.Local.Reminder( sessionId = session.id, ), - title = localizedStringFactory.reminderTitle(roomName), - body = localizedStringFactory.reminderBody(session.title), + title = localizedStringFactory.reminderTitle(roomName = roomName), + body = localizedStringFactory.reminderBody(sessionTitle = session.title), delivery = reminderDelivery, dismiss = reminderDelivery.plus( NotificationSchedulingService.REMINDER_DISMISS_OFFSET, diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt index 457ee165..62e3ac02 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultConferenceConfigProvider.kt @@ -9,12 +9,14 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.datetime.TimeZone class DefaultConferenceConfigProvider(private val conferenceRepository: ConferenceRepository, initialConference: Conference? = null) : ConferenceConfigProvider { private val log = Logger.withTag("DefaultConferenceConfigProvider") + private val conferenceMutex = Mutex() private val _currentConferenceState = MutableStateFlow(initialConference) val currentConferenceState: StateFlow = _currentConferenceState @@ -55,9 +57,11 @@ class DefaultConferenceConfigProvider(private val conferenceRepository: Conferen } private suspend fun getConference(): Conference { - if (currentConference != null) return currentConference!! - val conference = conferenceRepository.getSelected() - _currentConferenceState.update { conference } - return conference + currentConference?.let { return it } + return conferenceMutex.withLock { + currentConference ?: conferenceRepository.getSelected().also { conference -> + _currentConferenceState.value = conference + } + } } } diff --git a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt index a24b3a06..53b34b77 100644 --- a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt +++ b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt @@ -1,7 +1,6 @@ package co.touchlab.droidcon.web import app.cash.sqldelight.db.SqlDriver -import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.db.DroidconDatabase import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory import co.touchlab.droidcon.domain.service.AnalyticsService @@ -13,7 +12,6 @@ import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel import co.touchlab.droidcon.web.service.DefaultParseUrlViewService -import co.touchlab.droidcon.web.util.NotificationLocalizedStringFactory import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ObservableSettings import com.russhwolf.settings.Settings @@ -32,8 +30,6 @@ fun webAppModule(driver: SqlDriver? = null): Module = module { } single { ComposeResourceReader() } - single { NotificationLocalizedStringFactory() } - single { WebAnalyticsService() } single { DefaultParseUrlViewService() } diff --git a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt deleted file mode 100644 index 32e922cc..00000000 --- a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/util/NotificationLocalizedStringFactory.kt +++ /dev/null @@ -1,17 +0,0 @@ -package co.touchlab.droidcon.web.util - -import co.touchlab.droidcon.application.service.NotificationSchedulingService - -class NotificationLocalizedStringFactory : NotificationSchedulingService.LocalizedStringFactory { - - override fun reminderTitle(roomName: String?): String { - val ending = roomName?.let { " in $it" } ?: "" - return "Upcoming event$ending" - } - - override fun reminderBody(sessionTitle: String): String = "$sessionTitle is starting soon." - - override fun feedbackTitle(): String = "Feedback Time!" - - override fun feedbackBody(): String = "Your Feedback is Requested." -} From 110417d85908f65eff1fa55b4f7b1bf6566d5faf Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 May 2026 17:16:04 -0400 Subject: [PATCH 51/56] formatting --- .../impl/ComposeNotificationLocalizedStringFactory.kt | 3 +-- .../droidcon/domain/repository/SessionRepositoryTest.kt | 1 - .../kotlin/co/touchlab/droidcon/test/TestDatabase.kt | 5 +---- .../kotlin/co/touchlab/droidcon/test/TestEntityFactory.kt | 6 +----- .../kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt | 2 +- 5 files changed, 4 insertions(+), 13 deletions(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/ComposeNotificationLocalizedStringFactory.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/ComposeNotificationLocalizedStringFactory.kt index 752c4b4f..a4ffe86d 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/ComposeNotificationLocalizedStringFactory.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/ComposeNotificationLocalizedStringFactory.kt @@ -16,8 +16,7 @@ class ComposeNotificationLocalizedStringFactory : NotificationSchedulingService. return getString(Res.string.notification_reminder_title_base, ending) } - override suspend fun reminderBody(sessionTitle: String): String = - getString(Res.string.notification_reminder_body, sessionTitle) + override suspend fun reminderBody(sessionTitle: String): String = getString(Res.string.notification_reminder_body, sessionTitle) override suspend fun feedbackTitle(): String = getString(Res.string.notification_feedback_title) diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SessionRepositoryTest.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SessionRepositoryTest.kt index d1e1f559..0664a5eb 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SessionRepositoryTest.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SessionRepositoryTest.kt @@ -149,5 +149,4 @@ class SessionRepositoryTest { dateTimeService = TestSessionFactory.dateTimeService, sessionQueries = testDb.database.sessionQueries, ) - } diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestDatabase.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestDatabase.kt index 8949cf1d..1d0544d5 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestDatabase.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestDatabase.kt @@ -35,10 +35,7 @@ internal suspend fun DroidconDatabase.conferenceIdForName(conferenceName: String id to name }.awaitAsList().first { (_, name) -> name == conferenceName }.first -internal suspend fun seedSecondConference( - database: DroidconDatabase, - conferenceName: String = "Second Conference", -): Long { +internal suspend fun seedSecondConference(database: DroidconDatabase, conferenceName: String = "Second Conference"): Long { database.conferenceQueries.insert( conferenceName = conferenceName, conferenceTimeZone = TimeZone.of("Europe/Berlin"), diff --git a/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestEntityFactory.kt b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestEntityFactory.kt index ebb28326..03dcaba3 100644 --- a/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestEntityFactory.kt +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestEntityFactory.kt @@ -31,11 +31,7 @@ object TestEntityFactory { website = website, ) - fun sponsorGroup( - name: String = "Gold", - displayPriority: Int = 1, - isProminent: Boolean = true, - ): SponsorGroup = SponsorGroup( + fun sponsorGroup(name: String = "Gold", displayPriority: Int = 1, isProminent: Boolean = true): SponsorGroup = SponsorGroup( id = SponsorGroup.Id(name), displayPriority = displayPriority, isProminent = isProminent, diff --git a/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt b/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt index 81c5c2a7..f6260260 100644 --- a/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt +++ b/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt @@ -2,8 +2,8 @@ package co.touchlab.droidcon.test import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.worker.WebWorkerDriver -import co.touchlab.droidcon.db.DroidconDatabase import app.cash.sqldelight.driver.worker.expected.Worker +import co.touchlab.droidcon.db.DroidconDatabase actual suspend fun createInMemoryDriver(): SqlDriver { val driver = WebWorkerDriver( Worker( From 79425a20546d24a5391757f70cf4c3580b6226a2 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 29 May 2026 08:17:22 -0400 Subject: [PATCH 52/56] Fixing CI issues --- kotlin-js-store/yarn.lock | 177 +++++++++++++++++- shared/build.gradle.kts | 1 + shared/karma.config.d/sqljs-config.js | 28 +++ .../impl/SqlDelightProfileRepository.kt | 2 +- .../impl/SqlDelightDriverFactory.js.kt | 4 + .../co/touchlab/droidcon/util/TimeZoneInit.kt | 8 + .../droidcon/test/TestSqlDriver.js.kt | 5 + shared/webpack.config.d/sqljs-config.js | 19 ++ web/webpack.config.d/sqljs-config.js | 17 +- 9 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 shared/karma.config.d/sqljs-config.js create mode 100644 shared/src/jsMain/kotlin/co/touchlab/droidcon/util/TimeZoneInit.kt create mode 100644 shared/webpack.config.d/sqljs-config.js diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 1bdf7642..75c33472 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -123,6 +123,27 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -242,7 +263,7 @@ dependencies: "@types/node" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -497,6 +518,11 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + ajv-keywords@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" @@ -504,6 +530,16 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" +ajv@^6.12.5: + version "6.15.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.15.0.tgz#07e982c74626167aa7a2495c53817892d7139492" + integrity sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ajv@^8.0.0, ajv@^8.9.0: version "8.17.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" @@ -559,6 +595,11 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -847,6 +888,18 @@ cookie@~0.7.2: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +copy-webpack-plugin@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz#2d2c460c4c4695ec0a58afb2801a1205256c4e6b" + integrity sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA== + dependencies: + fast-glob "^3.2.7" + glob-parent "^6.0.1" + globby "^11.0.3" + normalize-path "^3.0.0" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -953,6 +1006,13 @@ diff@^7.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + dns-packet@^5.2.2: version "5.6.1" resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" @@ -1176,11 +1236,27 @@ extend@^3.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -fast-deep-equal@^3.1.3: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.2.7, fast-glob@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + fast-uri@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" @@ -1191,6 +1267,13 @@ fastest-levenshtein@^1.0.12: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== +fastq@^1.6.0: + version "1.20.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" + integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== + dependencies: + reusify "^1.0.4" + faye-websocket@^0.11.3: version "0.11.4" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" @@ -1338,13 +1421,20 @@ get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" -glob-parent@~5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob-to-regex.js@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz#2b323728271d133830850e32311f40766c5f6413" @@ -1379,6 +1469,18 @@ glob@^7.1.3, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +globby@^11.0.3: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -1503,6 +1605,11 @@ iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + import-local@^3.0.2: version "3.2.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" @@ -1573,7 +1680,7 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-glob@^4.0.1, is-glob@~4.0.1: +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -1691,6 +1798,11 @@ json-parse-even-better-errors@^2.3.1: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + json-schema-traverse@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" @@ -1862,12 +1974,17 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.2: +micromatch@^4.0.2, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -2156,6 +2273,11 @@ path-to-regexp@0.1.12: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -2191,6 +2313,11 @@ punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + qjobs@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" @@ -2203,6 +2330,11 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -2307,6 +2439,11 @@ retry@^0.13.1: resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + rfdc@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" @@ -2324,6 +2461,13 @@ run-applescript@^7.0.0: resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911" integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q== +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -2348,6 +2492,15 @@ safe-regex-test@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +schema-utils@^3.1.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.2: version "4.3.3" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" @@ -2390,7 +2543,7 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^6.0.2: +serialize-javascript@^6.0.0, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== @@ -2499,6 +2652,11 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + socket.io-adapter@~2.5.2: version "2.5.5" resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" @@ -2779,6 +2937,13 @@ update-browserslist-db@^1.1.4: escalade "^3.2.0" picocolors "^1.1.1" +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 74fdc3b5..e41de637 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -127,6 +127,7 @@ kotlin { implementation(npm("sql.js", "1.8.0")) implementation(npm("@cashapp/sqldelight-sqljs-worker", "2.2.1")) implementation(npm("@js-joda/timezone", "2.22.0")) + implementation(devNpm("copy-webpack-plugin", "9.1.0")) } commonTest.dependencies { diff --git a/shared/karma.config.d/sqljs-config.js b/shared/karma.config.d/sqljs-config.js new file mode 100644 index 00000000..dad5c35c --- /dev/null +++ b/shared/karma.config.d/sqljs-config.js @@ -0,0 +1,28 @@ +const path = require('path'); +const os = require('os'); + +const dist = path.resolve(__dirname, '../../node_modules/sql.js/dist'); +const wasm = path.join(dist, 'sql-wasm.wasm'); + +config.files.push({ + pattern: wasm, + served: true, + watched: false, + included: false, + nocache: false, +}); + +config.proxies['/sql-wasm.wasm'] = path.join('/absolute/', wasm); + +// https://github.com/ryanclark/karma-webpack/issues/498#issuecomment-790040818 +const output = { + path: path.join(os.tmpdir(), '_karma_webpack_') + Math.floor(Math.random() * 1000000), +}; +config.set({ + webpack: { ...config.webpack, output }, +}); +config.files.push({ + pattern: `${output.path}/**/*`, + watched: false, + included: false, +}); diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt index 11965bd7..216dba47 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightProfileRepository.kt @@ -63,7 +63,7 @@ class SqlDelightProfileRepository( sponsorGroupName = sponsorId.group, conferenceId = conferenceId, mapper = ::profileFactory, - ).executeAsList() + ).awaitAsList() override suspend fun allSync(conferenceId: Long): List = profileQueries.selectAll(conferenceId, mapper = ::profileFactory).awaitAsList() diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt index d2d174a1..33cf2f82 100644 --- a/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt @@ -3,6 +3,10 @@ package co.touchlab.droidcon.domain.repository.impl import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.worker.WebWorkerDriver import app.cash.sqldelight.driver.worker.expected.Worker +import co.touchlab.droidcon.util.TimezoneInit + +@Suppress("unused") +private val ensureTimezoneDataLoaded = TimezoneInit actual class SqlDelightDriverFactory { actual fun createDriver(): SqlDriver = WebWorkerDriver( diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/TimeZoneInit.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/TimeZoneInit.kt new file mode 100644 index 00000000..e6317e01 --- /dev/null +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/TimeZoneInit.kt @@ -0,0 +1,8 @@ +@file:JsModule("@js-joda/timezone") +@file:JsNonModule + +@file:Suppress("ktlint:standard:filename") + +package co.touchlab.droidcon.util + +external object TimezoneInit diff --git a/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt b/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt index f6260260..77d7f5ef 100644 --- a/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt +++ b/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt @@ -4,6 +4,11 @@ import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.worker.WebWorkerDriver import app.cash.sqldelight.driver.worker.expected.Worker import co.touchlab.droidcon.db.DroidconDatabase +import co.touchlab.droidcon.util.TimezoneInit + +@Suppress("unused") +private val ensureTimezoneDataLoaded = TimezoneInit + actual suspend fun createInMemoryDriver(): SqlDriver { val driver = WebWorkerDriver( Worker( diff --git a/shared/webpack.config.d/sqljs-config.js b/shared/webpack.config.d/sqljs-config.js new file mode 100644 index 00000000..300ef2b0 --- /dev/null +++ b/shared/webpack.config.d/sqljs-config.js @@ -0,0 +1,19 @@ +config.resolve = config.resolve || {}; +config.resolve.fallback = Object.assign(config.resolve.fallback || {}, { + fs: false, + path: false, + crypto: false, + os: false, +}); + +const path = require('path'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +const sqlJsDist = path.resolve(__dirname, '../../node_modules/sql.js/dist'); +config.plugins.push( + new CopyWebpackPlugin({ + patterns: [ + { from: path.join(sqlJsDist, 'sql-wasm.wasm'), to: '.' }, + ], + }), +); diff --git a/web/webpack.config.d/sqljs-config.js b/web/webpack.config.d/sqljs-config.js index 7fe5c973..300ef2b0 100644 --- a/web/webpack.config.d/sqljs-config.js +++ b/web/webpack.config.d/sqljs-config.js @@ -6,11 +6,14 @@ config.resolve.fallback = Object.assign(config.resolve.fallback || {}, { os: false, }); -// Serve sql.js WASM from node_modules (avoids needing copy-webpack-plugin) const path = require('path'); -if (config.devServer && config.devServer.static) { - config.devServer.static.push({ - directory: path.resolve(__dirname, '../../node_modules/sql.js/dist'), - watch: false - }); -} \ No newline at end of file +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +const sqlJsDist = path.resolve(__dirname, '../../node_modules/sql.js/dist'); +config.plugins.push( + new CopyWebpackPlugin({ + patterns: [ + { from: path.join(sqlJsDist, 'sql-wasm.wasm'), to: '.' }, + ], + }), +); From 4c943ab8fcb9e0be4a2c4329d0e4ca6d252a9f64 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 29 May 2026 10:14:11 -0400 Subject: [PATCH 53/56] Update SqlDelightConferenceRepository.kt --- .../impl/SqlDelightConferenceRepository.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt index f1197c8c..12bc0030 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt @@ -35,21 +35,21 @@ class SqlDelightConferenceRepository( } } - override suspend fun add(conference: Conference): Long { - conferenceQueries.insert( - conferenceName = conference.name, - conferenceTimeZone = conference.timeZone, - projectId = conference.projectId, - collectionName = conference.collectionName, - apiKey = conference.apiKey, - scheduleId = conference.scheduleId, - selected = conference.selected, - active = conference.active, - venueMap = conference.venueMap, - ) - // Return the last inserted ID - return conferenceQueries.lastInsertRowId().awaitAsOne() - } + override suspend fun add(conference: Conference): Long = + conferenceQueries.transactionWithResult { + conferenceQueries.insert( + conferenceName = conference.name, + conferenceTimeZone = conference.timeZone, + projectId = conference.projectId, + collectionName = conference.collectionName, + apiKey = conference.apiKey, + scheduleId = conference.scheduleId, + selected = conference.selected, + active = conference.active, + venueMap = conference.venueMap, + ) + conferenceQueries.lastInsertRowId().awaitAsOne() + } override suspend fun update(conference: Conference): Boolean { try { From 74fdee912596ddea295a96cc45cd0c7105e603b8 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 29 May 2026 10:44:11 -0400 Subject: [PATCH 54/56] Update SqlDelightConferenceRepository.kt --- .../impl/SqlDelightConferenceRepository.kt | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt index 12bc0030..5571da63 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt @@ -35,21 +35,20 @@ class SqlDelightConferenceRepository( } } - override suspend fun add(conference: Conference): Long = - conferenceQueries.transactionWithResult { - conferenceQueries.insert( - conferenceName = conference.name, - conferenceTimeZone = conference.timeZone, - projectId = conference.projectId, - collectionName = conference.collectionName, - apiKey = conference.apiKey, - scheduleId = conference.scheduleId, - selected = conference.selected, - active = conference.active, - venueMap = conference.venueMap, - ) - conferenceQueries.lastInsertRowId().awaitAsOne() - } + override suspend fun add(conference: Conference): Long = conferenceQueries.transactionWithResult { + conferenceQueries.insert( + conferenceName = conference.name, + conferenceTimeZone = conference.timeZone, + projectId = conference.projectId, + collectionName = conference.collectionName, + apiKey = conference.apiKey, + scheduleId = conference.scheduleId, + selected = conference.selected, + active = conference.active, + venueMap = conference.venueMap, + ) + conferenceQueries.lastInsertRowId().awaitAsOne() + } override suspend fun update(conference: Conference): Boolean { try { From f6db0b58631ee5c64110fb383f5226b3d0e108d3 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 29 May 2026 12:33:26 -0400 Subject: [PATCH 55/56] Adding a common AnalyticsService --- android/build.gradle.kts | 1 - .../co/touchlab/droidcon/android/MainApp.kt | 8 - .../service/impl/AndroidAnalyticsService.kt | 27 - gradle/libs.versions.toml | 5 +- .../Droidcon.xcodeproj/project.pbxproj | 4 - .../Droidcon/IOSAnalyticsService.swift | 9 - ios/Droidcon/Droidcon/Koin.swift | 2 +- .../droidcon/ios/DependencyInjection.kt | 5 +- kotlin-js-store/yarn.lock | 548 +++++++++++++++++- .../viewmodel/WaitForLoadedContextModel.kt | 6 - shared/build.gradle.kts | 1 + .../droidcon/util/FirebaseInit.android.kt | 5 + .../kotlin/co/touchlab/droidcon/Koin.kt | 9 + .../service/impl/DefaultAnalyticsService.kt | 26 + .../co/touchlab/droidcon/util/FirebaseInit.kt | 3 + .../droidcon/util/FirebaseInit.ios.kt | 5 + .../touchlab/droidcon/util/FirebaseInit.js.kt | 19 + .../droidcon/web/DependencyInjection.kt | 3 - .../kotlin/co/touchlab/droidcon/web/Main.kt | 4 +- .../droidcon/web/WebAnalyticsService.kt | 18 - 20 files changed, 621 insertions(+), 87 deletions(-) delete mode 100644 android/src/main/java/co/touchlab/droidcon/android/service/impl/AndroidAnalyticsService.kt delete mode 100644 ios/Droidcon/Droidcon/IOSAnalyticsService.swift create mode 100644 shared/src/androidMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.android.kt create mode 100644 shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAnalyticsService.kt create mode 100644 shared/src/commonMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.kt create mode 100644 shared/src/iosMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.ios.kt create mode 100644 shared/src/jsMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.js.kt delete mode 100644 web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 382d22d6..f2d20ddb 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -84,7 +84,6 @@ dependencies { implementation(libs.kotlinx.datetime) implementation(libs.coil.compose) implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) implementation(libs.firebase.messaging) diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt index 8db96d12..b92e451b 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt @@ -4,16 +4,12 @@ import android.app.Activity import android.app.Application import android.content.Context import android.content.SharedPreferences -import co.touchlab.droidcon.android.service.impl.AndroidAnalyticsService import co.touchlab.droidcon.android.service.impl.DefaultParseUrlViewService -import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule -import com.google.firebase.Firebase -import com.google.firebase.analytics.analytics import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ObservableSettings import com.russhwolf.settings.SharedPreferencesSettings @@ -47,10 +43,6 @@ class MainApp : single { ComposeResourceReader() } - - single { - AndroidAnalyticsService(firebaseAnalytics = Firebase.analytics) - } } + uiModule, ) } diff --git a/android/src/main/java/co/touchlab/droidcon/android/service/impl/AndroidAnalyticsService.kt b/android/src/main/java/co/touchlab/droidcon/android/service/impl/AndroidAnalyticsService.kt deleted file mode 100644 index c4631774..00000000 --- a/android/src/main/java/co/touchlab/droidcon/android/service/impl/AndroidAnalyticsService.kt +++ /dev/null @@ -1,27 +0,0 @@ -package co.touchlab.droidcon.android.service.impl - -import android.os.Bundle -import co.touchlab.droidcon.domain.service.AnalyticsService -import com.google.firebase.analytics.FirebaseAnalytics - -class AndroidAnalyticsService(private val firebaseAnalytics: FirebaseAnalytics) : AnalyticsService { - - override fun logEvent(name: String, params: Map) { - val bundle = Bundle() - params.keys.forEach { key -> - when (val obj = params[key]) { - is String -> bundle.putString(key, obj) - is Boolean -> bundle.putBoolean(key, obj) - is Int -> bundle.putInt(key, obj) - is Long -> bundle.putLong(key, obj) - is Double -> bundle.putDouble(key, obj) - is BooleanArray -> bundle.putBooleanArray(key, obj) - is IntArray -> bundle.putIntArray(key, obj) - is LongArray -> bundle.putLongArray(key, obj) - is DoubleArray -> bundle.putDoubleArray(key, obj) - else -> throw IllegalArgumentException("Unsupported type $obj with key $key") - } - } - firebaseAnalytics.logEvent(name, bundle) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 245772df..4e6597ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,12 +27,12 @@ hyperdrive = "0.1.148" multiplatformSettings = "1.3.0" sqlDelight = "2.2.1" -firebase-analytics = "22.5.0" firebase-crashlytics = "19.4.4" firebase-messaging = "24.1.2" firebase-bom = "34.7.0" firebase-crashlytics-gradle = "3.0.6" gms-google-services = "4.4.4" +gitlive-firebase = "2.4.0" # TODO: Update Compose libraries. There is currently a conflicing issue with the HorizontalPager compose-androidx-ui = "1.10.0" @@ -66,7 +66,6 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } kamel-image-default = { module = "media.kamel:kamel-image-default", version.ref = "kamelImageDefault" } kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinxBrowser" } -ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } material3-adaptive-navigation-suite = { module = "org.jetbrains.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavigationSuiteVersion" } multiplatform-settings-make-observable = { module = "com.russhwolf:multiplatform-settings-make-observable", version.ref = "multiplatformSettings" } sqliter = { module = "co.touchlab:sqliter-driver", version.ref = "sqliter" } @@ -74,9 +73,9 @@ compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } -firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "firebase-analytics" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "firebase-crashlytics" } firebase-messaging = { module = "com.google.firebase:firebase-messaging-ktx", version.ref = "firebase-messaging" } +gitlive-firebase-analytics = { module = "dev.gitlive:firebase-analytics", version.ref = "gitlive-firebase" } hyperdrive-multiplatformx-api = { module = "org.brightify.hyperdrive:multiplatformx-api", version.ref = "hyperdrive" } android-desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugaring" } uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } diff --git a/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj index eece57a1..861555e6 100644 --- a/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj +++ b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj @@ -17,7 +17,6 @@ 46B5284D249C5CF400A7725D /* Koin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46B5284C249C5CF400A7725D /* Koin.swift */; }; 684FAA8926B2C31800673AFF /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 684FAA8B26B2C31800673AFF /* Localizable.strings */; }; 68C86E9F26B31D6100008D15 /* LifecycleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68C86E9E26B31D6100008D15 /* LifecycleManager.swift */; }; - 8404D80E26C64B9E00AE200F /* IOSAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8404D80D26C64B9E00AE200F /* IOSAnalyticsService.swift */; }; A35DC2E328AB6C6F00C7B298 /* ComposeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35DC2E228AB6C6F00C7B298 /* ComposeController.swift */; }; A35DEF2228AA265C0072605A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = A35DEF2128AA265C0072605A /* Settings.bundle */; }; F1465F0123AA94BF0055F7C3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */; }; @@ -32,7 +31,6 @@ 46B5284C249C5CF400A7725D /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = ""; }; 684FAA8A26B2C31800673AFF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 68C86E9E26B31D6100008D15 /* LifecycleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifecycleManager.swift; sourceTree = ""; }; - 8404D80D26C64B9E00AE200F /* IOSAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSAnalyticsService.swift; sourceTree = ""; }; A35DC2E228AB6C6F00C7B298 /* ComposeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeController.swift; sourceTree = ""; }; A35DEF2128AA265C0072605A /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; F1465EFD23AA94BF0055F7C3 /* Droidcon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Droidcon.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -96,7 +94,6 @@ 1821427626B541720047DB71 /* Resources */, F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */, 46B5284C249C5CF400A7725D /* Koin.swift */, - 8404D80D26C64B9E00AE200F /* IOSAnalyticsService.swift */, F1465F0923AA94BF0055F7C3 /* Assets.xcassets */, F1465F0B23AA94BF0055F7C3 /* LaunchScreen.storyboard */, F1465F0E23AA94BF0055F7C3 /* Info.plist */, @@ -234,7 +231,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8404D80E26C64B9E00AE200F /* IOSAnalyticsService.swift in Sources */, 68C86E9F26B31D6100008D15 /* LifecycleManager.swift in Sources */, 1833221026B0CF5600D79482 /* DroidconApp.swift in Sources */, 46B5284D249C5CF400A7725D /* Koin.swift in Sources */, diff --git a/ios/Droidcon/Droidcon/IOSAnalyticsService.swift b/ios/Droidcon/Droidcon/IOSAnalyticsService.swift deleted file mode 100644 index 6197878e..00000000 --- a/ios/Droidcon/Droidcon/IOSAnalyticsService.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import Firebase -import DroidconKit - -final class IOSAnalyticsService: AnalyticsService { - func logEvent(name: String, params: [String: Any]) { - Analytics.logEvent(name, parameters: params) - } -} diff --git a/ios/Droidcon/Droidcon/Koin.swift b/ios/Droidcon/Droidcon/Koin.swift index a35e2ee2..2f98496b 100644 --- a/ios/Droidcon/Droidcon/Koin.swift +++ b/ios/Droidcon/Droidcon/Koin.swift @@ -4,7 +4,7 @@ import DroidconKit func startKoin() { let userDefaults = UserDefaults(suiteName: "DROIDCON2024_SETTINGS")! - let koinApplication = DependencyInjectionKt.doInitKoinIos(userDefaults: userDefaults, analyticsService: IOSAnalyticsService()) + let koinApplication = DependencyInjectionKt.doInitKoinIos(userDefaults: userDefaults) _koin = koinApplication.koin } diff --git a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt index fbb8ba35..2c7e09dc 100644 --- a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt +++ b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt @@ -1,6 +1,5 @@ package co.touchlab.droidcon.ios -import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin @@ -17,15 +16,13 @@ import org.koin.dsl.module import platform.Foundation.NSUserDefaults @OptIn(ExperimentalSettingsApi::class) -fun initKoinIos(userDefaults: NSUserDefaults, analyticsService: AnalyticsService): KoinApplication = initKoin( +fun initKoinIos(userDefaults: NSUserDefaults): KoinApplication = initKoin( module { single { NSUserDefaultsSettings(delegate = userDefaults) } single { ComposeResourceReader() } - single { analyticsService } - single { DefaultParseUrlViewService() } } + uiModule, ) diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 75c33472..93e991cb 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -17,6 +17,413 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz#f13c7c205915eb91ae54c557f5e92bddd8be0e83" integrity sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ== +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + +"@firebase/analytics-compat@0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.10.tgz#c98005075c019eb8255764a5279f0ff86b36b863" + integrity sha512-ia68RcLQLLMFWrM10JfmFod7eJGwqr4/uyrtzHpTDnxGX/6gNCBTOuxdAbyWIqXI5XmcMQdz9hDijGKOHgDfPw== + dependencies: + "@firebase/analytics" "0.10.4" + "@firebase/analytics-types" "0.8.2" + "@firebase/component" "0.6.7" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/analytics-types@0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.2.tgz#947f85346e404332aac6c996d71fd4a89cd7f87a" + integrity sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw== + +"@firebase/analytics@0.10.4": + version "0.10.4" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.4.tgz#dc68a86774f9ee4f980708e824157617fd2b8ef7" + integrity sha512-OJEl/8Oye/k+vJ1zV/1L6eGpc1XzAj+WG2TPznJ7PszL7sOFLBXkL9IjHfOCGDGpXeO3btozy/cYUqv4zgNeHg== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/installations" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/app-check-compat@0.3.11": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.3.11.tgz#0a5d1c72c91ba239e4dabf6fd698b27f082030ca" + integrity sha512-t01zaH3RJpKEey0nGduz3Is+uSz7Sj4U5nwOV6lWb+86s5xtxpIvBJzu/lKxJfYyfZ29eJwpdjEgT1/lm4iQyA== + dependencies: + "@firebase/app-check" "0.8.4" + "@firebase/app-check-types" "0.5.2" + "@firebase/component" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/app-check-interop-types@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz#455b6562c7a3de3ef75ea51f72dfec5829ad6997" + integrity sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ== + +"@firebase/app-check-types@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.2.tgz#1221bd09b471e11bb149252f16640a0a51043cbc" + integrity sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA== + +"@firebase/app-check@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.8.4.tgz#1c965d34527d1b924fc7bd51789119b3f817bf94" + integrity sha512-2tjRDaxcM5G7BEpytiDcIl+NovV99q8yEqRMKDbn4J4i/XjjuThuB4S+4PkmTnZiCbdLXQiBhkVxNlUDcfog5Q== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/app-compat@0.2.35": + version "0.2.35" + resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.2.35.tgz#ca918736e6b06bdd63eaed24ba213059ecd55f88" + integrity sha512-vgay/WRjeH0r97/Q6L6df2CMx7oyNFDsE5yPQ9oR1G+zx2eT0s8vNNh0WlKqQxUEWaOLRnXhQ8gy7uu0cBgTRg== + dependencies: + "@firebase/app" "0.10.5" + "@firebase/component" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/app-types@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.2.tgz#8cbcceba784753a7c0066a4809bc22f93adee080" + integrity sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ== + +"@firebase/app@0.10.5": + version "0.10.5" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.10.5.tgz#84d3c99b253366844335a411b568dd258800c794" + integrity sha512-iY/fNot+hWPk9sTX8aHMqlcX9ynRvpGkskWAdUZ2eQQdLo8d1hSFYcYNwPv0Q/frGMasw8udKWMcFOEpC9fG8g== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/auth-compat@0.5.9": + version "0.5.9" + resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.5.9.tgz#ab925dbe8baf0911fb4836c14403706132d751e8" + integrity sha512-RX8Zh/3zz2CsVbmYfgHkfUm4fAEPCl+KHVIImNygV5jTGDF6oKOhBIpf4Yigclyu8ESQKZ4elyN0MBYm9/7zGw== + dependencies: + "@firebase/auth" "1.7.4" + "@firebase/auth-types" "0.12.2" + "@firebase/component" "0.6.7" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + undici "5.28.4" + +"@firebase/auth-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz#927f1f2139a680b55fef0bddbff2c982b08587e8" + integrity sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ== + +"@firebase/auth-types@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.12.2.tgz#f12d890585866e53b6ab18b16fa4d425c52eee6e" + integrity sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w== + +"@firebase/auth@1.7.4": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.7.4.tgz#0dc8083314a61598c91cfe00cb96cf2cb3d74336" + integrity sha512-d2Fw17s5QesojwebrA903el20Li9/YGgkoOGJjagM4I1qAT36APa/FcZ+OX86KxbYKCtQKTMqraU8pxG7C2JWA== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + undici "5.28.4" + +"@firebase/component@0.6.7": + version "0.6.7" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.7.tgz#6fbffddb26833e1ed58bf296ad587cb330aee716" + integrity sha512-baH1AA5zxfaz4O8w0vDwETByrKTQqB5CDjRls79Sa4eAGAoERw4Tnung7XbMl3jbJ4B/dmmtsMrdki0KikwDYA== + dependencies: + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/database-compat@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-1.0.5.tgz#18c2314f169942ac315e46b68f86cbe64bafe063" + integrity sha512-NDSMaDjQ+TZEMDMmzJwlTL05kh1+0Y84C+kVMaOmNOzRGRM7VHi29I6YUhCetXH+/b1Wh4ZZRyp1CuWkd8s6hg== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/database" "1.0.5" + "@firebase/database-types" "1.0.3" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/database-types@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.3.tgz#1b764212dce88eca74b16da9d220cfea6814858e" + integrity sha512-39V/Riv2R3O/aUjYKh0xypj7NTNXNAK1bcgY5Kx+hdQPRS/aPTS8/5c0CGFYKgVuFbYlnlnhrCTYsh2uNhGwzA== + dependencies: + "@firebase/app-types" "0.9.2" + "@firebase/util" "1.9.6" + +"@firebase/database@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.5.tgz#09d7162b7dbc4533f17498ac6a76d5e757ab45be" + integrity sha512-cAfwBqMQuW6HbhwI3Cb/gDqZg7aR0OmaJ85WUxlnoYW2Tm4eR0hFl5FEijI3/gYPUiUcUPQvTkGV222VkT7KPw== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/auth-interop-types" "0.2.3" + "@firebase/component" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/firestore-compat@0.3.32": + version "0.3.32" + resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.3.32.tgz#1357ba5f80b83f33210d4fb49a1cd346cf95b291" + integrity sha512-at71mwK7a/mUXH0OgyY0+gUzedm/EUydDFYSFsBoO8DYowZ23Mgd6P4Rzq/Ll3zI/3xJN7LGe7Qp4iE/V/3Arg== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/firestore" "4.6.3" + "@firebase/firestore-types" "3.0.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/firestore-types@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.2.tgz#75c301acc5fa33943eaaa9570b963c55398cad2a" + integrity sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg== + +"@firebase/firestore@4.6.3": + version "4.6.3" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.6.3.tgz#87ad38dfd0a0f16e79682177102ee1328d59af44" + integrity sha512-d/+N2iUsiJ/Dc7fApdpdmmTXzwuTCromsdA1lKwYfZtMIOd1fI881NSLwK2wV4I38wkLnvfKJUV6WpU1f3/ONg== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + "@firebase/webchannel-wrapper" "1.0.0" + "@grpc/grpc-js" "~1.9.0" + "@grpc/proto-loader" "^0.7.8" + tslib "^2.1.0" + undici "5.28.4" + +"@firebase/functions-compat@0.3.11": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.3.11.tgz#9fdff8b174879b404501df7b8b519e5fb6d0b8ec" + integrity sha512-Qn+ts/M6Lj2/6i1cp5V5TRR+Hi9kyXyHbo+w9GguINJ87zxrCe6ulx3TI5AGQkoQa8YFHUhT3DMGmLFiJjWTSQ== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/functions" "0.11.5" + "@firebase/functions-types" "0.6.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/functions-types@0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.2.tgz#03b4ec9259d2f57548a3909d6a35ae35ad243552" + integrity sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w== + +"@firebase/functions@0.11.5": + version "0.11.5" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.11.5.tgz#e4187ae3ae262b0482114f7ad418600ca84f3459" + integrity sha512-qrHJ+l62mZiU5UZiVi84t/iLXZlhRuSvBQsa2qvNLgPsEWR7wdpWhRmVdB7AU8ndkSHJjGlMICqrVnz47sgU7Q== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/auth-interop-types" "0.2.3" + "@firebase/component" "0.6.7" + "@firebase/messaging-interop-types" "0.2.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + undici "5.28.4" + +"@firebase/installations-compat@0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.7.tgz#c430f34bfcfc008c92ca32fd11d6c84ab5dd7888" + integrity sha512-RPcbD+3nqHbnhVjIOpWK2H5qzZ8pAAAScceiWph0VNTqpKyPQ5tDcp4V5fS0ELpfgsHYvroMLDKfeHxpfvm8cw== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/installations" "0.6.7" + "@firebase/installations-types" "0.5.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/installations-types@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.2.tgz#4d4949e0e83ced7f36cbee009355cd305a36e158" + integrity sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA== + +"@firebase/installations@0.6.7": + version "0.6.7" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.7.tgz#4fc60ca86e838d7c45dfd1d4926d000060bd1079" + integrity sha512-i6iGoXRu5mX4rTsiMSSKrgh9pSEzD4hwBEzRh5kEhOTr8xN/wvQcCPZDSMVYKwM2XyCPBLVq0JzjyerwL0Rihg== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/util" "1.9.6" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/logger@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.2.tgz#74dfcfeedee810deb8a7080d5b7eba56aa16ffa2" + integrity sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A== + dependencies: + tslib "^2.1.0" + +"@firebase/messaging-compat@0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.9.tgz#a4cae54c9caf10a3a6c811152d5bd58f165337b7" + integrity sha512-5jN6wyhwPgBH02zOtmmoOeyfsmoD7ty48D1m0vVPsFg55RqN2Z3Q9gkZ5GmPklFPjTPLcxB1ObcHOZvThTkm7g== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/messaging" "0.12.9" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/messaging-interop-types@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.2.tgz#81042f7e9739733fa4571d17f6eb6869522754d0" + integrity sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA== + +"@firebase/messaging@0.12.9": + version "0.12.9" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.9.tgz#c3cb7a26a3488161273839bf65237f8c485ba661" + integrity sha512-IH+JJmzbFGZXV3+TDyKdqqKPVfKRqBBg2BfYYOy7cm7J+SwV+uJMe8EnDKYeQLEQhtpwciPfJ3qQXJs2lbxDTw== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/installations" "0.6.7" + "@firebase/messaging-interop-types" "0.2.2" + "@firebase/util" "1.9.6" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/performance-compat@0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.7.tgz#30e29934326888b164c67e5f3709c3a8e580a8d6" + integrity sha512-cb8ge/5iTstxfIGW+iiY+7l3FtN8gobNh9JSQNZgLC9xmcfBYWEs8IeEWMI6S8T+At0oHc3lv+b2kpRMUWr8zQ== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/performance" "0.6.7" + "@firebase/performance-types" "0.2.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/performance-types@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.2.tgz#7b78cd2ab2310bac89a63348d93e67e01eb06dd7" + integrity sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA== + +"@firebase/performance@0.6.7": + version "0.6.7" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.6.7.tgz#7d6c4e5ec61df7369d87fb4a5c0af4e0cedee69b" + integrity sha512-d+Q4ltjdJZqjzcdms5i0UC9KLYX7vKGcygZ+7zHA/Xk+bAbMD2CPU0nWTnlNFWifZWIcXZ/2mAMvaGMW3lypUA== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/installations" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/remote-config-compat@0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.7.tgz#8a7ac7658a7c9cc29a4ad5884bc224eaae950c38" + integrity sha512-Fq0oneQ4SluLnfr5/HfzRS1TZf1ANj1rWbCCW3+oC98An3nE+sCdp+FSuHsEVNwgMg4Tkwx9Oom2lkKeU+Vn+w== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/remote-config" "0.4.7" + "@firebase/remote-config-types" "0.3.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/remote-config-types@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.3.2.tgz#a5d1009c6fd08036c5cd4f28764e3cd694f966d5" + integrity sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA== + +"@firebase/remote-config@0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.4.7.tgz#1afd6f3089e3c66ed6909eb60d0eb1329d27c9ff" + integrity sha512-5oPNrPFLsbsjpq0lUEIXoDF2eJK7vAbyXe/DEuZQxnwJlfR7aQbtUlEkRgQWcicXpyDmAmDLo7q7lDbCYa6CpA== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/installations" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/storage-compat@0.3.8": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.3.8.tgz#0d6d66a683713953b2bd24494e83bddcbb562f3a" + integrity sha512-qDfY9kMb6Ch2hZb40sBjDQ8YPxbjGOxuT+gU1Z0iIVSSpSX0f4YpGJCypUXiA0T11n6InCXB+T/Dknh2yxVTkg== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/storage" "0.12.5" + "@firebase/storage-types" "0.8.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/storage-types@0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.2.tgz#edb321b8a3872a9f74e1f27de046f160021c8e1f" + integrity sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g== + +"@firebase/storage@0.12.5": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.12.5.tgz#9277b4f838572ba78f017aa6207c6d7545400846" + integrity sha512-nGWBOGFNr10j0LA4NJ3/Yh3us/lb0Q1xSIKZ38N6FcS+vY54nqJ7k3zE3PENregHC8+8txRow++A568G3v8hOA== + dependencies: + "@firebase/component" "0.6.7" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + undici "5.28.4" + +"@firebase/util@1.9.6": + version "1.9.6" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.6.tgz#56dc34e20fcbc0dd07b11b800f95f5d0417cbfb4" + integrity sha512-IBr1MZbp4d5MjBCXL3TW1dK/PDXX4yOGbiwRNh1oAbE/+ci5Uuvy9KIrsFYY80as1I0iOaD5oOMA9Q8j4TJWcw== + dependencies: + tslib "^2.1.0" + +"@firebase/vertexai-preview@0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@firebase/vertexai-preview/-/vertexai-preview-0.0.2.tgz#a17454e4899bf4b3fa07322fb204659e7cfa5868" + integrity sha512-NOOL63kFQRq45ioi5P+hlqj/4LNmvn1URhGjQdvyV54c1Irvoq26aW861PRRLjrSMIeNeiLtCLD5pe+ediepAg== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/component" "0.6.7" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.6" + tslib "^2.1.0" + +"@firebase/webchannel-wrapper@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.0.tgz#a0e11b39fa3ef56ed5333bf321f581037aeda033" + integrity sha512-zuWxyfXNbsKbm96HhXzainONPFqRcoZblQ++e9cAIGUuHfl2cFSBzW01jtesqWG/lqaUyX3H8O1y9oWboGNQBA== + +"@grpc/grpc-js@~1.9.0": + version "1.9.16" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.16.tgz#614f85036ac8e3c957374c1bd1ebb05934a79a1c" + integrity sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.8": + version "0.7.15" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz#4cdfbf35a35461fc843abe8b9e2c0770b5095e60" + integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -149,6 +556,58 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.5.tgz#d9315ad7cf3f30aac70bda3c068443dc6f143659" + integrity sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g== + +"@protobufjs/eventemitter@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz#d512cb26c0ae026091ee2c1167f1be6faf5c842a" + integrity sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg== + +"@protobufjs/fetch@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.1.tgz#4d6fc00c8fb64016a5c81b469d549046350f1065" + integrity sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.2.tgz#ae64fbc014ff44c8bfad03dd4c93cd2d6a4c82db" + integrity sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.1.tgz#eaee5900122c110a3dbcb728c0597014a2621774" + integrity sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg== + "@socket.io/component-emitter@~3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" @@ -287,6 +746,13 @@ dependencies: undici-types "~7.16.0" +"@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "25.9.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.1.tgz#3bda556db500ae4319c08e7fc9ab94f19013ba0b" + integrity sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg== + dependencies: + undici-types ">=7.24.0 <7.24.7" + "@types/qs@*": version "6.14.0" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" @@ -1274,7 +1740,7 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -faye-websocket@^0.11.3: +faye-websocket@0.11.4, faye-websocket@^0.11.3: version "0.11.4" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== @@ -1330,6 +1796,39 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +firebase@10.12.2: + version "10.12.2" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-10.12.2.tgz#9049286c5fafb6d686bb19ad93c7bb4a9e8756c0" + integrity sha512-ZxEdtSvP1I9su1yf32D8TIdgxtPgxwr6z3jYAR1TXS/t+fVfpoPc/N1/N2bxOco9mNjUoc+od34v5Fn4GeKs6Q== + dependencies: + "@firebase/analytics" "0.10.4" + "@firebase/analytics-compat" "0.2.10" + "@firebase/app" "0.10.5" + "@firebase/app-check" "0.8.4" + "@firebase/app-check-compat" "0.3.11" + "@firebase/app-compat" "0.2.35" + "@firebase/app-types" "0.9.2" + "@firebase/auth" "1.7.4" + "@firebase/auth-compat" "0.5.9" + "@firebase/database" "1.0.5" + "@firebase/database-compat" "1.0.5" + "@firebase/firestore" "4.6.3" + "@firebase/firestore-compat" "0.3.32" + "@firebase/functions" "0.11.5" + "@firebase/functions-compat" "0.3.11" + "@firebase/installations" "0.6.7" + "@firebase/installations-compat" "0.2.7" + "@firebase/messaging" "0.12.9" + "@firebase/messaging-compat" "0.2.9" + "@firebase/performance" "0.6.7" + "@firebase/performance-compat" "0.2.7" + "@firebase/remote-config" "0.4.7" + "@firebase/remote-config-compat" "0.2.7" + "@firebase/storage" "0.12.5" + "@firebase/storage-compat" "0.3.8" + "@firebase/util" "1.9.6" + "@firebase/vertexai-preview" "0.0.2" + flat@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" @@ -1605,6 +2104,11 @@ iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +idb@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -1913,6 +2417,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -1937,6 +2446,11 @@ log4js@^6.4.1: rfdc "^1.3.0" streamroller "^3.1.5" +long@^5.0.0, long@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== + lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" @@ -2300,6 +2814,24 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +protobufjs@^7.2.5: + version "7.6.1" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.6.1.tgz#6320bb08c3be7dcfc6f9193ee03d3a4643f1eb37" + integrity sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.5" + "@protobufjs/eventemitter" "^1.1.1" + "@protobufjs/fetch" "^1.1.1" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.2" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.1" + "@types/node" ">=13.7.0" + long "^5.3.2" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -2896,7 +3428,7 @@ tree-dump@^1.0.3, tree-dump@^1.1.0: resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.1.0.tgz#ab29129169dc46004414f5a9d4a3c6e89f13e8a4" integrity sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA== -tslib@^2.0.0: +tslib@^2.0.0, tslib@^2.1.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -2914,11 +3446,23 @@ ua-parser-js@^0.7.30: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.41.tgz#9f6dee58c389e8afababa62a4a2dc22edb69a452" integrity sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg== +"undici-types@>=7.24.0 <7.24.7": + version "7.24.6" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91" + integrity sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg== + undici-types@~7.16.0: version "7.16.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== +undici@5.28.4: + version "5.28.4" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" + integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== + dependencies: + "@fastify/busboy" "^2.0.0" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt index 0dd5ca5d..18171283 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt @@ -34,19 +34,13 @@ class WaitForLoadedContextModel( } suspend fun watchConferenceChanges() { - log.i { "watchConferenceChanges" } lifecycle.whileAttached { - log.i { "Starting to Sync Conferences" } launch { - log.i { "Observing conferenceConfigProvider" } conferenceConfigProvider.observeChanges().collect { conference -> if (conference != null) { - log.i { "WaitForLoadedContextModel: Emitting Conference!" } _state.emit(State.Ready(conference)) - withContext(Dispatchers.Default) { try { - log.i { "syncConferences" } syncService.syncConferences() } catch (e: Exception) { log.e(e) { "Failed to sync conferences" } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index e41de637..ad781fb3 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -87,6 +87,7 @@ kotlin { implementation(libs.stately.common) implementation(libs.koin.core) implementation(libs.korio) + implementation(libs.gitlive.firebase.analytics) } val mobileMain by creating { dependsOn(commonMain.get()) diff --git a/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.android.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.android.kt new file mode 100644 index 00000000..9e7ba1a3 --- /dev/null +++ b/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.android.kt @@ -0,0 +1,5 @@ +package co.touchlab.droidcon.util + +actual fun initializeFirebase() { + // Auto-initialized via google-services.json and FirebaseInitProvider. +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index a359b555..faa7ccab 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -27,6 +27,7 @@ import co.touchlab.droidcon.domain.repository.impl.SqlDelightRoomRepository import co.touchlab.droidcon.domain.repository.impl.SqlDelightSessionRepository import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorGroupRepository import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorRepository +import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.DateTimeService import co.touchlab.droidcon.domain.service.FeedbackService @@ -34,6 +35,7 @@ import co.touchlab.droidcon.domain.service.ScheduleService import co.touchlab.droidcon.domain.service.ServerApi import co.touchlab.droidcon.domain.service.SyncService import co.touchlab.droidcon.domain.service.UserIdProvider +import co.touchlab.droidcon.domain.service.impl.DefaultAnalyticsService import co.touchlab.droidcon.domain.service.impl.DefaultApiDataSource import co.touchlab.droidcon.domain.service.impl.DefaultConferenceConfigProvider import co.touchlab.droidcon.domain.service.impl.DefaultDateTimeService @@ -46,6 +48,7 @@ import co.touchlab.droidcon.domain.service.impl.json.AboutJsonResourceDataSource import co.touchlab.droidcon.domain.service.impl.json.JsonResourceReader import co.touchlab.droidcon.util.formatter.DateFormatter import co.touchlab.droidcon.util.formatter.KotlinXDateFormatter +import co.touchlab.droidcon.util.initializeFirebase import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout import kotlin.time.Clock @@ -60,6 +63,8 @@ import org.koin.core.scope.Scope import org.koin.dsl.module fun initKoin(additionalModules: List): KoinApplication { + initializeFirebase() + val koinApplication = startKoin { modules( additionalModules + @@ -265,6 +270,10 @@ private val coreModule = module { clock = get(), ) } + + single { + DefaultAnalyticsService() + } } internal inline fun Scope.getWith(vararg params: Any?): T = get(parameters = { parametersOf(*params) }) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAnalyticsService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAnalyticsService.kt new file mode 100644 index 00000000..5f996b4d --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAnalyticsService.kt @@ -0,0 +1,26 @@ +package co.touchlab.droidcon.domain.service.impl + +import co.touchlab.droidcon.domain.service.AnalyticsService +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.analytics.analytics + +class DefaultAnalyticsService : AnalyticsService { + + override fun logEvent(name: String, params: Map) { + Firebase.analytics.logEvent(name, params.toAnalyticsParameters()) + } + + private fun Map.toAnalyticsParameters(): Map? { + if (isEmpty()) { + return null + } + + return mapValues { (_, value) -> + when (value) { + is String, is Boolean, is Int, is Long, is Double -> value + is Float -> value.toDouble() + else -> throw IllegalArgumentException("Unsupported analytics parameter type: ${value::class.simpleName}") + } + } + } +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.kt new file mode 100644 index 00000000..8d54bd2a --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.kt @@ -0,0 +1,3 @@ +package co.touchlab.droidcon.util + +expect fun initializeFirebase() diff --git a/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.ios.kt b/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.ios.kt new file mode 100644 index 00000000..715f7ef9 --- /dev/null +++ b/shared/src/iosMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.ios.kt @@ -0,0 +1,5 @@ +package co.touchlab.droidcon.util + +actual fun initializeFirebase() { + // Initialized via FirebaseApp.configure() in AppDelegate. +} diff --git a/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.js.kt new file mode 100644 index 00000000..79eb4b1f --- /dev/null +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.js.kt @@ -0,0 +1,19 @@ +package co.touchlab.droidcon.util + +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseOptions +import dev.gitlive.firebase.initialize + +actual fun initializeFirebase() { + Firebase.initialize( + options = FirebaseOptions( + applicationId = "1:1091975587304:web:droidcon", + apiKey = "AIzaSyDJfGdSS15YDDg7CZCaAISCVv7YhzimvVA", + projectId = "droidcon-148cc", + databaseUrl = "https://droidcon-148cc.firebaseio.com", + storageBucket = "droidcon-148cc.appspot.com", + gcmSenderId = "1091975587304", + authDomain = "droidcon-148cc.firebaseapp.com", + ), + ) +} diff --git a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt index 53b34b77..16c7ac62 100644 --- a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt +++ b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt @@ -3,7 +3,6 @@ package co.touchlab.droidcon.web import app.cash.sqldelight.db.SqlDriver import co.touchlab.droidcon.db.DroidconDatabase import co.touchlab.droidcon.domain.repository.impl.SqlDelightDriverFactory -import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.droidcon.domain.service.impl.ComposeResourceReader import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin @@ -30,8 +29,6 @@ fun webAppModule(driver: SqlDriver? = null): Module = module { } single { ComposeResourceReader() } - single { WebAnalyticsService() } - single { DefaultParseUrlViewService() } single { JsNotificationService() } diff --git a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/Main.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/Main.kt index 013eb2f3..f76c6bb3 100644 --- a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/Main.kt +++ b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/Main.kt @@ -2,6 +2,7 @@ package co.touchlab.droidcon.web import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport +import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.droidcon.ui.MainView import co.touchlab.droidcon.ui.util.TimezoneInit import co.touchlab.droidcon.viewmodel.WaitForLoadedContextModel @@ -16,7 +17,8 @@ import org.koin.compose.koinInject @OptIn(ExperimentalComposeUiApi::class) suspend fun main() { - startKoin() + val koinApplication = startKoin() + koinApplication.koin.get().logEvent(AnalyticsService.EVENT_STARTED) val lifecycleScope = CoroutineScope(SupervisorJob()) + Dispatchers.Main diff --git a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt deleted file mode 100644 index c49c9d99..00000000 --- a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/WebAnalyticsService.kt +++ /dev/null @@ -1,18 +0,0 @@ -package co.touchlab.droidcon.web - -import co.touchlab.droidcon.domain.service.AnalyticsService -import co.touchlab.kermit.Logger - -// Firebase Analytics does not support Web KMP, so we're using Kermit for now. -class WebAnalyticsService : AnalyticsService { - - private val log = Logger.withTag("WebAnalytics") - - override fun logEvent(name: String, params: Map) { - if (params.isEmpty()) { - log.i { "Event: $name" } - } else { - log.i { "Event: $name params=$params" } - } - } -} From 15b8297e9d7df39d41fa7cd4f3e3b64363e3fab8 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 29 May 2026 14:59:50 -0400 Subject: [PATCH 56/56] Update build.yml --- .github/workflows/build.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d238af3..58207e13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,10 +25,4 @@ jobs: uses: android-actions/setup-android@v3 - name: Check, assemble Android, and compile iOS and JS - run: > - ./gradlew ktlintCheck assembleDebug - compileKotlinIosSimulatorArm64 - compileKotlinJs - :shared:allTests - :web:jsTest - --no-daemon \ No newline at end of file + run: ./gradlew ktlintCheck assembleDebug compileKotlinIosX64 compileKotlinJs --no-daemon \ No newline at end of file