diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb803b22d..58207e138 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,5 +24,5 @@ jobs: - name: Setup Android SDK 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 + - name: Check, assemble Android, and compile iOS and JS + run: ./gradlew ktlintCheck assembleDebug compileKotlinIosX64 compileKotlinJs --no-daemon \ No newline at end of file diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 382d22d6c..f2d20ddbd 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 9c1cec2f4..b92e451b7 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt @@ -4,18 +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.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 import com.russhwolf.settings.ObservableSettings import com.russhwolf.settings.SharedPreferencesSettings @@ -47,15 +41,7 @@ class MainApp : } single { - ClasspathResourceReader() - } - - single { - NotificationLocalizedStringFactory(context = get()) - } - - single { - AndroidAnalyticsService(firebaseAnalytics = Firebase.analytics) + ComposeResourceReader() } } + 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 c46317741..000000000 --- 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/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 0824b44f6..21180d0c1 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/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 01df3b826..000000000 --- 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 3e3555f07..3fdc2cffb 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/build.gradle.kts b/build.gradle.kts index 377a8daf6..3d01dd44a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,7 +45,3 @@ subprojects { } } } - -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ae85ade8..4e6597cac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,22 @@ [versions] ## SDK Versions +adaptive = "1.3.0-alpha02" +kamelImageDefault = "1.0.9" +kotlinxBrowser = "0.5.0" +material3AdaptiveNavigationSuiteVersion = "1.9.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 -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" @@ -23,15 +27,15 @@ 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-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.9.5" +compose-androidx-ui = "1.10.0" compose-compiler = "1.5.15" composeNavigation = "2.9.6" compose-jb = "1.9.3" @@ -52,19 +56,26 @@ 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] +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" } +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" } 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" } @@ -81,17 +92,22 @@ 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" } 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" } 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" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } @@ -115,8 +131,10 @@ 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" } +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/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da47..aaaabb3cb 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/Droidcon/Droidcon.xcodeproj/project.pbxproj b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj index 159ad0ce6..861555e6f 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 */; }; @@ -25,14 +24,13 @@ /* 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 = ""; }; 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 6197878e3..000000000 --- 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 a35e2ee21..2f98496ba 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/Droidcon/Droidcon/en.lproj/Localizable.strings b/ios/Droidcon/Droidcon/en.lproj/Localizable.strings index 0b70f5a2d..2861ee20f 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 3b658b172..2c7e09dc4 100644 --- a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt +++ b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt @@ -1,16 +1,11 @@ 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.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 @@ -18,32 +13,20 @@ 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( +fun initKoinIos(userDefaults: NSUserDefaults): KoinApplication = initKoin( module { - single { BundleProvider(bundle = NSBundle.mainBundle) } single { NSUserDefaultsSettings(delegate = userDefaults) } - single { BundleResourceReader(bundle = get().bundle) } - - single { IOSDateFormatter() } - - single { - NotificationLocalizedStringFactory(bundle = get().bundle) + single { + ComposeResourceReader() } - 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 21ee3bf5e..000000000 --- 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/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 807c0676b..000000000 --- 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/kotlin-js-store/wasm/yarn.lock b/kotlin-js-store/wasm/yarn.lock new file mode 100644 index 000000000..5f4567d64 --- /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 new file mode 100644 index 000000000..93e991cbb --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,3780 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# 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" + 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== + +"@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" + 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== + +"@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" + 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== + +"@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" + 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" + 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.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== + +"@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/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" + 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@^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" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + 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" + 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== + +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" + 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== + +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" + 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== + +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" + 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.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.3.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.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" + 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== + +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.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== + 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" + +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" + 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, 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" + 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" + +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" + 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" + +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" + 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" + 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.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== + 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-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" + 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@^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" + 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@github:Kotlin/karma#6.4.5": + version "6.4.4" + resolved "https://codeload.github.com/Kotlin/karma/tar.gz/239a8fc984584f0d96b1dd750e7a5e2c79da93a6" + 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@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" + +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.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" + 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" + +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" + 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== + +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: + 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.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" + 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" + is-path-inside "^3.0.3" + 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== + +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" + 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== + +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" + 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== + +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" + 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" + +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" + 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== + +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" + 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== + +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" + 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@^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" + 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.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== + 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== + +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" + 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" + +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" + 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.3.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, 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== + +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.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" + 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" + +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" + 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.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" + "@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.3" + 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/settings.gradle.kts b/settings.gradle.kts index a06e880c3..8542b8496 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/shared-ui/build.gradle.kts b/shared-ui/build.gradle.kts index 74510b38a..e06192280 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() @@ -66,6 +71,10 @@ kotlin { iosX64() iosArm64() iosSimulatorArm64() + js(IR) { + browser() + binaries.executable() + } version = "1.0" @@ -74,13 +83,10 @@ kotlin { implementation(projects.shared) api(libs.kermit) - api(libs.kermit.crashlytics) api(libs.kotlinx.coroutines.core) 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) @@ -100,8 +106,47 @@ kotlin { implementation(libs.zoomimage.composeResources) implementation(libs.hyperdrive.multiplatformx.api) - // implementation(libs.hyperdrive.multiplatformx.compose) + 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()) + dependencies { + api(libs.kermit.crashlytics) + implementation(libs.coil.compose) + implementation(libs.coil.network) + implementation(compose.components.resources) + } + } + + androidMain { + dependsOn(mobileMain) + } + iosMain { + dependsOn(mobileMain) } + + val iosArm64Main by getting { + dependsOn(iosMain.get()) + } + + val iosSimulatorArm64Main by getting { + dependsOn(iosMain.get()) + } + + val iosX64Main by getting { + dependsOn(iosMain.get()) + } + jsMain.dependencies { + implementation(kotlin("stdlib-js")) + implementation(libs.kotlinx.browser) + implementation(libs.zoomimage.compose) + implementation(libs.kamel.image.default) // add this + } + all { languageSettings.apply { optIn("kotlin.RequiresOptIn") 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 6f7813f4d..000000000 --- a/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.jvm.kt +++ /dev/null @@ -1,55 +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 - -// 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)) - } - } -} 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/shared-ui/src/commonMain/composeResources/drawable/event_note.xml b/shared-ui/src/commonMain/composeResources/drawable/event_note.xml new file mode 100644 index 000000000..208b9ddce --- /dev/null +++ b/shared-ui/src/commonMain/composeResources/drawable/event_note.xml @@ -0,0 +1,9 @@ + + + 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/BottomNavigationView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt index 4f1176615..8d27cc9b0 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,7 +1,5 @@ package co.touchlab.droidcon.ui -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 @@ -10,65 +8,91 @@ 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.NavigationBarItemDefaults -import androidx.compose.material3.Scaffold +import androidx.compose.material3.NavigationBarItemColors +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.NavigationRailItemColors import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier 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 +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable 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 + 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, + ) - 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.", @@ -78,8 +102,8 @@ internal fun BottomNavigationView(viewModel: ApplicationViewModel, currentConfer ApplicationViewModel.Tab.Sponsors -> SponsorsView(viewModel.sponsors) ApplicationViewModel.Tab.Settings -> SettingsView(viewModel.settings) } - } - } + }, + ) val feedback by viewModel.observePresentedFeedback.observeAsState() feedback?.let { 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 000000000..0694d7a85 --- /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 d8dae372f..ddd6f1f91 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() @@ -42,28 +28,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) { 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 734612eda..8a5a72f27 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,30 @@ 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/ui/session/SessionBlockView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionBlockView.kt index df0dcd47c..f4d0d39f0 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 ca7be9c63..4a37cf922 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,200 @@ 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.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, - ) + description?.let { + DescriptionView(it, descriptionLinks) + } - HorizontalDivider() + if (speakers.isNotEmpty()) { + Text( + text = "Speakers", + modifier = Modifier.fillMaxWidth().padding(Dimensions.Padding.default), + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + ) - speakers.forEach { speaker -> - SpeakerView(speaker) - } + 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)?, + attendingTapped: (Boolean) -> 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 = { + attendingTapped(!viewModel.isAttending) + viewModel.attendingTapped() + }, + writeFeedbackTapped = viewModel::writeFeedbackTapped, + onScrollStateChanged = { viewModel.scrollState = it }, + onBack = onBack, + ) + } +} + @Composable private fun HeaderView(title: String, locationInfo: String) { Column( @@ -274,7 +336,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/SessionListDetailPaneScaffold.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt new file mode 100644 index 000000000..5bc52b682 --- /dev/null +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SessionListDetailPaneScaffold.kt @@ -0,0 +1,118 @@ +package co.touchlab.droidcon.ui.session + +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 +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 +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.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 +fun SessionListDetailPaneScaffold(viewModel: BaseSessionListViewModel, title: String, emptyText: String, modifier: Modifier = Modifier) { + val scope = rememberCoroutineScope() + val navigator = rememberListDetailPaneScaffoldNavigator() + val presentedSessionDetail by viewModel.observePresentedSessionDetail.observeAsState() + val listPaneAdapted = navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] + val fullWidth = listPaneAdapted == PaneAdaptedValue.Hidden + 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, + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + AnimatedPane( + enterTransition = listEnterTransition, + exitTransition = listExitTransition, + ) { + SessionListView( + viewModel = viewModel, + title = title, + emptyText = emptyText, + ) { + scope.launch { + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail) + } + } + } + }, + detailPane = { + AnimatedPane( + enterTransition = detailEnterTransition, + exitTransition = detailExitTransition, + ) { + val detailViewModel = presentedSessionDetail + if (detailViewModel != null) { + SessionDetailView( + viewModel = detailViewModel, + showBackButton = fullWidth, + onBack = { + scope.launch { + navigator.navigateBack() + if (!fullWidth) { + delay(1000) + } + viewModel.presentedSessionDetail = null + } + }, + attendingTapped = { attending -> + if (!attending) { + scope.launch { + viewModel.presentedSessionDetail = null + navigator.navigateBack() + if (!fullWidth) { + delay(1000) + } + } + } + }, + ) + } else { + 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.25f), + 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 1007e1f33..2fb484ed9 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,17 +24,18 @@ 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 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 @@ -50,9 +51,9 @@ 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 import co.touchlab.droidcon.viewmodel.session.BaseSessionListViewModel import co.touchlab.droidcon.viewmodel.session.SessionDayViewModel import co.touchlab.kermit.Logger @@ -60,37 +61,32 @@ 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?.isEmpty() != false) { - EmptyView(emptyText) - } else { - val selectedDay by viewModel.observeSelectedDay.observeAsState() - val selectedTabIndex = viewModel.days?.indexOf(selectedDay) ?: 0 + ) { 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() + key(days) { + val selectedTabIndex = (viewModel.days?.indexOf(selectedDay) ?: 0).coerceAtLeast(0) val coroutineScope = rememberCoroutineScope() val pagerState = rememberPagerState( @@ -100,20 +96,13 @@ internal fun SessionListView(viewModel: BaseSessionListViewModel, title: String, }, ) - TabRow( + PrimaryTabRow( 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() - } + indicator = { + SessionDayTabIndicator( + selectedTabIndex = pagerState.currentPage, + tabCount = days?.size ?: 0, + ) }, ) { days?.forEachIndexed { index, daySchedule -> @@ -173,7 +162,7 @@ internal fun SessionListView(viewModel: BaseSessionListViewModel, title: String, horizontal = Dimensions.Padding.half, ), ) { - SessionBlockView(hourBlock) + SessionBlockView(hourBlock, onClick = onClick) } } } @@ -185,15 +174,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/commonMain/kotlin/co.touchlab.droidcon/ui/session/SpeakerDetailView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/session/SpeakerDetailView.kt index 8f5589efc..4cab2a3bf 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/settings/AboutView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/AboutView.kt index 42c403df1..8966909c4 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 @@ -4,15 +4,20 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row 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 +58,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/sponsors/SponsorDetailView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/sponsors/SponsorDetailView.kt index 416f58f5d..48ab2ae1c 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 e1755a5d6..2bf76e5e8 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 @@ -5,8 +5,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow 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 +14,9 @@ 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.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons @@ -27,6 +29,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,13 +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.window.core.layout.WindowSizeClass 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" @@ -113,32 +116,43 @@ 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 if (imageUrl != null) { DcAsyncImage( logTag = LOG_TAG, - model = imageUrl, + url = imageUrl, contentDescription = sponsor.name, ) } else { @@ -152,9 +166,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/ui/util/Image.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/util/Image.kt index 197c04ecf..f762002f4 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,34 +2,9 @@ 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) - .apply { - if (debug) { - logger(DebugLogger()) - } - } - .build() +@Composable +expect fun InitImageLoader() 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 b5680f415..43de68f36 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,12 +1,68 @@ 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 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 org.jetbrains.compose.resources.painterResource @Composable -internal expect fun __LocalImage(imageResourceName: String, modifier: Modifier, contentDescription: String?) - -@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/ui/venue/VenueView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt index 4df998c67..86d17ebf7 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/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/util/NavigationController.kt index 4068c74b6..eaa6325ae 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,6 +1,8 @@ package co.touchlab.droidcon.util import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally @@ -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 })) 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 9ea81320b..dc9fc889f 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 @@ -20,11 +16,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 +30,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 4254cc1d6..f191fa7e3 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 000000000..2b7746d2d --- /dev/null +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/ViewModelFactory.kt @@ -0,0 +1,230 @@ +package co.touchlab.droidcon.viewmodel + +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 +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.util.formatter.DateFormatter +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 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) { + 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/WaitForLoadedContextModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/WaitForLoadedContextModel.kt index e54a7ee68..181712838 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,7 +6,6 @@ import co.touchlab.droidcon.domain.service.ConferenceConfigProvider import co.touchlab.droidcon.domain.service.SyncService 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 @@ -15,7 +14,7 @@ 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() { @@ -36,17 +35,18 @@ class WaitForLoadedContextModel( suspend fun watchConferenceChanges() { lifecycle.whileAttached { - withContext(Dispatchers.IO) { - try { - syncService.syncConferences() - } catch (e: Exception) { - log.e(e) { "Failed to sync conferences" } - } - } - launch { conferenceConfigProvider.observeChanges().collect { conference -> - _state.emit(State.Ready(conference)) + if (conference != null) { + _state.emit(State.Ready(conference)) + withContext(Dispatchers.Default) { + try { + syncService.syncConferences() + } catch (e: Exception) { + log.e(e) { "Failed to sync conferences" } + } + } + } } } } 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 90350aba8..fa13dda05 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 @@ -3,11 +3,12 @@ package co.touchlab.droidcon.viewmodel.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 AgendaViewModel( sessionGateway: SessionGateway, - sessionDayFactory: SessionDayViewModel.Factory, - sessionDetailFactory: SessionDetailViewModel.Factory, + sessionDayFactory: ViewModelFactory.SessionDayViewModelFactory, + sessionDetailFactory: ViewModelFactory.SessionDetailViewModelFactory, sessionDetailScrollStateStorage: SessionDetailScrollStateStorage, dateTimeService: DateTimeService, conferenceConfigProvider: ConferenceConfigProvider, @@ -19,23 +20,4 @@ class AgendaViewModel( dateTimeService, 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 415110f3c..8bd6bbc3a 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 @@ -4,12 +4,14 @@ 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 kotlinx.datetime.TimeZone 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, @@ -43,7 +45,8 @@ abstract class BaseSessionListViewModel( .groupBy { it.session.startsAt.toConferenceDateTime( dateTimeService, - conferenceConfigProvider.getConferenceTimeZone(), + // 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-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 8cb96e642..0aa8a67db 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 @@ -4,11 +4,12 @@ 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, - 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 21ba307a9..22aa08cd7 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 @@ -2,11 +2,12 @@ package co.touchlab.droidcon.viewmodel.session 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 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 7a2c3210d..48d5bd56e 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 @@ -6,11 +6,13 @@ 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 kotlinx.datetime.TimeZone import org.brightify.hyperdrive.multiplatformx.BaseViewModel class SessionDayViewModel( - sessionBlockFactory: SessionBlockViewModel.Factory, + sessionBlockFactory: ViewModelFactory.SessionBlockViewModelFactory, dateFormatter: DateFormatter, dateTimeService: DateTimeService, private val conferenceConfigProvider: ConferenceConfigProvider, @@ -27,7 +29,7 @@ class SessionDayViewModel( .groupBy { it.session.startsAt.toConferenceDateTime( dateTimeService, - conferenceTimeZone = conferenceConfigProvider.getConferenceTimeZone(), + conferenceTimeZone = conferenceConfigProvider.getConferenceTimeZone() ?: TimeZone.UTC, ).startOfMinute } .map { (startsAt, items) -> @@ -43,26 +45,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 758a830e0..c0f59ac5b 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,13 @@ 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 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 @@ -26,9 +28,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, @@ -57,9 +59,10 @@ class SessionDetailViewModel( listOfNotNull( it.room?.name, with(dateTimeService) { + val timeZone = conferenceConfigProvider.getConferenceTimeZone() ?: 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() @@ -151,34 +154,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 dd0749753..971f3d4db 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 e571ddb40..484e70090 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 3c5552294..7371dafcf 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 ffc991025..2f93b8b35 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 2a4d1c59a..2932164ca 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 @@ -3,6 +3,7 @@ package co.touchlab.droidcon.viewmodel.settings 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 @@ -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 ac547f4fa..9a8d8930c 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 @@ -2,14 +2,15 @@ package co.touchlab.droidcon.viewmodel.sponsor 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 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 f5514181d..81588728c 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 2131db070..cb8b627aa 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 @@ -2,10 +2,11 @@ package co.touchlab.droidcon.viewmodel.sponsor 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( - 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 a64996bd7..deaf637d3 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 @@ -2,14 +2,16 @@ package co.touchlab.droidcon.viewmodel.sponsor 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 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(), sponsorGateway.observeSponsors() @@ -37,13 +39,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) - } } 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 75e2aad51..000000000 --- a/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/util/LocalImage.kt +++ /dev/null @@ -1,51 +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 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)) - } - } -} 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 000000000..e6679a5d4 --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/MainView.kt @@ -0,0 +1,10 @@ +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/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 000000000..f061b7570 --- /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 000000000..edb0bc5d1 --- /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 000000000..6bcdc97af --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Dialog.js.kt @@ -0,0 +1,31 @@ +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()) { + 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/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 000000000..6847cb673 --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/Image.js.kt @@ -0,0 +1,32 @@ +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) { + 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/util/NavigationBackPressWrapper.js.kt b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/NavigationBackPressWrapper.js.kt new file mode 100644 index 000000000..27fc3427c --- /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)) { + // No back-press handling needed on web. + content() +} 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 000000000..6ae49cd65 --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/util/TimeZoneInit.kt @@ -0,0 +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 new file mode 100644 index 000000000..448d7fc1b --- /dev/null +++ b/shared-ui/src/jsMain/kotlin/co.touchlab.droidcon/ui/venue/VenueView.kt @@ -0,0 +1,43 @@ +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.") + print(resource.exception.message) + } + } + } 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 new file mode 100644 index 000000000..e39fe88da --- /dev/null +++ b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/util/Image.mobile.kt @@ -0,0 +1,47 @@ +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) { + AsyncImage( + modifier = modifier, + model = url, + contentDescription = contentDescription, + onError = { + Logger.e( + logTag, + throwable = it.result.throwable, + message = { + "AsyncImage OnError Request = ${it.result.request}\n" + }, + ) + }, + ) +} + +@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/VenueBodyView.kt b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueBodyView.kt new file mode 100644 index 000000000..81919a9b1 --- /dev/null +++ b/shared-ui/src/mobileMain/kotlin/co/touchlab/droidcon/ui/venue/VenueBodyView.kt @@ -0,0 +1,43 @@ +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(), + ) + } + } + } +} diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 4b22ada92..ad781fb3f 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 { @@ -34,11 +37,7 @@ android { ) main.manifest.srcFile("src/androidMain/AndroidManifest.xml") } -} - -version = "1.0" -android { configurations { create("androidTestApi") create("androidTestDebugApi") @@ -49,6 +48,8 @@ android { } } +version = "1.0" + kotlin { compilerOptions { // common compiler options applied to all Kotlin source sets @@ -61,13 +62,19 @@ kotlin { iosX64() iosArm64() iosSimulatorArm64() + 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.kermit.crashlytics) api(libs.kotlinx.coroutines.core) api(libs.kotlinx.datetime) api(libs.multiplatformSettings.core) @@ -75,21 +82,78 @@ kotlin { implementation(libs.bundles.ktor.common) implementation(libs.bundles.sqldelight.common) + implementation(libs.sqldelight.async.extensions) implementation(libs.stately.common) implementation(libs.koin.core) implementation(libs.korio) + implementation(libs.gitlive.firebase.analytics) } - 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) + } + } + + // 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.js) + implementation(libs.sqldelight.driver.js) + 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 { + 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 { @@ -107,7 +171,10 @@ kotlin { } sqldelight { - databases.create("DroidconDatabase") { - packageName.set("co.touchlab.droidcon.db") + databases { + create("DroidconDatabase") { + packageName.set("co.touchlab.droidcon.db") + generateAsync = true + } } } diff --git a/shared/karma.config.d/sqljs-config.js b/shared/karma.config.d/sqljs-config.js new file mode 100644 index 000000000..dad5c35c3 --- /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/androidMain/kotlin/co/touchlab/droidcon/Koin.android.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/Koin.android.kt index 34fb8216f..10ffab02e 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/domain/repository/impl/SqlDelightDriverFactory.android.kt b/shared/src/androidMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.android.kt index 143c45746..2dcd18e0b 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,16 @@ package co.touchlab.droidcon.domain.repository.impl import android.content.Context +import app.cash.sqldelight.async.coroutines.synchronous import app.cash.sqldelight.db.SqlDriver 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 fun createDriver(): SqlDriver = AndroidSqliteDriver( + schema = DroidconDatabase.Schema.synchronous(), + context = context, + name = "droidcon.db", + ) } 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 cd4ed6012..000000000 --- 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 44bc8c963..000000000 --- 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/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 000000000..9e7ba1a38 --- /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/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 7b8ec5c1a..000000000 --- a/shared/src/androidMain/kotlin/co/touchlab/droidcon/util/formatter/AndroidDateFormatter.kt +++ /dev/null @@ -1,39 +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 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) - } - - private val minuteHourTimeFormat - get() = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()) - .apply { timeZone = java.util.TimeZone.getTimeZone(conferenceTimeZone.id) } - - override fun monthWithDay(date: LocalDate): String = shortDateFormat.format( - Date(with(dateTimeService) { date.atTime(0, 0).fromConferenceDateTime(conferenceTimeZone) }.toEpochMilliseconds()), - ).uppercase() - - override fun timeOnly(dateTime: LocalDateTime): String? = minuteHourTimeFormat.format( - Date(with(dateTimeService) { dateTime.fromConferenceDateTime(conferenceTimeZone) }.toEpochMilliseconds()), - ) - - override fun timeOnlyInterval(fromDateTime: LocalDateTime, toDateTime: LocalDateTime): String = - timeOnly(fromDateTime) + " - " + timeOnly(toDateTime) -} 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 000000000..f1df05834 --- /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/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/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 000000000..2a3f079a7 --- /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/DatabaseFactory.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/DatabaseFactory.kt new file mode 100644 index 000000000..833edaec0 --- /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 72bd2f04f..faa7ccab8 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -8,11 +8,9 @@ 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.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 +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.repository.impl.adapter.InstantSqlDelightAdapter +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 @@ -37,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 @@ -47,10 +46,12 @@ import co.touchlab.droidcon.domain.service.impl.DefaultSyncService import co.touchlab.droidcon.domain.service.impl.DefaultUserIdProvider 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 -import kotlinx.coroutines.runBlocking import kotlinx.datetime.TimeZone import kotlinx.serialization.json.Json import org.koin.core.KoinApplication @@ -62,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 + @@ -93,21 +96,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 } @@ -134,13 +123,8 @@ private val coreModule = module { // Add ConferenceConfigProvider single { - val conferenceRepository: ConferenceRepository = get() - val selectedConference = runBlocking { - conferenceRepository.getSelected() - } DefaultConferenceConfigProvider( conferenceRepository = get(), - initialConference = selectedConference, ) } @@ -150,6 +134,10 @@ private val coreModule = module { ) } + single { + KotlinXDateFormatter() + } + single { co.touchlab.droidcon.util.AppChecker( conferenceConfigProvider = get(), @@ -195,6 +183,7 @@ private val coreModule = module { conferenceRepository = get(), ) } + single(qualifier(DefaultSyncService.DataSource.Kind.Api)) { DefaultApiDataSource( client = get(), @@ -245,6 +234,9 @@ private val coreModule = module { aboutJsonResourceDataSource = get(), ) } + single { + ComposeNotificationLocalizedStringFactory() + } single { DefaultNotificationSchedulingService( sessionRepository = get(), @@ -278,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/application/service/NotificationSchedulingService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/NotificationSchedulingService.kt index 15860c12c..f3b4681ae 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 000000000..a4ffe86d3 --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/application/service/impl/ComposeNotificationLocalizedStringFactory.kt @@ -0,0 +1,24 @@ +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 a0bf096d7..d0d97fa3b 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,18 +41,23 @@ 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 }) } 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 +65,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 +96,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) @@ -99,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/gateway/impl/DefaultSessionGateway.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/gateway/impl/DefaultSessionGateway.kt index d8a4004be..104b07edc 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,8 +8,13 @@ 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 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 class DefaultSessionGateway( @@ -20,43 +25,68 @@ class DefaultSessionGateway( private val conferenceConfigProvider: ConferenceConfigProvider, ) : SessionGateway { - private val conferenceId get() = conferenceConfigProvider.getConferenceId() + private val log = Logger.withTag("DefaultSessionGateway") override fun observeSchedule(): Flow> = conferenceConfigProvider.observeChanges().flatMapLatest { conf -> - sessionRepository.observeAll(conf.id) + 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) } } 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 = 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() - private suspend fun scheduleItemForSession(session: Session): ScheduleItem = ScheduleItem( - session, - scheduleService.isInConflict(session), - session.room?.let { roomRepository.find(it, conferenceId) }, - profileRepository.getSpeakersBySession(session.id, conferenceId), - ) + 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 conferenceId = conferenceConfigProvider.getConferenceId() sessionRepository.setRsvp(session.id, Session.RSVP(attending, false), conferenceId) } override suspend fun setFeedback(session: Session, feedback: Session.Feedback) { + val conferenceId = conferenceConfigProvider.getConferenceId() sessionRepository.setFeedback(session.id, feedback, conferenceId) } - override suspend fun getScheduleItem(id: Session.Id): ScheduleItem? = - sessionRepository.find(id, conferenceId)?.let { scheduleItemForSession(it) } + override suspend fun getScheduleItem(id: Session.Id): ScheduleItem? { + 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 3b8b844e4..0dcafedfa 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,11 @@ 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.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map class DefaultSponsorGateway( @@ -18,19 +22,36 @@ class DefaultSponsorGateway( private val conferenceConfigProvider: ConferenceConfigProvider, ) : SponsorGateway { - override fun observeSponsors(): Flow> = - sponsorGroupRepository.observeAll(conferenceConfigProvider.getConferenceId()).map { groups -> - groups.map { group -> - SponsorGroupWithSponsors( - group, - sponsorRepository.allByGroupName(group.name, conferenceConfigProvider.getConferenceId()), - ) + 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), + ) + } + } } - } + } - override fun observeSponsorById(id: Sponsor.Id): Flow = - sponsorRepository.observe(id, conferenceConfigProvider.getConferenceId()) + 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 = - profileRepository.getSponsorRepresentatives(sponsorId, conferenceConfigProvider.getConferenceId()) + override suspend fun getRepresentatives(sponsorId: Sponsor.Id): List { + val conferenceId = conferenceConfigProvider.getConferenceId() + return profileRepository.getSponsorRepresentatives(sponsorId, conferenceId) + } } 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 61cd3d02a..4f1a0d224 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,11 +8,11 @@ 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 - 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 ecd978fdc..a3d4ad082 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 + 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 10d427416..8e7244a23 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 23b19b4a0..b4d471bc1 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 79b09b03e..b1a06a841 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 3f647de86..f5342265b 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/BaseRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/BaseRepository.kt index 08baf2af0..1bac0c253 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/SqlDelightConferenceRepository.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightConferenceRepository.kt index de9913bcc..5571da63f 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,7 @@ 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 import app.cash.sqldelight.coroutines.mapToOne @@ -21,11 +23,11 @@ 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 { - conferenceQueries.changeSelectedConference(conferenceId) + conferenceQueries.changeSelectedConference(conferenceId).await() return true } catch (e: Exception) { log.e(e) { "Error selecting conference" } @@ -33,7 +35,7 @@ class SqlDelightConferenceRepository( } } - override suspend fun add(conference: Conference): Long { + override suspend fun add(conference: Conference): Long = conferenceQueries.transactionWithResult { conferenceQueries.insert( conferenceName = conference.name, conferenceTimeZone = conference.timeZone, @@ -45,8 +47,7 @@ class SqlDelightConferenceRepository( active = conference.active, venueMap = conference.venueMap, ) - // Return the last inserted ID - return conferenceQueries.lastInsertRowId().executeAsOne() + 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 e0caa96f5..216dba47c 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,8 @@ 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 import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOne @@ -23,9 +26,9 @@ 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 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 +40,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, @@ -60,10 +63,10 @@ class SqlDelightProfileRepository( sponsorGroupName = sponsorId.group, conferenceId = conferenceId, mapper = ::profileFactory, - ).executeAsList() + ).awaitAsList() - 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) @@ -74,7 +77,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,12 +91,12 @@ class SqlDelightProfileRepository( ) } - override fun doDelete(id: Profile.Id, conferenceId: Long) { - profileQueries.delete(id.value, conferenceId) + override suspend fun doDelete(id: Profile.Id, conferenceId: Long) { + profileQueries.delete(id.value, conferenceId).await() } - 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 b1657dd50..5a411f801 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,8 @@ 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 import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOne @@ -14,7 +17,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) @@ -25,7 +28,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,11 +36,12 @@ class SqlDelightRoomRepository(private val roomQueries: RoomQueries) : ) } - override fun doDelete(id: Room.Id, conferenceId: Long) { - roomQueries.deleteById(id.value, conferenceId) + override suspend fun doDelete(id: Room.Id, conferenceId: Long) { + roomQueries.deleteById(id.value, conferenceId).await() } - 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 2c0c9041d..09df56a3c 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,9 @@ 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 import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOne @@ -9,19 +13,27 @@ 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.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,15 +59,16 @@ 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) - override fun doUpsert(entity: Session, conferenceId: Long) { + override suspend fun doUpsert(entity: Session, conferenceId: Long) { sessionQueries.upsert( id = entity.id.value, conferenceId = conferenceId, @@ -73,12 +86,12 @@ class SqlDelightSessionRepository(private val dateTimeService: DateTimeService, ) } - override fun doDelete(id: Session.Id, conferenceId: Long) { - sessionQueries.deleteById(id.value, conferenceId) + override suspend fun doDelete(id: Session.Id, conferenceId: Long) { + sessionQueries.deleteById(id.value, conferenceId).await() } - 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 a037acae1..522077345 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,10 +31,10 @@ 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 fun doUpsert(entity: SponsorGroup, conferenceId: Long) { + override suspend fun doUpsert(entity: SponsorGroup, conferenceId: Long) { sponsorGroupQueries.upsert( name = entity.id.value, conferenceId = conferenceId, @@ -41,7 +43,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 eb28e5982..9a1062a24 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,15 +28,15 @@ 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 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 +48,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/ConferenceConfigProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/ConferenceConfigProvider.kt index 39d443536..7117087e8 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,14 +5,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.datetime.TimeZone interface ConferenceConfigProvider { - fun getConferenceId(): Long - fun getConferenceTimeZone(): TimeZone + suspend fun getConferenceId(): Long + fun getConferenceTimeZone(): TimeZone? fun getProjectId(): String - fun getCollectionName(): String - fun getApiKey(): String - fun getScheduleId(): String - fun showVenueMap(): Boolean - fun observeChanges(): Flow + suspend fun getCollectionName(): String + suspend fun getApiKey(): String + suspend fun getScheduleId(): String + fun observeChanges(): Flow /** * Get the currently selected conference 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 000000000..5f996b4d7 --- /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/domain/service/impl/DefaultApiDataSource.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultApiDataSource.kt index bc1de1d57..78a71535f 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,29 +21,36 @@ class DefaultApiDataSource( private val json: Json, private val conferenceConfigProvider: ConferenceConfigProvider, ) : DefaultSyncService.DataSource { - override suspend fun getSpeakers(): List { + + private val log = Logger.withTag("DefaultApiDataSource") + + override suspend fun getSpeakers(): List? { + val scheduleId = conferenceConfigProvider.getScheduleId() 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() 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() 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 { + override suspend fun getSponsors(): SponsorsDto.SponsorCollectionDto? { + log.i { "gettingSponsors" } val projectId = conferenceConfigProvider.getProjectId() val collectionName = conferenceConfigProvider.getCollectionName() val apiKey = conferenceConfigProvider.getApiKey() @@ -54,7 +62,7 @@ class DefaultApiDataSource( return json.decodeFromString(SponsorsDto.SponsorCollectionDto.serializer(), jsonString) } - suspend fun getConferences(): ConferencesDto.ConferenceCollectionDto { + suspend fun getConferences(): ConferencesDto.ConferenceCollectionDto? { val projectId = conferenceConfigProvider.getProjectId() val apiKey = conferenceConfigProvider.getApiKey() val databaseName = "(default)" 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 52e988848..62e3ac029 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,30 +7,33 @@ 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.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.datetime.TimeZone -class DefaultConferenceConfigProvider(private val conferenceRepository: ConferenceRepository, initialConference: Conference) : +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 + val currentConferenceState: StateFlow = _currentConferenceState - private val currentConference: Conference + private val currentConference: Conference? get() = currentConferenceState.value - override fun getConferenceId(): Long = currentConference.id + override suspend fun getConferenceId(): Long = getConference().id - override fun getConferenceTimeZone(): TimeZone = currentConference.timeZone + override fun getConferenceTimeZone(): TimeZone? = currentConference?.timeZone override fun getProjectId(): String = "droidcon-148cc" - override fun getCollectionName(): String = currentConference.collectionName + override suspend fun getCollectionName(): String = getConference().collectionName - override fun getApiKey(): String = currentConference.apiKey + override suspend fun getApiKey(): String = getConference().apiKey - override fun getScheduleId(): String = currentConference.scheduleId - - override fun showVenueMap(): Boolean = true // Default to true, will be configurable per conference later + override suspend fun getScheduleId(): String = getConference().scheduleId override fun observeChanges(): Flow = conferenceRepository.observeSelected() @@ -40,8 +43,25 @@ class DefaultConferenceConfigProvider(private val conferenceRepository: Conferen // 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) + log.i { "DefaultConferenceConfigProvider: loadSelectedConference" } + conferenceRepository.observeSelected() + .map { it } + .catch { e -> + log.w(e) { "Error observing selected conference, emitting null" } + emit(null) + } + .collect { conference -> + log.i { "loadSelectedConference: Emitting Conference! $conference" } + _currentConferenceState.value = conference + } + } + + private suspend fun getConference(): Conference { + currentConference?.let { return it } + return conferenceMutex.withLock { + currentConference ?: conferenceRepository.getSelected().also { 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 620b5e6b1..b96b27be5 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,11 @@ 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/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultScheduleService.kt index 56f55f440..557c860a1 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/DefaultServerApi.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultServerApi.kt index 116d4712c..0f4661bb2 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/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultSyncService.kt index af4ff2799..d8f901dde 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() @@ -314,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 @@ -331,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 -> @@ -436,7 +444,7 @@ class DefaultSyncService( } } - private fun updateSponsorsFromDataSource( + private suspend fun updateSponsorsFromDataSource( sponsorSessionsGroups: List, sponsors: SponsorsDto.SponsorCollectionDto, conference: Conference, @@ -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/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/ResourceReader.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/ResourceReader.kt index aa295d153..24eef3134 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 140e090fa..48e6557b7 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 dab005b62..f79e056cd 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/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 000000000..8d54bd2ab --- /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/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 000000000..2b16db1d3 --- /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/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 000000000..3b7ec81f8 --- /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 000000000..ade47370a --- /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 000000000..d471b205b --- /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 000000000..0664a5eba --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/domain/repository/SessionRepositoryTest.kt @@ -0,0 +1,152 @@ +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 000000000..7500cc058 --- /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 000000000..38cfce415 --- /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 000000000..a0f65d152 --- /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 000000000..0dd29e10e --- /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 000000000..fed9131d6 --- /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 000000000..1d0544d52 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestDatabase.kt @@ -0,0 +1,51 @@ +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 000000000..03dcaba32 --- /dev/null +++ b/shared/src/commonTest/kotlin/co/touchlab/droidcon/test/TestEntityFactory.kt @@ -0,0 +1,78 @@ +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 000000000..a78ef1fd9 --- /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 000000000..11ea449e2 --- /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/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 000000000..6b9c0e36f --- /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/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 b20c448c5..96a700bae 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 2b94136a8..24f68c960 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/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 000000000..715f7ef9a --- /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/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 000000000..9445fdf1c --- /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/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt new file mode 100644 index 000000000..62710e318 --- /dev/null +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/Koin.js.kt @@ -0,0 +1,24 @@ +package co.touchlab.droidcon + +import co.touchlab.droidcon.application.service.NotificationService +import co.touchlab.droidcon.service.JsNotificationService +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.js.Js +import org.koin.core.module.Module +import org.koin.dsl.module + +actual val platformModule: Module = module { + single { + Js.create() + } + + single { + get() + } + + 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 000000000..33cf2f825 --- /dev/null +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/domain/repository/impl/SqlDelightDriverFactory.js.kt @@ -0,0 +1,17 @@ +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( + Worker( + js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)"""), + ), + ) +} 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 000000000..6ad8f0cff --- /dev/null +++ b/shared/src/jsMain/kotlin/co/touchlab/droidcon/service/JsNotificationService.kt @@ -0,0 +1,19 @@ +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/FirebaseInit.js.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/FirebaseInit.js.kt new file mode 100644 index 000000000..79eb4b1f2 --- /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/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 000000000..dd90f177a --- /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/TimeZoneInit.kt b/shared/src/jsMain/kotlin/co/touchlab/droidcon/util/TimeZoneInit.kt new file mode 100644 index 000000000..e6317e017 --- /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 new file mode 100644 index 000000000..77d7f5ef1 --- /dev/null +++ b/shared/src/jsTest/kotlin/co/touchlab/droidcon/test/TestSqlDriver.js.kt @@ -0,0 +1,20 @@ +package co.touchlab.droidcon.test + +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( + js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)"""), + ), + ) + DroidconDatabase.Schema.create(driver).await() + return driver +} diff --git a/shared/webpack.config.d/sqljs-config.js b/shared/webpack.config.d/sqljs-config.js new file mode 100644 index 000000000..300ef2b07 --- /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/build.gradle.kts b/web/build.gradle.kts new file mode 100644 index 000000000..52beb6b8c --- /dev/null +++ b/web/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.composeCompiler) +} + +kotlin { + js(IR) { + browser() + binaries.executable() + } + + /* + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + }*/ + + sourceSets { + 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) + implementation(libs.hyperdrive.multiplatformx.api) + } + } + val jsTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(libs.koin.test) + } + } + } +} diff --git a/web/src/jsMain/composeResources/drawable/compose-multiplatform.xml b/web/src/jsMain/composeResources/drawable/compose-multiplatform.xml new file mode 100644 index 000000000..1ffc948c2 --- /dev/null +++ b/web/src/jsMain/composeResources/drawable/compose-multiplatform.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 000000000..16c7ac628 --- /dev/null +++ b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/DependencyInjection.kt @@ -0,0 +1,53 @@ +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.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 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 { 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/jsMain/kotlin/co/touchlab/droidcon/web/Main.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/Main.kt new file mode 100644 index 000000000..f76c6bb3b --- /dev/null +++ b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/Main.kt @@ -0,0 +1,46 @@ +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 +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) +suspend fun main() { + val koinApplication = startKoin() + koinApplication.koin.get().logEvent(AnalyticsService.EVENT_STARTED) + + val lifecycleScope = CoroutineScope(SupervisorJob()) + Dispatchers.Main + + @Suppress("UNUSED_VARIABLE") + val tz = TimezoneInit + + val root = LifecycleGraph.Root("…") + + lifecycleScope.launch { + val cancelAttach = root.attach(lifecycleScope) + try { + awaitCancellation() + } finally { + cancelAttach.cancel() + } + } + + ComposeViewport { + val viewModel: WaitForLoadedContextModel = koinInject() + viewModel.lifecycle.removeFromParent() + root.addChild(viewModel.lifecycle) + + MainView(viewModel) + } +} diff --git a/web/src/jsMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt b/web/src/jsMain/kotlin/co/touchlab/droidcon/web/service/DefaultParseUrlViewService.kt new file mode 100644 index 000000000..fcde6fb5d --- /dev/null +++ b/web/src/jsMain/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/jsMain/resources/favicon.png b/web/src/jsMain/resources/favicon.png new file mode 100644 index 000000000..d3ed8bae1 Binary files /dev/null and b/web/src/jsMain/resources/favicon.png differ diff --git a/web/src/jsMain/resources/index.html b/web/src/jsMain/resources/index.html new file mode 100644 index 000000000..8259f7652 --- /dev/null +++ b/web/src/jsMain/resources/index.html @@ -0,0 +1,21 @@ + + + + + + Droidcon + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/jsMain/resources/styles.css b/web/src/jsMain/resources/styles.css new file mode 100644 index 000000000..0549b10f8 --- /dev/null +++ b/web/src/jsMain/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 diff --git a/web/webpack.config.d/sqljs-config.js b/web/webpack.config.d/sqljs-config.js new file mode 100644 index 000000000..300ef2b07 --- /dev/null +++ b/web/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: '.' }, + ], + }), +);