diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..b2068ac --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "idea": { + "url": "http://127.0.0.1:64342/stream", + "type": "http" + } + } +} \ No newline at end of file diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts deleted file mode 100644 index 18afca3..0000000 --- a/Android/app/build.gradle.kts +++ /dev/null @@ -1,64 +0,0 @@ -import java.util.Properties - -plugins { - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) - alias(libs.plugins.android.application) - id("skip-build-plugin") -} - -skip { -} - -android { - namespace = group as String - compileSdk = libs.versions.android.sdk.compile.get().toInt() - compileOptions { - sourceCompatibility = JavaVersion.toVersion(libs.versions.jvm.get()) - targetCompatibility = JavaVersion.toVersion(libs.versions.jvm.get()) - } - kotlinOptions { - jvmTarget = libs.versions.jvm.get().toString() - } - packaging { - jniLibs.keepDebugSymbols.add("**/*.so") - } - - defaultConfig { - minSdk = libs.versions.android.sdk.min.get().toInt() - targetSdk = libs.versions.android.sdk.compile.get().toInt() - // skip.tools.skip-build-plugin will automatically use Skip.env properties for: - // applicationId = PRODUCT_BUNDLE_IDENTIFIER - // versionCode = CURRENT_PROJECT_VERSION - // versionName = MARKETING_VERSION - } - - buildFeatures { - buildConfig = true - } - - // default signing configuration tries to load from keystore.properties - signingConfigs { - val keystorePropertiesFile = file("keystore.properties") - if (keystorePropertiesFile.isFile) { - create("release") { - val keystoreProperties = Properties() - keystoreProperties.load(keystorePropertiesFile.inputStream()) - keyAlias = keystoreProperties.getProperty("keyAlias") - keyPassword = keystoreProperties.getProperty("keyPassword") - storeFile = file(keystoreProperties.getProperty("storeFile")) - storePassword = keystoreProperties.getProperty("storePassword") - } - } - } - - buildTypes { - release { - signingConfig = signingConfigs.findByName("release") - isMinifyEnabled = true - isShrinkResources = true - isDebuggable = false // can be set to true for debugging release build, but needs to be false when uploading to store - proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") - } - } -} diff --git a/Android/app/proguard-rules.pro b/Android/app/proguard-rules.pro deleted file mode 100644 index 845ce70..0000000 --- a/Android/app/proguard-rules.pro +++ /dev/null @@ -1,6 +0,0 @@ --keeppackagenames ** --keep class skip.** { *; } --keep class com.sun.jna.** { *; } --keep class * implements com.sun.jna.** { *; } --keep class sats.price.** { *; } --dontwarn java.awt.** diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml deleted file mode 100644 index c6c7049..0000000 --- a/Android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/Android/app/src/main/ic_launcher-playstore.png b/Android/app/src/main/ic_launcher-playstore.png deleted file mode 100644 index 36bd74e..0000000 Binary files a/Android/app/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/Android/app/src/main/kotlin/sats/price/Main.kt b/Android/app/src/main/kotlin/sats/price/Main.kt deleted file mode 100644 index 4f40e99..0000000 --- a/Android/app/src/main/kotlin/sats/price/Main.kt +++ /dev/null @@ -1,125 +0,0 @@ -package sats.price - -import skip.lib.* -import skip.model.* -import skip.foundation.* -import skip.ui.* - -import android.Manifest -import android.app.Application -import androidx.activity.enableEdgeToEdge -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.runtime.saveable.rememberSaveableStateHolder -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.core.app.ActivityCompat - -internal val logger: SkipLogger = SkipLogger(subsystem = "sats.price", category = "SatsPrice") - -/// AndroidAppMain is the `android.app.Application` entry point, and must match `application android:name` in the AndroidMainfest.xml file. -open class AndroidAppMain: Application { - constructor() { - } - - override fun onCreate() { - super.onCreate() - logger.info("starting app") - ProcessInfo.launch(applicationContext) - } - - companion object { - } -} - -/// AndroidAppMain is initial `androidx.appcompat.app.AppCompatActivity`, and must match `activity android:name` in the AndroidMainfest.xml file. -open class MainActivity: AppCompatActivity { - constructor() { - } - - override fun onCreate(savedInstanceState: android.os.Bundle?) { - super.onCreate(savedInstanceState) - logger.info("starting activity") - UIApplication.launch(this) - enableEdgeToEdge() - - setContent { - val saveableStateHolder = rememberSaveableStateHolder() - saveableStateHolder.SaveableStateProvider(true) { - PresentationRootView(ComposeContext()) - } - } - - // Example of requesting permissions on startup. - // These must match the permissions in the AndroidManifest.xml file. - //let permissions = listOf( - // Manifest.permission.ACCESS_COARSE_LOCATION, - // Manifest.permission.ACCESS_FINE_LOCATION - // Manifest.permission.CAMERA, - // Manifest.permission.WRITE_EXTERNAL_STORAGE, - //) - //let requestTag = 1 - //ActivityCompat.requestPermissions(self, permissions.toTypedArray(), requestTag) - } - - override fun onSaveInstanceState(bundle: android.os.Bundle): Unit = super.onSaveInstanceState(bundle) - - override fun onRestoreInstanceState(bundle: android.os.Bundle) { - // Usually you restore your state in onCreate(). It is possible to restore it in onRestoreInstanceState() as well, but not very common. (onRestoreInstanceState() is called after onStart(), whereas onCreate() is called before onStart(). - logger.info("onRestoreInstanceState") - super.onRestoreInstanceState(bundle) - } - - override fun onRestart() { - logger.info("onRestart") - super.onRestart() - } - - override fun onStart() { - logger.info("onStart") - super.onStart() - } - - override fun onResume() { - logger.info("onResume") - super.onResume() - } - - override fun onPause() { - logger.info("onPause") - super.onPause() - } - - override fun onStop() { - logger.info("onStop") - super.onStop() - } - - override fun onDestroy() { - logger.info("onDestroy") - super.onDestroy() - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: kotlin.Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - logger.info("onRequestPermissionsResult: ${requestCode}") - } - - companion object { - } -} - -@Composable -internal fun PresentationRootView(context: ComposeContext) { - val colorScheme = if (isSystemInDarkTheme()) ColorScheme.dark else ColorScheme.light - PresentationRoot(defaultColorScheme = colorScheme, context = context) { ctx -> - val contentContext = ctx.content() - Box(modifier = ctx.modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - RootView().Compose(context = contentContext) - } - } -} diff --git a/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 036d09b..0000000 --- a/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 036d09b..0000000 --- a/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Android/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index 96c1588..0000000 Binary files a/Android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp deleted file mode 100644 index 3f6c910..0000000 Binary files a/Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index ec19b39..0000000 Binary files a/Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Android/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index ee4b53a..0000000 Binary files a/Android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp deleted file mode 100644 index 137fde2..0000000 Binary files a/Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index b7d113e..0000000 Binary files a/Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index af7f38c..0000000 Binary files a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp deleted file mode 100644 index 9d71a4b..0000000 Binary files a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 8f3dfd4..0000000 Binary files a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 6494ce9..0000000 Binary files a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp deleted file mode 100644 index 669b548..0000000 Binary files a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index ed5be46..0000000 Binary files a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index 5b9168a..0000000 Binary files a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp deleted file mode 100644 index d1df236..0000000 Binary files a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 864236d..0000000 Binary files a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/Android/app/src/main/res/values/ic_launcher_background.xml b/Android/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index c5d5899..0000000 --- a/Android/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #FFFFFF - \ No newline at end of file diff --git a/Android/fastlane/Appfile b/Android/fastlane/Appfile deleted file mode 100644 index bbcb0fb..0000000 --- a/Android/fastlane/Appfile +++ /dev/null @@ -1,11 +0,0 @@ -# This file contains the app distribution configuration -# for the Android half of the Skip app. -# You can find the documentation at https://docs.fastlane.tools - -# Load the shared Skip.env properties with the app info -require('dotenv') -Dotenv.load '../../Skip.env' -package_name(ENV['PRODUCT_BUNDLE_IDENTIFIER']) - -# Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one -json_key_file("fastlane/apikey.json") diff --git a/Android/fastlane/Fastfile b/Android/fastlane/Fastfile deleted file mode 100644 index 9f636a1..0000000 --- a/Android/fastlane/Fastfile +++ /dev/null @@ -1,49 +0,0 @@ -# This file contains the fastlane.tools configuration -# for the Android half of the Skip app. -# You can find the documentation at https://docs.fastlane.tools - -# Load the shared Skip.env properties with the app info -require('dotenv') -Dotenv.load '../../Skip.env' - -default_platform(:android) - -# use the Homebrew gradle rather than expecting a local gradlew -gradle_bin = (ENV['HOMEBREW_PREFIX'] ? ENV['HOMEBREW_PREFIX'] : "/opt/homebrew") + "/bin/gradle" - -default_platform(:android) - -desc "Build Skip Android App" -lane :build do |options| - build_config = (options[:release] ? "Release" : "Debug") - gradle( - task: "build${build_config}", - gradle_path: gradle_bin, - flags: "--warning-mode none -x lint" - ) -end - -desc "Test Skip Android App" -lane :test do - gradle( - task: "test", - gradle_path: gradle_bin - ) -end - -desc "Assemble Skip Android App" -lane :assemble do - gradle( - gradle_path: gradle_bin, - task: "bundleRelease" - ) - # sh "your_script.sh" -end - -desc "Deploy Skip Android App to Google Play" -lane :release do - assemble - upload_to_play_store( - aab: '../.build/Android/app/outputs/bundle/release/app-release.aab' - ) -end diff --git a/Android/fastlane/metadata/android/en-US/full_description.txt b/Android/fastlane/metadata/android/en-US/full_description.txt deleted file mode 100644 index c65ef9c..0000000 --- a/Android/fastlane/metadata/android/en-US/full_description.txt +++ /dev/null @@ -1 +0,0 @@ -A great new app built with Skip! diff --git a/Android/fastlane/metadata/android/en-US/short_description.txt b/Android/fastlane/metadata/android/en-US/short_description.txt deleted file mode 100644 index c65ef9c..0000000 --- a/Android/fastlane/metadata/android/en-US/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -A great new app built with Skip! diff --git a/Android/fastlane/metadata/android/en-US/title.txt b/Android/fastlane/metadata/android/en-US/title.txt deleted file mode 100644 index 38f0959..0000000 --- a/Android/fastlane/metadata/android/en-US/title.txt +++ /dev/null @@ -1 +0,0 @@ -SatsPrice diff --git a/Android/gradle.properties b/Android/gradle.properties deleted file mode 100644 index 1b8d060..0000000 --- a/Android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx4g -android.useAndroidX=true -kotlin.code.style=official diff --git a/Android/gradle/wrapper/gradle-wrapper.properties b/Android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9d2e5f8..0000000 --- a/Android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -#Sat Aug 31 09:41:39 EEST 2024 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip diff --git a/Android/settings.gradle.kts b/Android/settings.gradle.kts deleted file mode 100644 index 42a54ff..0000000 --- a/Android/settings.gradle.kts +++ /dev/null @@ -1,46 +0,0 @@ -// This gradle project is part of a conventional Skip app project. -// It invokes the shared build skip plugin logic, which included as part of the skip-unit buildSrc -// When built from Android Studio, it uses the BUILT_PRODUCTS_DIR folder to share the same build outputs as Xcode, otherwise it uses SwiftPM's .build/ folder -pluginManagement { - // local override of BUILT_PRODUCTS_DIR - if (System.getenv("BUILT_PRODUCTS_DIR") == null) { - //System.setProperty("BUILT_PRODUCTS_DIR", "${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData/MySkipProject-aqywrhrzhkbvfseiqgxuufbdwdft/Build/Products/Debug-iphonesimulator") - } - - // the source for the plugin is linked as part of the SkipUnit transpilation - val skipOutput = System.getenv("BUILT_PRODUCTS_DIR") ?: System.getProperty("BUILT_PRODUCTS_DIR") - - val outputExt = if (skipOutput != null) ".output" else "" // Xcode saves output in package-name.output; SPM has no suffix - val skipOutputs: File = if (skipOutput != null) { - // BUILT_PRODUCTS_DIR is set when building from Xcode, in which case we will use Xcode's DerivedData plugin output - file(skipOutput).resolve("../../../SourcePackages/plugins/") - } else { - exec { - // create transpiled Kotlin and generate Gradle projects from SwiftPM modules - commandLine("swift", "build") - workingDir = file("..") - } - // SPM output folder is a peer of the parent Package.swift - rootDir.resolve("../.build/plugins/outputs/") - } - - // load the Skip plugin (part of the skip-unit project), which handles configuring the Android project - // because this path is a symlink, we need to use the canonical path or gradle will mis-interpret it as a different build source - var pluginSource = skipOutputs.resolve("skip-unit${outputExt}/SkipUnit/skipstone/buildSrc/").canonicalFile - if (!pluginSource.isDirectory) { - // check new SwiftPM6 plugin "destination" folder for command-line builds - pluginSource = skipOutputs.resolve("skip-unit${outputExt}/SkipUnit/destination/skipstone/buildSrc/").canonicalFile - } - - if (!pluginSource.isDirectory) { - throw GradleException("Missing expected Skip output folder: ${pluginSource}. Run `swift build` in the root folder to create, or specify Xcode environment BUILT_PRODUCTS_DIR.") - } - includeBuild(pluginSource.path) { - name = "skip-plugins" - } -} - -plugins { - id("skip-plugin") apply true -} - diff --git a/Darwin/Assets.xcassets/AccentColor.colorset/Contents.json b/Darwin/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/Darwin/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json b/Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index ac2479c..0000000 --- a/Darwin/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "images" : [ - { - "filename" : "bitcoin-calculator-1024-no-alpha 1.png", - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "filename" : "bitcoin-calculator-16-no-alpha.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "bitcoin-calculator-32-no-alpha 1.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "bitcoin-calculator-32-no-alpha.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "bitcoin-calculator-64-no-alpha.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "bitcoin-calculator-128-no-alpha.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "bitcoin-calculator-256-no-alpha 1.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "bitcoin-calculator-256-no-alpha.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "bitcoin-calculator-512-no-alpha 1.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "bitcoin-calculator-512-no-alpha.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "bitcoin-calculator-1024-no-alpha.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-1024-no-alpha 1.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-1024-no-alpha 1.png deleted file mode 100644 index 6f3e42c..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-1024-no-alpha 1.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-1024-no-alpha.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-1024-no-alpha.png deleted file mode 100644 index 6f3e42c..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-1024-no-alpha.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-128-no-alpha.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-128-no-alpha.png deleted file mode 100644 index 3f70042..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-128-no-alpha.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-16-no-alpha.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-16-no-alpha.png deleted file mode 100644 index 8bb2df8..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-16-no-alpha.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-256-no-alpha 1.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-256-no-alpha 1.png deleted file mode 100644 index 8b63e9e..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-256-no-alpha 1.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-256-no-alpha.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-256-no-alpha.png deleted file mode 100644 index 8b63e9e..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-256-no-alpha.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-32-no-alpha 1.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-32-no-alpha 1.png deleted file mode 100644 index c6def4c..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-32-no-alpha 1.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-32-no-alpha.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-32-no-alpha.png deleted file mode 100644 index c6def4c..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-32-no-alpha.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-512-no-alpha 1.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-512-no-alpha 1.png deleted file mode 100644 index 6e499ba..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-512-no-alpha 1.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-512-no-alpha.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-512-no-alpha.png deleted file mode 100644 index 6e499ba..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-512-no-alpha.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-64-no-alpha.png b/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-64-no-alpha.png deleted file mode 100644 index 07b2c89..0000000 Binary files a/Darwin/Assets.xcassets/AppIcon.appiconset/bitcoin-calculator-64-no-alpha.png and /dev/null differ diff --git a/Darwin/Assets.xcassets/Contents.json b/Darwin/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Darwin/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Darwin/Entitlements.plist b/Darwin/Entitlements.plist deleted file mode 100644 index ee95ab7..0000000 --- a/Darwin/Entitlements.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - - diff --git a/Darwin/SatsPrice.xcconfig b/Darwin/SatsPrice.xcconfig deleted file mode 100644 index cbd413e..0000000 --- a/Darwin/SatsPrice.xcconfig +++ /dev/null @@ -1,56 +0,0 @@ -#include "../Skip.env" - -// Set the action that will be executed as part of the Xcode Run Script phase -// Setting to "launch" will build and run the app in the first open Android emulator or device -// Setting to "build" will just run gradle build, but will not launch the app -SKIP_ACTION = launch -//SKIP_ACTION = build - -ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon -ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor - -INFOPLIST_FILE = Info.plist -GENERATE_INFOPLIST_FILE = YES - -// The user-visible name of the app (localizable) -//INFOPLIST_KEY_CFBundleDisplayName = App Name -//INFOPLIST_KEY_LSApplicationCategoryType = public.app-category.utilities - -// iOS-specific Info.plist property keys -INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphone*] = YES -INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphone*] = YES -INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphone*] = YES -INFOPLIST_KEY_UIStatusBarStyle[sdk=iphone*] = UIStatusBarStyleDefault -INFOPLIST_KEY_UISupportedInterfaceOrientations[sdk=iphone*] = UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - -IPHONEOS_DEPLOYMENT_TARGET = 16.0 -MACOSX_DEPLOYMENT_TARGET = 13.0 -SUPPORTS_MACCATALYST = NO - -// iPhone + iPad -TARGETED_DEVICE_FAMILY = 1,2 - -// iPhone only -// TARGETED_DEVICE_FAMILY = 1 - -SWIFT_EMIT_LOC_STRINGS = YES - -// the name of the product module; this can be anything, but cannot conflict with any Swift module names -PRODUCT_MODULE_NAME = $(PRODUCT_NAME:c99extidentifier)App - -// On-device testing may need to override the bundle ID -// PRODUCT_BUNDLE_IDENTIFIER[config=Debug][sdk=iphoneos*] = cool.beans.BundleIdentifer - -SDKROOT = auto -SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx -SWIFT_EMIT_LOC_STRINGS = YES - -SWIFT_VERSION = 5.0 -//SWIFT_VERSION = 6.0 - -// Development team ID for on-device testing -CODE_SIGNING_REQUIRED = NO -CODE_SIGN_STYLE = Automatic -CODE_SIGN_ENTITLEMENTS = Entitlements.plist -//CODE_SIGNING_IDENTITY = - -//DEVELOPMENT_TEAM = diff --git a/Darwin/SatsPrice.xcodeproj/project.pbxproj b/Darwin/SatsPrice.xcodeproj/project.pbxproj deleted file mode 100644 index 8ca19ab..0000000 --- a/Darwin/SatsPrice.xcodeproj/project.pbxproj +++ /dev/null @@ -1,298 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 56; - objects = { - -/* Begin PBXBuildFile section */ - 49231BAC2AC5BCEF00F98ADF /* SatsPriceApp in Frameworks */ = {isa = PBXBuildFile; productRef = 49231BAB2AC5BCEF00F98ADF /* SatsPriceApp */; }; - 49231BAD2AC5BCEF00F98ADF /* SatsPriceApp in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 49231BAB2AC5BCEF00F98ADF /* SatsPriceApp */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 496BDBEE2B8A7E9C00C09264 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */; }; - 499CD43B2AC5B799001AE8D8 /* SatsPriceAppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F90C2B2A52156200F06D93 /* SatsPriceAppMain.swift */; }; - 499CD4402AC5B799001AE8D8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49F90C2F2A52156300F06D93 /* Assets.xcassets */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 49231BAD2AC5BCEF00F98ADF /* SatsPriceApp in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 4900101C2BACEA710000DE33 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 493609562A6B7EAE00C401E2 /* SatsPrice */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SatsPrice; path = ..; sourceTree = ""; }; - 496BDBEB2B89A47800C09264 /* SatsPrice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SatsPrice.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = ../Sources/SatsPrice/Resources/Localizable.xcstrings; sourceTree = ""; }; - 496EB72F2A6AE4DE00C1253A /* Skip.env */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Skip.env; path = ../Skip.env; sourceTree = ""; }; - 496EB72F2A6AE4DE00C1253B /* SatsPrice.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SatsPrice.xcconfig; sourceTree = ""; }; - 496EB72F2A6AE4DE00C1253C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; - 499AB9082B0581F4005E8330 /* plugins */ = {isa = PBXFileReference; lastKnownFileType = folder; name = plugins; path = ../../../SourcePackages/plugins; sourceTree = BUILT_PRODUCTS_DIR; }; - 49F90C2B2A52156200F06D93 /* SatsPriceAppMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SatsPriceAppMain.swift; path = Sources/SatsPriceAppMain.swift; sourceTree = SOURCE_ROOT; }; - 49F90C2F2A52156300F06D93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 49F90C312A52156300F06D93 /* Entitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Entitlements.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 499CD43C2AC5B799001AE8D8 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 49231BAC2AC5BCEF00F98ADF /* SatsPriceApp in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 496BDBEC2B89A47800C09264 /* Products */ = { - isa = PBXGroup; - children = ( - 496BDBEB2B89A47800C09264 /* SatsPrice.app */, - ); - name = Products; - sourceTree = ""; - }; - 49AB54462B066A7E007B79B2 /* SkipStone */ = { - isa = PBXGroup; - children = ( - 499AB9082B0581F4005E8330 /* plugins */, - ); - name = SkipStone; - sourceTree = ""; - }; - 49F90C1F2A52156200F06D93 = { - isa = PBXGroup; - children = ( - 496EB72F2A6AE4DE00C1253C /* README.md */, - 496EB72F2A6AE4DE00C1253A /* Skip.env */, - 496EB72F2A6AE4DE00C1253B /* SatsPrice.xcconfig */, - 496BDBED2B8A7E9C00C09264 /* Localizable.xcstrings */, - 493609562A6B7EAE00C401E2 /* SatsPrice */, - 49F90C2A2A52156200F06D93 /* App */, - 49AB54462B066A7E007B79B2 /* SkipStone */, - 496BDBEC2B89A47800C09264 /* Products */, - ); - sourceTree = ""; - }; - 49F90C2A2A52156200F06D93 /* App */ = { - isa = PBXGroup; - children = ( - 49F90C2B2A52156200F06D93 /* SatsPriceAppMain.swift */, - 49F90C2F2A52156300F06D93 /* Assets.xcassets */, - 49F90C312A52156300F06D93 /* Entitlements.plist */, - 4900101C2BACEA710000DE33 /* Info.plist */, - ); - name = App; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 499CD4382AC5B799001AE8D8 /* SatsPrice */ = { - isa = PBXNativeTarget; - buildConfigurationList = 499CD4412AC5B799001AE8D8 /* Build configuration list for PBXNativeTarget "SatsPrice" */; - buildPhases = ( - 499CD43A2AC5B799001AE8D8 /* Sources */, - 499CD43C2AC5B799001AE8D8 /* Frameworks */, - 499CD43E2AC5B799001AE8D8 /* Resources */, - 499CD4452AC5B869001AE8D8 /* Run skip gradle */, - 499CD44A2AC5B9C6001AE8D8 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = SatsPrice; - packageProductDependencies = ( - 49231BAB2AC5BCEF00F98ADF /* SatsPriceApp */, - ); - productName = App; - productReference = 496BDBEB2B89A47800C09264 /* SatsPrice.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 49F90C202A52156200F06D93 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1540; - }; - buildConfigurationList = 49F90C232A52156200F06D93 /* Build configuration list for PBXProject "SatsPrice" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 49F90C1F2A52156200F06D93; - packageReferences = ( - ); - productRefGroup = 496BDBEC2B89A47800C09264 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 499CD4382AC5B799001AE8D8 /* SatsPrice */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 499CD43E2AC5B799001AE8D8 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 499CD4402AC5B799001AE8D8 /* Assets.xcassets in Resources */, - 496BDBEE2B8A7E9C00C09264 /* Localizable.xcstrings in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 499CD4452AC5B869001AE8D8 /* Run skip gradle */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Run skip gradle"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = "/bin/sh -e"; - shellScript = "if [ \"${SKIP_ZERO}\" != \"\" ]; then\n\techo \"note: skipping skip due to SKIP_ZERO\"\n\texit 0\nelif [ \"${ENABLE_PREVIEWS}\" == \"YES\" ]; then\n\techo \"note: skipping skip due to ENABLE_PREVIEWS\"\n\texit 0\nelif [ \"${ACTION}\" == \"install\" ]; then\n\techo \"note: skipping skip due to archive install\"\n\texit 0\nelse\n\tSKIP_ACTION=\"${SKIP_ACTION:-launch}\"\nfi\nPATH=${BUILD_ROOT}/Debug:${BUILD_ROOT}/../../SourcePackages/artifacts/skip/skip/skip.artifactbundle/macos:${PATH}:${HOMEBREW_PREFIX:-/opt/homebrew}/bin\necho \"note: running gradle build with: $(which skip) gradle -p ${PWD}/../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\"\nskip gradle -p ../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\n"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 499CD43A2AC5B799001AE8D8 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 499CD43B2AC5B799001AE8D8 /* SatsPriceAppMain.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 499CD4422AC5B799001AE8D8 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CURRENT_PROJECT_VERSION = 10; - DEVELOPMENT_TEAM = S99A5B637C; - ENABLE_APP_SANDBOX = YES; - ENABLE_PREVIEWS = YES; - INFOPLIST_KEY_CFBundleDisplayName = SatsPrice; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; - "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MARKETING_VERSION = 1.2.0; - }; - name = Debug; - }; - 499CD4432AC5B799001AE8D8 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CURRENT_PROJECT_VERSION = 10; - DEVELOPMENT_TEAM = S99A5B637C; - ENABLE_APP_SANDBOX = YES; - ENABLE_PREVIEWS = YES; - INFOPLIST_KEY_CFBundleDisplayName = SatsPrice; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; - "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MARKETING_VERSION = 1.2.0; - }; - name = Release; - }; - 49F90C4B2A52156300F06D93 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* SatsPrice.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 49F90C4C2A52156300F06D93 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* SatsPrice.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SWIFT_COMPILATION_MODE = wholemodule; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 499CD4412AC5B799001AE8D8 /* Build configuration list for PBXNativeTarget "SatsPrice" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 499CD4422AC5B799001AE8D8 /* Debug */, - 499CD4432AC5B799001AE8D8 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 49F90C232A52156200F06D93 /* Build configuration list for PBXProject "SatsPrice" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 49F90C4B2A52156300F06D93 /* Debug */, - 49F90C4C2A52156300F06D93 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCSwiftPackageProductDependency section */ - 49231BAB2AC5BCEF00F98ADF /* SatsPriceApp */ = { - isa = XCSwiftPackageProductDependency; - productName = SatsPriceApp; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 49F90C202A52156200F06D93 /* Project object */; -} diff --git a/Darwin/SatsPrice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Darwin/SatsPrice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/Darwin/SatsPrice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Darwin/Sources/SatsPriceAppMain.swift b/Darwin/Sources/SatsPriceAppMain.swift deleted file mode 100644 index 1126b8e..0000000 --- a/Darwin/Sources/SatsPriceAppMain.swift +++ /dev/null @@ -1,10 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org - -import SwiftUI -import SatsPrice - -/// The entry point to the app simply loads the App implementation from SPM module. -@main struct AppMain: App, SatsPriceApp { -} diff --git a/Darwin/fastlane/AppStore.xcconfig b/Darwin/fastlane/AppStore.xcconfig deleted file mode 100644 index 2dd4ae8..0000000 --- a/Darwin/fastlane/AppStore.xcconfig +++ /dev/null @@ -1,5 +0,0 @@ -// Additional properties included by the Fastfile build_app - -// This file can be used to override various properties from Skip.env -//PRODUCT_BUNDLE_IDENTIFIER = -//DEVELOPMENT_TEAM = diff --git a/Darwin/fastlane/Appfile b/Darwin/fastlane/Appfile deleted file mode 100644 index 2d9792e..0000000 --- a/Darwin/fastlane/Appfile +++ /dev/null @@ -1,8 +0,0 @@ -# For more information about the Appfile, see: -# https://docs.fastlane.tools/advanced/#appfile - -require('dotenv') -Dotenv.load '../../Skip.env' -#app_identifier(ENV['PRODUCT_BUNDLE_IDENTIFIER']) - -# apple_id("my@email") diff --git a/Darwin/fastlane/Deliverfile b/Darwin/fastlane/Deliverfile deleted file mode 100644 index 67e07dd..0000000 --- a/Darwin/fastlane/Deliverfile +++ /dev/null @@ -1,28 +0,0 @@ - -copyright "#{Time.now.year}" -default_language("en-US") - -force(true) # Skip HTML report verification -automatic_release(true) -skip_screenshots(false) -precheck_include_in_app_purchases(false) - -#skip_binary_upload(true) -submit_for_review(true) - -submission_information({ - add_id_info_serves_ads: false, - add_id_info_uses_idfa: false, - add_id_info_tracks_install: false, - add_id_info_tracks_action: false, - add_id_info_limits_tracking: false, - content_rights_has_rights: false, - content_rights_contains_third_party_content: false, - export_compliance_contains_third_party_cryptography: false, - export_compliance_encryption_updated: false, - export_compliance_platform: 'ios', - export_compliance_compliance_required: false, - export_compliance_uses_encryption: false, - export_compliance_is_exempt: false, - export_compliance_contains_proprietary_cryptography: false -}) diff --git a/Darwin/fastlane/Fastfile b/Darwin/fastlane/Fastfile deleted file mode 100644 index 874953c..0000000 --- a/Darwin/fastlane/Fastfile +++ /dev/null @@ -1,32 +0,0 @@ -# This file contains the fastlane.tools configuration -# for the iOS half of the Skip app. -# You can find the documentation at https://docs.fastlane.tools - -default_platform(:ios) - -lane :assemble do |options| - # only build the iOS side of the app - ENV["SKIP_ZERO"] = "true" - build_app( - sdk: "iphoneos", - xcconfig: "fastlane/AppStore.xcconfig", - xcargs: "-skipPackagePluginValidation -skipMacroValidation", - derived_data_path: "../.build/Darwin/DerivedData", - output_directory: "../.build/fastlane/Darwin", - skip_archive: ENV["FASTLANE_SKIP_ARCHIVE"] == "YES", - skip_codesigning: ENV["FASTLANE_SKIP_CODESIGNING"] == "YES" - ) -end - -lane :release do |options| - desc "Build and release app" - - assemble - - upload_to_app_store( - api_key_path: "fastlane/apikey.json", - app_rating_config_path: "fastlane/metadata/rating.json", - release_notes: { default: "Fixes and improvements." } - ) -end - diff --git a/Darwin/fastlane/metadata/en-US/description.txt b/Darwin/fastlane/metadata/en-US/description.txt deleted file mode 100644 index c65ef9c..0000000 --- a/Darwin/fastlane/metadata/en-US/description.txt +++ /dev/null @@ -1 +0,0 @@ -A great new app built with Skip! diff --git a/Darwin/fastlane/metadata/en-US/keywords.txt b/Darwin/fastlane/metadata/en-US/keywords.txt deleted file mode 100644 index 0fb6d6c..0000000 --- a/Darwin/fastlane/metadata/en-US/keywords.txt +++ /dev/null @@ -1 +0,0 @@ -app,key,words diff --git a/Darwin/fastlane/metadata/en-US/privacy_url.txt b/Darwin/fastlane/metadata/en-US/privacy_url.txt deleted file mode 100644 index 575f001..0000000 --- a/Darwin/fastlane/metadata/en-US/privacy_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://example.org/privacy/ diff --git a/Darwin/fastlane/metadata/en-US/release_notes.txt b/Darwin/fastlane/metadata/en-US/release_notes.txt deleted file mode 100644 index 2e531b6..0000000 --- a/Darwin/fastlane/metadata/en-US/release_notes.txt +++ /dev/null @@ -1 +0,0 @@ -Bug fixes and performance improvements. diff --git a/Darwin/fastlane/metadata/en-US/software_url.txt b/Darwin/fastlane/metadata/en-US/software_url.txt deleted file mode 100644 index 0ce62b7..0000000 --- a/Darwin/fastlane/metadata/en-US/software_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://example.org/app/ diff --git a/Darwin/fastlane/metadata/en-US/subtitle.txt b/Darwin/fastlane/metadata/en-US/subtitle.txt deleted file mode 100644 index 2289704..0000000 --- a/Darwin/fastlane/metadata/en-US/subtitle.txt +++ /dev/null @@ -1 +0,0 @@ -A new Skip app diff --git a/Darwin/fastlane/metadata/en-US/support_url.txt b/Darwin/fastlane/metadata/en-US/support_url.txt deleted file mode 100644 index fcfa1f7..0000000 --- a/Darwin/fastlane/metadata/en-US/support_url.txt +++ /dev/null @@ -1 +0,0 @@ -https://example.org/support/ diff --git a/Darwin/fastlane/metadata/en-US/title.txt b/Darwin/fastlane/metadata/en-US/title.txt deleted file mode 100644 index 38f0959..0000000 --- a/Darwin/fastlane/metadata/en-US/title.txt +++ /dev/null @@ -1 +0,0 @@ -SatsPrice diff --git a/Darwin/fastlane/metadata/en-US/version_whats_new.txt b/Darwin/fastlane/metadata/en-US/version_whats_new.txt deleted file mode 100644 index 5d21c24..0000000 --- a/Darwin/fastlane/metadata/en-US/version_whats_new.txt +++ /dev/null @@ -1 +0,0 @@ -New features and better performance. diff --git a/Darwin/fastlane/metadata/rating.json b/Darwin/fastlane/metadata/rating.json deleted file mode 100644 index b003379..0000000 --- a/Darwin/fastlane/metadata/rating.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "alcoholTobaccoOrDrugUseOrReferences": "NONE", - "contests": "NONE", - "gamblingSimulated": "NONE", - "horrorOrFearThemes": "NONE", - "matureOrSuggestiveThemes": "NONE", - "medicalOrTreatmentInformation": "NONE", - "profanityOrCrudeHumor": "NONE", - "sexualContentGraphicAndNudity": "NONE", - "sexualContentOrNudity": "NONE", - "violenceCartoonOrFantasy": "NONE", - "violenceRealisticProlongedGraphicOrSadistic": "NONE", - "violenceRealistic": "NONE", - "gambling": false, - "seventeenPlus": false, - "unrestrictedWebAccess": false -} diff --git a/Package.swift b/Package.swift deleted file mode 100644 index d95705b..0000000 --- a/Package.swift +++ /dev/null @@ -1,37 +0,0 @@ -// swift-tools-version: 5.9 -// This is a Skip (https://skip.tools) package, -// containing a Swift Package Manager project -// that will use the Skip build plugin to transpile the -// Swift Package, Sources, and Tests into an -// Android Gradle Project with Kotlin sources and JUnit tests. -import PackageDescription - -let package = Package( - name: "sats-price", - defaultLocalization: "en", - platforms: [.iOS(.v16), .macOS(.v13), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)], - products: [ - .library(name: "SatsPriceApp", type: .dynamic, targets: ["SatsPrice"]), - ], - dependencies: [ - .package(url: "https://source.skip.tools/skip.git", from: "1.0.7"), - .package(url: "https://source.skip.tools/skip-ui.git", from: "1.0.0"), - .package(url: "https://source.skip.tools/skip-foundation.git", from: "1.0.0"), - .package(url: "https://source.skip.tools/skip-model.git", from: "1.0.0"), - .package(url: "https://source.skip.tools/skip-sql.git", "0.0.0"..<"2.0.0") - ], - targets: [ - .target( - name: "SatsPrice", - dependencies: [ - .product(name: "SkipUI", package: "skip-ui"), - .product(name: "SkipFoundation", package: "skip-foundation"), - .product(name: "SkipModel", package: "skip-model"), - .product(name: "SkipSQLPlus", package: "skip-sql") - ], - resources: [.process("Resources")], - plugins: [.plugin(name: "skipstone", package: "skip")] - ), - .testTarget(name: "SatsPriceTests", dependencies: ["SatsPrice", .product(name: "SkipTest", package: "skip")], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]), - ] -) diff --git a/README.md b/README.md index ca243a8..79f99d4 100644 --- a/README.md +++ b/README.md @@ -29,53 +29,55 @@ height="70">](https://github.com/tyiu/sats-price/releases) iOS 16.0+ • macOS 13.0+ • Android 10.0+ - +## Kotlin Multiplatform -## Building +This is a Kotlin Multiplatform project targeting Android, iOS, Web, Desktop (JVM). -This is a free [Skip](https://skip.tools) dual-platform app project. -It builds a native app for both iOS and Android. +* [/iosApp](./iosApp/iosApp) contains an iOS application. Even if you’re sharing your UI with Compose Multiplatform, you + need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. -This project is both a stand-alone Swift Package Manager module, -as well as an Xcode project that builds and transpiles the project -into a Kotlin Gradle project for Android using the Skip plugin. +* [/shared](./shared/src) is for code that will be shared across your Compose Multiplatform applications. It contains + several subfolders: + - [commonMain](./shared/src/commonMain/kotlin) is for code that’s common for all targets. + - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For + example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, + the [iosMain](./shared/src/iosMain/kotlin) folder would be the right place for such calls. Similarly, if you want + to edit the Desktop (JVM) specific part, the [jvmMain](./shared/src/jvmMain/kotlin) + folder is the appropriate location. -Building the module requires that Skip be installed using -[Homebrew](https://brew.sh) with `brew install skiptools/skip/skip`. +### Running the apps -This will also install the necessary transpiler prerequisites: -Kotlin, Gradle, and the Android build tools. +Use the run configurations provided by the run widget in your IDE's toolbar. You can also use these commands and +options: -Installation prerequisites can be confirmed by running `skip checkup`. +- Android app: `./gradlew :androidApp:assembleDebug` +- Desktop app: + - Hot reload: `./gradlew :desktopApp:hotRun --auto` + - Standard run: `./gradlew :desktopApp:run` +- Web app: + - Wasm target (faster, modern browsers): `./gradlew :webApp:wasmJsBrowserDevelopmentRun` + - JS target (slower, supports older browsers): `./gradlew :webApp:jsBrowserDevelopmentRun` +- iOS app: open the [/iosApp](./iosApp) directory in Xcode and run it from there. -## Testing +### Running tests -The module can be tested using the standard `swift test` command -or by running the test target for the macOS destination in Xcode, -which will run the Swift tests as well as the transpiled -Kotlin JUnit tests in the Robolectric Android simulation environment. +Use the run button in your IDE's editor gutter, or run tests using Gradle tasks: -Parity testing can be performed with `skip test`, -which will output a table of the test results for both platforms. - -## Running - -Xcode and Android Studio must be downloaded and installed in order to -run the app in the iOS simulator / Android emulator. -An Android emulator must already be running, which can be launched from -Android Studio's Device Manager. - -To run both the Swift and Kotlin apps simultaneously, -launch the SatsPriceApp target from Xcode. -A build phases runs the "Launch Android APK" script that -will deploy the transpiled app a running Android emulator or connected device. -Logging output for the iOS app can be viewed in the Xcode console, and in -Android Studio's logcat tab for the transpiled Kotlin app. +- Android tests: `./gradlew :shared:testAndroidHostTest` +- Desktop tests: `./gradlew :shared:jvmTest` +- Web tests: + - Wasm target: `./gradlew :shared:wasmJsTest` + - JS target: `./gradlew :shared:jsTest` +- iOS tests: `./gradlew :shared:iosSimulatorArm64Test` ## Attribution This project depends on [Skip](https://skip.tools) to build as a multi-platform app. +This project uses [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html), +[Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform/#compose-multiplatform), and +[Kotlin/Wasm](https://kotl.in/wasm/). + The [Bitcoin Calculator](https://www.flaticon.com/free-icons/bitcoin-calculator) icon was created by Icon home and licensed as free for personal and commercial use with attribution. The following free APIs are used: @@ -83,4 +85,6 @@ The following free APIs are used: - [Get Exchange Rates](https://docs.cdp.coinbase.com/coinbase-app/docs/api-exchange-rates#get-exchange-rates) - [Get Spot Price](https://docs.cdp.coinbase.com/coinbase-app/docs/api-prices#get-spot-price) - CoinGecko - - [Coin Price by IDs](https://docs.coingecko.com/reference/simple-price) \ No newline at end of file + - [Coin Price by IDs](https://docs.coingecko.com/reference/simple-price) + + \ No newline at end of file diff --git a/SatsPrice.iml b/SatsPrice.iml new file mode 100644 index 0000000..482334b --- /dev/null +++ b/SatsPrice.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Skip.env b/Skip.env deleted file mode 100644 index da235c2..0000000 --- a/Skip.env +++ /dev/null @@ -1,20 +0,0 @@ -// The configuration file for your Skip App (https://skip.tools). -// Properties specified here are shared between -// Darwin/SatsPrice.xcconfig and Android/settings.gradle.kts -// and will be included in the app's metadata files -// Info.plist and AndroidManifest.xml - -// PRODUCT_NAME is the default title of the app, which must match the app's Swift module name -PRODUCT_NAME = SatsPrice - -// PRODUCT_BUNDLE_IDENTIFIER is the unique id for both the iOS and Android app -PRODUCT_BUNDLE_IDENTIFIER = xyz.tyiu.SatsPrice - -// The semantic version of the app -MARKETING_VERSION = 1.2.0 - -// The build number specifying the internal app version -CURRENT_PROJECT_VERSION = 10 - -// The package name for the Android entry point, referenced by the AndroidManifest.xml -ANDROID_PACKAGE_NAME = sats.price diff --git a/Sources/SatsPrice/ContentView.swift b/Sources/SatsPrice/ContentView.swift deleted file mode 100644 index 83d8487..0000000 --- a/Sources/SatsPrice/ContentView.swift +++ /dev/null @@ -1,139 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org - -import Combine -import SwiftUI - -public struct ContentView: View { - let model: SatsPriceModel - - @StateObject private var satsViewModel: SatsViewModel - - private let dateFormatter: DateFormatter - - init(model: SatsPriceModel) { - self.model = model - - _satsViewModel = StateObject(wrappedValue: SatsViewModel(model: model)) - - dateFormatter = DateFormatter() - dateFormatter.dateStyle = .short - dateFormatter.timeStyle = .short - } - - public var addCurrencyView: some View { - NavigationLink( - destination: { - CurrencyPickerView(satsViewModel: satsViewModel) - }, - label: { - Text("Change Currencies") - } - ) - } - - public var body: some View { - NavigationStack { - Form { - Section { - Picker("Price Source", selection: $satsViewModel.priceSource) { - ForEach(PriceSource.allCases, id: \.self) { - Text($0.description) - } - } - - HStack { - TextField("1 BTC to \(satsViewModel.currentCurrency.identifier)", text: satsViewModel.btcToCurrencyString(for: satsViewModel.currentCurrency)) - .disabled(satsViewModel.priceSource != .manual) -#if os(iOS) || SKIP - .keyboardType(.decimalPad) -#endif - if satsViewModel.priceSource != .manual { - Button(action: { - Task { - await satsViewModel.updatePrice() - } - }) { - Image(systemName: "arrow.clockwise.circle") - } - } - } - } header: { - Text("1 BTC to \(satsViewModel.currentCurrency.identifier)") - } footer: { - if satsViewModel.priceSource != .manual, let lastUpdated = satsViewModel.lastUpdated { - Text("Last updated: \(dateFormatter.string(from: lastUpdated))") - } - } - - Section { - HStack { - Text("Sats") - TextField("Sats", text: $satsViewModel.satsString) -#if os(iOS) || SKIP - .keyboardType(.numberPad) -#endif - } - - HStack { - Text("BTC") - TextField("BTC", text: $satsViewModel.btcString) -#if os(iOS) || SKIP - .keyboardType(.decimalPad) -#endif - } - } footer: { - if satsViewModel.exceedsMaximum { - Text("\(SatsViewModel.MAXIMUM_BTC.formatBTCString()) BTC is the maximum.") - } - } - - Section { - HStack { - Text(satsViewModel.currentCurrency.identifier) - TextField(satsViewModel.currentCurrency.identifier, text: satsViewModel.currencyValueString(for: satsViewModel.currentCurrency)) -#if os(iOS) || SKIP - .keyboardType(.decimalPad) -#endif - } - - if satsViewModel.priceSource != .manual { - ForEach(satsViewModel.selectedCurrencies.sorted { $0.identifier < $1.identifier }.filter { $0 != satsViewModel.currentCurrency }, id: \.identifier) { currency in - HStack { - Text(currency.identifier) - TextField(currency.identifier, text: satsViewModel.currencyValueString(for: currency)) -#if os(iOS) || SKIP - .keyboardType(.decimalPad) -#endif - } - .tag(currency.identifier) - } - } - } - - if satsViewModel.priceSource != .manual { - addCurrencyView - } - } - .task { - await satsViewModel.pullSelectedCurrenciesFromDB() - await satsViewModel.updatePrice() - } - .onChange(of: satsViewModel.priceSource) { newPriceSource in - satsViewModel.lastUpdated = nil - Task { - await satsViewModel.updatePrice() - } - } -#if os(macOS) - .formStyle(.grouped) -#endif - } - } -} - -#Preview { - let satsPriceModel = try! SatsPriceModel(url: nil) - ContentView(model: satsPriceModel) -} diff --git a/Sources/SatsPrice/CurrencyPickerView.swift b/Sources/SatsPrice/CurrencyPickerView.swift deleted file mode 100644 index 3623702..0000000 --- a/Sources/SatsPrice/CurrencyPickerView.swift +++ /dev/null @@ -1,85 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// CurrencyPickerView.swift -// sats-price -// -// Created by Terry Yiu on 11/10/24. -// - -import SwiftUI - -struct CurrencyPickerView: View { - @ObservedObject var satsViewModel: SatsViewModel - - var body: some View { - let currentCurrency = satsViewModel.currentCurrency - - List { - Section("Current Currency") { - let currentCurrency = satsViewModel.currentCurrency - if let localizedCurrency = Locale.current.localizedString(forCurrencyCode: currentCurrency.identifier) { - Text("\(currentCurrency.identifier) - \(localizedCurrency)") - } else { - Text(currentCurrency.identifier) - } - } - - if !satsViewModel.selectedCurrencies.isEmpty { - Section("Selected Currencies") { - ForEach(satsViewModel.selectedCurrencies.filter { $0 != currentCurrency }.sorted { $0.identifier < $1.identifier }, id: \.identifier) { currency in - Button( - action: { - satsViewModel.removeSelectedCurrency(currency) - }, - label: { - HStack { - Group { - if let localizedCurrency = Locale.current.localizedString(forCurrencyCode: currency.identifier) { - Text("\(currency.identifier) - \(localizedCurrency)") - } else { - Text(currency.identifier) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - - Image(systemName: "checkmark") - } - } - ) - .buttonStyle(.plain) - } - } - } - - Section("Currencies") { - ForEach(satsViewModel.currencies.filter { $0 != currentCurrency && !satsViewModel.selectedCurrencies.contains($0) }, id: \.identifier) { currency in - Button( - action: { - satsViewModel.addSelectedCurrency(currency) - }, - label: { - if let localizedCurrency = Locale.current.localizedString(forCurrencyCode: currency.identifier) { - Text("\(currency.identifier) - \(localizedCurrency)") - } else { - Text(currency.identifier) - } - } - ) - .buttonStyle(.plain) - } - } - } - .onDisappear(perform: { - Task { - await satsViewModel.updatePrice() - } - }) - } -} - -#Preview { - let satsPriceModel = try! SatsPriceModel(url: nil) - CurrencyPickerView(satsViewModel: SatsViewModel(model: satsPriceModel)) -} diff --git a/Sources/SatsPrice/Model/SatsPriceModel.swift b/Sources/SatsPrice/Model/SatsPriceModel.swift deleted file mode 100644 index 962bf06..0000000 --- a/Sources/SatsPrice/Model/SatsPriceModel.swift +++ /dev/null @@ -1,156 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// SatsPriceModel.swift -// sats-price -// -// Created by Terry Yiu on 11/15/24. -// - -import Foundation -import OSLog -import Observation -import SkipSQL - -public struct SelectedCurrency: Identifiable { - public var id: String { - currencyCode - } - - public var currencyCode: String -} - -/// Notification posted by the model when selected currencies change. -extension Notification.Name { - public static var selectedCurrenciesDidChange: Notification.Name { - return Notification.Name("selectedCurrenciesChange") - } -} - -/// Payload of `selectedCurrenciesDidChange` notifications. -public struct SelectedCurrenciesChange { - public let inserts: [SelectedCurrency] - /// Nil set means all records were deleted. - public let deletes: Set? - - public init(inserts: [SelectedCurrency] = [], deletes: [String]? = []) { - self.inserts = inserts - self.deletes = deletes == nil ? nil : Set(deletes!) - } -} - -public actor SatsPriceModel { - private let ctx: SQLContext - private var schemaInitializationResult: Result? - - public init(url: URL?) throws { - ctx = try SQLContext(path: url?.path ?? ":memory:", flags: [.readWrite, .create], logLevel: .info, configuration: .platform) - } - - public func selectedCurrencies() throws -> [SelectedCurrency] { - do { - try initializeSchema() - let statement = try ctx.prepare(sql: "SELECT currencyCode FROM SelectedCurrency") - defer { - do { - try statement.close() - } catch { - logger.warning("Failed to close statement: \(error)") - } - } - - var selectedCurrencies: [SelectedCurrency] = [] - - while try statement.next() { - let currencyCode = statement.string(at: 0) ?? "" - selectedCurrencies.append(SelectedCurrency(currencyCode: currencyCode)) - } - - return selectedCurrencies - } catch { - logger.error("Failed to get selected currencies from DB. Error: \(error)") - throw error - } - } - - @discardableResult - public func insert(_ selectedCurrency: SelectedCurrency) throws -> [SelectedCurrency] { - try initializeSchema() - let statement = try ctx.prepare(sql: "INSERT INTO SelectedCurrency (currencyCode) VALUES (?)") - defer { - do { - try statement.close() - } catch { - logger.warning("Failed to close statement: \(error)") - } - } - - var insertedItems: [SelectedCurrency] = [] - try ctx.transaction { - statement.reset() - let values = Self.bindingValues(for: selectedCurrency) - try statement.update(parameters: values) - - insertedItems.append(selectedCurrency) - } - NotificationCenter.default.post(name: .selectedCurrenciesDidChange, object: SelectedCurrenciesChange(inserts: insertedItems)) - return insertedItems - } - - private static func bindingValues(for selectedCurrency: SelectedCurrency) -> [SQLValue] { - return [ - .text(selectedCurrency.currencyCode) - ] - } - - @discardableResult - public func deleteSelectedCurrency(currencyCode: String) throws -> Int { - try initializeSchema() - try ctx.exec(sql: "DELETE FROM SelectedCurrency WHERE currencyCode = ?", parameters: [.text(currencyCode)]) - NotificationCenter.default.post(name: .selectedCurrenciesDidChange, object: SelectedCurrenciesChange(deletes: [currencyCode])) - return Int(ctx.changes) - } - - private func initializeSchema() throws { - switch schemaInitializationResult { - case .success: - return - case .failure(let failure): - throw failure - case nil: - break - } - - do { - var currentVersion = try currentSchemaVersion() - currentVersion = try migrateSchema(v: Int64(1), current: currentVersion, ddl: """ - CREATE TABLE SelectedCurrency (currencyCode TEXT PRIMARY KEY NOT NULL) - """) - // Future column additions, etc here... - schemaInitializationResult = .success(()) - } catch { - schemaInitializationResult = .failure(error) - throw error - } - } - - private func currentSchemaVersion() throws -> Int64 { - try ctx.exec(sql: "CREATE TABLE IF NOT EXISTS SchemaVersion (id INTEGER PRIMARY KEY, version INTEGER)") - try ctx.exec(sql: "INSERT OR IGNORE INTO SchemaVersion (id, version) VALUES (0, 0)") - return try ctx.query(sql: "SELECT version FROM SchemaVersion").first?.first?.integerValue ?? Int64(0) - } - - private func migrateSchema(v version: Int64, current: Int64, ddl: String) throws -> Int64 { - guard current < version else { - return current - } - let startTime = Date.now - try ctx.transaction { - try ctx.exec(sql: ddl) - try ctx.exec(sql: "UPDATE SchemaVersion SET version = ?", parameters: [.integer(version)]) - } - logger.log("Updated database schema to \(version) in \(Date.now.timeIntervalSince1970 - startTime.timeIntervalSince1970)") - return version - } -} diff --git a/Sources/SatsPrice/Network/CoinGeckoPriceFetcher.swift b/Sources/SatsPrice/Network/CoinGeckoPriceFetcher.swift deleted file mode 100644 index 2a2ab96..0000000 --- a/Sources/SatsPrice/Network/CoinGeckoPriceFetcher.swift +++ /dev/null @@ -1,92 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// CoinGeckoPriceFetcher.swift -// SatsPrice -// -// Created by Terry Yiu on 2/19/24. -// - -import Foundation - -private struct CoinGeckoPriceResponse: Codable { -#if !SKIP - let bitcoin: [String: Decimal] -#else - let bitcoin: [String: String] -#endif -} - -class CoinGeckoPriceFetcher : PriceFetcher { - func urlString(toCurrency currency: Locale.Currency) -> String { - "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=\(currency.identifier.lowercased())&precision=18" - } - - func urlString(toCurrencies currencies: [Locale.Currency]) -> String { - let currenciesString = currencies.map { $0.identifier.lowercased() }.joined(separator: ",") - return "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=\(currenciesString)&precision=18" - } - - func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? { - do { - guard let urlComponents = URLComponents(string: urlString(toCurrency: currency)), let url = urlComponents.url else { - return nil - } - - let (data, _) = try await URLSession.shared.data(from: url, delegate: nil) - - let priceResponse = try JSONDecoder().decode(CoinGeckoPriceResponse.self, from: data) - guard let price = priceResponse.bitcoin[currency.identifier.lowercased()] else { - return nil - } - -#if !SKIP - return price -#else - return Decimal(price) -#endif - } catch { - return nil - } - } - - func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency : Decimal] { - do { - guard !currencies.isEmpty else { - return [:] - } - - if currencies.count == 1, let currency = currencies.first { - guard let price = try await convertBTC(toCurrency: currency) else { - return [:] - } - - return [currency: price] - } - - guard let urlComponents = URLComponents(string: urlString(toCurrencies: currencies)), let url = urlComponents.url else { - return [:] - } - - let (data, _) = try await URLSession.shared.data(from: url, delegate: nil) - - let priceResponse = try JSONDecoder().decode(CoinGeckoPriceResponse.self, from: data) - - var results: [Locale.Currency : Decimal] = [:] - for currency in currencies { - if let price = priceResponse.bitcoin[currency.identifier.lowercased()] { -#if !SKIP - results[currency] = price -#else - results[currency] = Decimal(price) -#endif - } - } - - return results - } catch { - return [:] - } - } -} diff --git a/Sources/SatsPrice/Network/CoinbasePriceFetcher.swift b/Sources/SatsPrice/Network/CoinbasePriceFetcher.swift deleted file mode 100644 index 5aae5d5..0000000 --- a/Sources/SatsPrice/Network/CoinbasePriceFetcher.swift +++ /dev/null @@ -1,107 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// CoinbasePriceFetcher.swift -// SatsPrice -// -// Created by Terry Yiu on 2/19/24. -// - -import Foundation - -private struct CoinbasePriceResponse: Codable { - let data: CoinbasePrice -} - -private struct CoinbasePrice: Codable { - let amount: String - let base: String - let currency: String -} - -private struct CoinbaseExchangeRatesResponse: Codable { - let data: CoinbaseExchangeRatesResponseData -} - -private struct CoinbaseExchangeRatesResponseData: Codable { - let currency: String - let rates: [String: String] -} - -class CoinbasePriceFetcher : PriceFetcher { - func urlString(toCurrency currency: Locale.Currency) -> String { - "https://api.coinbase.com/v2/prices/BTC-\(currency.identifier)/spot" - } - - private static let urlStringForAllCurrencies: String = "https://api.coinbase.com/v2/exchange-rates?currency=BTC" - - func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? { - do { - guard let urlComponents = URLComponents(string: urlString(toCurrency: currency)), let url = urlComponents.url else { - return nil - } - - let (data, _) = try await URLSession.shared.data(from: url, delegate: nil) - - let coinbasePriceResponse = try JSONDecoder().decode(CoinbasePriceResponse.self, from: data) - let coinbasePrice = coinbasePriceResponse.data - - guard coinbasePrice.base == "BTC" && coinbasePrice.currency == currency.identifier else { - return nil - } - -#if !SKIP - return Decimal(string: coinbasePrice.amount) -#else - return Decimal(coinbasePrice.amount) -#endif - } catch { - return nil - } - } - - func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency : Decimal] { - do { - guard !currencies.isEmpty else { - return [:] - } - - if currencies.count == 1, let currency = currencies.first { - guard let price = try await convertBTC(toCurrency: currency) else { - return [:] - } - - return [currency: price] - } - - guard let urlComponents = URLComponents(string: CoinbasePriceFetcher.urlStringForAllCurrencies), let url = urlComponents.url else { - return [:] - } - - let (data, _) = try await URLSession.shared.data(from: url, delegate: nil) - - let coinbaseExchangeRatesResponse = try JSONDecoder().decode(CoinbaseExchangeRatesResponse.self, from: data) - let rates = coinbaseExchangeRatesResponse.data.rates - - guard coinbaseExchangeRatesResponse.data.currency == "BTC" else { - return [:] - } - - var results: [Locale.Currency : Decimal] = [:] - for currency in currencies { - if let price = rates[currency.identifier] { -#if !SKIP - results[currency] = Decimal(string: price) -#else - results[currency] = Decimal(price) -#endif - } - } - - return results - } catch { - return [:] - } - } -} diff --git a/Sources/SatsPrice/Network/FakePriceFetcher.swift b/Sources/SatsPrice/Network/FakePriceFetcher.swift deleted file mode 100644 index d52a013..0000000 --- a/Sources/SatsPrice/Network/FakePriceFetcher.swift +++ /dev/null @@ -1,33 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// FakePriceFetcher.swift -// SatsPrice -// -// Created by Terry Yiu on 2/21/24. -// - -#if DEBUG -import Foundation - -/// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call. -class FakePriceFetcher: PriceFetcher { - func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? { - randomPrice() - } - - func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency : Decimal] { - guard !currencies.isEmpty else { - return [:] - } - - let prices = currencies.map { _ in randomPrice() } - return Dictionary(uniqueKeysWithValues: zip(currencies, prices)) - } - - private func randomPrice() -> Decimal { - Decimal(Double.random(in: 10000...100000)) - } -} -#endif diff --git a/Sources/SatsPrice/Network/ManualPriceFetcher.swift b/Sources/SatsPrice/Network/ManualPriceFetcher.swift deleted file mode 100644 index e515804..0000000 --- a/Sources/SatsPrice/Network/ManualPriceFetcher.swift +++ /dev/null @@ -1,30 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// ManualPriceFetcher.swift -// SatsPrice -// -// Created by Terry Yiu on 8/29/24. -// - -import Foundation - -/// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call. -class ManualPriceFetcher: PriceFetcher { - var prices: [Locale.Currency: Decimal] = [:] - - func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? { - prices[currency] - } - - func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency : Decimal] { - guard !currencies.isEmpty else { - return [:] - } - - let filteredCurrencies = currencies.filter { prices.keys.contains($0) } - let priceValues = filteredCurrencies.map { prices[$0, default: Decimal(0)] } - return Dictionary(uniqueKeysWithValues: zip(filteredCurrencies, priceValues)) - } -} diff --git a/Sources/SatsPrice/Network/PriceFetcher.swift b/Sources/SatsPrice/Network/PriceFetcher.swift deleted file mode 100644 index 223abe1..0000000 --- a/Sources/SatsPrice/Network/PriceFetcher.swift +++ /dev/null @@ -1,16 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// PriceFetcher.swift -// SatsPrice -// -// Created by Terry Yiu on 2/19/24. -// - -import Foundation - -protocol PriceFetcher { - func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? - func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency: Decimal] -} diff --git a/Sources/SatsPrice/Network/PriceFetcherDelegator.swift b/Sources/SatsPrice/Network/PriceFetcherDelegator.swift deleted file mode 100644 index e2e6d12..0000000 --- a/Sources/SatsPrice/Network/PriceFetcherDelegator.swift +++ /dev/null @@ -1,49 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// PriceFetcherDelegator.swift -// SatsPrice -// -// Created by Terry Yiu on 2/20/24. -// - -import Foundation - -class PriceFetcherDelegator: PriceFetcher { - private let coinbasePriceFetcher = CoinbasePriceFetcher() - private let coinGeckoPriceFetcher = CoinGeckoPriceFetcher() - private let manualPriceFetcher = ManualPriceFetcher() -#if DEBUG - private let fakePriceFetcher = FakePriceFetcher() -#endif - - var priceSource: PriceSource - - init(_ priceSource: PriceSource) { - self.priceSource = priceSource - } - - private var delegate: PriceFetcher { - switch priceSource { - case .coinbase: - coinbasePriceFetcher - case .coingecko: - coinGeckoPriceFetcher - case .manual: - manualPriceFetcher -#if DEBUG - case .fake: - fakePriceFetcher -#endif - } - } - - func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? { - return try await delegate.convertBTC(toCurrency: currency) - } - - func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency : Decimal] { - return try await delegate.convertBTC(toCurrencies: currencies) - } -} diff --git a/Sources/SatsPrice/Network/PriceSource.swift b/Sources/SatsPrice/Network/PriceSource.swift deleted file mode 100644 index 172fb85..0000000 --- a/Sources/SatsPrice/Network/PriceSource.swift +++ /dev/null @@ -1,45 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// PriceSource.swift -// SatsPrice -// -// Created by Terry Yiu on 2/20/24. -// - -import Foundation - -enum PriceSource: CaseIterable, CustomStringConvertible { - - static var allCases: [PriceSource] { -#if DEBUG - [.coinbase, .coingecko, .manual, .fake] -#else - [.coinbase, .coingecko, .manual] -#endif - } - - case coinbase - case coingecko - case manual - -#if DEBUG - case fake -#endif - - var description: String { - switch self { - case .coinbase: - "Coinbase" - case .coingecko: - "CoinGecko" - case .manual: - "Manual" -#if DEBUG - case .fake: - "Fake" -#endif - } - } -} diff --git a/Sources/SatsPrice/Resources/Localizable.xcstrings b/Sources/SatsPrice/Resources/Localizable.xcstrings deleted file mode 100644 index 4c69c72..0000000 --- a/Sources/SatsPrice/Resources/Localizable.xcstrings +++ /dev/null @@ -1,49 +0,0 @@ -{ - "sourceLanguage" : "en", - "strings" : { - "" : { - - }, - "%@ - %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@ - %2$@" - } - } - } - }, - "%@ BTC is the maximum." : { - - }, - "1 BTC to %@" : { - - }, - "BTC" : { - - }, - "Change Currencies" : { - - }, - "Currencies" : { - - }, - "Current Currency" : { - - }, - "Last updated: %@" : { - - }, - "Price Source" : { - - }, - "Sats" : { - - }, - "Selected Currencies" : { - - } - }, - "version" : "1.0" -} \ No newline at end of file diff --git a/Sources/SatsPrice/Resources/Module.xcassets/Contents.json b/Sources/SatsPrice/Resources/Module.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Sources/SatsPrice/Resources/Module.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/SatsPrice/SatsPrice.swift b/Sources/SatsPrice/SatsPrice.swift deleted file mode 100644 index ef5a551..0000000 --- a/Sources/SatsPrice/SatsPrice.swift +++ /dev/null @@ -1,6 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org - -public class SatsPriceModule { -} diff --git a/Sources/SatsPrice/SatsPriceApp.swift b/Sources/SatsPrice/SatsPriceApp.swift deleted file mode 100644 index c218656..0000000 --- a/Sources/SatsPrice/SatsPriceApp.swift +++ /dev/null @@ -1,46 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org - -import Foundation -import OSLog -import SwiftUI - -let logger: Logger = Logger(subsystem: "xyz.tyiu.SatsPrice", category: "SatsPrice") - -/// The Android SDK number we are running against, or `nil` if not running on Android -let androidSDK = ProcessInfo.processInfo.environment["android.os.Build.VERSION.SDK_INT"].flatMap({ Int($0) }) - -/// The shared data model. -private let model = try! SatsPriceModel(url: URL.documentsDirectory.appendingPathComponent("satsprice.sqlite")) - -/// The shared top-level view for the app, loaded from the platform-specific App delegates below. -/// -/// The default implementation merely loads the `ContentView` for the app and logs a message. -public struct RootView : View { - public init() { - } - - public var body: some View { - ContentView(model: model) - .task { - logger.log("Welcome to Skip on \(androidSDK != nil ? "Android" : "Darwin")!") - logger.warning("Skip app logs are viewable in the Xcode console for iOS; Android logs can be viewed in Studio or using adb logcat") - } - } -} - -#if !SKIP -public protocol SatsPriceApp : App { -} - -/// The entry point to the SatsPrice app. -/// The concrete implementation is in the SatsPriceApp module. -public extension SatsPriceApp { - var body: some Scene { - WindowGroup { - RootView() - } - } -} -#endif diff --git a/Sources/SatsPrice/SatsViewModel.swift b/Sources/SatsPrice/SatsViewModel.swift deleted file mode 100644 index 4dff5fb..0000000 --- a/Sources/SatsPrice/SatsViewModel.swift +++ /dev/null @@ -1,425 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// -// SatsViewModel.swift -// SatsPrice -// -// Created by Terry Yiu on 2/19/24. -// - -import Foundation -import SwiftUI - -class SatsViewModel: ObservableObject { - static let MAXIMUM_BTC = Decimal(21000000) - - private static let SATS_IN_BTC = Decimal(100000000) - - let model: SatsPriceModel - - @Published var lastUpdated: Date? - - @Published var priceSourceInternal: PriceSource = .coinbase - let priceFetcherDelegator = PriceFetcherDelegator(.coinbase) - - @Published var satsStringInternal: String = "" - @Published var btcStringInternal: String = "" - @Published var selectedCurrencies = Set() - @Published var currencyValueStrings: [Locale.Currency: String] = [:] - - @Published var currencyPrices: [Locale.Currency: Decimal] = [:] - @Published var currencyPriceStrings: [Locale.Currency: String] = [:] - - let currentCurrency: Locale.Currency = Locale.current.currency ?? Locale.Currency("USD") - - init(model: SatsPriceModel) { - self.model = model - } - - var currencies: [Locale.Currency] { - let commonISOCurrencyCodes = Set(Locale.commonISOCurrencyCodes) - if commonISOCurrencyCodes.contains(currentCurrency.identifier) { - return Locale.commonISOCurrencyCodes.map { Locale.Currency($0) } - } else { - var commonAndCurrentCurrencies = Locale.commonISOCurrencyCodes - commonAndCurrentCurrencies.append(currentCurrency.identifier) - commonAndCurrentCurrencies.sort() - return commonAndCurrentCurrencies.map { Locale.Currency($0) } - } - } - - @MainActor - func pullSelectedCurrenciesFromDB() async { - do { - let selectedCurrencies = Set(try await model.selectedCurrencies().compactMap { Locale.Currency($0.currencyCode) }) - let currenciesToAdd = selectedCurrencies.subtracting(self.selectedCurrencies) - let currenciesToRemove = self.selectedCurrencies.subtracting(selectedCurrencies) - - self.selectedCurrencies.subtract(currenciesToRemove) - self.selectedCurrencies.formUnion(currenciesToAdd) - } catch { - logger.error("Unable to pull selected currencies from DB. Error: \(error)") - } - } - - func addSelectedCurrency(_ currency: Locale.Currency) { - selectedCurrencies.insert(currency) - Task { - try await model.insert(SelectedCurrency(currencyCode: currency.identifier)) - } - } - - func removeSelectedCurrency(_ currency: Locale.Currency) { - selectedCurrencies.remove(currency) - Task { - try await model.deleteSelectedCurrency(currencyCode: currency.identifier) - } - } - - var priceSource: PriceSource { - get { - priceSourceInternal - } - set { - priceSourceInternal = newValue - priceFetcherDelegator.priceSource = newValue - } - } - - @MainActor - func updatePrice() async { - do { - let currencies = Set([currentCurrency] + selectedCurrencies) - let prices = try await priceFetcherDelegator.convertBTC(toCurrencies: Array(currencies)) - - currencyPrices = prices - updateCurrencyPriceStrings() - updateCurrencyValueStrings() - } catch { - clearCurrencyValueStrings() - } - lastUpdated = Date.now - } - - func updateCurrencyPriceStrings() { - currencyPriceStrings = Dictionary( - uniqueKeysWithValues: currencyPrices.map { ($0.key, $0.value.formatString(currency: $0.key)) } - ) - } - - private func priceWithoutGroupingSeparator(_ priceString: String) -> String { - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = .decimal - let decimalSeparator = numberFormatter.decimalSeparator - - return priceString.filter { - $0.isDigit || String($0) == decimalSeparator - } - } - - var satsString: String { - get { - satsStringInternal - } - set { - let oldPriceWithoutGroupingSeparator = priceWithoutGroupingSeparator(satsStringInternal) - let newPriceWithoutGroupingSeparator = priceWithoutGroupingSeparator(newValue) - - guard oldPriceWithoutGroupingSeparator != newPriceWithoutGroupingSeparator else { - return - } - - satsStringInternal = newPriceWithoutGroupingSeparator - - if let sats { -#if !SKIP - // Formatting the internal string after modifying it only if the platform is Apple. - // Apple does not seem to call get after set until after focus is moved to a different component. - // Android, on the other hand, does call get immediately after set, - // which causes text entry issues if the user keeps on entering input. - satsStringInternal = sats.formatSatsString() - - let btc = sats / SatsViewModel.SATS_IN_BTC -#else - let btc = sats.divide(SatsViewModel.SATS_IN_BTC, 20, java.math.RoundingMode.DOWN) -#endif - btcStringInternal = btc.formatBTCString() - - updateCurrencyValueStrings() - } else { - btcStringInternal = "" - clearCurrencyValueStrings() - } - } - } - - var btcString: String { - get { - btcStringInternal - } - set { - let oldPriceWithoutGroupingSeparator = priceWithoutGroupingSeparator(btcStringInternal) - let newPriceWithoutGroupingSeparator = priceWithoutGroupingSeparator(newValue) - - guard oldPriceWithoutGroupingSeparator != newPriceWithoutGroupingSeparator else { - return - } - - btcStringInternal = newPriceWithoutGroupingSeparator - - if let btc { -#if !SKIP - // Formatting the internal string after modifying it only if the platform is Apple. - // Apple does not seem to call get after set until after focus is moved to a different component. - // Android, on the other hand, does call get immediately after set, - // which causes text entry issues if the user keeps on entering input. - btcStringInternal = btc.formatBTCString() -#endif - - let sats = btc * SatsViewModel.SATS_IN_BTC - satsStringInternal = sats.formatSatsString() - - updateCurrencyValueStrings() - } else { - satsStringInternal = "" - clearCurrencyValueStrings() - } - } - } - - func updateCurrencyValueStrings(excludedCurrency: Locale.Currency? = nil) { - if let btc { - let currencies = Set([currentCurrency] + selectedCurrencies) - .filter { $0 != excludedCurrency } - - for currency in currencies { - if let btcToCurrency = btcToCurrency(for: currency) { - currencyValueStrings[currency] = (btc * btcToCurrency).formatString(currency: currency) - } else { - currencyValueStrings[currency] = "" - } - } - } else { - clearCurrencyValueStrings() - } - } - - func clearCurrencyValueStrings() { - for currency in currencyValueStrings.keys { - currencyValueStrings[currency] = "" - } - } - - func currencyValueString(for currency: Locale.Currency) -> Binding { - Binding( - get: { - self.currencyValueStrings[currency, default: ""] - }, - set: { newValue in - let oldPriceWithoutGroupingSeparator = self.priceWithoutGroupingSeparator(self.currencyValueStrings[currency] ?? "") - let newPriceWithoutGroupingSeparator = self.priceWithoutGroupingSeparator(newValue) - - guard oldPriceWithoutGroupingSeparator != newPriceWithoutGroupingSeparator else { - return - } - - self.currencyValueStrings[currency] = newPriceWithoutGroupingSeparator - - if let currencyValue = self.currencyValue(for: currency) { - if let btcToCurrency = self.currencyPrices[currency] { -#if !SKIP - let btc = currencyValue / btcToCurrency -#else - let btc = currencyValue.divide(btcToCurrency, 20, java.math.RoundingMode.DOWN) -#endif - self.btcStringInternal = btc.formatBTCString() - - let sats = btc * SatsViewModel.SATS_IN_BTC - self.satsStringInternal = sats.formatSatsString() - -#if !SKIP - // Formatting the internal string after modifying it only if the platform is Apple. - // Apple does not seem to call get after set until after focus is moved to a different component. - // Android, on the other hand, does call get immediately after set, - // which causes text entry issues if the user keeps on entering input. - self.updateCurrencyValueStrings(excludedCurrency: nil) -#else - self.updateCurrencyValueStrings(excludedCurrency: currency) -#endif - } else { - self.satsStringInternal = "" - self.btcStringInternal = "" - self.clearCurrencyValueStrings() - } - } else { - self.satsStringInternal = "" - self.btcStringInternal = "" - self.clearCurrencyValueStrings() - } - } - ) - } - - func currencyValue(for currency: Locale.Currency) -> Decimal? { - guard let currencyValueString = currencyValueStrings[currency] else { - return nil - } - -#if !SKIP - return Decimal(string: priceWithoutGroupingSeparator(currencyValueString)) -#else - do { - return Decimal(currencyValueString) - } catch { - return nil - } -#endif - } - - func btcToCurrency(for currency: Locale.Currency) -> Decimal? { - currencyPrices[currency] - } - - func btcToCurrencyString(for currency: Locale.Currency) -> Binding { - Binding( - get: { - self.currencyPriceStrings[currency, default: ""] - }, - set: { newValue in - let oldPriceWithoutGroupingSeparator = self.priceWithoutGroupingSeparator(self.currencyPriceStrings[currency, default: ""]) - let newPriceWithoutGroupingSeparator = self.priceWithoutGroupingSeparator(newValue) - - guard oldPriceWithoutGroupingSeparator != newPriceWithoutGroupingSeparator else { - return - } - - self.currencyPriceStrings[currency] = newPriceWithoutGroupingSeparator - -#if !SKIP - if let newPrice = Decimal(string: newPriceWithoutGroupingSeparator), self.currencyPrices[currency] != newPrice { - self.currencyPrices[currency] = newPrice - - // Formatting the internal string after modifying it only if the platform is Apple. - // Apple does not seem to call get after set until after focus is moved to a different - // component. Android, on the other hand, does call get immediately after set, - // which causes text entry issues if the user keeps on entering input. - self.currencyPriceStrings[currency] = newPrice.formatString(currency: currency) - - if let btc = self.btc { - self.currencyValueStrings[currency] = (btc * newPrice).formatString(currency: currency) - } else { - self.currencyValueStrings[currency] = "" - } - } -#else - do { - if let newPrice = Decimal(newPriceWithoutGroupingSeparator), self.currencyPrices[currency] != newPrice { - self.currencyPrices[currency] = newPrice - - if let btc = self.btc { - self.currencyValueStrings[currency] = (btc * newPrice).formatString() - } else { - self.currencyValueStrings[currency] = "" - } - } - } catch { - self.currencyPrices.removeValue(forKey: currency) - } -#endif - } - ) - } - - var sats: Decimal? { - let priceWithoutGroupingSeparator = priceWithoutGroupingSeparator(satsStringInternal) -#if !SKIP - return Decimal(string: priceWithoutGroupingSeparator) -#else - do { - return Decimal(priceWithoutGroupingSeparator) - } catch { - return nil - } -#endif - } - - var btc: Decimal? { - let priceWithoutGroupingSeparator = priceWithoutGroupingSeparator(btcStringInternal) -#if !SKIP - return Decimal(string: priceWithoutGroupingSeparator) -#else - do { - return Decimal(priceWithoutGroupingSeparator) - } catch { - return nil - } -#endif - } - - var exceedsMaximum: Bool { - if let btc, btc > SatsViewModel.MAXIMUM_BTC { - return true - } - return false - } -} - -extension Decimal { - func formatString() -> String { -#if !SKIP - return String(describing: self) -#else - return stripTrailingZeros().toPlainString() -#endif - } - - func formatString(minimumFractionDigits: Int, maximumFractionDigits: Int, usesGroupingSeparator: Bool) -> String { - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = .decimal - numberFormatter.minimumFractionDigits = minimumFractionDigits - numberFormatter.maximumFractionDigits = maximumFractionDigits - numberFormatter.usesGroupingSeparator = usesGroupingSeparator -#if !SKIP - return numberFormatter.string(from: NSDecimalNumber(decimal: self)) ?? String(describing: self) -#else - return numberFormatter.string(from: android.icu.math.BigDecimal(self as java.math.BigDecimal) as NSNumber) ?? stripTrailingZeros().toPlainString() -#endif - } - - func formatSatsString() -> String { - formatString(minimumFractionDigits: 0, maximumFractionDigits: 0, usesGroupingSeparator: true) - } - - func formatBTCString() -> String { - formatString(minimumFractionDigits: 0, maximumFractionDigits: 8, usesGroupingSeparator: true) - } - - func formatString(currency: Locale.Currency) -> String { -#if !SKIP - let currencyFormatter = NumberFormatter() - currencyFormatter.numberStyle = .currency - currencyFormatter.currencyCode = currency.identifier - - return formatString( - minimumFractionDigits: currencyFormatter.minimumFractionDigits, - maximumFractionDigits: currencyFormatter.maximumFractionDigits, - usesGroupingSeparator: currencyFormatter.usesGroupingSeparator - ) -#else - let javaCurrency = java.util.Currency.getInstance(currency.identifier) - return formatString( - minimumFractionDigits: javaCurrency.getDefaultFractionDigits(), - maximumFractionDigits: javaCurrency.getDefaultFractionDigits(), - usesGroupingSeparator: true - ) -#endif - } -} - -private extension Character { - var isDigit: Bool { - self >= "0" && self <= "9" - } -} diff --git a/Sources/SatsPrice/Skip/skip.yml b/Sources/SatsPrice/Skip/skip.yml deleted file mode 100644 index f0bf5ee..0000000 --- a/Sources/SatsPrice/Skip/skip.yml +++ /dev/null @@ -1,3 +0,0 @@ -# Configuration file for https://skip.tools project -build: - contents: diff --git a/Tests/SatsPriceTests/SatsViewModelTests.swift b/Tests/SatsPriceTests/SatsViewModelTests.swift deleted file mode 100644 index 8a2ad7a..0000000 --- a/Tests/SatsPriceTests/SatsViewModelTests.swift +++ /dev/null @@ -1,105 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org -// -// SatsViewModelTests.swift -// SatsPriceTests -// -// Created by Terry Yiu on 2/19/24. -// - -import Foundation -import XCTest -@testable import SatsPrice - -final class SatsViewModelTests: XCTestCase { - - let currency = Locale.Currency("USD") - - func testSatsViewModel() throws { - let satsPriceModel = try XCTUnwrap(SatsPriceModel(url: nil)) - let satsViewModel = SatsViewModel(model: satsPriceModel) - satsViewModel.btcToCurrencyString(for: currency).wrappedValue = "54321" - - // Test BTC updates. - satsViewModel.btcString = "1" -#if !SKIP - XCTAssertEqual(satsViewModel.btc, Decimal(string: "1")) - XCTAssertEqual(satsViewModel.sats, Decimal(string: "100000000")) - XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal(string: "54321")) -#else - XCTAssertEqual(satsViewModel.btc, Decimal("1")) - XCTAssertEqual(satsViewModel.sats, Decimal("100000000")) - XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal("54321")) -#endif - XCTAssertEqual(satsViewModel.btcString, "1") - XCTAssertEqual(satsViewModel.satsString, "100,000,000") - XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "54,321.00") - - // Test Sats updates. - satsViewModel.satsString = "200000000" -#if !SKIP - XCTAssertEqual(satsViewModel.btc, Decimal(string: "2")) - XCTAssertEqual(satsViewModel.sats, Decimal(string: "200000000")) - XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal(string: "108642")) -#else - XCTAssertEqual(satsViewModel.btc, Decimal("2")) - XCTAssertEqual(satsViewModel.sats, Decimal("200000000")) - XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal("108642")) -#endif - XCTAssertEqual(satsViewModel.btcString, "2") - XCTAssertEqual(satsViewModel.satsString, "200,000,000") - XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "108,642.00") - - // Test currency value updates. - satsViewModel.currencyValueString(for: currency).wrappedValue = "162963" -#if !SKIP - XCTAssertEqual(satsViewModel.btc, Decimal(string: "3")) - XCTAssertEqual(satsViewModel.sats, Decimal(string: "300000000")) - XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal(string: "162963")) -#else - XCTAssertEqual(satsViewModel.btc, Decimal("3")) - XCTAssertEqual(satsViewModel.sats, Decimal("300000000")) - XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal("162963")) -#endif - XCTAssertEqual(satsViewModel.btcString, "3") - XCTAssertEqual(satsViewModel.satsString, "300,000,000") - XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "162,963.00") - - // Test fractional amounts. - // Precision between platforms on this calculation is different so we have different assertions for each. - satsViewModel.currencyValueString(for: currency).wrappedValue = "1" -#if !SKIP - XCTAssertEqual(satsViewModel.btc, Decimal(string: "0.00001841")) - XCTAssertEqual(satsViewModel.btcString, "0.00001841") - XCTAssertEqual(satsViewModel.sats, Decimal(string: "1841")) - XCTAssertEqual(satsViewModel.satsString, "1,841") - XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal(string: "1")) -#else - XCTAssertEqual(satsViewModel.btc, Decimal("0.00001841")) - XCTAssertEqual(satsViewModel.btcString, "0.00001841") - XCTAssertEqual(satsViewModel.sats, Decimal("1841")) - XCTAssertEqual(satsViewModel.satsString, "1,841") - XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal("1")) -#endif - XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "1.00") - - // Test large amounts that exceed the cap of 21M BTC. - satsViewModel.currencyValueString(for: currency).wrappedValue = "11407419999999" -#if !SKIP - XCTAssertEqual(satsViewModel.btc, Decimal(string: "210000184.09084884")) - XCTAssertEqual(satsViewModel.btcString, "210,000,184.09084884") - XCTAssertEqual(satsViewModel.sats, Decimal(string: "21000018409084884")) - XCTAssertEqual(satsViewModel.satsString, "21,000,018,409,084,884") - XCTAssertEqual(satsViewModel.currencyValue(for: currency), Decimal(string: "11407419999999")) -#else - XCTAssertEqual(satsViewModel.btc, Decimal("210000184.09084884")) - XCTAssertEqual(satsViewModel.btcString, "210000184.09084884") - XCTAssertEqual(satsViewModel.sats, Decimal("21000018409084884")) - XCTAssertEqual(satsViewModel.satsString, "21000018409084884") - XCTAssertEqual(satsViewModel.currencyValue, Decimal("11407419999999")) -#endif - XCTAssertEqual(satsViewModel.currencyValueString(for: currency).wrappedValue, "11,407,419,999,999.00") - } - -} diff --git a/Tests/SatsPriceTests/Skip/skip.yml b/Tests/SatsPriceTests/Skip/skip.yml deleted file mode 100644 index f7eb021..0000000 --- a/Tests/SatsPriceTests/Skip/skip.yml +++ /dev/null @@ -1,3 +0,0 @@ -# Configuration file for https://skip.tools project -#build: -# contents: \ No newline at end of file diff --git a/Tests/SatsPriceTests/XCSkipTests.swift b/Tests/SatsPriceTests/XCSkipTests.swift deleted file mode 100644 index d9edbba..0000000 --- a/Tests/SatsPriceTests/XCSkipTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// This is free software: you can redistribute and/or modify it -// under the terms of the GNU General Public License 3.0 -// as published by the Free Software Foundation https://fsf.org - -import Foundation -#if os(macOS) // Skip transpiled tests only run on macOS targets -import SkipTest - -/// This test case will run the transpiled tests for the Skip module. -@available(macOS 13, macCatalyst 16, *) -final class XCSkipTests: XCTestCase, XCGradleHarness { - public func testSkipModule() async throws { - // Run the transpiled JUnit tests for the current test module. - // These tests will be executed locally using Robolectric. - // Connected device or emulator tests can be run by setting the - // `ANDROID_SERIAL` environment variable to an `adb devices` - // ID in the scheme's Run settings. - // - // Note that it isn't currently possible to filter the tests to run. - try await runGradleTests() - } -} -#endif - -/// True when running in a transpiled Java runtime environment -let isJava = ProcessInfo.processInfo.environment["java.io.tmpdir"] != nil -/// True when running within an Android environment (either an emulator or device) -let isAndroid = isJava && ProcessInfo.processInfo.environment["ANDROID_ROOT"] != nil -/// True is the transpiled code is currently running in the local Robolectric test environment -let isRobolectric = isJava && !isAndroid -/// True if the system's `Int` type is 32-bit. -let is32BitInteger = Int64(Int.max) == Int64(Int32.max) diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts new file mode 100644 index 0000000..36cac3b --- /dev/null +++ b/androidApp/build.gradle.kts @@ -0,0 +1,54 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeCompiler) +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + } +} +dependencies { + implementation(project(":shared")) + + implementation(libs.androidx.activity.compose) + + implementation(libs.compose.uiToolingPreview) + debugImplementation(libs.compose.uiTooling) +} + +android { + namespace = "xyz.tyiu.satsprice" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + applicationId = "xyz.tyiu.SatsPrice" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = 11 + versionName = "2.0.0" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } +} \ No newline at end of file diff --git a/androidApp/proguard-rules.pro b/androidApp/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/androidApp/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cdba621 --- /dev/null +++ b/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/androidApp/src/main/kotlin/xyz/tyiu/satsprice/MainActivity.kt b/androidApp/src/main/kotlin/xyz/tyiu/satsprice/MainActivity.kt new file mode 100644 index 0000000..a887cc8 --- /dev/null +++ b/androidApp/src/main/kotlin/xyz/tyiu/satsprice/MainActivity.kt @@ -0,0 +1,25 @@ +package xyz.tyiu.satsprice + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + setContent { + App() + } + } +} + +@Preview +@Composable +fun AppAndroidPreview() { + App() +} \ No newline at end of file diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..5bc112a Binary files /dev/null and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..5bc112a Binary files /dev/null and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..0f6284c Binary files /dev/null and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..0f6284c Binary files /dev/null and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..9d5ee17 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..9d5ee17 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..c03cb07 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..c03cb07 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..27f2460 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..27f2460 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml new file mode 100644 index 0000000..f35dea9 --- /dev/null +++ b/androidApp/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + SatsPrice + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..d83d348 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + // this is necessary to avoid the plugins to be loaded multiple times + // in each subproject's classloader + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidMultiplatformLibrary) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + alias(libs.plugins.kotlinJvm) apply false + alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.kotlinSerialization) apply false +} \ No newline at end of file diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts new file mode 100644 index 0000000..37374ee --- /dev/null +++ b/desktopApp/build.gradle.kts @@ -0,0 +1,38 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) +} + +dependencies { + implementation(project(":shared")) + + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutinesSwing) + + implementation(libs.compose.uiToolingPreview) +} + +compose.desktop { + application { + mainClass = "xyz.tyiu.satsprice.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "xyz.tyiu.satsprice" + packageVersion = "1.0.0" + + macOS { + iconFile.set(project.file("icons/icon.icns")) + } + windows { + iconFile.set(project.file("icons/icon.ico")) + } + linux { + iconFile.set(project.file("icons/icon.png")) + } + } + } +} \ No newline at end of file diff --git a/desktopApp/icons/icon.icns b/desktopApp/icons/icon.icns new file mode 100644 index 0000000..59f2ef1 Binary files /dev/null and b/desktopApp/icons/icon.icns differ diff --git a/desktopApp/icons/icon.ico b/desktopApp/icons/icon.ico new file mode 100644 index 0000000..40663f5 Binary files /dev/null and b/desktopApp/icons/icon.ico differ diff --git a/desktopApp/icons/icon.png b/desktopApp/icons/icon.png new file mode 100644 index 0000000..de8e6d3 Binary files /dev/null and b/desktopApp/icons/icon.png differ diff --git a/desktopApp/src/main/kotlin/xyz/tyiu/satsprice/main.kt b/desktopApp/src/main/kotlin/xyz/tyiu/satsprice/main.kt new file mode 100644 index 0000000..025b753 --- /dev/null +++ b/desktopApp/src/main/kotlin/xyz/tyiu/satsprice/main.kt @@ -0,0 +1,15 @@ +package xyz.tyiu.satsprice + +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "SatsPrice", + icon = painterResource("icon.png"), + ) { + App() + } +} \ No newline at end of file diff --git a/desktopApp/src/main/resources/icon.png b/desktopApp/src/main/resources/icon.png new file mode 100644 index 0000000..90a18d0 Binary files /dev/null and b/desktopApp/src/main/resources/icon.png differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..ee114b2 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,10 @@ +#Kotlin +kotlin.code.style=official +kotlin.daemon.jvmargs=-Xmx3072M +#Gradle +org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 +org.gradle.configuration-cache=true +org.gradle.caching=true +#Android +android.nonTransitiveRClass=true +android.useAndroidX=true \ No newline at end of file diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..5301309 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e8f7f2860374272b4c64643d36d8f0dc/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/77785cb3076de1b8f41fcbc6540556c0/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e8f7f2860374272b4c64643d36d8f0dc/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/77785cb3076de1b8f41fcbc6540556c0/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/3932798aff0bd10c6286eaaff40c6f0a/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/6fa62408db9bcb00d7e180251d5f068c/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e8f7f2860374272b4c64643d36d8f0dc/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/77785cb3076de1b8f41fcbc6540556c0/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/da24493caf79e03d5c254365c5ad7ac1/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/df839bfdbcd8a8e829b9cf1be59c478b/redirect +toolchainVendor=AZUL +toolchainVersion=21 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..ad7a2bc --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,68 @@ +[versions] +agp = "9.0.1" +android-compileSdk = "36" +android-minSdk = "24" +android-targetSdk = "36" +androidx-activity = "1.13.0" +androidx-appcompat = "1.7.1" +androidx-core = "1.19.0" +androidx-espresso = "3.7.0" +androidx-lifecycle = "2.11.0-beta01" +androidx-testExt = "1.3.0" +composeMultiplatform = "1.11.1" +composeMaterialIcons = "1.7.3" +junit = "4.13.2" +kotlin = "2.4.10" +kotlin-wrappers = "2026.7.7" +kotlinx-coroutines = "1.11.0" +kotlinx-datetime = "0.8.0" +kotlinx-serialization = "1.11.0" +ktor = "3.5.2" +bignum = "0.3.10" +material3 = "1.11.0-alpha07" +mokoResources = "0.26.4" + +[libraries] +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +junit = { module = "junit:junit", version.ref = "junit" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" } +androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } +compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" } +androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } +compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" } +compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" } +compose-materialIconsCore = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "composeMaterialIcons" } +compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } +compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } +moko-resources = { module = "dev.icerock.moko:resources", version.ref = "mokoResources" } +moko-resourcesCompose = { module = "dev.icerock.moko:resources-compose", version.ref = "mokoResources" } +kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } +kotlinx-coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +wrappers-browser = { module = "org.jetbrains.kotlin-wrappers:kotlin-browser", version.ref = "kotlin-wrappers" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinxJson = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } +bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "bignum" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } +composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } +composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +mokoResources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "mokoResources" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..75927bd --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,8 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/icons/source-1024.png b/icons/source-1024.png new file mode 100644 index 0000000..94d30c7 Binary files /dev/null and b/icons/source-1024.png differ diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig new file mode 100644 index 0000000..83ffed1 --- /dev/null +++ b/iosApp/Configuration/Config.xcconfig @@ -0,0 +1,7 @@ +TEAM_ID= + +PRODUCT_NAME=SatsPrice +PRODUCT_BUNDLE_IDENTIFIER=xyz.tyiu.SatsPrice + +CURRENT_PROJECT_VERSION=11 +MARKETING_VERSION=2.0.0 \ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..af35cb2 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,400 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + D44B8BA1944015B589E35F36 /* SatsPrice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SatsPrice.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + D262C3356B38E7E13FFE64E4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 3739940346DC183122FC20F8 /* iosApp */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 24D66E9CCFCEB2D844328B79 /* Configuration */ = { + isa = PBXFileSystemSynchronizedRootGroup; + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = Configuration; + sourceTree = ""; + }; + 55D8B06C58654F251C5B3381 /* iosApp */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D262C3356B38E7E13FFE64E4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = iosApp; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + C2B5A146484C5EC128AEFD65 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 5941F29C28B5C326171BDC0A = { + isa = PBXGroup; + children = ( + 24D66E9CCFCEB2D844328B79 /* Configuration */, + 55D8B06C58654F251C5B3381 /* iosApp */, + BD6824195A13970C074F9FAE /* Products */, + ); + sourceTree = ""; + }; + BD6824195A13970C074F9FAE /* Products */ = { + isa = PBXGroup; + children = ( + D44B8BA1944015B589E35F36 /* SatsPrice.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3739940346DC183122FC20F8 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = B1A19F3C0B96B70D9AB9599E /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + 20CB400CC39EEB23909EAA0F /* Compile Kotlin Framework */, + BC432998CDD86B2CBAA34707 /* Copy Moko Resources */, + 5BCD356C334571BCAA0967CA /* Sources */, + C2B5A146484C5EC128AEFD65 /* Frameworks */, + 96EB3DBC5DECE4CD4D246992 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 55D8B06C58654F251C5B3381 /* iosApp */, + ); + name = iosApp; + packageProductDependencies = ( + ); + productName = iosApp; + productReference = D44B8BA1944015B589E35F36 /* SatsPrice.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8882155D13AD2F794C45B4D1 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 3739940346DC183122FC20F8 = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = 3FD2DE231BB18DD5222C67BE /* Build configuration list for PBXProject "iosApp" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5941F29C28B5C326171BDC0A; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = BD6824195A13970C074F9FAE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3739940346DC183122FC20F8 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 96EB3DBC5DECE4CD4D246992 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 20CB400CC39EEB23909EAA0F /* Compile Kotlin Framework */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n"; + }; + BC432998CDD86B2CBAA34707 /* Copy Moko Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Moko Resources"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/../gradlew\" -p \"$SRCROOT/../\" :shared:copyFrameworkResourcesToApp \\\n -Pmoko.resources.PLATFORM_NAME=\"$PLATFORM_NAME\" \\\n -Pmoko.resources.CONFIGURATION=\"$CONFIGURATION\" \\\n -Pmoko.resources.ARCHS=\"$ARCHS\" \\\n -Pmoko.resources.BUILT_PRODUCTS_DIR=\"$BUILT_PRODUCTS_DIR\" \\\n -Pmoko.resources.CONTENTS_FOLDER_PATH=\"$CONTENTS_FOLDER_PATH\" \n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5BCD356C334571BCAA0967CA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + ABCADF8A4EA6428EC4290AD1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + EFC1D6A447B1BE51E6D06B01 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 24D66E9CCFCEB2D844328B79 /* Configuration */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F40BE5646F9865EB7D8E4EAA /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 24D66E9CCFCEB2D844328B79 /* Configuration */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F92E24F6E6DFD929A8901A4E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3FD2DE231BB18DD5222C67BE /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F40BE5646F9865EB7D8E4EAA /* Debug */, + EFC1D6A447B1BE51E6D06B01 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B1A19F3C0B96B70D9AB9599E /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ABCADF8A4EA6428EC4290AD1 /* Debug */, + F92E24F6E6DFD929A8901A4E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8882155D13AD2F794C45B4D1 /* Project object */; +} diff --git a/Darwin/SatsPrice.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 90% rename from Darwin/SatsPrice.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 919434a..fe1aa71 100644 --- a/Darwin/SatsPrice.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,4 @@ - + \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..e8d1d0d --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors": [ + { + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9bb756d --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,36 @@ +{ + "images": [ + { + "filename": "app-icon-1024.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ], + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png new file mode 100644 index 0000000..94d30c7 Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png differ diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..c47b5f2 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift new file mode 100644 index 0000000..27c8f2e --- /dev/null +++ b/iosApp/iosApp/ContentView.swift @@ -0,0 +1,218 @@ +import Shared +import SwiftUI + +struct ContentView: View { + @StateObject private var viewModel = ConverterViewModel() + @State private var showCurrencyPicker = false + + var body: some View { + NavigationView { + Group { + if let state = viewModel.state { + form(for: state) + } else { + ProgressView() + } + } + .navigationTitle("SatsPrice") + } + } + + @ViewBuilder + private func form(for state: IosConverterState) -> some View { + Form { + Section( + header: Text(IosLocalizationKt.localizedFormattedString( + resource: MR.strings.shared.btc_to_currency, + args: [state.defaultCurrencyCode] + )), + footer: Text(state.statusLine) + ) { + Picker( + IosLocalizationKt.localizedString(resource: MR.strings.shared.price_source), + selection: Binding( + get: { state.sourceName }, + set: { viewModel.onSourceSelected($0) } + ) + ) { + ForEach(viewModel.sourceNames, id: \.self) { name in + Text(name).tag(name) + } + } + + HStack { + if state.isManualSource { + NumericField( + placeholder: IosLocalizationKt.localizedString(resource: MR.strings.shared.rate_label), + value: state.manualRateInput, + keyboardType: .decimalPad, + sanitize: sanitizeDecimalInput, + onChange: { viewModel.onManualRateChanged($0) } + ) + } else if !state.defaultCurrencyRate.isEmpty { + Text(state.defaultCurrencyRate) + Spacer() + } else { + Spacer() + } + if state.isLoading { + ProgressView() + } else { + Button { + viewModel.refresh() + } label: { + Image(systemName: "arrow.clockwise") + } + .buttonStyle(.borderless) + } + } + } + + if let error = state.errorMessage { + Section { + HStack { + Text(error).foregroundColor(.red) + Spacer() + Button(IosLocalizationKt.localizedString(resource: MR.strings.shared.retry)) { viewModel.refresh() } + } + } + } + + Section(IosLocalizationKt.localizedString(resource: MR.strings.shared.bitcoin_section_title)) { + amountRow( + label: IosLocalizationKt.localizedString(resource: MR.strings.shared.sats_label), + value: state.satsAmount, + keyboardType: .numberPad, + sanitize: sanitizeIntegerInput, + onChange: { viewModel.onSatsAmountChanged($0) } + ) + amountRow( + label: IosLocalizationKt.localizedString(resource: MR.strings.shared.btc_label), + value: state.btcAmount, + keyboardType: .decimalPad, + sanitize: sanitizeDecimalInput, + onChange: { viewModel.onBtcAmountChanged($0) } + ) + if state.exceedsMaxSupply { + Text(IosLocalizationKt.localizedString(resource: MR.strings.shared.exceeds_max_supply)) + .font(.caption) + .foregroundColor(.red) + } + } + + Section(IosLocalizationKt.localizedString(resource: MR.strings.shared.currencies_section_title)) { + Button( + state.selectedCurrencyCodes.isEmpty + ? IosLocalizationKt.localizedString(resource: MR.strings.shared.add_currency) + : IosLocalizationKt.localizedFormattedString( + resource: MR.strings.shared.currencies_selected_count, + args: [state.selectedCurrencyCodes.count] + ) + ) { + showCurrencyPicker = true + } + + ForEach(state.fiatRows, id: \.code) { row in + amountRow( + label: row.code, + value: row.amount, + keyboardType: .decimalPad, + sanitize: sanitizeDecimalInput, + onChange: { viewModel.onFiatAmountChanged(code: row.code, value: $0) } + ) + } + .onDelete { indexSet in + for index in indexSet { + viewModel.onFiatCurrencyToggled(state.fiatRows[index].code) + } + } + } + } + .sheet(isPresented: $showCurrencyPicker) { + CurrencyPickerSheet( + currencies: state.availableCurrencies, + selectedCodes: state.selectedCurrencyCodes, + localeCurrencyCode: state.localeCurrencyCode, + onToggle: { viewModel.onFiatCurrencyToggled($0) } + ) + } + } + + @ViewBuilder + private func amountRow( + label: String, + value: String, + keyboardType: UIKeyboardType, + sanitize: @escaping (String) -> String, + onChange: @escaping (String) -> Void + ) -> some View { + HStack { + Text(label) + Spacer() + NumericField( + placeholder: label, + value: value, + keyboardType: keyboardType, + sanitize: sanitize, + onChange: onChange, + alignment: .trailing + ) + } + } +} + +/// A text field whose displayed text is always forced back to [sanitize]'s output, even for +/// characters typed via a hardware keyboard or pasted in — `.keyboardType` alone only restricts +/// the on-screen keyboard's layout, and a `TextField` bound purely to external/async state +/// (like the shared Kotlin `PriceViewModel`'s `StateFlow`) doesn't reliably self-correct what a +/// user has already typed into its own internal editing buffer. Local `@State` does, since +/// SwiftUI re-renders a view synchronously whenever its own `@State` changes. +private struct NumericField: View { + let placeholder: String + let value: String + let keyboardType: UIKeyboardType + let sanitize: (String) -> String + let onChange: (String) -> Void + var alignment: TextAlignment = .leading + + @State private var text: String = "" + + var body: some View { + TextField(placeholder, text: $text) + .keyboardType(keyboardType) + .multilineTextAlignment(alignment) + .onAppear { text = value } + .onChange(of: text) { _, newValue in + let sanitized = sanitize(newValue) + if sanitized != newValue { + text = sanitized + } + if sanitized != value { + onChange(sanitized) + } + } + .onChange(of: value) { _, newValue in + if newValue != text { + text = newValue + } + } + } +} + +private func sanitizeDecimalInput(_ raw: String) -> String { + var result = "" + var seenDot = false + for char in raw { + if char.isNumber { + result.append(char) + } else if char == "." && !seenDot { + result.append(char) + seenDot = true + } + } + return result +} + +private func sanitizeIntegerInput(_ raw: String) -> String { + raw.filter { $0.isNumber } +} diff --git a/iosApp/iosApp/ConverterViewModel.swift b/iosApp/iosApp/ConverterViewModel.swift new file mode 100644 index 0000000..24c40a2 --- /dev/null +++ b/iosApp/iosApp/ConverterViewModel.swift @@ -0,0 +1,45 @@ +import Foundation +import Shared + +@MainActor +final class ConverterViewModel: ObservableObject { + @Published private(set) var state: IosConverterState? + + private let bridge = IosPriceViewModel() + + var sourceNames: [String] { bridge.sourceNames } + + init() { + bridge.observeState { [weak self] newState in + self?.state = newState + } + } + + func refresh() { + bridge.refresh() + } + + func onBtcAmountChanged(_ value: String) { + bridge.onBtcAmountChanged(value: value) + } + + func onSatsAmountChanged(_ value: String) { + bridge.onSatsAmountChanged(value: value) + } + + func onFiatAmountChanged(code: String, value: String) { + bridge.onFiatAmountChanged(code: code, value: value) + } + + func onFiatCurrencyToggled(_ code: String) { + bridge.onFiatCurrencyToggled(code: code) + } + + func onSourceSelected(_ name: String) { + bridge.onSourceSelected(name: name) + } + + func onManualRateChanged(_ value: String) { + bridge.onManualRateChanged(value: value) + } +} diff --git a/iosApp/iosApp/CurrencyPickerSheet.swift b/iosApp/iosApp/CurrencyPickerSheet.swift new file mode 100644 index 0000000..0be08b3 --- /dev/null +++ b/iosApp/iosApp/CurrencyPickerSheet.swift @@ -0,0 +1,52 @@ +import Shared +import SwiftUI + +struct CurrencyPickerSheet: View { + let currencies: [CurrencyInfo] + let selectedCodes: [String] + let localeCurrencyCode: String? + let onToggle: (String) -> Void + + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationView { + List(currencies, id: \.code) { info in + Button { + onToggle(info.code) + } label: { + HStack { + Text(currencyLabel(for: info)) + .foregroundColor(.primary) + Spacer() + if selectedCodes.contains(info.code) { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + } + } + .navigationTitle(IosLocalizationKt.localizedString(resource: MR.strings.shared.currencies_section_title)) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(IosLocalizationKt.localizedString(resource: MR.strings.shared.done)) { dismiss() } + } + } + } + } + + private func currencyLabel(for info: CurrencyInfo) -> String { + if info.code == localeCurrencyCode { + return IosLocalizationKt.localizedFormattedString( + resource: MR.strings.shared.currency_option_label_local, + args: [info.code, info.displayName] + ) + } else { + return IosLocalizationKt.localizedFormattedString( + resource: MR.strings.shared.currency_option_label, + args: [info.code, info.displayName] + ) + } + } +} diff --git a/Darwin/Info.plist b/iosApp/iosApp/Info.plist similarity index 61% rename from Darwin/Info.plist rename to iosApp/iosApp/Info.plist index 6d5f8f6..f7cb731 100644 --- a/Darwin/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -1,8 +1,8 @@ - - ITSAppUsesNonExemptEncryption - - - + + CADisableMinimumFrameDurationOnPhone + + + \ No newline at end of file diff --git a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..c47b5f2 --- /dev/null +++ b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift new file mode 100644 index 0000000..927e0b9 --- /dev/null +++ b/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/kotlin-js-store/wasm/yarn.lock b/kotlin-js-store/wasm/yarn.lock new file mode 100644 index 0000000..249cfeb --- /dev/null +++ b/kotlin-js-store/wasm/yarn.lock @@ -0,0 +1,266 @@ +# 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== + +"@js-joda/timezone@2.25.1": + version "2.25.1" + resolved "https://registry.yarnpkg.com/@js-joda/timezone/-/timezone-2.25.1.tgz#4628f18d51af1b197a410834d344418f48a8a264" + integrity sha512-s79ts8bXrWqM9dIBKc0AdgGuAUFpu9gmzYhOCPHJlks/Sf7FSbJHRauWlFYUwjSTZevimqthEvJycrwrVz5m4g== + +"@messageformat/core@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@messageformat/core/-/core-3.1.0.tgz#d4d2f5c3555228a6b5980b122a02b53dfc6458bd" + integrity sha512-UxAnjecnRG4u2iaggwIyylYPHmk5BTErJcKmWyAKTXqYgSW1bFLp4D7fIzuh6bk17Qfcmf3qtufdrstCB23nBA== + dependencies: + "@messageformat/date-skeleton" "^1.0.0" + "@messageformat/number-skeleton" "^1.0.0" + "@messageformat/parser" "^5.0.0" + "@messageformat/runtime" "^3.0.1" + make-plural "^7.0.0" + safe-identifier "^0.4.1" + +"@messageformat/date-skeleton@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz#3bad068cbf5873d14592cfc7a73dd4d8615e2739" + integrity sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A== + +"@messageformat/number-skeleton@^1.0.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz#e7c245c41a1b2722bc59dad68f4d454f761bc9b4" + integrity sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg== + +"@messageformat/parser@^5.0.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@messageformat/parser/-/parser-5.1.1.tgz#ca7d6c18e9f3f6b6bc984a465dac16da00106055" + integrity sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg== + dependencies: + moo "^0.5.1" + +"@messageformat/runtime@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@messageformat/runtime/-/runtime-3.0.2.tgz#7186cd772c55298cb5d2f2ec402af266db06af15" + integrity sha512-dkIPDCjXcfhSHgNE1/qV6TeczQZR59Yx0xXeafVKgK3QVWoxc38ljwpksUpnzCGvN151KUbCJTDZVmahtf1YZw== + dependencies: + make-plural "^7.0.0" + +"@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== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.20.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.20.0.tgz#304b3636add88ba7d936760dd50ece006dea95f9" + integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +bcp-47@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bcp-47/-/bcp-47-2.1.0.tgz#7e80734c3338fe8320894981dccf4968c3092df6" + integrity sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w== + dependencies: + is-alphabetical "^2.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + +css-loader@6.7.3: + version "6.7.3" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.3.tgz#1e8799f3ccc5874fdd55461af51137fcc5befbcd" + integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.19" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.3.8" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-uri@^3.0.1: + version "3.1.5" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.5.tgz#610f37419a030270430cecd68d74e3d4d96725d0" + integrity sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw== + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + +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== + +make-plural@^7.0.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-7.5.0.tgz#ff99598640d82ec7a69bea15d34ecc50152fc0db" + integrity sha512-0booA+aVYyVFoR67JBHdfVk0U08HmrBH2FrtmBqBa+NldlqXv/G2Z9VQuQq6Wgp2jDWdybEWGfBkk1cq5264WA== + +mini-css-extract-plugin@2.7.5: + version "2.7.5" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz#afbb344977659ec0f1f6e050c7aea456b121cfc5" + integrity sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ== + dependencies: + schema-utils "^4.0.0" + +moo@^0.5.1: + version "0.5.3" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.3.tgz#dfcdb40ff6f1a03c34af5df7f9717cecfa88c38c" + integrity sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA== + +nanoid@^3.3.17: + version "3.3.18" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913" + integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w== + +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== + +postcss-modules-extract-imports@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== + +postcss-modules-local-by-default@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368" + integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c" + integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-selector-parser@^7.0.0: + version "7.1.5" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz#ee7b090724eb60739b530660e5b402200e694a97" + integrity sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.19: + version "8.5.26" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620" + integrity sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ== + dependencies: + nanoid "^3.3.17" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +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== + +safe-identifier@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb" + integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w== + +schema-utils@^4.0.0: + 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" + +semver@^7.3.8: + version "7.8.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" + integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== + +source-map-js@^1.2.1: + 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== + +style-loader@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.2.tgz#eaebca714d9e462c19aa1e3599057bc363924899" + integrity sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw== + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +ws@8.20.1: + version "8.20.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.20.1.tgz#91a9ae2b312ccf98e0a85ec499b48cef45ab0ddb" + integrity sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w== diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock new file mode 100644 index 0000000..30fecbc --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,3555 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@^0.6.1": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz#f13c7c205915eb91ae54c557f5e92bddd8be0e83" + integrity sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + +"@js-joda/timezone@2.25.1": + version "2.25.1" + resolved "https://registry.yarnpkg.com/@js-joda/timezone/-/timezone-2.25.1.tgz#4628f18d51af1b197a410834d344418f48a8a264" + integrity sha512-s79ts8bXrWqM9dIBKc0AdgGuAUFpu9gmzYhOCPHJlks/Sf7FSbJHRauWlFYUwjSTZevimqthEvJycrwrVz5m4g== + +"@jsonjoy.com/base64@17.67.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-17.67.0.tgz#7eeda3cb41138d77a90408fd2e42b2aba10576d7" + integrity sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw== + +"@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@17.67.0", "@jsonjoy.com/buffers@^17.65.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz#5c58dbcdeea8824ce296bd1cfce006c2eb167b3d" + integrity sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw== + +"@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@17.67.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz#3635fd8769d77e19b75dc5574bc9756019b2e591" + integrity sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q== + +"@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/fs-core@4.68.1": + version "4.68.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-core/-/fs-core-4.68.1.tgz#15fed282af2a8efbd1a335a83ae1bd3507fa0237" + integrity sha512-V5oZ4Gt9WJKyQef0n9cAd0N9qjSkIBm3E4MYsgNIWBk5aINCDPKxMPo1i29rBxqiT4Ixf1epklqV9VJMKIxwlw== + dependencies: + "@jsonjoy.com/fs-node-builtins" "4.68.1" + "@jsonjoy.com/fs-node-utils" "4.68.1" + thingies "^2.5.0" + +"@jsonjoy.com/fs-fsa@4.68.1": + version "4.68.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-fsa/-/fs-fsa-4.68.1.tgz#009ebc90396622d37df984691fbe8bad51fdedea" + integrity sha512-HCG72UioncuO7Gw09XNVG+S85e3cq2hrUC/mexBrsWsa3mI7eePkkqWie3uVYbtsb64OR9YGQs5SqaufDRYBcg== + dependencies: + "@jsonjoy.com/fs-core" "4.68.1" + "@jsonjoy.com/fs-node-builtins" "4.68.1" + "@jsonjoy.com/fs-node-utils" "4.68.1" + thingies "^2.5.0" + +"@jsonjoy.com/fs-node-builtins@4.68.1": + version "4.68.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.68.1.tgz#3c4ee9877e12d76eab8611663bea127d0e261fc5" + integrity sha512-HK1BTksysokNZxNspqDH0yPaqN9YgR/AYIlYiIaU2Ys4BOk5CdybI7r6BgiZuiiPiV8n4sK/kZdice7Znpy2Kw== + +"@jsonjoy.com/fs-node-to-fsa@4.68.1": + version "4.68.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.68.1.tgz#d0dab7f007f9e01dcd1e62d2e27ec0fdd0126554" + integrity sha512-lpKmU4X9e/oh8GIuAI7EXaS5QiLNM3KD15CkdhfS6PYmrGvoJqKQcyEfnLgnnaGslh/PFUMYSIZBCf2ejJGw8g== + dependencies: + "@jsonjoy.com/fs-fsa" "4.68.1" + "@jsonjoy.com/fs-node-builtins" "4.68.1" + "@jsonjoy.com/fs-node-utils" "4.68.1" + +"@jsonjoy.com/fs-node-utils@4.68.1": + version "4.68.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.68.1.tgz#25fa70c1c11986eef75c1e6c52caea9fe2e69778" + integrity sha512-/GxfW1DWm9SCdkfbvqevLO/P5duobQfmKkHXxdMIDbcZMQeAgooAstIfZhkXpATzq9QbCQsnoWFM/dGHdZfndw== + dependencies: + "@jsonjoy.com/fs-node-builtins" "4.68.1" + glob-to-regex.js "^1.0.1" + +"@jsonjoy.com/fs-node@4.68.1": + version "4.68.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node/-/fs-node-4.68.1.tgz#0ca716e6c3ef5ed8887fbd4b4557094f99293d19" + integrity sha512-R5D9mWtqdURzcOWj1vdXr3APCwX0xchtFT+kmW7fXLNDifWdDrnh26jSID8pdnUfFBxTyfHtFtTL/NWKzIH7kQ== + dependencies: + "@jsonjoy.com/fs-core" "4.68.1" + "@jsonjoy.com/fs-node-builtins" "4.68.1" + "@jsonjoy.com/fs-node-utils" "4.68.1" + "@jsonjoy.com/fs-print" "4.68.1" + "@jsonjoy.com/fs-snapshot" "4.68.1" + glob-to-regex.js "^1.0.0" + thingies "^2.5.0" + +"@jsonjoy.com/fs-print@4.68.1": + version "4.68.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-print/-/fs-print-4.68.1.tgz#bd830f90764155fb131320624248d33e720d6130" + integrity sha512-oGeZOGPYKK9v1CgeVeEDsLomH1lCnslSpqUN5GmPzrmAVGQlsmsdcXNA2O4lV8Y4xkuSuynx2ITBkUHJVaTbow== + dependencies: + "@jsonjoy.com/fs-node-utils" "4.68.1" + tree-dump "^1.1.0" + +"@jsonjoy.com/fs-snapshot@4.68.1": + version "4.68.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.68.1.tgz#672d82cb65dad18c3a1fcf7d22254d037e6a794a" + integrity sha512-XZfP0FDZN32bbc4t2bZN2qRrYHg5AktJnzk22HRoKGK4BprrbNRH2k5ceSNS/kupKYcofCs+O841+xaAbjnxwQ== + dependencies: + "@jsonjoy.com/buffers" "^17.65.0" + "@jsonjoy.com/fs-node-utils" "4.68.1" + "@jsonjoy.com/json-pack" "^17.65.0" + "@jsonjoy.com/util" "^17.65.0" + +"@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-pack@^17.65.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz#8dd8ff65dd999c5d4d26df46c63915c7bdec093a" + integrity sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w== + dependencies: + "@jsonjoy.com/base64" "17.67.0" + "@jsonjoy.com/buffers" "17.67.0" + "@jsonjoy.com/codegen" "17.67.0" + "@jsonjoy.com/json-pointer" "17.67.0" + "@jsonjoy.com/util" "17.67.0" + hyperdyperid "^1.2.0" + thingies "^2.5.0" + tree-dump "^1.1.0" + +"@jsonjoy.com/json-pointer@17.67.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz#74439573dc046e0c9a3a552fb94b391bc75313b8" + integrity sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA== + dependencies: + "@jsonjoy.com/util" "17.67.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@17.67.0", "@jsonjoy.com/util@^17.65.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-17.67.0.tgz#7c4288fc3808233e55c7610101e7bb4590cddd3f" + integrity sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew== + dependencies: + "@jsonjoy.com/buffers" "17.67.0" + "@jsonjoy.com/codegen" "17.67.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== + +"@messageformat/core@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@messageformat/core/-/core-3.1.0.tgz#d4d2f5c3555228a6b5980b122a02b53dfc6458bd" + integrity sha512-UxAnjecnRG4u2iaggwIyylYPHmk5BTErJcKmWyAKTXqYgSW1bFLp4D7fIzuh6bk17Qfcmf3qtufdrstCB23nBA== + dependencies: + "@messageformat/date-skeleton" "^1.0.0" + "@messageformat/number-skeleton" "^1.0.0" + "@messageformat/parser" "^5.0.0" + "@messageformat/runtime" "^3.0.1" + make-plural "^7.0.0" + safe-identifier "^0.4.1" + +"@messageformat/date-skeleton@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz#3bad068cbf5873d14592cfc7a73dd4d8615e2739" + integrity sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A== + +"@messageformat/number-skeleton@^1.0.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz#e7c245c41a1b2722bc59dad68f4d454f761bc9b4" + integrity sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg== + +"@messageformat/parser@^5.0.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@messageformat/parser/-/parser-5.1.1.tgz#ca7d6c18e9f3f6b6bc984a465dac16da00106055" + integrity sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg== + dependencies: + moo "^0.5.1" + +"@messageformat/runtime@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@messageformat/runtime/-/runtime-3.0.2.tgz#7186cd772c55298cb5d2f2ec402af266db06af15" + integrity sha512-dkIPDCjXcfhSHgNE1/qV6TeczQZR59Yx0xXeafVKgK3QVWoxc38ljwpksUpnzCGvN151KUbCJTDZVmahtf1YZw== + dependencies: + make-plural "^7.0.0" + +"@noble/hashes@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@peculiar/asn1-cms@^2.6.0", "@peculiar/asn1-cms@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz#b48a8389319228f929e9acd8cee8da6c858738de" + integrity sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + "@peculiar/asn1-x509-attr" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-csr@^2.6.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz#c9bb5dec2eaff824a705e82a4a58d45e6d2c35d0" + integrity sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-ecc@^2.6.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz#d51ab2b07eca98e0cf492d051e98bbd0a071305a" + integrity sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-pfx@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz#8e189b6455e2bf9e5f921bb150ea86d7e7d1875d" + integrity sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg== + dependencies: + "@peculiar/asn1-cms" "^2.8.0" + "@peculiar/asn1-pkcs8" "^2.8.0" + "@peculiar/asn1-rsa" "^2.8.0" + "@peculiar/asn1-schema" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-pkcs8@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz#a46cf8857b9b063896afa41d2b8b2aa6a07a70a2" + integrity sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-pkcs9@^2.6.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz#6d62697af0bbd4f30fdf0d23b4018f3f09620de3" + integrity sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ== + dependencies: + "@peculiar/asn1-cms" "^2.8.0" + "@peculiar/asn1-pfx" "^2.8.0" + "@peculiar/asn1-pkcs8" "^2.8.0" + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + "@peculiar/asn1-x509-attr" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-rsa@^2.6.0", "@peculiar/asn1-rsa@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz#9d98d0fc42fec50119d2881b8a9925d36daaea73" + integrity sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-schema@^2.6.0", "@peculiar/asn1-schema@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz#69699f84259b2161607cabfc34e512a4023dbef9" + integrity sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q== + dependencies: + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-x509-attr@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz#bd168e3f5e8bc23e56b1a97891f9f2fb7f730204" + integrity sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-x509@^2.6.0", "@peculiar/asn1-x509@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz#9958a9ef35dec8426aabad78ffe8798e318b06e2" + integrity sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/utils@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@peculiar/utils/-/utils-2.0.3.tgz#a27ca4c4b73652e110f19a7d16d664f458a5528e" + integrity sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ== + dependencies: + tslib "^2.8.1" + +"@peculiar/x509@^1.14.2": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@peculiar/x509/-/x509-1.14.3.tgz#2c44c2b89474346afec38a0c2803ec4fb8ce959e" + integrity sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA== + dependencies: + "@peculiar/asn1-cms" "^2.6.0" + "@peculiar/asn1-csr" "^2.6.0" + "@peculiar/asn1-ecc" "^2.6.0" + "@peculiar/asn1-pkcs9" "^2.6.0" + "@peculiar/asn1-rsa" "^2.6.0" + "@peculiar/asn1-schema" "^2.6.0" + "@peculiar/asn1-x509" "^2.6.0" + pvtsutils "^1.3.6" + reflect-metadata "^0.2.2" + tslib "^2.8.1" + tsyringe "^4.10.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== + +"@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.9" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" + integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz#9d34c88c0c9ee62b9a6e4d9f8ab8d7e29688e6b4" + integrity sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw== + 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.9" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz#b746a8bb6c389af7a31141397bb539f775b0ae84" + integrity sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.6.tgz#2d724b2c990dcb8c8444063f3580a903f6d500cc" + integrity sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "^2" + +"@types/express@^4.17.25": + version "4.17.25" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.25.tgz#070c8c73a6fee6936d65c195dbbfb7da5026649b" + integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "^1" + +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + +"@types/http-proxy@^1.17.8": + version "1.17.17" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.17.tgz#d9e2c4571fe3507343cb210cd41790375e59a533" + integrity sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw== + dependencies: + "@types/node" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node@*", "@types/node@>=10.0.0": + version "26.2.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.2.0.tgz#5a4875a862fda8fdc57de8faa579bb81ecba1685" + integrity sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg== + dependencies: + undici-types "~8.3.0" + +"@types/qs@*": + version "6.15.1" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.15.1.tgz#8606884272c63f0db96986bd3548650d8a9388bf" + integrity sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw== + +"@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/serve-static@^2": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-2.2.0.tgz#d4a447503ead0d1671132d1ab6bd58b805d8de6a" + integrity sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + +"@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", "@types/ws@^8.5.12": + 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.18.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940" + integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.20.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.20.0.tgz#304b3636add88ba7d936760dd50ece006dea95f9" + integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== + 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.2.2: + 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== + +asn1js@^3.0.10, asn1js@^3.0.6: + version "3.0.10" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.10.tgz#df26c874c8a8b41ca605efea47b2ad07551013dd" + integrity sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg== + dependencies: + pvtsutils "^1.3.6" + pvutils "^1.1.5" + tslib "^2.8.1" + +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.11.12: + version "2.11.13" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz#660073103c1bee93e54df55f117b7528adf6af19" + integrity sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ== + +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== + +bcp-47@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bcp-47/-/bcp-47-2.1.0.tgz#7e80734c3338fe8320894981dccf4968c3092df6" + integrity sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w== + dependencies: + is-alphabetical "^2.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + +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.19.0, body-parser@~1.20.5: + version "1.20.6" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.6.tgz#60c789c78e0992d906da0a29d71ae01d15c1ed76" + integrity sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g== + 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.1" + iconv-lite "~0.4.24" + on-finished "~2.4.1" + qs "~6.15.1" + raw-body "~2.5.3" + type-is "~1.6.18" + unpipe "~1.0.0" + +bonjour-service@^1.2.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.4.4.tgz#8ff5b85fa0641e0996bf40bcf61fc0c6727b66d3" + integrity sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.18" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.18.tgz#3ce74d89885136be1535341f8c3d4425c29a5cab" + integrity sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.2: + version "2.1.4" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.4.tgz#589dab11c0018d0366be64cd8bf12c8dbecc8326" + integrity sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg== + 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.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.8.tgz#a3c79ceb70028527e5da7dafc887f3200b5168c0" + integrity sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA== + dependencies: + baseline-browser-mapping "^2.11.12" + caniuse-lite "^1.0.30001809" + electron-to-chromium "^1.5.402" + node-releases "^2.0.53" + update-browserslist-db "^1.3.0" + +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, bytes@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +bytestreamjs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bytestreamjs/-/bytestreamjs-2.0.1.tgz#a32947c7ce389a6fa11a09a9a563d0a45889535e" + integrity sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ== + +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.30001809: + version "1.0.30001809" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz#e6cf71f14ddfe008f114dd2a846923be3c03a07b" + integrity sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ== + +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.8.1: + 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.7" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454" + integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA== + +cookie@~0.7.1, cookie@~0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@~2.8.5: + version "2.8.6" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.6.tgz#ff5dd69bd95e547503820d29aba4f8faf8dfec96" + integrity sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw== + 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" + +css-loader@6.7.3: + version "6.7.3" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.3.tgz#1e8799f3ccc5874fdd55461af51137fcc5befbcd" + integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.19" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.3.8" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +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, debug@~4.4.1: + 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" + +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.5.0" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.5.0.tgz#2792e886f2422894545947cc80e1a444496c5976" + integrity sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw== + 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, 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, destroy@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" + integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.402: + version "1.5.403" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz#8a6e422bc68c5d2fa4d8c5fa2ba9a583c3d4452c" + integrity sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA== + +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.9" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.9.tgz#fd17e9f4e3a256423592574b60ac262e91af4b82" + integrity sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg== + dependencies: + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + "@types/ws" "^8.5.12" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.7.2" + cors "~2.8.5" + debug "~4.4.1" + engine.io-parser "~5.2.1" + ws "~8.21.0" + +enhanced-resolve@^5.17.3: + version "5.24.5" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz#b4dad3255b7545f07ba5535189868e9f85f47573" + integrity sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.3.3" + +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.21.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.21.0.tgz#04a251be79f92548541f37d13c8b6f22940c3bae" + integrity sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow== + +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.2" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b" + integrity sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw== + 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.22.1: + version "4.22.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.22.2.tgz#c17ae0981e5efc24b22272f0e041c4662503b700" + integrity sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "~1.20.5" + 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.15.1" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "~0.19.0" + serve-static "~1.16.2" + setprototypeof "1.2.0" + statuses "~2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-uri@^3.0.1: + version "3.1.5" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.5.tgz#610f37419a030270430cecd68d74e3d4d96725d0" + integrity sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +finalhandler@~1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88" + integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg== + 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.2" + unpipe "~1.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.4.4" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.4.tgz#aeeca2a506303f0cee61c59e6c9f2a88d2f29fc6" + integrity sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q== + +follow-redirects@^1.0.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" + integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== + +foreground-child@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regex.js@^1.0.0, glob-to-regex.js@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz#2b323728271d133830850e32311f40766c5f6413" + integrity sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ== + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^10.4.5: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3, glob@^7.1.7: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2, hasown@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + 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@~1.8.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.1" + +http-errors@~2.0.0, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + +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.10" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz#b2df7b705203d7a8c269ac8450cf96b00c532f94" + integrity sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ== + 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.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" + +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" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +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, inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +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.5.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.5.0.tgz#7d4b6c39f9392fb61cf807de6e425e22c10e061f" + integrity sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w== + +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + +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.2" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.2.tgz#3e07450a8080ebce3fbf0cac494f4d2ab324e082" + integrity sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA== + dependencies: + hasown "^2.0.3" + +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-network-error@^1.0.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.3.2.tgz#9460bc30f8419a4bca77114f4de88a3ee5e0c519" + integrity sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA== + +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.1" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.1.tgz#327897b26832a3eb117da6c27492d04ca132594f" + integrity sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw== + 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.3.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848" + integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.1.tgz#4eafd31bbe684a747a6e8f3e4ad373e53979ced4" + integrity sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ== + dependencies: + glob "^7.1.3" + minimatch "^9.0.3" + webpack-merge "^4.1.5" + +"karma@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.14.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.14.1.tgz#f7e0da3f58aaea03fea01074d840b5f739ed7ddc" + integrity sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA== + dependencies: + picocolors "^1.1.1" + shell-quote "^1.8.4" + +loader-runner@^4.2.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.2.tgz#9913d3a15971f8f635915e601fb5c9d495d918e9" + integrity sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +make-plural@^7.0.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-7.5.0.tgz#ff99598640d82ec7a69bea15d34ecc50152fc0db" + integrity sha512-0booA+aVYyVFoR67JBHdfVk0U08HmrBH2FrtmBqBa+NldlqXv/G2Z9VQuQq6Wgp2jDWdybEWGfBkk1cq5264WA== + +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.68.1" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.68.1.tgz#e42fa69c873305a9cd0b28a8d230b96efacd8b5d" + integrity sha512-OD+IDRUvIxu3QHL+nFm9gdyugInD27FDJ+sl4B5QgomPHXMlbw+GP918P8VNKu2FkNlVeqBkpzkwROpamVifRw== + dependencies: + "@jsonjoy.com/fs-core" "4.68.1" + "@jsonjoy.com/fs-fsa" "4.68.1" + "@jsonjoy.com/fs-node" "4.68.1" + "@jsonjoy.com/fs-node-builtins" "4.68.1" + "@jsonjoy.com/fs-node-to-fsa" "4.68.1" + "@jsonjoy.com/fs-node-utils" "4.68.1" + "@jsonjoy.com/fs-print" "4.68.1" + "@jsonjoy.com/fs-snapshot" "4.68.1" + "@jsonjoy.com/json-pack" "^1.11.0" + "@jsonjoy.com/util" "^1.9.0" + glob-to-regex.js "^1.0.1" + thingies "^2.5.0" + tree-dump "^1.0.3" + tslib "^2.0.0" + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34, mime-types@~2.1.35: + 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== + +mini-css-extract-plugin@2.7.5: + version "2.7.5" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz#afbb344977659ec0f1f6e050c7aea456b121cfc5" + integrity sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ== + dependencies: + schema-utils "^4.0.0" + +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.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.3, minimatch@^9.0.4, minimatch@^9.0.5: + version "9.0.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" + integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== + dependencies: + brace-expansion "^2.0.2" + +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.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b" + integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== + +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" + +moo@^0.5.1: + version "0.5.3" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.3.tgz#dfcdb40ff6f1a03c34af5df7f9717cecfa88c38c" + integrity sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA== + +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" + +nanoid@^3.3.17: + version "3.3.18" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913" + integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w== + +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-releases@^2.0.53: + version "2.0.53" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.53.tgz#0fe5ad8a7935e075172f574e56f0c20f07fa41af" + integrity sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ== + +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, object-inspect@^1.13.4: + 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.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.13" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz#9b22ec16bc3ab88d05a0c7e369869421401ab17d" + integrity sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA== + +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.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== + +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" + +pkijs@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pkijs/-/pkijs-3.4.0.tgz#d9164def30ff6d97be2d88966d5e36192499ca9c" + integrity sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw== + dependencies: + "@noble/hashes" "1.4.0" + asn1js "^3.0.6" + bytestreamjs "^2.0.1" + pvtsutils "^1.3.6" + pvutils "^1.1.3" + tslib "^2.8.1" + +postcss-modules-extract-imports@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== + +postcss-modules-local-by-default@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368" + integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c" + integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-selector-parser@^7.0.0: + version "7.1.5" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz#ee7b090724eb60739b530660e5b402200e694a97" + integrity sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.19: + version "8.5.26" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620" + integrity sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ== + dependencies: + nanoid "^3.3.17" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +pvtsutils@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.6.tgz#ec46e34db7422b9e4fdc5490578c1883657d6001" + integrity sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg== + dependencies: + tslib "^2.8.1" + +pvutils@^1.1.3, pvutils@^1.1.5: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.2.0.tgz#4b5487a9cccd52d275b0538775790a3ca982bc22" + integrity sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg== + +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.15.1: + version "6.15.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b" + integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A== + dependencies: + es-define-property "^1.0.1" + side-channel "^1.1.1" + +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: + version "1.3.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.3.0.tgz#d7f19be812bb62721472b45d3be219ef09572b47" + integrity sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw== + +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.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2" + integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + 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" + +reflect-metadata@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + +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.12" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rfdc@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-applescript@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911" + integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-identifier@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb" + integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w== + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" + integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-5.5.0.tgz#4c9ab7c7c9f35f18fb6a9882c253eb0e6bd6557b" + integrity sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew== + dependencies: + "@peculiar/x509" "^1.14.2" + pkijs "^3.3.3" + +semver@^7.3.8: + version "7.8.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" + integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== + +send@~0.19.0, send@~0.19.1: + version "0.19.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29" + integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "~0.5.2" + http-errors "~2.0.1" + mime "1.6.0" + ms "2.1.3" + on-finished "~2.4.1" + range-parser "~1.2.1" + statuses "~2.0.2" + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.2" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.2.tgz#2988e3612106d78a5e4849ddff552ce7bd3d9bcb" + integrity sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ== + dependencies: + accepts "~1.3.8" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.8.0" + mime-types "~2.1.35" + parseurl "~1.3.3" + +serve-static@~1.16.2: + version "1.16.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9" + integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "~0.19.1" + +setprototypeof@1.2.0, 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.4: + version "1.10.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.10.0.tgz#482033e192e4f5c07151521ffa03400ec71b1b0f" + integrity sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA== + +side-channel-list@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127" + integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + +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.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab" + integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + side-channel-list "^1.0.1" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +socket.io-adapter@~2.5.2: + version "2.5.8" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz#c9fcabf602dbd5b09258ca98e5ec6a7ae4360698" + integrity sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw== + dependencies: + debug "~4.4.1" + ws "~8.21.0" + +socket.io-parser@~4.2.4: + version "4.2.7" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.7.tgz#679e51fe24d1c81df90fc5f7efe4a5f432fe99c0" + integrity sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.4.1" + +socket.io@^4.7.2: + version "4.8.3" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.3.tgz#ca6ba1431c69532e1e0a6f496deebeb601dbc4df" + integrity sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.4.1" + 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, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-loader@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-5.0.0.tgz#f593a916e1cc54471cfc8851b905c8a845fc7e38" + integrity sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA== + dependencies: + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +"statuses@>= 1.5.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== + +statuses@~2.0.1, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +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": + 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@^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": + 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@^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.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.2.0.tgz#d22a269522836a627af8d04b5c3fd2c7fa3e32e3" + integrity sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w== + dependencies: + ansi-regex "^6.2.2" + +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== + +style-loader@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.2.tgz#eaebca714d9e462c19aa1e3599057bc363924899" + integrity sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw== + +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.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160" + integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A== + +terser-webpack-plugin@^5.3.11: + version "5.6.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz#47bc41bd8b8fab8383b62ec763b7394829097e7b" + integrity sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + terser "^5.31.1" + +terser@^5.31.1: + version "5.49.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.49.2.tgz#cc08cfb6097e5da650bfd45c769ebd15c514fc23" + integrity sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ== + 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.6.1" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-2.6.1.tgz#4eb28e2585a75288f1765a0b08f1dfa020357a6f" + integrity sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw== + +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.7" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.7.tgz#26f4db11d1601ce8012dcb8a798ece1c06a99059" + integrity sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw== + +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, 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@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.0, tslib@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tsyringe@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/tsyringe/-/tsyringe-4.10.0.tgz#d0c95815d584464214060285eaaadd94aa03299c" + integrity sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw== + dependencies: + tslib "^1.9.3" + +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@~8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.3.0.tgz#44e9fc9f3244648cdea35e4f9bb2d681e9410809" + integrity sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ== + +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: + 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.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz#a71c28dd22f505481dbc4689087b18d933e90afd" + integrity sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, 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.5.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.5.2.tgz#e12e82d84674266fc1c6dbfe38891b92ff0522ec" + integrity sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg== + dependencies: + 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.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.3.tgz#7f36a78be7ac88833fd87757edee31469a9e47d3" + integrity sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ== + dependencies: + "@types/bonjour" "^3.5.13" + "@types/connect-history-api-fallback" "^1.5.4" + "@types/express" "^4.17.25" + "@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.8.1" + connect-history-api-fallback "^2.0.0" + express "^4.22.1" + 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 "^5.5.0" + 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.5.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.5.1.tgz#76c2418486dcc02b2aa0694c104176c2858fe84a" + integrity sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw== + +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.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.5.tgz#569d22764ab21f2de20af0e74b411e8ae5a0fa46" + integrity sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA== + 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": + 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@^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.20.1: + version "8.20.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.20.1.tgz#91a9ae2b312ccf98e0a85ec499b48cef45ab0ddb" + integrity sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w== + +ws@^8.18.0, ws@~8.21.0: + version "8.21.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.3.tgz#660b4faddb6a3e575c86e078126919961f4de4fc" + integrity sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw== + +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.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.2.tgz#c56731dca0d2788ae0866dd3c83907d6bab85f7d" + integrity sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w== + 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.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.3.tgz#779dffe6bcafec596a7172e983289a588647faaa" + integrity sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g== + 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 new file mode 100644 index 0000000..c794c14 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,37 @@ +rootProject.name = "SatsPrice" + +pluginManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +include(":androidApp") +include(":desktopApp") +include(":shared") +include(":webApp") \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 0000000..65478d1 --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,112 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.mokoResources) +} + +kotlin { + listOf( + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "Shared" + isStatic = true + export(libs.moko.resources) + } + } + + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + + jvm() + + js { + browser() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + } + + android { + namespace = "xyz.tyiu.satsprice.shared" + compileSdk = libs.versions.android.compileSdk.get().toInt() + minSdk = libs.versions.android.minSdk.get().toInt() + + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + } + androidResources { + enable = true + } + withHostTest { + isIncludeAndroidResources = true + } + withDeviceTestBuilder { + sourceSetTreeName = "test" + }.configure { + instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + } + + sourceSets { + androidMain.dependencies { + implementation(libs.compose.uiToolingPreview) + implementation(libs.compose.uiTooling) + implementation(libs.ktor.client.okhttp) + } + commonMain.dependencies { + implementation(libs.compose.runtime) + implementation(libs.compose.foundation) + implementation(libs.compose.material3) + implementation(libs.compose.materialIconsCore) + implementation(libs.compose.ui) + implementation(libs.compose.uiToolingPreview) + api(libs.moko.resources) + api(libs.moko.resourcesCompose) + implementation(libs.androidx.lifecycle.viewmodelCompose) + implementation(libs.androidx.lifecycle.runtimeCompose) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.json) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.contentNegotiation) + implementation(libs.ktor.serialization.kotlinxJson) + implementation(libs.bignum) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutinesTest) + implementation(libs.ktor.client.mock) + } + iosMain.dependencies { + implementation(libs.ktor.client.darwin) + } + jvmMain.dependencies { + implementation(libs.ktor.client.cio) + } + jsMain.dependencies { + implementation(libs.wrappers.browser) + implementation(libs.ktor.client.js) + } + wasmJsMain.dependencies { + implementation(libs.ktor.client.cio) + } + } +} + +dependencies { + androidRuntimeClasspath(libs.compose.uiTooling) +} + +multiplatformResources { + resourcesPackage.set("xyz.tyiu.satsprice.shared") +} \ No newline at end of file diff --git a/shared/src/androidHostTest/kotlin/xyz/tyiu/satsprice/SharedLogicAndroidHostTest.kt b/shared/src/androidHostTest/kotlin/xyz/tyiu/satsprice/SharedLogicAndroidHostTest.kt new file mode 100644 index 0000000..3bf05bb --- /dev/null +++ b/shared/src/androidHostTest/kotlin/xyz/tyiu/satsprice/SharedLogicAndroidHostTest.kt @@ -0,0 +1,12 @@ +package xyz.tyiu.satsprice + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SharedLogicAndroidHostTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/Platform.android.kt b/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/Platform.android.kt new file mode 100644 index 0000000..196cfa1 --- /dev/null +++ b/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/Platform.android.kt @@ -0,0 +1,9 @@ +package xyz.tyiu.satsprice + +import android.os.Build + +class AndroidPlatform : Platform { + override val name: String = "Android ${Build.VERSION.SDK_INT}" +} + +actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.android.kt b/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.android.kt new file mode 100644 index 0000000..2b1dd63 --- /dev/null +++ b/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.android.kt @@ -0,0 +1,21 @@ +package xyz.tyiu.satsprice + +import java.util.Currency +import java.util.Locale + +actual fun systemCurrencies(): List = + Currency.getAvailableCurrencies() + .map { CurrencyInfo(it.currencyCode, it.getDisplayName(Locale.getDefault())) } + .sortedBy { it.code } + +actual fun localeCurrencyCode(): String? = try { + Currency.getInstance(Locale.getDefault())?.currencyCode +} catch (e: IllegalArgumentException) { + null +} + +actual fun currencyDecimalDigits(code: String): Int = try { + Currency.getInstance(code).defaultFractionDigits.takeIf { it >= 0 } ?: 2 +} catch (e: IllegalArgumentException) { + 2 +} diff --git a/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.android.kt b/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.android.kt new file mode 100644 index 0000000..9b83a6e --- /dev/null +++ b/shared/src/androidMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.android.kt @@ -0,0 +1,6 @@ +package xyz.tyiu.satsprice.ui + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +actual val screenHorizontalPadding: Dp = 4.dp diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/App.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/App.kt new file mode 100644 index 0000000..12f47c7 --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/App.kt @@ -0,0 +1,19 @@ +package xyz.tyiu.satsprice + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import xyz.tyiu.satsprice.ui.PriceScreen + +@Composable +@Preview +fun App() { + MaterialTheme( + colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme(), + ) { + PriceScreen() + } +} diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/Platform.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/Platform.kt new file mode 100644 index 0000000..5864e8d --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/Platform.kt @@ -0,0 +1,7 @@ +package xyz.tyiu.satsprice + +interface Platform { + val name: String +} + +expect fun getPlatform(): Platform \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt new file mode 100644 index 0000000..4325d3e --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.kt @@ -0,0 +1,12 @@ +package xyz.tyiu.satsprice + +data class CurrencyInfo(val code: String, val displayName: String) + +/** All ISO 4217 currencies the current platform knows about, with localized display names. */ +expect fun systemCurrencies(): List + +/** The currency associated with the user's current locale, if the platform can determine one. */ +expect fun localeCurrencyCode(): String? + +/** The conventional number of decimal places for [code] (e.g. 2 for USD, 0 for JPY, 3 for BHD). */ +expect fun currencyDecimalDigits(code: String): Int diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/CoinGeckoExchangeRateSource.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/CoinGeckoExchangeRateSource.kt new file mode 100644 index 0000000..470a6d7 --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/CoinGeckoExchangeRateSource.kt @@ -0,0 +1,46 @@ +package xyz.tyiu.satsprice.data + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonPrimitive +import xyz.tyiu.satsprice.systemCurrencies +import kotlin.time.Clock + +@Serializable +internal data class CoinGeckoSimplePriceResponse( + val bitcoin: Map = emptyMap(), +) + +class CoinGeckoExchangeRateSource( + private val httpClient: HttpClient, +) : ExchangeRateSource { + override val id: String = "coingecko" + override val displayName: String = "CoinGecko" + + private val requestedVsCurrencies: String by lazy { + systemCurrencies().joinToString(",") { it.code.lowercase() } + } + + override suspend fun getRates(base: String): ExchangeRates { + val response: CoinGeckoSimplePriceResponse = httpClient + .get("https://api.coingecko.com/api/v3/simple/price") { + parameter("ids", "bitcoin") + parameter("vs_currencies", requestedVsCurrencies) + parameter("precision", "full") + } + .body() + + return ExchangeRates( + base = "BTC", + rates = response.bitcoin + .mapKeys { (code, _) -> code.uppercase() } + .mapValues { (_, value) -> BigDecimal.parseString(value.jsonPrimitive.content) }, + fetchedAt = Clock.System.now(), + ) + } +} diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/CoinbaseExchangeRateSource.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/CoinbaseExchangeRateSource.kt new file mode 100644 index 0000000..ca5bb99 --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/CoinbaseExchangeRateSource.kt @@ -0,0 +1,41 @@ +package xyz.tyiu.satsprice.data + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import kotlinx.serialization.Serializable +import kotlin.time.Clock + +@Serializable +internal data class CoinbaseExchangeRatesResponse( + val data: CoinbaseExchangeRatesData, +) + +@Serializable +internal data class CoinbaseExchangeRatesData( + val currency: String, + val rates: Map, +) + +class CoinbaseExchangeRateSource( + private val httpClient: HttpClient, +) : ExchangeRateSource { + override val id: String = "coinbase" + override val displayName: String = "Coinbase" + + override suspend fun getRates(base: String): ExchangeRates { + val response: CoinbaseExchangeRatesResponse = httpClient + .get("https://api.coinbase.com/v2/exchange-rates") { + parameter("currency", base) + } + .body() + + return ExchangeRates( + base = response.data.currency, + rates = response.data.rates.mapValues { (_, value) -> BigDecimal.parseString(value) }, + fetchedAt = Clock.System.now(), + ) + } +} diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/ExchangeRateSource.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/ExchangeRateSource.kt new file mode 100644 index 0000000..03eea40 --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/ExchangeRateSource.kt @@ -0,0 +1,22 @@ +package xyz.tyiu.satsprice.data + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import kotlin.time.Instant + +/** + * A source of BTC exchange rates. Implementations fetch rates for one unit of [base] + * expressed in various currency codes, so new providers can be added without touching + * conversion or UI code. + */ +interface ExchangeRateSource { + val id: String + val displayName: String + + suspend fun getRates(base: String = "BTC"): ExchangeRates +} + +data class ExchangeRates( + val base: String, + val rates: Map, + val fetchedAt: Instant, +) diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/HttpClientFactory.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/HttpClientFactory.kt new file mode 100644 index 0000000..26c7a61 --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/HttpClientFactory.kt @@ -0,0 +1,21 @@ +package xyz.tyiu.satsprice.data + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +fun createHttpClient(): HttpClient = HttpClient { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + } + ) + } + install(HttpTimeout) { + requestTimeoutMillis = 10_000 + connectTimeoutMillis = 10_000 + } +} diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/ManualExchangeRateSource.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/ManualExchangeRateSource.kt new file mode 100644 index 0000000..5d4f060 --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/data/ManualExchangeRateSource.kt @@ -0,0 +1,22 @@ +package xyz.tyiu.satsprice.data + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import kotlin.time.Clock + +/** A source backed by a rate the user types in themselves, rather than a network fetch. */ +class ManualExchangeRateSource : ExchangeRateSource { + override val id: String = "manual" + override val displayName: String = "Manual" + + var currencyCode: String = "USD" + var rate: BigDecimal? = null + + override suspend fun getRates(base: String): ExchangeRates { + val value = rate ?: throw IllegalStateException("Enter a BTC price in $currencyCode below") + return ExchangeRates( + base = "BTC", + rates = mapOf(currencyCode to value), + fetchedAt = Clock.System.now(), + ) + } +} diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverter.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverter.kt new file mode 100644 index 0000000..2166c11 --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverter.kt @@ -0,0 +1,47 @@ +package xyz.tyiu.satsprice.domain + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import com.ionspin.kotlin.bignum.decimal.DecimalMode +import com.ionspin.kotlin.bignum.decimal.RoundingMode +import xyz.tyiu.satsprice.data.ExchangeRates + +/** + * Pure conversion math between BTC, Sats, and a fiat currency, given a base-BTC + * [ExchangeRates] snapshot (1 BTC = rates[currency] units of that currency). + * + * Uses arbitrary-precision [BigDecimal] throughout (rather than Double) so + * conversions don't lose precision or overflow for very large amounts. + */ +object CurrencyConverter { + val SATS_PER_BTC: BigDecimal = BigDecimal.fromLong(100_000_000) + + /** The hard-capped maximum number of bitcoin that will ever exist. */ + val MAX_BTC_SUPPLY: BigDecimal = BigDecimal.fromLong(21_000_000) + + /** [MAX_BTC_SUPPLY] expressed in Sats. */ + val MAX_SATS_SUPPLY: BigDecimal = MAX_BTC_SUPPLY.multiply(SATS_PER_BTC) + + /** Divisions aren't guaranteed to terminate, so cap at a generous 50 significant digits. */ + private val DIVISION_MODE = DecimalMode( + decimalPrecision = 50, + roundingMode = RoundingMode.ROUND_HALF_AWAY_FROM_ZERO, + ) + + fun btcToSats(btc: BigDecimal): BigDecimal = btc.multiply(SATS_PER_BTC) + + fun satsToBtc(sats: BigDecimal): BigDecimal = sats.divide(SATS_PER_BTC, DIVISION_MODE) + + fun btcToFiat(btc: BigDecimal, rates: ExchangeRates, currency: String): BigDecimal? = + rates.rates[currency]?.let { rate -> btc.multiply(rate) } + + fun fiatToBtc(fiat: BigDecimal, rates: ExchangeRates, currency: String): BigDecimal? = + rates.rates[currency] + ?.takeIf { rate -> !rate.isZero() } + ?.let { rate -> fiat.divide(rate, DIVISION_MODE) } + + fun satsToFiat(sats: BigDecimal, rates: ExchangeRates, currency: String): BigDecimal? = + btcToFiat(satsToBtc(sats), rates, currency) + + fun fiatToSats(fiat: BigDecimal, rates: ExchangeRates, currency: String): BigDecimal? = + fiatToBtc(fiat, rates, currency)?.let { btc -> btcToSats(btc) } +} diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/NumberFormat.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/NumberFormat.kt new file mode 100644 index 0000000..7376a0d --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/domain/NumberFormat.kt @@ -0,0 +1,66 @@ +package xyz.tyiu.satsprice.domain + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import com.ionspin.kotlin.bignum.decimal.RoundingMode + +private val PLAIN_DECIMAL_INPUT = Regex("""-?\d+(\.\d+)?""") + +/** Parses a plain decimal string (as typed into an amount field), or null if incomplete/invalid. */ +fun String.toBigDecimalOrNull(): BigDecimal? { + if (!PLAIN_DECIMAL_INPUT.matches(this)) return null + return try { + BigDecimal.parseString(this) + } catch (e: Exception) { + null + } +} + +/** Strips a raw text-field edit down to digits and at most one decimal point. */ +fun sanitizeDecimalInput(raw: String): String { + val sb = StringBuilder() + var seenDot = false + for (c in raw) { + when { + c.isDigit() -> sb.append(c) + c == '.' && !seenDot -> { + sb.append(c) + seenDot = true + } + } + } + return sb.toString() +} + +/** Strips a raw text-field edit down to digits only (for whole-unit fields like Sats). */ +fun sanitizeIntegerInput(raw: String): String = raw.filter { it.isDigit() } + +/** Formats [value] with up to [maxDecimals] decimal places, trimming trailing zeros. */ +fun formatAmount(value: BigDecimal, maxDecimals: Int): String { + val rounded = value.roundToDigitPositionAfterDecimalPoint( + digitPosition = maxDecimals.toLong(), + roundingMode = RoundingMode.ROUND_HALF_AWAY_FROM_ZERO, + ) + val text = rounded.toStringExpanded() + + if ('.' !in text) return text + + val trimmed = text.trimEnd('0').trimEnd('.') + return when { + trimmed.isEmpty() || trimmed == "-" -> "0" + else -> trimmed + } +} + +/** Formats [value] with exactly [decimals] decimal places, zero-padded rather than trimmed. */ +fun formatAmountFixed(value: BigDecimal, decimals: Int): String { + val rounded = value.roundToDigitPositionAfterDecimalPoint( + digitPosition = decimals.toLong(), + roundingMode = RoundingMode.ROUND_HALF_AWAY_FROM_ZERO, + ) + val text = rounded.toStringExpanded() + if (decimals <= 0) return text.substringBefore('.') + + val whole = text.substringBefore('.') + val fraction = text.substringAfter('.', "") + return "$whole.${fraction.padEnd(decimals, '0')}" +} diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt new file mode 100644 index 0000000..63fb9bc --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/ConverterUiStateDisplay.kt @@ -0,0 +1,29 @@ +package xyz.tyiu.satsprice.ui + +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import xyz.tyiu.satsprice.domain.CurrencyConverter +import xyz.tyiu.satsprice.domain.toBigDecimalOrNull +import kotlin.time.Instant + +/** Derived display strings/flags shared between the Compose UI and the iOS SwiftUI bridge. */ + +fun ConverterUiState.statusLine(): String { + val updated = lastUpdated?.let { "updated ${it.toDateTimeString()}" } ?: "loading rates…" + return if (sourceName.isEmpty()) updated else "via $sourceName, $updated" +} + +fun ConverterUiState.exceedsMaxSupply(): Boolean { + val btcOverCap = btcAmount.toBigDecimalOrNull()?.let { it > CurrencyConverter.MAX_BTC_SUPPLY } ?: false + val satsOverCap = satsAmount.toBigDecimalOrNull()?.let { it > CurrencyConverter.MAX_SATS_SUPPLY } ?: false + return btcOverCap || satsOverCap +} + +/** The current "1 BTC = ?" rate in [ConverterUiState.defaultCurrencyCode], as a plain number. */ +fun ConverterUiState.defaultCurrencyRate(): String = + rateDisplays[defaultCurrencyCode]?.takeIf { it.isNotEmpty() } ?: "" + +private fun Instant.toDateTimeString(): String { + val local = toLocalDateTime(TimeZone.currentSystemDefault()) + return local.toString().substringBefore('.').replace('T', ' ') +} diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.kt new file mode 100644 index 0000000..201b73b --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.kt @@ -0,0 +1,10 @@ +package xyz.tyiu.satsprice.ui + +import androidx.compose.ui.unit.Dp + +/** + * Horizontal inset from the screen edge to the section cards. Differs per platform because + * touch-target density and window scale differ enough that a shared constant looks wrong on + * at least one of them (e.g. Android phones want a tighter edge than a desktop window does). + */ +expect val screenHorizontalPadding: Dp diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt new file mode 100644 index 0000000..dc90c7b --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceScreen.kt @@ -0,0 +1,311 @@ +package xyz.tyiu.satsprice.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import dev.icerock.moko.resources.compose.stringResource +import xyz.tyiu.satsprice.CurrencyInfo +import xyz.tyiu.satsprice.shared.MR + +private val SectionColors + @Composable get() = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer) + +@Composable +fun PriceScreen( + viewModel: PriceViewModel = viewModel { PriceViewModel() }, +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical)) + .verticalScroll(rememberScrollState()) + .padding(horizontal = screenHorizontalPadding, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) { + Text("SatsPrice", style = MaterialTheme.typography.headlineMedium) + Text( + text = state.statusLine(), + style = MaterialTheme.typography.bodySmall, + ) + } + + Card(modifier = Modifier.fillMaxWidth(), colors = SectionColors) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(stringResource(MR.strings.price_source), style = MaterialTheme.typography.bodyMedium) + DropdownSelector( + selectedLabel = state.sourceName, + options = viewModel.availableSources, + optionLabel = { it }, + onSelected = viewModel::onSourceSelected, + ) + } + + Text( + text = stringResource(MR.strings.btc_to_currency, state.defaultCurrencyCode), + style = MaterialTheme.typography.bodyMedium, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (state.isManualSource) { + OutlinedTextField( + value = state.manualRateInput, + onValueChange = viewModel::onManualRateChanged, + label = { Text(stringResource(MR.strings.rate_label)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true, + modifier = Modifier.weight(1f), + ) + } else { + val rate = state.defaultCurrencyRate() + if (rate.isNotEmpty()) { + Text( + text = rate, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.weight(1f), + ) + } else { + Spacer(modifier = Modifier.weight(1f)) + } + } + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } else { + IconButton(onClick = { viewModel.refresh() }) { + Icon( + Icons.Default.Refresh, + contentDescription = stringResource(MR.strings.refresh_content_description), + ) + } + } + } + } + } + + state.errorMessage?.let { message -> + Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) { + Row( + modifier = Modifier.fillMaxWidth().padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(message, color = MaterialTheme.colorScheme.onErrorContainer) + TextButton(onClick = { viewModel.refresh() }) { Text(stringResource(MR.strings.retry)) } + } + } + } + + val exceedsMaxSupply = state.exceedsMaxSupply() + + Card(modifier = Modifier.fillMaxWidth(), colors = SectionColors) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text(stringResource(MR.strings.bitcoin_section_title), style = MaterialTheme.typography.titleMedium) + + OutlinedTextField( + value = state.satsAmount, + onValueChange = viewModel::onSatsAmountChanged, + label = { Text(stringResource(MR.strings.sats_label)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true, + isError = exceedsMaxSupply, + modifier = Modifier.fillMaxWidth(), + ) + + OutlinedTextField( + value = state.btcAmount, + onValueChange = viewModel::onBtcAmountChanged, + label = { Text(stringResource(MR.strings.btc_label)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true, + isError = exceedsMaxSupply, + modifier = Modifier.fillMaxWidth(), + ) + + if (exceedsMaxSupply) { + Text( + text = stringResource(MR.strings.exceeds_max_supply), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } + } + + Card(modifier = Modifier.fillMaxWidth(), colors = SectionColors) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(stringResource(MR.strings.currencies_section_title), style = MaterialTheme.typography.titleMedium) + MultiCurrencySelector( + selectedCodes = state.selectedFiatCurrencies, + options = state.availableFiatCurrencies, + localeCurrencyCode = state.localeCurrencyCode, + onToggle = viewModel::onFiatCurrencyToggled, + ) + } + + state.selectedFiatCurrencies.forEach { code -> + key(code) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + value = state.fiatAmounts[code].orEmpty(), + onValueChange = { viewModel.onFiatAmountChanged(code, it) }, + label = { Text(code) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true, + modifier = Modifier.weight(1f), + ) + IconButton(onClick = { viewModel.onFiatCurrencyToggled(code) }) { + Icon( + Icons.Default.Close, + contentDescription = stringResource( + MR.strings.remove_currency_content_description, + code, + ), + ) + } + } + } + } + } + } + } + } +} + +@Composable +private fun MultiCurrencySelector( + selectedCodes: List, + options: List, + localeCurrencyCode: String?, + onToggle: (String) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + Box { + OutlinedButton(onClick = { expanded = true }) { + Text( + if (selectedCodes.isEmpty()) { + stringResource(MR.strings.add_currency) + } else { + stringResource(MR.strings.currencies_selected_count, selectedCodes.size) + }, + ) + } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + options.forEach { info -> + val checked = info.code in selectedCodes + val label = if (info.code == localeCurrencyCode) { + stringResource(MR.strings.currency_option_label_local, info.code, info.displayName) + } else { + stringResource(MR.strings.currency_option_label, info.code, info.displayName) + } + DropdownMenuItem( + text = { Text(label) }, + leadingIcon = { Checkbox(checked = checked, onCheckedChange = { onToggle(info.code) }) }, + onClick = { onToggle(info.code) }, + ) + } + } + } +} + +@Composable +private fun DropdownSelector( + selectedLabel: String, + options: List, + optionLabel: (T) -> String, + onSelected: (T) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + Box { + OutlinedButton(onClick = { expanded = true }) { + Text(selectedLabel) + } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + options.forEach { option -> + DropdownMenuItem( + text = { Text(optionLabel(option)) }, + onClick = { + onSelected(option) + expanded = false + }, + ) + } + } + } +} diff --git a/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt new file mode 100644 index 0000000..95284fd --- /dev/null +++ b/shared/src/commonMain/kotlin/xyz/tyiu/satsprice/ui/PriceViewModel.kt @@ -0,0 +1,264 @@ +package xyz.tyiu.satsprice.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import io.ktor.client.HttpClient +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import xyz.tyiu.satsprice.CurrencyInfo +import xyz.tyiu.satsprice.currencyDecimalDigits +import xyz.tyiu.satsprice.data.CoinbaseExchangeRateSource +import xyz.tyiu.satsprice.data.CoinGeckoExchangeRateSource +import xyz.tyiu.satsprice.data.ExchangeRateSource +import xyz.tyiu.satsprice.data.ExchangeRates +import xyz.tyiu.satsprice.data.ManualExchangeRateSource +import xyz.tyiu.satsprice.data.createHttpClient +import xyz.tyiu.satsprice.domain.CurrencyConverter +import xyz.tyiu.satsprice.domain.formatAmount +import xyz.tyiu.satsprice.domain.formatAmountFixed +import xyz.tyiu.satsprice.domain.sanitizeDecimalInput +import xyz.tyiu.satsprice.domain.sanitizeIntegerInput +import xyz.tyiu.satsprice.domain.toBigDecimalOrNull +import xyz.tyiu.satsprice.localeCurrencyCode +import xyz.tyiu.satsprice.systemCurrencies +import kotlin.time.Instant + +private const val AUTO_REFRESH_INTERVAL_MILLIS = 60_000L + +data class ConverterUiState( + val btcAmount: String = "1", + val satsAmount: String = formatAmount(CurrencyConverter.SATS_PER_BTC, 0), + val selectedFiatCurrencies: List = listOf("USD"), + val fiatAmounts: Map = emptyMap(), + val rateDisplays: Map = emptyMap(), + val availableFiatCurrencies: List = emptyList(), + val localeCurrencyCode: String? = null, + val defaultCurrencyCode: String = "USD", + val sourceName: String = "", + val isManualSource: Boolean = false, + val manualRateInput: String = "", + val isLoading: Boolean = true, + val errorMessage: String? = null, + val lastUpdated: Instant? = null, +) + +private sealed interface EditedField { + data object Btc : EditedField + data object Sats : EditedField + data class Fiat(val code: String) : EditedField +} + +class PriceViewModel( + httpClient: HttpClient = createHttpClient(), + private val manualSource: ManualExchangeRateSource = ManualExchangeRateSource(), + private val coinbaseSource: ExchangeRateSource = CoinbaseExchangeRateSource(httpClient), + private val coinGeckoSource: ExchangeRateSource = CoinGeckoExchangeRateSource(httpClient), +) : ViewModel() { + + private val sources: List = listOf(coinbaseSource, coinGeckoSource, manualSource) + val availableSources: List = sources.map { it.displayName } + + private var currentSource: ExchangeRateSource = coinbaseSource + private val systemCurrencyList: List by lazy { systemCurrencies() } + private val localCurrencyCode: String? by lazy { + localeCurrencyCode()?.takeIf { code -> systemCurrencyList.any { it.code == code } } + } + private val defaultCurrencyCode: String by lazy { localCurrencyCode ?: "USD" } + private val decimalDigitsCache = mutableMapOf() + private fun decimalDigitsFor(code: String) = decimalDigitsCache.getOrPut(code) { currencyDecimalDigits(code) } + + private val _uiState = MutableStateFlow( + ConverterUiState( + selectedFiatCurrencies = listOf(defaultCurrencyCode), + localeCurrencyCode = localCurrencyCode, + defaultCurrencyCode = defaultCurrencyCode, + sourceName = coinbaseSource.displayName, + ), + ) + val uiState: StateFlow = _uiState.asStateFlow() + + private var rates: ExchangeRates? = null + + init { + manualSource.currencyCode = defaultCurrencyCode + viewModelScope.launch { + while (isActive) { + refresh() + delay(AUTO_REFRESH_INTERVAL_MILLIS) + } + } + } + + fun refresh() { + if (currentSource === manualSource && manualSource.rate == null) { + _uiState.update { it.copy(isLoading = false, errorMessage = null) } + return + } + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + try { + val newRates = currentSource.getRates("BTC") + rates = newRates + _uiState.update { state -> + val available = systemCurrencyList + .filter { newRates.rates.containsKey(it.code) } + .let { list -> + val (matching, rest) = list.partition { it.code == defaultCurrencyCode } + matching + rest + } + val availableCodes = available.map { it.code }.toSet() + val selection = state.selectedFiatCurrencies.filter { it in availableCodes } + .ifEmpty { listOfNotNull(available.firstOrNull()?.code) } + recomputeFromKnownField( + state.copy( + isLoading = false, + errorMessage = null, + availableFiatCurrencies = available, + selectedFiatCurrencies = selection, + lastUpdated = newRates.fetchedAt, + ), + newRates, + ) + } + } catch (e: Exception) { + // Not localized: this shared ViewModel has no platform Context to resolve a moko-resources + // string on Android, and no locale-aware synchronous resolution path that works everywhere. + _uiState.update { + it.copy(isLoading = false, errorMessage = e.message ?: "Could not fetch exchange rates") + } + } + } + } + + fun onBtcAmountChanged(value: String) = updateAmount(EditedField.Btc, sanitizeDecimalInput(value)) + fun onSatsAmountChanged(value: String) = updateAmount(EditedField.Sats, sanitizeIntegerInput(value)) + fun onFiatAmountChanged(code: String, value: String) = + updateAmount(EditedField.Fiat(code), sanitizeDecimalInput(value)) + + fun onFiatCurrencyToggled(code: String) { + _uiState.update { state -> + val newSelection = if (code in state.selectedFiatCurrencies) { + state.selectedFiatCurrencies - code + } else { + state.selectedFiatCurrencies + code + } + val newState = state.copy(selectedFiatCurrencies = newSelection) + rates?.let { recomputeFromKnownField(newState, it) } ?: newState + } + } + + fun onSourceSelected(displayName: String) { + val selected = sources.firstOrNull { it.displayName == displayName } ?: return + currentSource = selected + rates = null + _uiState.update { + it.copy( + sourceName = selected.displayName, + isManualSource = selected === manualSource, + rateDisplays = emptyMap(), + ) + } + refresh() + } + + fun onManualRateChanged(value: String) { + val sanitized = sanitizeDecimalInput(value) + _uiState.update { it.copy(manualRateInput = sanitized) } + sanitized.toBigDecimalOrNull()?.let { parsed -> + manualSource.rate = parsed + refresh() + } + } + + private fun updateAmount(field: EditedField, rawValue: String) { + _uiState.update { state -> + val currentValue = when (field) { + EditedField.Btc -> state.btcAmount + EditedField.Sats -> state.satsAmount + is EditedField.Fiat -> state.fiatAmounts[field.code].orEmpty() + } + // Some platform text fields (notably SwiftUI's) can re-fire the binding's setter with + // the field's current, unchanged text just from gaining focus. Treat that as a no-op + // rather than re-deriving every other field from this one's rounded display value. + if (rawValue == currentValue) return@update state + + val withRaw = state.withField(field, rawValue) + val amount = rawValue.toBigDecimalOrNull() + val currentRates = rates + if (amount == null || currentRates == null) return@update withRaw + + val btcValue: BigDecimal? = when (field) { + EditedField.Btc -> amount + EditedField.Sats -> CurrencyConverter.satsToBtc(amount) + is EditedField.Fiat -> CurrencyConverter.fiatToBtc(amount, currentRates, field.code) + } + if (btcValue == null) return@update withRaw + + withRaw.copy( + btcAmount = if (field == EditedField.Btc) withRaw.btcAmount else formatAmount(btcValue, 8), + satsAmount = if (field == EditedField.Sats) { + withRaw.satsAmount + } else { + formatAmount(CurrencyConverter.btcToSats(btcValue), 0) + }, + fiatAmounts = withRaw.fiatAmounts + state.selectedFiatCurrencies + .filterNot { code -> field is EditedField.Fiat && field.code == code } + .associateWith { code -> + CurrencyConverter.btcToFiat(btcValue, currentRates, code) + ?.let { formatAmountFixed(it, decimalDigitsFor(code)) } + ?: withRaw.fiatAmounts[code].orEmpty() + }, + ) + } + } + + private fun ConverterUiState.withField(field: EditedField, value: String): ConverterUiState = when (field) { + EditedField.Btc -> copy(btcAmount = value) + EditedField.Sats -> copy(satsAmount = value) + is EditedField.Fiat -> copy(fiatAmounts = fiatAmounts + (field.code to value)) + } + + /** Recomputes every other field from whichever field currently holds a valid number. */ + private fun recomputeFromKnownField(state: ConverterUiState, rates: ExchangeRates): ConverterUiState { + val btc = state.btcAmount.toBigDecimalOrNull() + val sats = state.satsAmount.toBigDecimalOrNull() + var sourceFiatCode: String? = null + + val btcValue: BigDecimal? = when { + btc != null -> btc + sats != null -> CurrencyConverter.satsToBtc(sats) + else -> state.selectedFiatCurrencies.firstNotNullOfOrNull { code -> + state.fiatAmounts[code]?.toBigDecimalOrNull() + ?.let { amount -> CurrencyConverter.fiatToBtc(amount, rates, code) } + ?.also { sourceFiatCode = code } + } + } + + val rateDisplays = (state.selectedFiatCurrencies + state.defaultCurrencyCode).distinct().associateWith { code -> + rates.rates[code]?.let { formatAmountFixed(it, decimalDigitsFor(code)) } ?: "" + } + + if (btcValue == null) return state.copy(rateDisplays = rateDisplays) + + return state.copy( + btcAmount = if (btc != null) state.btcAmount else formatAmount(btcValue, 8), + satsAmount = if (sats != null) state.satsAmount else formatAmount(CurrencyConverter.btcToSats(btcValue), 0), + fiatAmounts = state.selectedFiatCurrencies.associateWith { code -> + if (code == sourceFiatCode) { + state.fiatAmounts[code].orEmpty() + } else { + CurrencyConverter.btcToFiat(btcValue, rates, code) + ?.let { formatAmountFixed(it, decimalDigitsFor(code)) } + ?: state.fiatAmounts[code].orEmpty() + } + }, + rateDisplays = rateDisplays, + ) + } +} diff --git a/shared/src/commonMain/moko-resources/base/strings.xml b/shared/src/commonMain/moko-resources/base/strings.xml new file mode 100644 index 0000000..01772eb --- /dev/null +++ b/shared/src/commonMain/moko-resources/base/strings.xml @@ -0,0 +1,18 @@ + + Price Source + 1 BTC to %1$s + Rate + Refresh + Retry + Bitcoin + Sats + BTC + Exceeds the 21,000,000 BTC maximum supply + Currencies + Add currency + %1$d selected + Remove %1$s + %1$s - %2$s + %1$s - %2$s (Local) + Done + diff --git a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/SharedCommonTest.kt b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/SharedCommonTest.kt new file mode 100644 index 0000000..0a613a2 --- /dev/null +++ b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/SharedCommonTest.kt @@ -0,0 +1,12 @@ +package xyz.tyiu.satsprice + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SharedCommonTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +} \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/SystemCurrenciesTest.kt b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/SystemCurrenciesTest.kt new file mode 100644 index 0000000..de273fe --- /dev/null +++ b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/SystemCurrenciesTest.kt @@ -0,0 +1,25 @@ +package xyz.tyiu.satsprice + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SystemCurrenciesTest { + + @Test + fun systemCurrencies_includesUsdWithADisplayName() { + val currencies = systemCurrencies() + + assertTrue(currencies.isNotEmpty()) + val usd = currencies.firstOrNull { it.code == "USD" } + assertTrue(usd != null, "expected USD to be present") + assertTrue(usd.displayName.isNotBlank()) + } + + @Test + fun currencyDecimalDigits_matchesKnownConventions() { + assertEquals(2, currencyDecimalDigits("USD")) + assertEquals(0, currencyDecimalDigits("JPY")) + assertEquals(3, currencyDecimalDigits("BHD")) + } +} diff --git a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/data/CoinGeckoExchangeRateSourceTest.kt b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/data/CoinGeckoExchangeRateSourceTest.kt new file mode 100644 index 0000000..579bdb9 --- /dev/null +++ b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/data/CoinGeckoExchangeRateSourceTest.kt @@ -0,0 +1,46 @@ +package xyz.tyiu.satsprice.data + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.http.HttpHeaders +import io.ktor.http.headersOf +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class CoinGeckoExchangeRateSourceTest { + + private val sampleResponse = """ + {"bitcoin":{"usd":62808.12345678,"eur":54291.5}} + """.trimIndent() + + @Test + fun getRates_parsesCoinGeckoResponseIntoExchangeRates() = runTest { + var requestedUrl = "" + val mockEngine = MockEngine { request -> + requestedUrl = request.url.toString() + respond( + content = sampleResponse, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + val client = HttpClient(mockEngine) { + install(ContentNegotiation) { json() } + } + + val source = CoinGeckoExchangeRateSource(client) + val result = source.getRates("BTC") + + assertEquals("BTC", result.base) + assertEquals(BigDecimal.parseString("62808.12345678"), result.rates["USD"]) + assertEquals(BigDecimal.parseString("54291.5"), result.rates["EUR"]) + assertTrue("ids=bitcoin" in requestedUrl) + assertTrue("vs_currencies=" in requestedUrl) + assertTrue("precision=full" in requestedUrl) + } +} diff --git a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/data/CoinbaseExchangeRateSourceTest.kt b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/data/CoinbaseExchangeRateSourceTest.kt new file mode 100644 index 0000000..b86d598 --- /dev/null +++ b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/data/CoinbaseExchangeRateSourceTest.kt @@ -0,0 +1,40 @@ +package xyz.tyiu.satsprice.data + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.http.HttpHeaders +import io.ktor.http.headersOf +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class CoinbaseExchangeRateSourceTest { + + private val sampleResponse = """ + {"data":{"currency":"BTC","rates":{"USD":"50000.12","EUR":"46000.5"}}} + """.trimIndent() + + @Test + fun getRates_parsesCoinbaseResponseIntoExchangeRates() = runTest { + val mockEngine = MockEngine { request -> + respond( + content = sampleResponse, + headers = headersOf(HttpHeaders.ContentType, "application/json"), + ) + } + val client = HttpClient(mockEngine) { + install(ContentNegotiation) { json() } + } + + val source = CoinbaseExchangeRateSource(client) + val result = source.getRates("BTC") + + assertEquals("BTC", result.base) + assertEquals(BigDecimal.parseString("50000.12"), result.rates["USD"]) + assertEquals(BigDecimal.parseString("46000.5"), result.rates["EUR"]) + } +} diff --git a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/data/ManualExchangeRateSourceTest.kt b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/data/ManualExchangeRateSourceTest.kt new file mode 100644 index 0000000..819023f --- /dev/null +++ b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/data/ManualExchangeRateSourceTest.kt @@ -0,0 +1,40 @@ +package xyz.tyiu.satsprice.data + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class ManualExchangeRateSourceTest { + + @Test + fun getRates_throwsWhenNoRateHasBeenSet() = runTest { + val source = ManualExchangeRateSource() + assertFailsWith { source.getRates("BTC") } + } + + @Test + fun getRates_returnsTheConfiguredRate() = runTest { + val source = ManualExchangeRateSource() + source.rate = BigDecimal.parseString("65000.5") + + val result = source.getRates("BTC") + + assertEquals("BTC", result.base) + assertEquals(BigDecimal.parseString("65000.5"), result.rates["USD"]) + assertEquals(setOf("USD"), result.rates.keys) + } + + @Test + fun getRates_usesTheConfiguredCurrencyCode() = runTest { + val source = ManualExchangeRateSource() + source.currencyCode = "EUR" + source.rate = BigDecimal.parseString("60000") + + val result = source.getRates("BTC") + + assertEquals(BigDecimal.parseString("60000"), result.rates["EUR"]) + assertEquals(setOf("EUR"), result.rates.keys) + } +} diff --git a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverterTest.kt b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverterTest.kt new file mode 100644 index 0000000..6e49a92 --- /dev/null +++ b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/domain/CurrencyConverterTest.kt @@ -0,0 +1,67 @@ +package xyz.tyiu.satsprice.domain + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import xyz.tyiu.satsprice.data.ExchangeRates +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.time.Instant + +class CurrencyConverterTest { + + private val rates = ExchangeRates( + base = "BTC", + rates = mapOf( + "USD" to BigDecimal.fromLong(50_000), + "ZERO" to BigDecimal.fromLong(0), + ), + fetchedAt = Instant.fromEpochMilliseconds(0), + ) + + @Test + fun btcToSats_convertsWholeBitcoin() { + assertEquals(BigDecimal.fromLong(100_000_000), CurrencyConverter.btcToSats(BigDecimal.fromLong(1))) + } + + @Test + fun satsToBtc_isInverseOfBtcToSats() { + assertEquals(BigDecimal.parseString("1.5"), CurrencyConverter.satsToBtc(BigDecimal.fromLong(150_000_000))) + } + + @Test + fun btcToFiat_usesRateForCurrency() { + assertEquals(BigDecimal.fromLong(100_000), CurrencyConverter.btcToFiat(BigDecimal.fromLong(2), rates, "USD")) + } + + @Test + fun fiatToBtc_isInverseOfBtcToFiat() { + assertEquals(BigDecimal.fromLong(2), CurrencyConverter.fiatToBtc(BigDecimal.fromLong(100_000), rates, "USD")) + } + + @Test + fun fiatToBtc_returnsNullForUnknownCurrency() { + assertNull(CurrencyConverter.fiatToBtc(BigDecimal.fromLong(100), rates, "XYZ")) + } + + @Test + fun fiatToBtc_returnsNullForZeroRate() { + assertNull(CurrencyConverter.fiatToBtc(BigDecimal.fromLong(100), rates, "ZERO")) + } + + @Test + fun satsToFiat_and_fiatToSats_roundTrip() { + val fiat = CurrencyConverter.satsToFiat(BigDecimal.fromLong(100_000_000), rates, "USD") + assertEquals(BigDecimal.fromLong(50_000), fiat) + + val sats = CurrencyConverter.fiatToSats(BigDecimal.fromLong(50_000), rates, "USD") + assertEquals(BigDecimal.fromLong(100_000_000), sats) + } + + @Test + fun handlesAmountsFarBeyondDoubleOrLongRange() { + // 10^30 BTC: overflows both Double's exact-integer range and Long.MAX_VALUE. + val hugeBtc = BigDecimal.parseString("1000000000000000000000000000000") + val sats = CurrencyConverter.btcToSats(hugeBtc) + assertEquals(BigDecimal.parseString("100000000000000000000000000000000000000"), sats) + } +} diff --git a/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/domain/NumberFormatTest.kt b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/domain/NumberFormatTest.kt new file mode 100644 index 0000000..1dae861 --- /dev/null +++ b/shared/src/commonTest/kotlin/xyz/tyiu/satsprice/domain/NumberFormatTest.kt @@ -0,0 +1,79 @@ +package xyz.tyiu.satsprice.domain + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class NumberFormatTest { + + @Test + fun formatAmount_trimsTrailingZeros() { + assertEquals("1.5", formatAmount(BigDecimal.parseString("1.5"), 8)) + } + + @Test + fun formatAmount_dropsDecimalPointWhenWhole() { + assertEquals("100000000", formatAmount(BigDecimal.fromLong(100_000_000), 0)) + assertEquals("1", formatAmount(BigDecimal.fromLong(1), 8)) + } + + @Test + fun formatAmount_roundsToRequestedPrecision() { + assertEquals("0.12346", formatAmount(BigDecimal.parseString("0.123456"), 5)) + } + + @Test + fun formatAmount_handlesAmountsBeyondLongRange() { + val huge = BigDecimal.parseString("123456789012345678901234567890.5") + assertEquals("123456789012345678901234567890.5", formatAmount(huge, 8)) + } + + @Test + fun toBigDecimalOrNull_parsesPlainDecimals() { + assertEquals(BigDecimal.parseString("1.5"), "1.5".toBigDecimalOrNull()) + assertEquals(BigDecimal.fromLong(100), "100".toBigDecimalOrNull()) + } + + @Test + fun toBigDecimalOrNull_rejectsIncompleteOrInvalidInput() { + assertNull("".toBigDecimalOrNull()) + assertNull("-".toBigDecimalOrNull()) + assertNull("1.".toBigDecimalOrNull()) + assertNull("1.2.3".toBigDecimalOrNull()) + assertNull("1e10".toBigDecimalOrNull()) + } + + @Test + fun sanitizeDecimalInput_stripsNonDigitsAndKeepsOnlyFirstDot() { + assertEquals("123.45", sanitizeDecimalInput("123.45")) + assertEquals("123.45", sanitizeDecimalInput("abc123.45xyz")) + assertEquals("1.2345", sanitizeDecimalInput("1.2.3.45")) + assertEquals("", sanitizeDecimalInput("-")) + assertEquals("1", sanitizeDecimalInput("-1")) + } + + @Test + fun sanitizeIntegerInput_stripsEverythingButDigits() { + assertEquals("12345", sanitizeIntegerInput("12345")) + assertEquals("12345", sanitizeIntegerInput("1a2b3c4d5")) + assertEquals("100", sanitizeIntegerInput("1.00")) + assertEquals("", sanitizeIntegerInput("-")) + } + + @Test + fun formatAmountFixed_padsWithTrailingZeros() { + assertEquals("100.00", formatAmountFixed(BigDecimal.fromLong(100), 2)) + assertEquals("100.50", formatAmountFixed(BigDecimal.parseString("100.5"), 2)) + } + + @Test + fun formatAmountFixed_roundsToRequestedPrecision() { + assertEquals("0.12", formatAmountFixed(BigDecimal.parseString("0.1234"), 2)) + } + + @Test + fun formatAmountFixed_dropsDecimalPointWhenZeroDecimals() { + assertEquals("100", formatAmountFixed(BigDecimal.fromLong(100), 0)) + } +} diff --git a/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt b/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt new file mode 100644 index 0000000..82d08c7 --- /dev/null +++ b/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/IosPriceViewModel.kt @@ -0,0 +1,78 @@ +package xyz.tyiu.satsprice + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import xyz.tyiu.satsprice.ui.ConverterUiState +import xyz.tyiu.satsprice.ui.PriceViewModel +import xyz.tyiu.satsprice.ui.defaultCurrencyRate +import xyz.tyiu.satsprice.ui.exceedsMaxSupply +import xyz.tyiu.satsprice.ui.statusLine + +data class FiatRow(val code: String, val amount: String, val rateDisplay: String) + +data class IosConverterState( + val btcAmount: String, + val satsAmount: String, + val exceedsMaxSupply: Boolean, + val fiatRows: List, + val availableCurrencies: List, + val selectedCurrencyCodes: List, + val localeCurrencyCode: String?, + val defaultCurrencyCode: String, + val sourceName: String, + val isManualSource: Boolean, + val manualRateInput: String, + val isLoading: Boolean, + val errorMessage: String?, + val statusLine: String, + val defaultCurrencyRate: String, +) + +/** + * Swift-facing bridge over [PriceViewModel]. Exposes a flattened, Map-free state shape + * (Kotlin/Native's ObjC/Swift export handles `List` far better than `Map`) + * and observes via a plain callback rather than raw `Flow`, so no extra Swift interop + * tooling (e.g. SKIE) is required. + */ +class IosPriceViewModel { + private val viewModel = PriceViewModel() + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + val sourceNames: List get() = viewModel.availableSources + + fun observeState(onChange: (IosConverterState) -> Unit) { + scope.launch { + viewModel.uiState.collect { state -> onChange(state.toIosState()) } + } + } + + fun refresh() = viewModel.refresh() + fun onBtcAmountChanged(value: String) = viewModel.onBtcAmountChanged(value) + fun onSatsAmountChanged(value: String) = viewModel.onSatsAmountChanged(value) + fun onFiatAmountChanged(code: String, value: String) = viewModel.onFiatAmountChanged(code, value) + fun onFiatCurrencyToggled(code: String) = viewModel.onFiatCurrencyToggled(code) + fun onSourceSelected(name: String) = viewModel.onSourceSelected(name) + fun onManualRateChanged(value: String) = viewModel.onManualRateChanged(value) +} + +private fun ConverterUiState.toIosState(): IosConverterState = IosConverterState( + btcAmount = btcAmount, + satsAmount = satsAmount, + exceedsMaxSupply = exceedsMaxSupply(), + fiatRows = selectedFiatCurrencies.map { code -> + FiatRow(code, fiatAmounts[code].orEmpty(), rateDisplays[code].orEmpty()) + }, + availableCurrencies = availableFiatCurrencies, + selectedCurrencyCodes = selectedFiatCurrencies, + localeCurrencyCode = localeCurrencyCode, + defaultCurrencyCode = defaultCurrencyCode, + sourceName = sourceName, + isManualSource = isManualSource, + manualRateInput = manualRateInput, + isLoading = isLoading, + errorMessage = errorMessage, + statusLine = statusLine(), + defaultCurrencyRate = defaultCurrencyRate(), +) diff --git a/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/Platform.ios.kt b/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/Platform.ios.kt new file mode 100644 index 0000000..97324bf --- /dev/null +++ b/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/Platform.ios.kt @@ -0,0 +1,9 @@ +package xyz.tyiu.satsprice + +import platform.UIKit.UIDevice + +class IOSPlatform : Platform { + override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion +} + +actual fun getPlatform(): Platform = IOSPlatform() \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.ios.kt b/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.ios.kt new file mode 100644 index 0000000..5aeb877 --- /dev/null +++ b/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.ios.kt @@ -0,0 +1,20 @@ +package xyz.tyiu.satsprice + +import platform.Foundation.* + +actual fun systemCurrencies(): List { + @Suppress("UNCHECKED_CAST") + val codes = NSLocale.Companion.ISOCurrencyCodes as List + return codes + .map { code -> CurrencyInfo(code, NSLocale.currentLocale.localizedStringForCurrencyCode(code) ?: code) } + .sortedBy { it.code } +} + +actual fun localeCurrencyCode(): String? = NSLocale.currentLocale.currencyCode + +actual fun currencyDecimalDigits(code: String): Int { + val formatter = NSNumberFormatter() + formatter.numberStyle = NSNumberFormatterCurrencyStyle + formatter.currencyCode = code + return formatter.maximumFractionDigits.toInt() +} diff --git a/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.ios.kt b/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.ios.kt new file mode 100644 index 0000000..54a1839 --- /dev/null +++ b/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.ios.kt @@ -0,0 +1,8 @@ +package xyz.tyiu.satsprice.ui + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +// PriceScreen.kt is unused on iOS (native SwiftUI is used instead), but this module still +// needs an actual to satisfy the expect declaration for this target. +actual val screenHorizontalPadding: Dp = 16.dp diff --git a/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/ui/IosLocalization.kt b/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/ui/IosLocalization.kt new file mode 100644 index 0000000..ed8f939 --- /dev/null +++ b/shared/src/iosMain/kotlin/xyz/tyiu/satsprice/ui/IosLocalization.kt @@ -0,0 +1,11 @@ +package xyz.tyiu.satsprice.ui + +import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.desc.ResourceFormatted +import dev.icerock.moko.resources.desc.StringDesc +import dev.icerock.moko.resources.desc.desc + +fun localizedString(resource: StringResource): String = resource.desc().localized() + +fun localizedFormattedString(resource: StringResource, args: List): String = + StringDesc.ResourceFormatted(resource, args).localized() diff --git a/shared/src/iosTest/kotlin/xyz/tyiu/satsprice/SharedLogicIOSTest.kt b/shared/src/iosTest/kotlin/xyz/tyiu/satsprice/SharedLogicIOSTest.kt new file mode 100644 index 0000000..f81ebce --- /dev/null +++ b/shared/src/iosTest/kotlin/xyz/tyiu/satsprice/SharedLogicIOSTest.kt @@ -0,0 +1,12 @@ +package xyz.tyiu.satsprice + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SharedLogicIOSTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +} \ No newline at end of file diff --git a/shared/src/jsMain/kotlin/xyz/tyiu/satsprice/Platform.js.kt b/shared/src/jsMain/kotlin/xyz/tyiu/satsprice/Platform.js.kt new file mode 100644 index 0000000..e0611e4 --- /dev/null +++ b/shared/src/jsMain/kotlin/xyz/tyiu/satsprice/Platform.js.kt @@ -0,0 +1,14 @@ +package xyz.tyiu.satsprice + +import web.navigator.navigator + +class JsPlatform : Platform { + private val userAgent = navigator.userAgent + private val browserList = listOf("Chrome", "Firefox", "Safari", "Edge") + + override val name: String = userAgent.findAnyOf(browserList, ignoreCase = true) + ?.let { (startIndex) -> userAgent.substring(startIndex).substringBefore(" ") } + ?: "Unknown" +} + +actual fun getPlatform(): Platform = JsPlatform() \ No newline at end of file diff --git a/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/Platform.jvm.kt b/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/Platform.jvm.kt new file mode 100644 index 0000000..1049723 --- /dev/null +++ b/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/Platform.jvm.kt @@ -0,0 +1,7 @@ +package xyz.tyiu.satsprice + +class JVMPlatform : Platform { + override val name: String = "Java ${System.getProperty("java.version")}" +} + +actual fun getPlatform(): Platform = JVMPlatform() \ No newline at end of file diff --git a/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.jvm.kt b/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.jvm.kt new file mode 100644 index 0000000..2b1dd63 --- /dev/null +++ b/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.jvm.kt @@ -0,0 +1,21 @@ +package xyz.tyiu.satsprice + +import java.util.Currency +import java.util.Locale + +actual fun systemCurrencies(): List = + Currency.getAvailableCurrencies() + .map { CurrencyInfo(it.currencyCode, it.getDisplayName(Locale.getDefault())) } + .sortedBy { it.code } + +actual fun localeCurrencyCode(): String? = try { + Currency.getInstance(Locale.getDefault())?.currencyCode +} catch (e: IllegalArgumentException) { + null +} + +actual fun currencyDecimalDigits(code: String): Int = try { + Currency.getInstance(code).defaultFractionDigits.takeIf { it >= 0 } ?: 2 +} catch (e: IllegalArgumentException) { + 2 +} diff --git a/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.jvm.kt b/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.jvm.kt new file mode 100644 index 0000000..8dc4032 --- /dev/null +++ b/shared/src/jvmMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.jvm.kt @@ -0,0 +1,6 @@ +package xyz.tyiu.satsprice.ui + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +actual val screenHorizontalPadding: Dp = 24.dp diff --git a/shared/src/jvmTest/kotlin/xyz/tyiu/satsprice/SharedLogicDesktopTest.kt b/shared/src/jvmTest/kotlin/xyz/tyiu/satsprice/SharedLogicDesktopTest.kt new file mode 100644 index 0000000..0ba75d8 --- /dev/null +++ b/shared/src/jvmTest/kotlin/xyz/tyiu/satsprice/SharedLogicDesktopTest.kt @@ -0,0 +1,12 @@ +package xyz.tyiu.satsprice + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SharedLogicDesktopTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +} \ No newline at end of file diff --git a/shared/src/wasmJsMain/kotlin/xyz/tyiu/satsprice/Platform.wasmJs.kt b/shared/src/wasmJsMain/kotlin/xyz/tyiu/satsprice/Platform.wasmJs.kt new file mode 100644 index 0000000..d0ead5e --- /dev/null +++ b/shared/src/wasmJsMain/kotlin/xyz/tyiu/satsprice/Platform.wasmJs.kt @@ -0,0 +1,7 @@ +package xyz.tyiu.satsprice + +class WasmPlatform : Platform { + override val name: String = "Web with Kotlin/Wasm" +} + +actual fun getPlatform(): Platform = WasmPlatform() \ No newline at end of file diff --git a/shared/src/webMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.web.kt b/shared/src/webMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.web.kt new file mode 100644 index 0000000..2ec3fcd --- /dev/null +++ b/shared/src/webMain/kotlin/xyz/tyiu/satsprice/SystemCurrencies.web.kt @@ -0,0 +1,34 @@ +@file:OptIn(kotlin.js.ExperimentalWasmJsInterop::class) + +package xyz.tyiu.satsprice + +private fun jsSupportedCurrencyCodes(): JsArray = js("Intl.supportedValuesOf('currency')") + +private fun jsCurrencyDisplayName(code: String): String = + js("new Intl.DisplayNames(['en'], { type: 'currency' }).of(code)") + +actual fun systemCurrencies(): List = + jsSupportedCurrencyCodes() + .toList() + .map { jsCode -> + val code = jsCode.toString() + CurrencyInfo(code, jsCurrencyDisplayName(code)) + } + .sortedBy { it.code } + +/** + * No web API infers a currency from a locale: `Intl.NumberFormat`/`Intl.Locale` require an + * explicit `currency` option rather than deriving one, and there's no TC39 proposal that adds + * this. Doing it reliably would mean maintaining our own region-to-currency table, so this is + * left undetected on web rather than guessing. + */ +actual fun localeCurrencyCode(): String? = null + +private fun jsCurrencyFractionDigits(code: String): Int = + js("new Intl.NumberFormat('en', { style: 'currency', currency: code }).resolvedOptions().maximumFractionDigits") + +actual fun currencyDecimalDigits(code: String): Int = try { + jsCurrencyFractionDigits(code) +} catch (e: Exception) { + 2 +} diff --git a/shared/src/webMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.web.kt b/shared/src/webMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.web.kt new file mode 100644 index 0000000..e88ede7 --- /dev/null +++ b/shared/src/webMain/kotlin/xyz/tyiu/satsprice/ui/Dimensions.web.kt @@ -0,0 +1,6 @@ +package xyz.tyiu.satsprice.ui + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +actual val screenHorizontalPadding: Dp = 16.dp diff --git a/shared/src/webTest/kotlin/xyz/tyiu/satsprice/SharedLogicWebTest.kt b/shared/src/webTest/kotlin/xyz/tyiu/satsprice/SharedLogicWebTest.kt new file mode 100644 index 0000000..cb0361c --- /dev/null +++ b/shared/src/webTest/kotlin/xyz/tyiu/satsprice/SharedLogicWebTest.kt @@ -0,0 +1,12 @@ +package xyz.tyiu.satsprice + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SharedLogicWebTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +} \ No newline at end of file diff --git a/webApp/build.gradle.kts b/webApp/build.gradle.kts new file mode 100644 index 0000000..e668023 --- /dev/null +++ b/webApp/build.gradle.kts @@ -0,0 +1,58 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.mokoResources) +} + +kotlin { + js { + browser() + binaries.executable() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + commonMain.dependencies { + implementation(project(":shared")) + + implementation(libs.compose.ui) + + implementation(npm("@js-joda/timezone", "2.25.1")) + } + } +} + +multiplatformResources { + resourcesPackage.set("xyz.tyiu.satsprice.webApp") +} + +// moko-resources loads string/plural/image/file resources at runtime via fetch('./localization/...'), +// not via a webpack import, so webpack's production bundler has no reason to know that directory +// exists and never copies it into the distribution. (The dev server happens to serve it anyway, since +// it serves the whole compiled package directory as static files — which is why this only breaks in +// production.) Copy it into the actual distribution output ourselves so a static production build is +// self-contained. +val npmPackageName = "${rootProject.name}-${project.name}" +mapOf( + "wasmJsBrowserDistribution" to "wasm", + "jsBrowserDistribution" to "js", +).forEach { (taskName, targetDir) -> + tasks.named(taskName) { + val localizationSrc = rootProject.layout.buildDirectory + .dir("$targetDir/packages/$npmPackageName/kotlin/localization") + doLast { + val src = localizationSrc.get().asFile + if (src.exists()) { + src.copyRecursively(outputs.files.singleFile.resolve("localization"), overwrite = true) + } + } + } +} \ No newline at end of file diff --git a/webApp/karma.config.d/moko-resources-generated.js b/webApp/karma.config.d/moko-resources-generated.js new file mode 100644 index 0000000..7ca093d --- /dev/null +++ b/webApp/karma.config.d/moko-resources-generated.js @@ -0,0 +1,21 @@ +// workaround from https://github.com/ryanclark/karma-webpack/issues/498#issuecomment-790040818 + +const output = { + path: require("os").tmpdir() + '/' + '_karma_webpack_' + Math.floor(Math.random() * 1000000), +} + +const optimization = { + runtimeChunk: true +} + +config.set( + { + webpack: {... createWebpackConfig(), output, optimization}, + files: config.files.concat([{ + pattern: `${output.path}/**/*`, + watched: false, + included: false, + }] + ) + } +) \ No newline at end of file diff --git a/webApp/src/webMain/kotlin/xyz/tyiu/satsprice/main.kt b/webApp/src/webMain/kotlin/xyz/tyiu/satsprice/main.kt new file mode 100644 index 0000000..f822270 --- /dev/null +++ b/webApp/src/webMain/kotlin/xyz/tyiu/satsprice/main.kt @@ -0,0 +1,17 @@ +package xyz.tyiu.satsprice + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport + +@OptIn(ExperimentalWasmJsInterop::class) +@JsModule("@js-joda/timezone") +external object JsJodaTimeZoneModule + +private val jsJodaTz = JsJodaTimeZoneModule + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + ComposeViewport { + App() + } +} \ No newline at end of file diff --git a/webApp/src/webMain/resources/favicon-192.png b/webApp/src/webMain/resources/favicon-192.png new file mode 100644 index 0000000..27f2460 Binary files /dev/null and b/webApp/src/webMain/resources/favicon-192.png differ diff --git a/webApp/src/webMain/resources/favicon.ico b/webApp/src/webMain/resources/favicon.ico new file mode 100644 index 0000000..d0c7ef7 Binary files /dev/null and b/webApp/src/webMain/resources/favicon.ico differ diff --git a/webApp/src/webMain/resources/index.html b/webApp/src/webMain/resources/index.html new file mode 100644 index 0000000..161d586 --- /dev/null +++ b/webApp/src/webMain/resources/index.html @@ -0,0 +1,22 @@ + + + + + + SatsPrice + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webApp/src/webMain/resources/styles.css b/webApp/src/webMain/resources/styles.css new file mode 100644 index 0000000..0549b10 --- /dev/null +++ b/webApp/src/webMain/resources/styles.css @@ -0,0 +1,7 @@ +html, body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} \ No newline at end of file diff --git a/webApp/webpack.config.d/moko-resources-generated.js b/webApp/webpack.config.d/moko-resources-generated.js new file mode 100644 index 0000000..ec85a79 --- /dev/null +++ b/webApp/webpack.config.d/moko-resources-generated.js @@ -0,0 +1,39 @@ +// noinspection JSUnnecessarySemicolon +;(function(config) { + const path = require('path'); + const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + + config.module.rules.push( + { + test: /\.(.*)/, + resource: [ + path.resolve(__dirname, "kotlin/assets"), + path.resolve(__dirname, "kotlin/files"), + path.resolve(__dirname, "kotlin/images"), + path.resolve(__dirname, "kotlin/localization"), + ], + type: 'asset/resource' + } + ); + + config.plugins.push(new MiniCssExtractPlugin()) + config.module.rules.push( + { + test: /\.css$/, + resource: [ + path.resolve(__dirname, "kotlin/fonts"), + ], + use: ['style-loader', 'css-loader'] + } + ) + + config.module.rules.push( + { + test: /\.(otf|ttf)?$/, + resource: [ + path.resolve(__dirname, "kotlin/fonts"), + ], + type: 'asset/resource', + } + ) +})(config); \ No newline at end of file