diff --git a/.gitignore b/.gitignore index 228a7a77..c8a2648f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ sample/desktopApp/cache sample/desktopApp/DawnCache sample/desktopApp/GPUCache sample/desktopApp/*.log -site \ No newline at end of file +site +/.kotlin \ No newline at end of file diff --git a/.run/androidApp.run.xml b/.run/androidApp.run.xml new file mode 100644 index 00000000..e0945421 --- /dev/null +++ b/.run/androidApp.run.xml @@ -0,0 +1,74 @@ + + + + + \ No newline at end of file diff --git a/.run/desktopApp.run.xml b/.run/desktopApp.run.xml index 57d2fc8c..b249809b 100644 --- a/.run/desktopApp.run.xml +++ b/.run/desktopApp.run.xml @@ -1,23 +1,23 @@ - - - - - - true - true - false - - + + + + + + true + true + false + + \ No newline at end of file diff --git a/.run/iosApp.run.xml b/.run/iosApp.run.xml new file mode 100644 index 00000000..72ffbc97 --- /dev/null +++ b/.run/iosApp.run.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/.run/wasmJsApp.run.xml b/.run/wasmJsApp.run.xml new file mode 100644 index 00000000..578894e5 --- /dev/null +++ b/.run/wasmJsApp.run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/README.desktop.md b/README.desktop.md index 8efb087d..345a771d 100644 --- a/README.desktop.md +++ b/README.desktop.md @@ -21,6 +21,7 @@ Please use the following example as a reference. ```kotlin fun main() = application { + addTempDirectoryRemovalHook() Window(onCloseRequest = ::exitApplication) { var restartRequired by remember { mutableStateOf(false) } var downloading by remember { mutableStateOf(0F) } diff --git a/README.md b/README.md index 222ce97c..50666d70 100644 --- a/README.md +++ b/README.md @@ -347,7 +347,7 @@ class GreetJsMessageHandler : IJsMessageHandler { "Greet Handler Get Message: $message" } val param = processParams(message) - val data = GreetModel("KMM Received ${param.message}") + val data = GreetModel("KMP Received ${param.message}") callback(dataToJsonString(data)) } } diff --git a/build.gradle.kts b/build.gradle.kts index ce4f5448..e25f44d3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,7 @@ subprojects { // Optionally configure plugin configure { - version.set("1.0.1") + version.set("1.6.0") filter { exclude("**/generated/**") include("**/kotlin/**") diff --git a/docs/communication.md b/docs/communication.md index 7d433757..f21e15ed 100644 --- a/docs/communication.md +++ b/docs/communication.md @@ -34,7 +34,7 @@ MaterialTheme { onClick = { webViewNavigator.evaluateJavaScript( """ - document.getElementById("subtitle").innerText = "Hello from KMM!"; + document.getElementById("subtitle").innerText = "Hello from KMP!"; """.trimIndent(), ) { // handle the result @@ -89,7 +89,7 @@ class GreetJsMessageHandler : IJsMessageHandler { "Greet Handler Get Message: $message" } val param = processParams(message) - val data = GreetModel("KMM Received ${param.message}") + val data = GreetModel("KMP Received ${param.message}") callback(dataToJsonString(data)) } } diff --git a/gradle.properties b/gradle.properties index 41436b1c..84364242 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,17 +9,13 @@ kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.androidSourceSetLayoutVersion=2 #Android android.useAndroidX=true -android.compileSdk=34 -android.targetSdk=34 +android.compileSdk=36 +android.targetSdk=36 android.minSdk=21 #Versions -kotlin.version=1.9.23 -agp.version=8.1.1 -compose.version=1.7.1 -coroutines.version=1.8.0 GROUP=io.github.kevinnzou POM_ARTIFACT_ID=compose-webview-multiplatform -VERSION_NAME=1.9.40 +VERSION_NAME=2.0.0 POM_NAME=Compose WebView Multiplatform POM_INCEPTION_YEAR=2023 POM_DESCRIPTION=WebView for JetBrains Compose Multiplatform diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 15d27ab8..f1ec3d85 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,33 +1,35 @@ [versions] -agp = "8.1.1" -compose = "1.7.1" -kotlin = "1.9.23" -coroutines = "1.8.0" -voyager = "1.0.0-rc10" +agp = "8.9.3" +compose = "1.8.0" +kotlin = "2.1.20" +coroutines = "1.10.2" +voyager = "1.1.0-beta01" [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } compose-multiplatorm = { id = "org.jetbrains.compose", version.ref = "compose" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } dokka = "org.jetbrains.dokka:1.9.0" kotlin-atomicfu = { id = "org.jetbrains.kotlin.plugin.atomicfu", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -ktlint = "org.jlleitschuh.gradle.ktlint:11.6.1" +ktlint = "org.jlleitschuh.gradle.ktlint:12.3.0" maven-publish = "com.vanniktech.maven.publish:0.25.3" [libraries] -android-activity-compose = "androidx.activity:activity-compose:1.8.2" -android-appcompat = "androidx.appcompat:appcompat:1.6.1" -android-webkit = "androidx.webkit:webkit:1.10.0" -compose-navigation = "org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha08" +android-activity-compose = "androidx.activity:activity-compose:1.10.1" +android-appcompat = "androidx.appcompat:appcompat:1.7.0" +android-webkit = "androidx.webkit:webkit:1.13.0" +compose-navigation = "org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha13" +material-icons-core = "org.jetbrains.compose.material:material-icons-core:1.7.3" kcef = "dev.datlag:kcef:2024.04.20.3" kermit = "co.touchlab:kermit:2.0.3" -kotlin-atomicfu = "org.jetbrains.kotlinx:atomicfu:0.23.2" +kotlin-atomicfu = "org.jetbrains.kotlinx:atomicfu:0.27.0" kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlin-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } -kotlin-serialization-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0" +kotlin-serialization-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1" kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock new file mode 100644 index 00000000..184cf16c --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,2886 @@ +# 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.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@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/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@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== + +"@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.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + 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.3.5": + 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.18" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.18.tgz#101e033b3ca06695f3d73c587cd7f9eb348135d1" + integrity sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA== + dependencies: + "@types/node" "*" + +"@types/estree@^1.0.5": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz#41fec4ea20e9c7b22f024ab88a95c6bb288f51b8" + integrity sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.2.tgz#7be9e337a5745d6b43ef5b0c352dad94a7f0c256" + integrity sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "*" + +"@types/express@^4.17.13": + version "4.17.22" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.22.tgz#14cfcf120f7eb56ebb8ca77b7fa9a14d21de7c96" + integrity sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.16" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.16.tgz#dee360707b35b3cc85afcde89ffeebff7d7f9240" + integrity sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w== + dependencies: + "@types/node" "*" + +"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@>=10.0.0": + version "22.15.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.18.tgz#2f8240f7e932f571c2d45f555ba0b6c3f7a75963" + integrity sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg== + dependencies: + undici-types "~6.21.0" + +"@types/qs@*": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1": + 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@*", "@types/serve-static@^1.13.10": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/sockjs@^0.3.33": + 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.5": + 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.12.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.12.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.12.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@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@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-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + +acorn@^8.14.0, acorn@^8.7.1: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +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-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" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@1.20.3, body-parser@^1.19.0: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.3.0.tgz#80d867430b5a0da64e82a8047fc1e355bdb71722" + integrity sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + 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.21.10: + version "4.24.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.5.tgz#aa0f5b8560fe81fde84c6dcb38f759bafba0e11b" + integrity sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw== + dependencies: + caniuse-lite "^1.0.30001716" + electron-to-chromium "^1.5.149" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + +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== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001716: + version "1.0.30001718" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82" + integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw== + +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.5.3: + 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" + +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" + +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@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +compressible@~2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.8.0" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.0.tgz#09420efc96e11a0f44f3a558de59e321364180f7" + integrity sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA== + dependencies: + bytes "3.1.2" + compressible "~2.0.18" + debug "2.6.9" + negotiator "~0.6.4" + on-headers "~1.0.2" + safe-buffer "5.2.1" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +cookie@~0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.3: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== + +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0, debug@^4.3.4, debug@^4.3.5: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + +debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + +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" + +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.149: + version "1.5.155" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz#809dd0ae9ae1db87c358e0c0c17c09a2ffc432d1" + integrity sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng== + +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== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + +engine.io@~6.6.0: + version "6.6.4" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.4.tgz#0a89a3e6b6c1d4b0c2a2a637495e7c149ec8d8ee" + integrity sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g== + dependencies: + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.7.2" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + +enhanced-resolve@^5.17.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" + integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@~2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.2.tgz#22a5ed2fd7ce0cbcff1d1474cf4909a44bdb6e85" + integrity sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + punycode "^1.4.1" + safe-regex-test "^1.1.0" + +envinfo@^7.7.3: + version "7.14.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +express@^4.17.3: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + +follow-redirects@^1.0.0: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +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-monkey@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + +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" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +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-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@^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" + +glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.6.0.tgz#7c64f1ea3b36818ccae3d3fb48b6974208e984f8" + integrity sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" + integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== + +http-proxy-middleware@^2.0.3: + version "2.0.9" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" + integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +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-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +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@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.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== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.1.tgz#4eafd31bbe684a747a6e8f3e4ad373e53979ced4" + integrity sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ== + dependencies: + glob "^7.1.3" + minimatch "^9.0.3" + webpack-merge "^4.1.5" + +karma@6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.4.tgz#dfa5a426cf5a8b53b43cd54ef0d0d09742351492" + integrity sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w== + dependencies: + "@colors/colors" "1.5.0" + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.7.2" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kotlin-web-helpers@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-2.0.0.tgz#b112096b273c1e733e0b86560998235c09a19286" + integrity sha512-xkVGl60Ygn/zuLkDPx+oHj7jeLR7hCvoNF99nhwXMn8a3ApB4lLiC9pk4ol4NHPjyoCbvQctBqvzUcp8pkqyWw== + dependencies: + format-util "^1.0.5" + +launch-editor@^2.6.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.10.0.tgz#5ca3edfcb9667df1e8721310f3a40f1127d4bc42" + integrity sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + +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@^3.4.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +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": + 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.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@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== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1, minimatch@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.3: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +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@10.7.3: + version "10.7.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.3.tgz#ae32003cabbd52b59aece17846056a68eb4b0752" + integrity sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +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== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + 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.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +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" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.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@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.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== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, 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-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + 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" + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0, schema-utils@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.1.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a" + integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +socket.io-adapter@~2.5.2: + version "2.5.5" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" + integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg== + dependencies: + debug "~4.3.4" + ws "~8.17.1" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.7.2: + version "4.8.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a" + integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.6.0" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-loader@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-5.0.0.tgz#f593a916e1cc54471cfc8851b905c8a845fc7e38" + integrity sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA== + dependencies: + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + +string-width@^4.1.0, 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_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@^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-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.10: + version "5.3.14" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" + +terser@^5.31.1: + version "5.39.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.39.2.tgz#5a1626030724a672e2e5b5c9cd9070308c20e8f9" + integrity sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.14.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +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.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +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" + +typescript@5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + +ua-parser-js@^0.7.30: + version "0.7.40" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.40.tgz#c87d83b7bb25822ecfa6397a0da5903934ea1562" + integrity sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" + integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.1" + "@webpack-cli/info" "^2.0.2" + "@webpack-cli/serve" "^2.0.5" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@4.15.2: + version "4.15.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" + integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.5" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + launch-editor "^2.6.0" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.4" + ws "^8.13.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@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.94.0: + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + 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 "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== + +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" + +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.13.0: + version "8.18.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" + integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== + +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + +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, yargs-parser@^20.2.9: + 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-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, yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +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/sample/androidApp/build.gradle.kts b/sample/androidApp/build.gradle.kts index 034201be..310b94fb 100644 --- a/sample/androidApp/build.gradle.kts +++ b/sample/androidApp/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.android.application) alias(libs.plugins.compose.multiplatorm) } diff --git a/sample/androidApp/src/androidMain/AndroidManifest.xml b/sample/androidApp/src/androidMain/AndroidManifest.xml index dcd70691..b1eb2f33 100644 --- a/sample/androidApp/src/androidMain/AndroidManifest.xml +++ b/sample/androidApp/src/androidMain/AndroidManifest.xml @@ -8,20 +8,21 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" - android:usesCleartextTraffic="true" android:supportsRtl="true" - android:theme="@style/Theme.AppCompat.Light.NoActionBar"> + android:usesCleartextTraffic="true"> + + android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode" + android:exported="true" + android:theme="@android:style/Theme.Material.Light.NoActionBar"> + diff --git a/sample/androidApp/src/androidMain/kotlin/com/multiplatform/webview/MainActivity.kt b/sample/androidApp/src/androidMain/kotlin/com/multiplatform/webview/MainActivity.kt index 0d0ddd7e..7b560096 100644 --- a/sample/androidApp/src/androidMain/kotlin/com/multiplatform/webview/MainActivity.kt +++ b/sample/androidApp/src/androidMain/kotlin/com/multiplatform/webview/MainActivity.kt @@ -1,14 +1,17 @@ package com.multiplatform.webview +import android.graphics.Color import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity +import androidx.activity.enableEdgeToEdge import com.kevinnzou.sample.MainWebView -class MainActivity : AppCompatActivity() { +class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT)) setContent { MainWebView() } diff --git a/sample/desktopApp/build.gradle.kts b/sample/desktopApp/build.gradle.kts index 7353c2f6..b3bc11b6 100644 --- a/sample/desktopApp/build.gradle.kts +++ b/sample/desktopApp/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.compose.multiplatorm) } diff --git a/sample/desktopApp/src/jvmMain/kotlin/main.kt b/sample/desktopApp/src/jvmMain/kotlin/main.kt index ee0183ed..a5404d3a 100644 --- a/sample/desktopApp/src/jvmMain/kotlin/main.kt +++ b/sample/desktopApp/src/jvmMain/kotlin/main.kt @@ -1,13 +1,30 @@ +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text +import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import com.kevinnzou.sample.MainWebView +import com.multiplatform.webview.util.addTempDirectoryRemovalHook import dev.datlag.kcef.KCEF import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -15,7 +32,8 @@ import java.io.File fun main() = application { - Window(onCloseRequest = ::exitApplication) { + addTempDirectoryRemovalHook() + Window(onCloseRequest = ::exitApplication, title = "Compose WebView Multiplatform") { var restartRequired by remember { mutableStateOf(false) } var downloading by remember { mutableStateOf(0F) } var initialized by remember { mutableStateOf(false) } @@ -49,13 +67,15 @@ fun main() = } } - if (restartRequired) { - Text(text = "Restart required.") - } else { - if (initialized) { - MainWebView() + MaterialTheme { + if (restartRequired) { + RestartRequiredScreen() } else { - Text(text = "Downloading $downloading%") + if (initialized) { + MainWebView() + } else { + LoadingScreen(downloadProgress = downloading) + } } } @@ -66,3 +86,87 @@ fun main() = } } } + +@Composable +fun LoadingScreen(downloadProgress: Float) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + CircularProgressIndicator( + modifier = Modifier.size(32.dp), + color = Color.Black, + strokeWidth = 2.dp, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Compose WebView Multiplatform", + fontSize = 18.sp, + color = Color.Black, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Initializing...", + fontSize = 12.sp, + color = Color.Black.copy(alpha = 0.7f), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + LinearProgressIndicator( + progress = downloadProgress / 100f, + modifier = + Modifier + .size( + width = 200.dp, + height = 4.dp, + ), + color = Color.Black, + backgroundColor = Color.Black.copy(alpha = 0.2f), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "${downloadProgress.toInt()}%", + fontSize = 12.sp, + color = Color.Black, + ) + } + } +} + +@Composable +fun RestartRequiredScreen() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = "Restart Required", + fontSize = 18.sp, + color = Color.Black, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Please restart the application to continue.", + fontSize = 12.sp, + color = Color.Black.copy(alpha = 0.7f), + ) + } + } +} diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index 3f264631..f26339e3 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -1,20 +1,23 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig + plugins { alias(libs.plugins.android.library) alias(libs.plugins.compose.multiplatorm) alias(libs.plugins.kotlin.atomicfu) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.kotlin.serialization) } @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) kotlin { - targetHierarchy.default() + applyDefaultHierarchyTemplate() androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "17" - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) } } @@ -30,14 +33,33 @@ kotlin { } } - sourceSets { + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser { + val rootDirPath = project.rootDir.path + val projectDirPath = project.projectDir.path + commonWebpackConfig { + devServer = + (devServer ?: KotlinWebpackConfig.DevServer()) + .apply { + static = + (static ?: mutableListOf()).apply { + add(rootDirPath) + add(projectDirPath) + } + } + } + } + } + sourceSets { commonMain.dependencies { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) implementation(compose.components.resources) implementation(libs.compose.navigation) + implementation(libs.material.icons.core) implementation(libs.kermit) implementation(libs.kotlin.serialization.json) implementation(libs.kotlin.atomicfu) @@ -57,7 +79,6 @@ kotlin { api(libs.android.appcompat) implementation(libs.kotlin.coroutines.android) } - val desktopMain by getting desktopMain.dependencies { implementation(compose.desktop.common) @@ -68,10 +89,8 @@ kotlin { android { namespace = "com.kevinnzou.sample" - compileSdk = 34 + compileSdk = 36 - sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") - sourceSets["main"].res.srcDirs("src/androidMain/res") sourceSets["main"].resources.srcDirs("src/commonMain/resources") defaultConfig { diff --git a/sample/shared/src/androidMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.android.kt b/sample/shared/src/androidMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.android.kt index 14790874..452de600 100644 --- a/sample/shared/src/androidMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.android.kt +++ b/sample/shared/src/androidMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.android.kt @@ -33,22 +33,24 @@ actual fun getPlatformWebViewParams(): PlatformWebViewParams? { contract = ActivityResultContracts.StartActivityForResult(), ) { result: ActivityResult -> if (result.resultCode != Activity.RESULT_OK) { - Toast.makeText( - webViewChromeClient.context, - "resultCode is not RESULT_OK (value: ${result.resultCode})", - Toast.LENGTH_SHORT, - ).show() + Toast + .makeText( + webViewChromeClient.context, + "resultCode is not RESULT_OK (value: ${result.resultCode})", + Toast.LENGTH_SHORT, + ).show() webViewChromeClient.cancelFileChooser() return@rememberLauncherForActivityResult } val intent = result.data if (intent == null) { - Toast.makeText( - webViewChromeClient.context, - "result intent is null", - Toast.LENGTH_SHORT, - ).show() + Toast + .makeText( + webViewChromeClient.context, + "result intent is null", + Toast.LENGTH_SHORT, + ).show() webViewChromeClient.cancelFileChooser() return@rememberLauncherForActivityResult } @@ -60,18 +62,19 @@ actual fun getPlatformWebViewParams(): PlatformWebViewParams? { singleFile != null -> webViewChromeClient.onReceiveFiles(arrayOf(singleFile)) multiFiles != null -> webViewChromeClient.onReceiveFiles(multiFiles.toTypedArray()) else -> { - Toast.makeText( - webViewChromeClient.context, - "data and clipData is null", - Toast.LENGTH_SHORT, - ).show() + Toast + .makeText( + webViewChromeClient.context, + "data and clipData is null", + Toast.LENGTH_SHORT, + ).show() webViewChromeClient.cancelFileChooser() } } } LaunchedEffect(key1 = fileChooserIntent) { - if (fileChooserIntent != null) { + fileChooserIntent?.let { fileChooserIntent -> try { launcher.launch(fileChooserIntent) } catch (e: ActivityNotFoundException) { diff --git a/sample/shared/src/commonMain/resources/assets/index.html b/sample/shared/src/commonMain/composeResources/files/samples/index.html similarity index 59% rename from sample/shared/src/commonMain/resources/assets/index.html rename to sample/shared/src/commonMain/composeResources/files/samples/index.html index 17bd845f..ba6abdfe 100644 --- a/sample/shared/src/commonMain/resources/assets/index.html +++ b/sample/shared/src/commonMain/composeResources/files/samples/index.html @@ -6,8 +6,10 @@ Compose WebView Multiplatform -

Compose WebView Multiplatform

-

Basic Html Test

- +

Compose WebView Multiplatform

+

Basic Html Test

+
+ +
- \ No newline at end of file + diff --git a/sample/shared/src/commonMain/resources/assets/script.js b/sample/shared/src/commonMain/composeResources/files/samples/script.js similarity index 100% rename from sample/shared/src/commonMain/resources/assets/script.js rename to sample/shared/src/commonMain/composeResources/files/samples/script.js diff --git a/sample/shared/src/commonMain/composeResources/files/samples/styles.css b/sample/shared/src/commonMain/composeResources/files/samples/styles.css new file mode 100644 index 00000000..c463d584 --- /dev/null +++ b/sample/shared/src/commonMain/composeResources/files/samples/styles.css @@ -0,0 +1,477 @@ +/* styles.css */ + +:root { + --primary: #4e54c8; + --secondary: #8f94fb; + --white: #ffffff; + --shadow: rgba(0, 0, 0, 0.1); + --text-shadow: rgba(0, 0, 0, 0.3); +} + +body { + background: linear-gradient(135deg, var(--primary), var(--secondary)); + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + box-shadow: inset 0 0 20px var(--shadow); + padding: 2rem; + box-sizing: border-box; +} + +.container { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 2rem; + max-width: 800px; + width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +h1, h2, h3 { + text-align: center; + color: var(--white); + text-shadow: 1px 1px 4px var(--text-shadow); + margin: 0 0 1rem; +} + +h1 { + font-size: 2.5rem; + font-weight: 700; +} + +h2 { + font-size: 1.8rem; + font-weight: 600; +} + +p { + color: var(--white); + line-height: 1.6; + margin: 0 0 1.5rem; +} + +.btn { + background: rgba(255, 255, 255, 0.2); + color: var(--white); + border: none; + padding: 0.8rem 1.5rem; + border-radius: 50px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.btn-primary { + background: var(--primary); + border: 1px solid rgba(255, 255, 255, 0.5); +} + +.btn-secondary { + background: var(--secondary); + border: 1px solid rgba(255, 255, 255, 0.5); +} + +.btn-group { + display: flex; + gap: 1rem; + justify-content: center; + margin: 2rem 0; +} + +.subtitle { + color: var(--white); + text-align: center; + font-size: 1.2rem; + margin-bottom: 2rem; + opacity: 0.9; + text-shadow: 1px 1px 2px var(--text-shadow); +} + +.btn-custom { + background: linear-gradient(45deg, var(--primary), var(--secondary)); + color: var(--white); + border: none; + padding: 0.8rem 2rem; + border-radius: 50px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + position: relative; + overflow: hidden; +} + +.btn-custom:hover { + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); +} + +.btn-custom::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transform: translateX(-100%); + transition: transform 0.6s ease; +} + +input[type="file"] { + display: none; +} + +label[for^="file-upload"] { + background: rgba(255, 255, 255, 0.2); + color: var(--white); + border: none; + padding: 0.8rem 1.5rem; + border-radius: 50px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); + display: inline-block; + text-align: center; + position: relative; + overflow: hidden; +} + +label[for^="file-upload"]:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +label[for^="file-upload"]::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transform: translateX(-100%); + transition: transform 0.6s ease; +} + +label[for^="file-upload"]:hover::after { + transform: translateX(100%); +} + +.upload-input { + display: none; +} + +.upload-label { + background: rgba(255, 255, 255, 0.2); + color: var(--white); + border: 2px dashed rgba(255, 255, 255, 0.3); + padding: 1.5rem; + border-radius: 15px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + display: block; + text-align: center; + position: relative; + overflow: hidden; + backdrop-filter: blur(5px); +} + +.upload-label:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.upload-container { + display: flex; + flex-direction: column; + gap: 1.5rem; + max-width: 600px; + margin: 0 auto; +} + +.upload-card { + background: rgba(78, 84, 200, 0.3) !important; + border-radius: 15px !important; + padding: 1.5rem !important; + border: 2px solid rgba(255, 255, 255, 0.4) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2) !important; + margin-bottom: 1.5rem !important; +} + +/* Ensure all upload cards have consistent styling */ +.upload-container .upload-card { + background: rgba(78, 84, 200, 0.3) !important; + border-radius: 15px !important; + padding: 1.5rem !important; + border: 2px solid rgba(255, 255, 255, 0.4) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2) !important; + margin-bottom: 1.5rem !important; +} + +/* Specific fix for first upload card */ +.upload-container .upload-card:first-of-type { + background: rgba(78, 84, 200, 0.3) !important; + border-radius: 15px !important; + padding: 1.5rem !important; + border: 2px solid rgba(255, 255, 255, 0.4) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2) !important; +} + +.file-upload-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + margin: 2rem 0; +} + +.file-upload-preview { + width: 200px; + height: 200px; + border-radius: 10px; + object-fit: cover; + border: rgba(255, 255, 255, 0.2) 1px solid; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + display: none; +} + +.file-upload-info { + color: var(--white); + text-align: center; + font-size: 0.9rem; + opacity: 0.8; + max-width: 80%; +} + +.file-upload-container { + display: flex; + flex-direction: column; + gap: 1rem; + background: rgba(255, 255, 255, 0.1); + padding: 1.5rem; + border-radius: 15px; + margin: 1rem 0; +} + +.file-upload-card { + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + padding: 1rem; + border: 1px dashed rgba(255, 255, 255, 0.3); + transition: all 0.3s ease; +} + +.file-upload-card:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.5); +} + +.file-upload-label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.9rem; + color: var(--white); + opacity: 0.9; +} + +.preview-area { + display: flex; + flex-direction: column; + gap: 1.5rem; + margin: 2rem 0; + width: 100%; + display: none; +} + +.preview-area:not(:empty) { + display: flex; +} + +.preview-item { + position: relative; + border-radius: 8px; + overflow: hidden; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 1rem; +} + +.image-preview { + aspect-ratio: 1/1; + padding: 1rem; +} + +.video-preview { + aspect-ratio: 16/9; + min-height: 120px; + padding: 1rem; +} + +.audio-preview { + height: 80px; + aspect-ratio: unset; + padding: 1rem; +} + +.image-preview img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.video-preview video { + width: 100%; + height: 100%; +} + +.video-preview video { + width: 100%; + height: 100%; +} + +.audio-preview audio { + width: 90%; +} + +.preview-item-audio { + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.2); +} + +.preview-item-audio audio { + width: 90%; + max-width: 200px; +} + +.preview-item-document { + width: 100%; + height: 160px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; + text-align: center; +} + +.preview-item-name { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.6); + color: white; + font-size: 0.7rem; + padding: 0.3rem; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +audio::-webkit-media-controls-panel { + background: rgba(255, 255, 255, 0.1); + border-radius: 0 0 8px 8px; +} + +.file-name { + color: var(--white); + font-size: 0.9rem; + margin-top: 0.5rem; + text-align: center; + word-break: break-word; + padding: 0 0.5rem; +} + +.preview-item-remove { + position: absolute; + top: 0.3rem; + right: 0.3rem; + background: rgba(0, 0, 0, 0.6); + color: white; + border: none; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; +} + +.preview-item-remove:hover { + background: rgba(255, 0, 0, 0.8); + transform: scale(1.1); +} + +.preview-item-document { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; + text-align: center; +} + +.preview-item-document-icon { + font-size: 3rem; + margin-bottom: 0.5rem; + color: rgba(255, 255, 255, 0.7); +} + +.preview-item-document-name { + font-size: 0.8rem; + color: var(--white); + word-break: break-all; + padding: 0 0.5rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 768px) { + h1 { + font-size: 2rem; + } + h2 { + font-size: 1.5rem; + } + .container { + padding: 1.5rem; + } + .btn-group { + flex-direction: column; + align-items: center; + } +} diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/BasicWebViewSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/BasicWebViewSample.kt index d9f3a214..29e31519 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/BasicWebViewSample.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/BasicWebViewSample.kt @@ -1,25 +1,29 @@ package com.kevinnzou.sample import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars import androidx.compose.material.Button import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -54,88 +58,102 @@ import kotlinx.coroutines.flow.filter @Composable internal fun BasicWebViewSample(navHostController: NavHostController? = null) { val initialUrl = "https://github.com/KevinnZou/compose-webview-multiplatform" - val state = rememberWebViewState(url = initialUrl) - DisposableEffect(Unit) { - state.webSettings.apply { + val state = + rememberWebViewState(url = initialUrl) { logSeverity = KLogSeverity.Debug customUserAgentString = "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1) AppleWebKit/625.20 (KHTML, like Gecko) Version/14.3.43 Safari/625.20" + iOSWebSettings.isInspectable = true } - - onDispose { } - } val navigator = rememberWebViewNavigator() var textFieldValue by remember(state.lastLoadedUrl) { mutableStateOf(state.lastLoadedUrl) } MaterialTheme { - Column { - TopAppBar( - title = { Text(text = "WebView Sample") }, - navigationIcon = { - IconButton(onClick = { - if (navigator.canGoBack) { - navigator.navigateBack() - } else { - navHostController?.popBackStack() + Scaffold { innerPadding -> + Column( + modifier = + Modifier + .padding(innerPadding) + .fillMaxSize(), + ) { + TopAppBar( + modifier = + Modifier + .background( + color = MaterialTheme.colors.primary, + ).padding( + top = + WindowInsets.statusBars + .asPaddingValues() + .calculateTopPadding(), + ), + title = { Text(text = "WebView Sample") }, + navigationIcon = { + IconButton(onClick = { + if (navigator.canGoBack) { + navigator.navigateBack() + } else { + navHostController?.popBackStack() + } + }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + ) } - }) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = "Back", + }, + ) + + Row { + Box(modifier = Modifier.weight(1f)) { + if (state.errorsForCurrentRequest.isNotEmpty()) { + Image( + imageVector = Icons.Default.Close, + contentDescription = "Error", + colorFilter = ColorFilter.tint(Color.Red), + modifier = + Modifier + .align(Alignment.CenterEnd) + .padding(8.dp), + ) + } + + OutlinedTextField( + value = textFieldValue ?: "", + onValueChange = { textFieldValue = it }, + modifier = Modifier.fillMaxWidth(), ) } - }, - ) - Row { - Box(modifier = Modifier.weight(1f)) { - if (state.errorsForCurrentRequest.isNotEmpty()) { - Image( - imageVector = Icons.Default.Close, - contentDescription = "Error", - colorFilter = ColorFilter.tint(Color.Red), - modifier = - Modifier - .align(Alignment.CenterEnd) - .padding(8.dp), - ) + Button( + onClick = { + textFieldValue?.let { + navigator.loadUrl(it) + } + }, + modifier = Modifier.align(Alignment.CenterVertically), + ) { + Text("Go") } + } - OutlinedTextField( - value = textFieldValue ?: "", - onValueChange = { textFieldValue = it }, + val loadingState = state.loadingState + if (loadingState is LoadingState.Loading) { + LinearProgressIndicator( + progress = loadingState.progress, modifier = Modifier.fillMaxWidth(), ) } - Button( - onClick = { - textFieldValue?.let { - navigator.loadUrl(it) - } - }, - modifier = Modifier.align(Alignment.CenterVertically), - ) { - Text("Go") - } - } - - val loadingState = state.loadingState - if (loadingState is LoadingState.Loading) { - LinearProgressIndicator( - progress = loadingState.progress, - modifier = Modifier.fillMaxWidth(), + WebView( + state = state, + modifier = + Modifier + .fillMaxSize(), + navigator = navigator, ) } - - WebView( - state = state, - modifier = - Modifier - .fillMaxSize(), - navigator = navigator, - ) } } } diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/DRMVideoSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/DRMVideoSample.kt index 21a7b1ed..ee9b2ee7 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/DRMVideoSample.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/DRMVideoSample.kt @@ -1,10 +1,16 @@ package com.kevinnzou.sample +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons @@ -42,30 +48,49 @@ internal fun DRMVideoSample(navHostController: NavHostController? = null) { val navigator = rememberWebViewNavigator() MaterialTheme { - Column { - TopAppBar( - title = { Text(text = "DRM Video Sample") }, - navigationIcon = { - IconButton(onClick = { - if (navigator.canGoBack) { - navigator.navigateBack() - } else { - navHostController?.popBackStack() - } - }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - ) - } - }, - ) + Scaffold { innerPadding -> + Column( + modifier = + Modifier + .padding(innerPadding) + .fillMaxSize(), + ) { + Column { + TopAppBar( + modifier = + Modifier + .background( + color = MaterialTheme.colors.primary, + ).padding( + top = + WindowInsets.statusBars + .asPaddingValues() + .calculateTopPadding(), + ), + title = { Text(text = "DRM Video Sample") }, + navigationIcon = { + IconButton(onClick = { + if (navigator.canGoBack) { + navigator.navigateBack() + } else { + navHostController?.popBackStack() + } + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + ) + } + }, + ) - WebView( - state = state, - modifier = Modifier.fillMaxSize(), - navigator = navigator, - ) + WebView( + state = state, + modifier = Modifier.fillMaxSize(), + navigator = navigator, + ) + } + } } } } diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.kt index a6b9fa69..07b9dc5f 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.kt @@ -1,11 +1,17 @@ package com.kevinnzou.sample +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons @@ -13,10 +19,12 @@ import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.navigation.NavHostController import com.multiplatform.webview.util.KLogSeverity import com.multiplatform.webview.web.PlatformWebViewParams import com.multiplatform.webview.web.WebView +import com.multiplatform.webview.web.WebViewFileReadType import com.multiplatform.webview.web.rememberWebViewNavigator import com.multiplatform.webview.web.rememberWebViewStateWithHTMLFile @@ -27,43 +35,73 @@ import com.multiplatform.webview.web.rememberWebViewStateWithHTMLFile */ @Composable internal fun FileChooseWebViewSample(navHostController: NavHostController? = null) { - val webViewState = rememberWebViewStateWithHTMLFile(fileName = "fileChoose.html") + val webViewState = + rememberWebViewStateWithHTMLFile( + fileName = "fileChoose.html", + readType = WebViewFileReadType.ASSET_RESOURCES, + ) val webViewNavigator = rememberWebViewNavigator() LaunchedEffect(Unit) { + webViewState.webSettings.zoomLevel = 1.0 + webViewState.webSettings.apply { zoomLevel = 1.0 logSeverity = KLogSeverity.Debug + backgroundColor = Color.White androidWebSettings.apply { isAlgorithmicDarkeningAllowed = true safeBrowsingEnabled = true allowFileAccess = false } + iOSWebSettings.apply { + backgroundColor = Color.White + underPageBackgroundColor = Color.White + } } } MaterialTheme { - Column { - TopAppBar( - title = { Text(text = "Html Sample") }, - navigationIcon = { - IconButton(onClick = { - navHostController?.popBackStack() - }) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = "Back", + Scaffold { innerPadding -> + Column( + modifier = + Modifier + .padding(innerPadding) + .fillMaxSize(), + ) { + Column { + TopAppBar( + modifier = + Modifier + .background( + color = MaterialTheme.colors.primary, + ).padding( + top = + WindowInsets.statusBars + .asPaddingValues() + .calculateTopPadding(), + ), + title = { Text(text = "Html Sample") }, + navigationIcon = { + IconButton(onClick = { + navHostController?.popBackStack() + }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + ) + } + }, + ) + + Box(Modifier.fillMaxSize()) { + WebView( + state = webViewState, + modifier = Modifier.fillMaxSize(), + captureBackPresses = false, + navigator = webViewNavigator, + platformWebViewParams = getPlatformWebViewParams(), ) } - }, - ) - - Box(Modifier.fillMaxSize()) { - WebView( - state = webViewState, - modifier = Modifier.fillMaxSize(), - captureBackPresses = false, - navigator = webViewNavigator, - platformWebViewParams = getPlatformWebViewParams(), - ) + } } } } diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/HtmlWebViewSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/HtmlWebViewSample.kt index 37f629de..4978266a 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/HtmlWebViewSample.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/HtmlWebViewSample.kt @@ -1,13 +1,18 @@ package com.kevinnzou.sample -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons @@ -17,8 +22,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import co.touchlab.kermit.Logger @@ -30,9 +35,11 @@ import com.multiplatform.webview.jsbridge.WebViewJsBridge import com.multiplatform.webview.jsbridge.rememberWebViewJsBridge import com.multiplatform.webview.util.KLogSeverity import com.multiplatform.webview.web.WebView +import com.multiplatform.webview.web.WebViewFileReadType import com.multiplatform.webview.web.WebViewState import com.multiplatform.webview.web.rememberWebViewNavigator import com.multiplatform.webview.web.rememberWebViewStateWithHTMLFile +import compose_webview_multiplatform.sample.shared.generated.resources.Res import kotlinx.coroutines.flow.filter /** @@ -49,7 +56,8 @@ internal fun BasicWebViewWithHTMLSample(navHostController: NavHostController? = val html = HtmlRes.html val webViewState = rememberWebViewStateWithHTMLFile( - fileName = "index.html", + fileName = Res.getUri("files/samples/index.html"), + readType = WebViewFileReadType.COMPOSE_RESOURCE_FILES, ) // val webViewState = rememberWebViewStateWithHTMLData(html) val webViewNavigator = rememberWebViewNavigator() @@ -60,49 +68,79 @@ internal fun BasicWebViewWithHTMLSample(navHostController: NavHostController? = initJsBridge(jsBridge) } MaterialTheme { - Column { - TopAppBar( - title = { Text(text = "Html Sample") }, - navigationIcon = { - IconButton(onClick = { - navHostController?.popBackStack() - }) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = "Back", - ) - } - }, - ) + Scaffold { innerPadding -> + Column( + modifier = + Modifier + .padding(innerPadding) + .fillMaxSize(), + ) { + Column { + TopAppBar( + modifier = + Modifier + .background( + color = MaterialTheme.colors.primary, + ).padding( + top = + WindowInsets.statusBars + .asPaddingValues() + .calculateTopPadding(), + ), + title = { Text(text = "Html Sample") }, + navigationIcon = { + IconButton(onClick = { + navHostController?.popBackStack() + }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + ) + } + }, + actions = { + // Evaluate JavaScript button + val jsCode = + """ + document.getElementById("subtitle").innerText = "Hello from KMP!"; + window.kmpJsBridge.callNative("Greet",JSON.stringify({message: "Hello"}), + function (data) { + document.getElementById("subtitle").innerText = data; + console.log("Greet from Native: " + data); + } + ); + callJS(); + """.trimIndent() - Box(Modifier.fillMaxSize()) { - WebView( - state = webViewState, - modifier = Modifier.fillMaxSize(), - captureBackPresses = false, - navigator = webViewNavigator, - webViewJsBridge = jsBridge, - ) - Button( - onClick = { - webViewNavigator.evaluateJavaScript( - """ - document.getElementById("subtitle").innerText = "Hello from KMM!"; - window.kmpJsBridge.callNative("Greet",JSON.stringify({message: "Hello"}), - function (data) { - document.getElementById("subtitle").innerText = data; - console.log("Greet from Native: " + data); - } - ); - callJS(); - """.trimIndent(), - ) { - jsRes = it - } - }, - modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 50.dp), - ) { - Text(jsRes) + Button( + onClick = { + webViewNavigator.evaluateJavaScript(jsCode) { + jsRes = it + } + }, + modifier = Modifier.padding(horizontal = 4.dp), + colors = + ButtonDefaults.buttonColors( + backgroundColor = Color.White, + contentColor = MaterialTheme.colors.primary, + ), + ) { + Text( + "Run JS", + style = MaterialTheme.typography.caption, + ) + } + }, + ) + + // WebView without overlay buttons + WebView( + state = webViewState, + modifier = Modifier.fillMaxSize(), + captureBackPresses = false, + navigator = webViewNavigator, + webViewJsBridge = jsBridge, + ) } } } diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/InterceptRequestSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/InterceptRequestSample.kt index 5b8fedca..2cc96b77 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/InterceptRequestSample.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/InterceptRequestSample.kt @@ -1,18 +1,23 @@ package com.kevinnzou.sample import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars import androidx.compose.material.Button import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons @@ -91,81 +96,97 @@ internal fun InterceptRequestSample(navHostController: NavHostController? = null mutableStateOf(state.lastLoadedUrl) } MaterialTheme { - Column { - TopAppBar( - title = { Text(text = "Intercept Request Sample") }, - navigationIcon = { - IconButton(onClick = { - if (navigator.canGoBack) { - navigator.navigateBack() - } else { - navHostController?.popBackStack() + Scaffold { innerPadding -> + Column( + modifier = + Modifier + .padding(innerPadding) + .fillMaxSize(), + ) { + Column { + TopAppBar( + modifier = + Modifier + .background( + color = MaterialTheme.colors.primary, + ).padding( + top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), + ), + title = { Text(text = "Intercept Request Sample") }, + navigationIcon = { + IconButton(onClick = { + if (navigator.canGoBack) { + navigator.navigateBack() + } else { + navHostController?.popBackStack() + } + }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + ) + } + }, + ) + + @Suppress("ktlint:standard:max-line-length") + Text( + text = + "This sample demonstrates how to intercept requests in WebView. " + + "When the URL contains 'kotlin', the request will be redirected to 'https://kotlinlang.org/docs/multiplatform.html'.", + modifier = Modifier.padding(8.dp), + ) + + Row { + Box(modifier = Modifier.weight(1f)) { + if (state.errorsForCurrentRequest.isNotEmpty()) { + Image( + imageVector = Icons.Default.Close, + contentDescription = "Error", + colorFilter = ColorFilter.tint(Color.Red), + modifier = + Modifier + .align(Alignment.CenterEnd) + .padding(8.dp), + ) + } + + OutlinedTextField( + value = textFieldValue ?: "", + onValueChange = { textFieldValue = it }, + modifier = Modifier.fillMaxWidth(), + ) } - }) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = "Back", - ) - } - }, - ) - @Suppress("ktlint:standard:max-line-length") - Text( - text = - "This sample demonstrates how to intercept requests in WebView. " + - "When the URL contains 'kotlin', the request will be redirected to 'https://kotlinlang.org/docs/multiplatform.html'.", - modifier = Modifier.padding(8.dp), - ) + Button( + onClick = { + textFieldValue?.let { + navigator.loadUrl(it) + } + }, + modifier = Modifier.align(Alignment.CenterVertically), + ) { + Text("Go") + } + } - Row { - Box(modifier = Modifier.weight(1f)) { - if (state.errorsForCurrentRequest.isNotEmpty()) { - Image( - imageVector = Icons.Default.Close, - contentDescription = "Error", - colorFilter = ColorFilter.tint(Color.Red), - modifier = - Modifier - .align(Alignment.CenterEnd) - .padding(8.dp), + val loadingState = state.loadingState + if (loadingState is LoadingState.Loading) { + LinearProgressIndicator( + progress = loadingState.progress, + modifier = Modifier.fillMaxWidth(), ) } - OutlinedTextField( - value = textFieldValue ?: "", - onValueChange = { textFieldValue = it }, - modifier = Modifier.fillMaxWidth(), + WebView( + state = state, + modifier = + Modifier + .fillMaxSize(), + navigator = navigator, ) } - - Button( - onClick = { - textFieldValue?.let { - navigator.loadUrl(it) - } - }, - modifier = Modifier.align(Alignment.CenterVertically), - ) { - Text("Go") - } } - - val loadingState = state.loadingState - if (loadingState is LoadingState.Loading) { - LinearProgressIndicator( - progress = loadingState.progress, - modifier = Modifier.fillMaxWidth(), - ) - } - - WebView( - state = state, - modifier = - Modifier - .fillMaxSize(), - navigator = navigator, - ) } } } diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/MidiSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/MidiSample.kt index 0e64ea8a..5f7a3220 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/MidiSample.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/MidiSample.kt @@ -1,10 +1,16 @@ package com.kevinnzou.sample +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons @@ -42,30 +48,46 @@ internal fun MidiSample(navHostController: NavHostController? = null) { val navigator = rememberWebViewNavigator() MaterialTheme { - Column { - TopAppBar( - title = { Text(text = "Midi SysEx Sample") }, - navigationIcon = { - IconButton(onClick = { - if (navigator.canGoBack) { - navigator.navigateBack() - } else { - navHostController?.popBackStack() - } - }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - ) - } - }, - ) + Scaffold { innerPadding -> + Column( + modifier = + Modifier + .padding(innerPadding) + .fillMaxSize(), + ) { + Column { + TopAppBar( + modifier = + Modifier + .background( + color = MaterialTheme.colors.primary, + ).padding( + top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), + ), + title = { Text(text = "Midi SysEx Sample") }, + navigationIcon = { + IconButton(onClick = { + if (navigator.canGoBack) { + navigator.navigateBack() + } else { + navHostController?.popBackStack() + } + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + ) + } + }, + ) - WebView( - state = state, - modifier = Modifier.fillMaxSize(), - navigator = navigator, - ) + WebView( + state = state, + modifier = Modifier.fillMaxSize(), + navigator = navigator, + ) + } + } } } } diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/VoyagerNavigatonSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/VoyagerNavigatonSample.kt index 6f0428e3..27d4e7d2 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/VoyagerNavigatonSample.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/VoyagerNavigatonSample.kt @@ -1,16 +1,23 @@ package com.kevinnzou.sample +import androidx.compose.foundation.background import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars import androidx.compose.material.BottomNavigation import androidx.compose.material.BottomNavigationItem import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import cafe.adriel.voyager.navigator.tab.CurrentTab import cafe.adriel.voyager.navigator.tab.LocalTabNavigator @@ -28,6 +35,13 @@ fun VoyagerNavigationSample(navHostController: NavHostController? = null) { Scaffold( topBar = { TopAppBar( + modifier = + Modifier + .background( + color = MaterialTheme.colors.primary, + ).padding( + top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), + ), title = { Text(text = "SaveState Sample") }, navigationIcon = { IconButton(onClick = { diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt index cdfb1ea2..60550c1f 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt @@ -2,14 +2,21 @@ package com.kevinnzou.sample import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars import androidx.compose.material.Button import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold import androidx.compose.material.Text +import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -58,39 +65,57 @@ internal fun WebViewApp() { @Composable fun MainScreen(controller: NavController) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Button(onClick = { - controller.navigate("basic") - }) { - Text("Basic Sample", fontSize = 18.sp) - } - Spacer(modifier = Modifier.height(20.dp)) - Button(onClick = { - controller.navigate("html") - }) { - Text("HTML Sample", fontSize = 18.sp) - } - Spacer(modifier = Modifier.height(20.dp)) - Button(onClick = { - controller.navigate("tab") - }) { - Text("SaveState Sample", fontSize = 18.sp) - } - Spacer(modifier = Modifier.height(20.dp)) - Button(onClick = { - controller.navigate("intercept") - }) { - Text("Intercept Request Sample", fontSize = 18.sp) - } - Spacer(modifier = Modifier.height(20.dp)) - Button(onClick = { - controller.navigate("file") - }) { - Text("File Choose Sample", fontSize = 18.sp) + Scaffold { innerPadding -> + TopAppBar( + modifier = + Modifier + .background( + color = MaterialTheme.colors.primary, + ).padding( + top = + WindowInsets.statusBars + .asPaddingValues() + .calculateTopPadding(), + ), + title = { Text(text = "WebView Sample") }, + ) + Column( + modifier = + Modifier + .padding(innerPadding) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Button(onClick = { + controller.navigate("basic") + }) { + Text("Basic Sample", fontSize = 18.sp) + } + Spacer(modifier = Modifier.height(20.dp)) + Button(onClick = { + controller.navigate("html") + }) { + Text("HTML Sample", fontSize = 18.sp) + } + Spacer(modifier = Modifier.height(20.dp)) + Button(onClick = { + controller.navigate("tab") + }) { + Text("SaveState Sample", fontSize = 18.sp) + } + Spacer(modifier = Modifier.height(20.dp)) + Button(onClick = { + controller.navigate("intercept") + }) { + Text("Intercept Request Sample", fontSize = 18.sp) + } + Spacer(modifier = Modifier.height(20.dp)) + Button(onClick = { + controller.navigate("file") + }) { + Text("File Choose Sample", fontSize = 18.sp) + } } } } diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/jsbridge/GreetJsMessageHandler.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/jsbridge/GreetJsMessageHandler.kt index 29a9ae56..85447ce2 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/jsbridge/GreetJsMessageHandler.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/jsbridge/GreetJsMessageHandler.kt @@ -15,9 +15,7 @@ import kotlinx.coroutines.launch * Created By Kevin Zou On 2023/12/6 */ class GreetJsMessageHandler : IJsMessageHandler { - override fun methodName(): String { - return "Greet" - } + override fun methodName(): String = "Greet" override fun handle( message: JsMessage, @@ -28,7 +26,7 @@ class GreetJsMessageHandler : IJsMessageHandler { "Greet Handler Get Message: $message" } val param = processParams(message) - val data = GreetModel("KMM Received ${param.message}") + val data = GreetModel("KMP Received ${param.message}") callback(dataToJsonString(data)) // EventBus.post(NavigationEvent()) navigator?.coroutineScope?.launch { diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/model/GreetModel.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/model/GreetModel.kt index affad370..41ae73a1 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/model/GreetModel.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/model/GreetModel.kt @@ -6,4 +6,6 @@ import kotlinx.serialization.Serializable * Created By Kevin Zou On 2023/12/6 */ @Serializable -data class GreetModel(val message: String) +data class GreetModel( + val message: String, +) diff --git a/sample/shared/src/commonMain/resources/assets/fileChoose.html b/sample/shared/src/commonMain/resources/assets/fileChoose.html index c59b8543..71e471a3 100644 --- a/sample/shared/src/commonMain/resources/assets/fileChoose.html +++ b/sample/shared/src/commonMain/resources/assets/fileChoose.html @@ -1,25 +1,177 @@ -
- -
+ Compose WebView Multiplatform -

Compose WebView Multiplatform

-
- image: - -
-
- video: - -
-
- audio: - -
+
+

Compose WebView Multiplatform

+
+
+
+ +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+ - \ No newline at end of file + diff --git a/sample/shared/src/commonMain/resources/assets/styles.css b/sample/shared/src/commonMain/resources/assets/styles.css index 010d0e6a..c463d584 100644 --- a/sample/shared/src/commonMain/resources/assets/styles.css +++ b/sample/shared/src/commonMain/resources/assets/styles.css @@ -1,15 +1,477 @@ /* styles.css */ +:root { + --primary: #4e54c8; + --secondary: #8f94fb; + --white: #ffffff; + --shadow: rgba(0, 0, 0, 0.1); + --text-shadow: rgba(0, 0, 0, 0.3); +} + body { - background-color: e0e8f0; + background: linear-gradient(135deg, var(--primary), var(--secondary)); display: flex; justify-content: center; align-items: center; flex-direction: column; - height: 100vh; + min-height: 100vh; margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + box-shadow: inset 0 0 20px var(--shadow); + padding: 2rem; + box-sizing: border-box; +} + +.container { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 2rem; + max-width: 800px; + width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +h1, h2, h3 { + text-align: center; + color: var(--white); + text-shadow: 1px 1px 4px var(--text-shadow); + margin: 0 0 1rem; +} + +h1 { + font-size: 2.5rem; + font-weight: 700; +} + +h2 { + font-size: 1.8rem; + font-weight: 600; +} + +p { + color: var(--white); + line-height: 1.6; + margin: 0 0 1.5rem; +} + +.btn { + background: rgba(255, 255, 255, 0.2); + color: var(--white); + border: none; + padding: 0.8rem 1.5rem; + border-radius: 50px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.btn-primary { + background: var(--primary); + border: 1px solid rgba(255, 255, 255, 0.5); +} + +.btn-secondary { + background: var(--secondary); + border: 1px solid rgba(255, 255, 255, 0.5); +} + +.btn-group { + display: flex; + gap: 1rem; + justify-content: center; + margin: 2rem 0; +} + +.subtitle { + color: var(--white); + text-align: center; + font-size: 1.2rem; + margin-bottom: 2rem; + opacity: 0.9; + text-shadow: 1px 1px 2px var(--text-shadow); +} + +.btn-custom { + background: linear-gradient(45deg, var(--primary), var(--secondary)); + color: var(--white); + border: none; + padding: 0.8rem 2rem; + border-radius: 50px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + position: relative; + overflow: hidden; +} + +.btn-custom:hover { + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); +} + +.btn-custom::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transform: translateX(-100%); + transition: transform 0.6s ease; +} + +input[type="file"] { + display: none; +} + +label[for^="file-upload"] { + background: rgba(255, 255, 255, 0.2); + color: var(--white); + border: none; + padding: 0.8rem 1.5rem; + border-radius: 50px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); + display: inline-block; + text-align: center; + position: relative; + overflow: hidden; +} + +label[for^="file-upload"]:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +label[for^="file-upload"]::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transform: translateX(-100%); + transition: transform 0.6s ease; +} + +label[for^="file-upload"]:hover::after { + transform: translateX(100%); +} + +.upload-input { + display: none; +} + +.upload-label { + background: rgba(255, 255, 255, 0.2); + color: var(--white); + border: 2px dashed rgba(255, 255, 255, 0.3); + padding: 1.5rem; + border-radius: 15px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + display: block; + text-align: center; + position: relative; + overflow: hidden; + backdrop-filter: blur(5px); +} + +.upload-label:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.upload-container { + display: flex; + flex-direction: column; + gap: 1.5rem; + max-width: 600px; + margin: 0 auto; +} + +.upload-card { + background: rgba(78, 84, 200, 0.3) !important; + border-radius: 15px !important; + padding: 1.5rem !important; + border: 2px solid rgba(255, 255, 255, 0.4) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2) !important; + margin-bottom: 1.5rem !important; +} + +/* Ensure all upload cards have consistent styling */ +.upload-container .upload-card { + background: rgba(78, 84, 200, 0.3) !important; + border-radius: 15px !important; + padding: 1.5rem !important; + border: 2px solid rgba(255, 255, 255, 0.4) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2) !important; + margin-bottom: 1.5rem !important; +} + +/* Specific fix for first upload card */ +.upload-container .upload-card:first-of-type { + background: rgba(78, 84, 200, 0.3) !important; + border-radius: 15px !important; + padding: 1.5rem !important; + border: 2px solid rgba(255, 255, 255, 0.4) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2) !important; +} + +.file-upload-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + margin: 2rem 0; +} + +.file-upload-preview { + width: 200px; + height: 200px; + border-radius: 10px; + object-fit: cover; + border: rgba(255, 255, 255, 0.2) 1px solid; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + display: none; +} + +.file-upload-info { + color: var(--white); + text-align: center; + font-size: 0.9rem; + opacity: 0.8; + max-width: 80%; +} + +.file-upload-container { + display: flex; + flex-direction: column; + gap: 1rem; + background: rgba(255, 255, 255, 0.1); + padding: 1.5rem; + border-radius: 15px; + margin: 1rem 0; +} + +.file-upload-card { + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + padding: 1rem; + border: 1px dashed rgba(255, 255, 255, 0.3); + transition: all 0.3s ease; +} + +.file-upload-card:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.5); +} + +.file-upload-label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.9rem; + color: var(--white); + opacity: 0.9; +} + +.preview-area { + display: flex; + flex-direction: column; + gap: 1.5rem; + margin: 2rem 0; + width: 100%; + display: none; +} + +.preview-area:not(:empty) { + display: flex; +} + +.preview-item { + position: relative; + border-radius: 8px; + overflow: hidden; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 1rem; +} + +.image-preview { + aspect-ratio: 1/1; + padding: 1rem; +} + +.video-preview { + aspect-ratio: 16/9; + min-height: 120px; + padding: 1rem; } -h1, h2 { + +.audio-preview { + height: 80px; + aspect-ratio: unset; + padding: 1rem; +} + +.image-preview img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.video-preview video { + width: 100%; + height: 100%; +} + +.video-preview video { + width: 100%; + height: 100%; +} + +.audio-preview audio { + width: 90%; +} + +.preview-item-audio { + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.2); +} + +.preview-item-audio audio { + width: 90%; + max-width: 200px; +} + +.preview-item-document { + width: 100%; + height: 160px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; text-align: center; - color: ffffff; -} \ No newline at end of file +} + +.preview-item-name { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.6); + color: white; + font-size: 0.7rem; + padding: 0.3rem; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +audio::-webkit-media-controls-panel { + background: rgba(255, 255, 255, 0.1); + border-radius: 0 0 8px 8px; +} + +.file-name { + color: var(--white); + font-size: 0.9rem; + margin-top: 0.5rem; + text-align: center; + word-break: break-word; + padding: 0 0.5rem; +} + +.preview-item-remove { + position: absolute; + top: 0.3rem; + right: 0.3rem; + background: rgba(0, 0, 0, 0.6); + color: white; + border: none; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; +} + +.preview-item-remove:hover { + background: rgba(255, 0, 0, 0.8); + transform: scale(1.1); +} + +.preview-item-document { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; + text-align: center; +} + +.preview-item-document-icon { + font-size: 3rem; + margin-bottom: 0.5rem; + color: rgba(255, 255, 255, 0.7); +} + +.preview-item-document-name { + font-size: 0.8rem; + color: var(--white); + word-break: break-all; + padding: 0 0.5rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 768px) { + h1 { + font-size: 2rem; + } + h2 { + font-size: 1.5rem; + } + .container { + padding: 1.5rem; + } + .btn-group { + flex-direction: column; + align-items: center; + } +} diff --git a/sample/shared/src/wasmJsMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.wasmJs.kt b/sample/shared/src/wasmJsMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.wasmJs.kt new file mode 100644 index 00000000..ea9a31a8 --- /dev/null +++ b/sample/shared/src/wasmJsMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.wasmJs.kt @@ -0,0 +1,7 @@ +package com.kevinnzou.sample + +import androidx.compose.runtime.Composable +import com.multiplatform.webview.web.PlatformWebViewParams + +@Composable +actual fun getPlatformWebViewParams(): PlatformWebViewParams? = null diff --git a/sample/shared/src/wasmJsMain/kotlin/com/kevinnzou/sample/main.wasmJs.kt b/sample/shared/src/wasmJsMain/kotlin/com/kevinnzou/sample/main.wasmJs.kt new file mode 100644 index 00000000..0f595537 --- /dev/null +++ b/sample/shared/src/wasmJsMain/kotlin/com/kevinnzou/sample/main.wasmJs.kt @@ -0,0 +1,8 @@ +package com.kevinnzou.sample + +import androidx.compose.runtime.Composable + +actual fun getPlatformName(): String = "Web" + +@Composable +fun MainWebView() = WebViewApp() diff --git a/sample/wasmJsApp/build.gradle.kts b/sample/wasmJsApp/build.gradle.kts new file mode 100644 index 00000000..81bc0b98 --- /dev/null +++ b/sample/wasmJsApp/build.gradle.kts @@ -0,0 +1,43 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.compose.multiplatorm) +} + +kotlin { + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + outputModuleName = "sample" + browser { + val rootDirPath = project.rootDir.path + val projectDirPath = project.projectDir.path + commonWebpackConfig { + outputFileName = "sample.js" + devServer = + (devServer ?: KotlinWebpackConfig.DevServer()).apply { + static = + (static ?: mutableListOf()).apply { + // Serve sources to debug inside browser + add(rootDirPath) + add(projectDirPath) + } + } + } + } + binaries.executable() + } + + sourceSets { + wasmJsMain.dependencies { + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.ui) + implementation(compose.runtime) + implementation(project(":sample:shared")) + } + } +} diff --git a/sample/wasmJsApp/src/wasmJsMain/kotlin/main.kt b/sample/wasmJsApp/src/wasmJsMain/kotlin/main.kt new file mode 100644 index 00000000..53e40755 --- /dev/null +++ b/sample/wasmJsApp/src/wasmJsMain/kotlin/main.kt @@ -0,0 +1,11 @@ +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport +import com.kevinnzou.sample.MainWebView +import kotlinx.browser.document + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + ComposeViewport(document.body!!) { + MainWebView() + } +} diff --git a/sample/wasmJsApp/src/wasmJsMain/resources/assets/fileChoose.html b/sample/wasmJsApp/src/wasmJsMain/resources/assets/fileChoose.html new file mode 100644 index 00000000..71e471a3 --- /dev/null +++ b/sample/wasmJsApp/src/wasmJsMain/resources/assets/fileChoose.html @@ -0,0 +1,177 @@ + + + + + Compose WebView Multiplatform + + +
+

Compose WebView Multiplatform

+
+
+
+ +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+ + + diff --git a/sample/wasmJsApp/src/wasmJsMain/resources/assets/styles.css b/sample/wasmJsApp/src/wasmJsMain/resources/assets/styles.css new file mode 100644 index 00000000..c463d584 --- /dev/null +++ b/sample/wasmJsApp/src/wasmJsMain/resources/assets/styles.css @@ -0,0 +1,477 @@ +/* styles.css */ + +:root { + --primary: #4e54c8; + --secondary: #8f94fb; + --white: #ffffff; + --shadow: rgba(0, 0, 0, 0.1); + --text-shadow: rgba(0, 0, 0, 0.3); +} + +body { + background: linear-gradient(135deg, var(--primary), var(--secondary)); + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + box-shadow: inset 0 0 20px var(--shadow); + padding: 2rem; + box-sizing: border-box; +} + +.container { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 2rem; + max-width: 800px; + width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +h1, h2, h3 { + text-align: center; + color: var(--white); + text-shadow: 1px 1px 4px var(--text-shadow); + margin: 0 0 1rem; +} + +h1 { + font-size: 2.5rem; + font-weight: 700; +} + +h2 { + font-size: 1.8rem; + font-weight: 600; +} + +p { + color: var(--white); + line-height: 1.6; + margin: 0 0 1.5rem; +} + +.btn { + background: rgba(255, 255, 255, 0.2); + color: var(--white); + border: none; + padding: 0.8rem 1.5rem; + border-radius: 50px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.btn-primary { + background: var(--primary); + border: 1px solid rgba(255, 255, 255, 0.5); +} + +.btn-secondary { + background: var(--secondary); + border: 1px solid rgba(255, 255, 255, 0.5); +} + +.btn-group { + display: flex; + gap: 1rem; + justify-content: center; + margin: 2rem 0; +} + +.subtitle { + color: var(--white); + text-align: center; + font-size: 1.2rem; + margin-bottom: 2rem; + opacity: 0.9; + text-shadow: 1px 1px 2px var(--text-shadow); +} + +.btn-custom { + background: linear-gradient(45deg, var(--primary), var(--secondary)); + color: var(--white); + border: none; + padding: 0.8rem 2rem; + border-radius: 50px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + position: relative; + overflow: hidden; +} + +.btn-custom:hover { + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); +} + +.btn-custom::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transform: translateX(-100%); + transition: transform 0.6s ease; +} + +input[type="file"] { + display: none; +} + +label[for^="file-upload"] { + background: rgba(255, 255, 255, 0.2); + color: var(--white); + border: none; + padding: 0.8rem 1.5rem; + border-radius: 50px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); + display: inline-block; + text-align: center; + position: relative; + overflow: hidden; +} + +label[for^="file-upload"]:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +label[for^="file-upload"]::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transform: translateX(-100%); + transition: transform 0.6s ease; +} + +label[for^="file-upload"]:hover::after { + transform: translateX(100%); +} + +.upload-input { + display: none; +} + +.upload-label { + background: rgba(255, 255, 255, 0.2); + color: var(--white); + border: 2px dashed rgba(255, 255, 255, 0.3); + padding: 1.5rem; + border-radius: 15px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + display: block; + text-align: center; + position: relative; + overflow: hidden; + backdrop-filter: blur(5px); +} + +.upload-label:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.upload-container { + display: flex; + flex-direction: column; + gap: 1.5rem; + max-width: 600px; + margin: 0 auto; +} + +.upload-card { + background: rgba(78, 84, 200, 0.3) !important; + border-radius: 15px !important; + padding: 1.5rem !important; + border: 2px solid rgba(255, 255, 255, 0.4) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2) !important; + margin-bottom: 1.5rem !important; +} + +/* Ensure all upload cards have consistent styling */ +.upload-container .upload-card { + background: rgba(78, 84, 200, 0.3) !important; + border-radius: 15px !important; + padding: 1.5rem !important; + border: 2px solid rgba(255, 255, 255, 0.4) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2) !important; + margin-bottom: 1.5rem !important; +} + +/* Specific fix for first upload card */ +.upload-container .upload-card:first-of-type { + background: rgba(78, 84, 200, 0.3) !important; + border-radius: 15px !important; + padding: 1.5rem !important; + border: 2px solid rgba(255, 255, 255, 0.4) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2) !important; +} + +.file-upload-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + margin: 2rem 0; +} + +.file-upload-preview { + width: 200px; + height: 200px; + border-radius: 10px; + object-fit: cover; + border: rgba(255, 255, 255, 0.2) 1px solid; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + display: none; +} + +.file-upload-info { + color: var(--white); + text-align: center; + font-size: 0.9rem; + opacity: 0.8; + max-width: 80%; +} + +.file-upload-container { + display: flex; + flex-direction: column; + gap: 1rem; + background: rgba(255, 255, 255, 0.1); + padding: 1.5rem; + border-radius: 15px; + margin: 1rem 0; +} + +.file-upload-card { + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + padding: 1rem; + border: 1px dashed rgba(255, 255, 255, 0.3); + transition: all 0.3s ease; +} + +.file-upload-card:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.5); +} + +.file-upload-label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.9rem; + color: var(--white); + opacity: 0.9; +} + +.preview-area { + display: flex; + flex-direction: column; + gap: 1.5rem; + margin: 2rem 0; + width: 100%; + display: none; +} + +.preview-area:not(:empty) { + display: flex; +} + +.preview-item { + position: relative; + border-radius: 8px; + overflow: hidden; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 1rem; +} + +.image-preview { + aspect-ratio: 1/1; + padding: 1rem; +} + +.video-preview { + aspect-ratio: 16/9; + min-height: 120px; + padding: 1rem; +} + +.audio-preview { + height: 80px; + aspect-ratio: unset; + padding: 1rem; +} + +.image-preview img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.video-preview video { + width: 100%; + height: 100%; +} + +.video-preview video { + width: 100%; + height: 100%; +} + +.audio-preview audio { + width: 90%; +} + +.preview-item-audio { + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.2); +} + +.preview-item-audio audio { + width: 90%; + max-width: 200px; +} + +.preview-item-document { + width: 100%; + height: 160px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; + text-align: center; +} + +.preview-item-name { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.6); + color: white; + font-size: 0.7rem; + padding: 0.3rem; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +audio::-webkit-media-controls-panel { + background: rgba(255, 255, 255, 0.1); + border-radius: 0 0 8px 8px; +} + +.file-name { + color: var(--white); + font-size: 0.9rem; + margin-top: 0.5rem; + text-align: center; + word-break: break-word; + padding: 0 0.5rem; +} + +.preview-item-remove { + position: absolute; + top: 0.3rem; + right: 0.3rem; + background: rgba(0, 0, 0, 0.6); + color: white; + border: none; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; +} + +.preview-item-remove:hover { + background: rgba(255, 0, 0, 0.8); + transform: scale(1.1); +} + +.preview-item-document { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; + text-align: center; +} + +.preview-item-document-icon { + font-size: 3rem; + margin-bottom: 0.5rem; + color: rgba(255, 255, 255, 0.7); +} + +.preview-item-document-name { + font-size: 0.8rem; + color: var(--white); + word-break: break-all; + padding: 0 0.5rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 768px) { + h1 { + font-size: 2rem; + } + h2 { + font-size: 1.5rem; + } + .container { + padding: 1.5rem; + } + .btn-group { + flex-direction: column; + align-items: center; + } +} diff --git a/sample/wasmJsApp/src/wasmJsMain/resources/index.html b/sample/wasmJsApp/src/wasmJsMain/resources/index.html new file mode 100644 index 00000000..df1e92a5 --- /dev/null +++ b/sample/wasmJsApp/src/wasmJsMain/resources/index.html @@ -0,0 +1,14 @@ + + + + + + Sample + + + + + +
+ + \ No newline at end of file diff --git a/sample/wasmJsApp/src/wasmJsMain/resources/styles.css b/sample/wasmJsApp/src/wasmJsMain/resources/styles.css new file mode 100644 index 00000000..0549b10f --- /dev/null +++ b/sample/wasmJsApp/src/wasmJsMain/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/settings.gradle.kts b/settings.gradle.kts index 519d0258..e8f68a34 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,4 +21,5 @@ dependencyResolutionManagement { include(":webview") include(":sample:androidApp") include(":sample:desktopApp") +include(":sample:wasmJsApp") include(":sample:shared") diff --git a/webview/build.gradle.kts b/webview/build.gradle.kts index 191bc1b2..6d4196ee 100644 --- a/webview/build.gradle.kts +++ b/webview/build.gradle.kts @@ -1,7 +1,11 @@ @file:Suppress("UNUSED_VARIABLE", "OPT_IN_USAGE") +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig + plugins { alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.android.library) alias(libs.plugins.compose.multiplatorm) alias(libs.plugins.dokka) @@ -11,8 +15,7 @@ plugins { kotlin { // explicitApi = ExplicitApiMode.Strict - - targetHierarchy.default() + applyDefaultHierarchyTemplate() androidTarget { publishLibraryVariants("release") @@ -32,6 +35,26 @@ kotlin { iosTarget.setUpiOSObserver() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser { + val rootDirPath = project.rootDir.path + val projectDirPath = project.projectDir.path + commonWebpackConfig { + devServer = + (devServer ?: KotlinWebpackConfig.DevServer()) + .apply { + static = + (static ?: mutableListOf()) + .apply { + add(rootDirPath) + add(projectDirPath) + } + } + } + } + } + sourceSets { commonMain.dependencies { implementation(compose.runtime) @@ -63,10 +86,6 @@ android { compileSdk = (findProperty("android.compileSdk") as String).toInt() namespace = "com.multiplatform.webview" - sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") - sourceSets["main"].res.srcDirs("src/androidMain/res") - sourceSets["main"].resources.srcDirs("src/commonMain/resources") - defaultConfig { minSdk = (findProperty("android.minSdk") as String).toInt() } diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/cookie/AndroidCookieManager.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/cookie/AndroidCookieManager.kt index dc1f6484..13548ee2 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/cookie/AndroidCookieManager.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/cookie/AndroidCookieManager.kt @@ -30,21 +30,18 @@ object AndroidCookieManager : CookieManager { if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_COOKIE_INFO)) { cookies = CookieManagerCompat.getCookieInfo( - androidCookieManager, url, + androidCookieManager, + url, ) } else { val cookiesString: String? = androidCookieManager.getCookie(url) if (!cookiesString.isNullOrBlank()) { - cookies = - cookiesString.split("; ".toRegex()) - .dropLastWhile { it.isEmpty() } + cookies = cookiesString.split("; ".toRegex()).dropLastWhile(String::isEmpty) } } for (cookie in cookies) { - val cookieParams = - cookie.split(";".toRegex()) - .dropLastWhile { it.isEmpty() } + val cookieParams = cookie.split(";".toRegex()).dropLastWhile(String::isEmpty) if (cookieParams.isEmpty()) continue diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/util/getPlatform.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/util/getPlatform.kt index e34b7515..0799a148 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/util/getPlatform.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/util/getPlatform.kt @@ -2,13 +2,9 @@ package com.multiplatform.webview.util import android.os.Build -internal actual fun getPlatform(): Platform { - return Platform.Android -} +internal actual fun getPlatform(): Platform = Platform.Android -internal actual fun getPlatformVersion(): String { - return Build.VERSION.RELEASE -} +internal actual fun getPlatformVersion(): String = Build.VERSION.RELEASE internal actual fun getPlatformVersionDouble(): Double { val systemVersion = getPlatformVersion() diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt index e7f33d3c..3397b9fe 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat +import androidx.core.graphics.createBitmap import androidx.webkit.WebSettingsCompat import androidx.webkit.WebViewFeature import com.multiplatform.webview.jsbridge.WebViewJsBridge @@ -165,76 +166,77 @@ fun AccompanistWebView( AndroidView( factory = { context -> - (factory?.invoke(context) ?: WebView(context)).apply { - onCreated(this) + (factory?.invoke(context) ?: WebView(context)) + .apply { + onCreated(this) - this.layoutParams = layoutParams + this.layoutParams = layoutParams - state.viewState?.let { - this.restoreState(it) - } - - chromeClient.context = context - webChromeClient = chromeClient - webViewClient = client + state.viewState?.let { + this.restoreState(it) + } - // Avoid covering other components - this.setLayerType(state.webSettings.androidWebSettings.layerType, null) + chromeClient.context = context + webChromeClient = chromeClient + webViewClient = client - settings.apply { - state.webSettings.let { - javaScriptEnabled = it.isJavaScriptEnabled - userAgentString = it.customUserAgentString - allowFileAccessFromFileURLs = it.allowFileAccessFromFileURLs - allowUniversalAccessFromFileURLs = it.allowUniversalAccessFromFileURLs - setSupportZoom(it.supportZoom) - } + // Avoid covering other components + this.setLayerType(state.webSettings.androidWebSettings.layerType, null) - state.webSettings.androidWebSettings.let { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - safeBrowsingEnabled = it.safeBrowsingEnabled + settings.apply { + state.webSettings.let { + javaScriptEnabled = it.isJavaScriptEnabled + userAgentString = it.customUserAgentString + allowFileAccessFromFileURLs = it.allowFileAccessFromFileURLs + allowUniversalAccessFromFileURLs = it.allowUniversalAccessFromFileURLs + setSupportZoom(it.supportZoom) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - isAlgorithmicDarkeningAllowed = it.isAlgorithmicDarkeningAllowed + + state.webSettings.androidWebSettings.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + safeBrowsingEnabled = it.safeBrowsingEnabled + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + isAlgorithmicDarkeningAllowed = it.isAlgorithmicDarkeningAllowed + } + setBackgroundColor(state.webSettings.backgroundColor.toArgb()) + allowFileAccess = it.allowFileAccess + textZoom = it.textZoom + useWideViewPort = it.useWideViewPort + standardFontFamily = it.standardFontFamily + defaultFontSize = it.defaultFontSize + loadsImagesAutomatically = it.loadsImagesAutomatically + domStorageEnabled = it.domStorageEnabled + mediaPlaybackRequiresUserGesture = it.mediaPlaybackRequiresUserGesture } - setBackgroundColor(state.webSettings.backgroundColor.toArgb()) - allowFileAccess = it.allowFileAccess - textZoom = it.textZoom - useWideViewPort = it.useWideViewPort - standardFontFamily = it.standardFontFamily - defaultFontSize = it.defaultFontSize - loadsImagesAutomatically = it.loadsImagesAutomatically - domStorageEnabled = it.domStorageEnabled - mediaPlaybackRequiresUserGesture = it.mediaPlaybackRequiresUserGesture - } - } - if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { - val nightModeFlags = - resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES) { - WebSettingsCompat.setForceDark( - this.settings, - WebSettingsCompat.FORCE_DARK_ON, - ) - } else { - WebSettingsCompat.setForceDark( - this.settings, - WebSettingsCompat.FORCE_DARK_OFF, - ) } + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + val nightModeFlags = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES) { + WebSettingsCompat.setForceDark( + this.settings, + WebSettingsCompat.FORCE_DARK_ON, + ) + } else { + WebSettingsCompat.setForceDark( + this.settings, + WebSettingsCompat.FORCE_DARK_OFF, + ) + } - if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { - WebSettingsCompat.setForceDarkStrategy( - this.settings, - WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY, - ) + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + WebSettingsCompat.setForceDarkStrategy( + this.settings, + WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY, + ) + } } + }.also { + val androidWebView = AndroidWebView(it, scope, webViewJsBridge) + state.webView = androidWebView + webViewJsBridge?.webView = androidWebView } - }.also { - val androidWebView = AndroidWebView(it, scope, webViewJsBridge) - state.webView = androidWebView - webViewJsBridge?.webView = androidWebView - } }, modifier = modifier, onReset = {}, @@ -466,7 +468,11 @@ open class AccompanistWebChromeClient : WebChromeClient() { } if (androidPermission != null) { - if (ContextCompat.checkSelfPermission(context, androidPermission) == PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission( + context, + androidPermission, + ) == PackageManager.PERMISSION_GRANTED + ) { grantedPermissions.add(resource) KLogger.d { "onPermissionRequest permission [$androidPermission] was already granted for resource [$resource]" @@ -488,11 +494,9 @@ open class AccompanistWebChromeClient : WebChromeClient() { } } - override fun getDefaultVideoPoster(): Bitmap? { - return if (state.webSettings.androidWebSettings.hideDefaultVideoPoster) { - Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888) - } else { - super.getDefaultVideoPoster() + override fun getDefaultVideoPoster(): Bitmap? = + when { + state.webSettings.androidWebSettings.hideDefaultVideoPoster -> createBitmap(50, 50) + else -> super.getDefaultVideoPoster() } - } } diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt index f95e5d13..bf2a1b0e 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt @@ -48,11 +48,30 @@ class AndroidWebView( webView.loadDataWithBaseURL(baseUrl, html, mimeType, encoding, historyUrl) } - override suspend fun loadHtmlFile(fileName: String) { - KLogger.d { - "loadHtmlFile: $fileName" + override suspend fun loadHtmlFile( + fileName: String, + readType: WebViewFileReadType, + ) { + KLogger.d { "loadHtmlFile: $fileName, readType: $readType" } + try { + when (readType) { + WebViewFileReadType.ASSET_RESOURCES -> { + // Assumes fileName is the path within the assets/ directory + webView.loadUrl("file:///android_asset/$fileName") + } + + WebViewFileReadType.COMPOSE_RESOURCE_FILES -> { + // Assumes fileName is the path within the composeResources/files directory + // fileName here is expected to be the URI from Res.getUri() + webView.loadUrl(fileName) + } + } + } catch (e: Exception) { + KLogger.e(e) { "Error loading HTML file: $fileName" } + val errorHtml = + "

Error

Could not load file: $fileName. Error: ${e.message}

" + webView.loadDataWithBaseURL(null, errorHtml, "text/html", "UTF-8", null) } - webView.loadUrl("file:///android_asset/$fileName") } override fun postUrl( @@ -127,9 +146,7 @@ class AndroidWebView( webViewJsBridge?.dispatch(JsMessage(id, method, params)) } - override fun scrollOffset(): Pair { - return Pair(webView.scrollX, webView.scrollY) - } + override fun scrollOffset(): Pair = Pair(webView.scrollX, webView.scrollY) override fun saveState(): WebViewBundle? { val bundle = WebViewBundle() diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt index 7146ac72..2f3e1259 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt @@ -31,13 +31,16 @@ actual fun ActualWebView( onCreated = onCreated, onDispose = onDispose, client = platformWebViewParams?.client ?: remember { AccompanistWebViewClient() }, - chromeClient = platformWebViewParams?.chromeClient ?: remember { AccompanistWebChromeClient() }, + chromeClient = + platformWebViewParams?.chromeClient ?: remember { AccompanistWebChromeClient() }, factory = { factory(WebViewFactoryParam(it)) }, ) } /** Android WebView factory parameters: a context. */ -actual data class WebViewFactoryParam(val context: Context) +actual data class WebViewFactoryParam( + val context: Context, +) /** Default WebView factory for Android. */ actual fun defaultWebViewFactory(param: WebViewFactoryParam) = android.webkit.WebView(param.context) diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebViewBundle.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebViewBundle.android.kt similarity index 100% rename from webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebViewBundle.kt rename to webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebViewBundle.android.kt diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/jsbridge/IJsMessageHandler.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/jsbridge/IJsMessageHandler.kt index d93c0ea5..5ab45a01 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/jsbridge/IJsMessageHandler.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/jsbridge/IJsMessageHandler.kt @@ -35,13 +35,15 @@ interface IJsMessageHandler { /** * Decode the params of [JsMessage] to the given type. */ -inline fun IJsMessageHandler.processParams(message: JsMessage): T { - return Json.decodeFromString(message.params) -} +inline fun IJsMessageHandler.processParams(message: JsMessage): T = + Json.decodeFromString( + message.params, + ) /** * Encode the given data to a JSON string. */ -inline fun IJsMessageHandler.dataToJsonString(res: T): String { - return Json.encodeToString(res) -} +inline fun IJsMessageHandler.dataToJsonString(res: T): String = + Json.encodeToString( + res, + ) diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/jsbridge/WebViewJsBridge.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/jsbridge/WebViewJsBridge.kt index 417800d4..c4e5fb18 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/jsbridge/WebViewJsBridge.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/jsbridge/WebViewJsBridge.kt @@ -14,38 +14,36 @@ import com.multiplatform.webview.web.WebViewNavigator * A bridge that can be used to communicate between native and web. */ @Immutable -open class WebViewJsBridge(val navigator: WebViewNavigator? = null, val jsBridgeName: String = "kmpJsBridge") { +open class WebViewJsBridge( + val navigator: WebViewNavigator? = null, + val jsBridgeName: String = "kmpJsBridge", +) { private val jsMessageDispatcher = JsMessageDispatcher() var webView: IWebView? = null - fun register(handler: IJsMessageHandler) { - jsMessageDispatcher.registerJSHandler(handler) - } + fun register(handler: IJsMessageHandler) = jsMessageDispatcher.registerJSHandler(handler) - fun unregister(handler: IJsMessageHandler) { - jsMessageDispatcher.unregisterJSHandler(handler) - } + fun unregister(handler: IJsMessageHandler) = jsMessageDispatcher.unregisterJSHandler(handler) - fun clear() { - jsMessageDispatcher.clear() - } + fun clear() = jsMessageDispatcher.clear() - fun dispatch(message: JsMessage) { - jsMessageDispatcher.dispatch(message, navigator) { - onCallback(it, message.callbackId) - } - } + fun dispatch(message: JsMessage) = + jsMessageDispatcher.dispatch( + message, + navigator, + fun(it: String) { + onCallback(it, message.callbackId) + }, + ) private fun onCallback( data: String, callbackId: Int, - ) { - webView?.evaluateJavaScript("window.$jsBridgeName.onCallback($callbackId, '$data')") - } + ) = webView?.evaluateJavaScript("window.$jsBridgeName.onCallback($callbackId, '$data')") } /** * Create a [WebViewJsBridge] that is remembered across Compositions. */ @Composable -fun rememberWebViewJsBridge(navigator: WebViewNavigator? = null): WebViewJsBridge = remember { WebViewJsBridge(navigator) } +fun rememberWebViewJsBridge(navigator: WebViewNavigator? = null) = remember { WebViewJsBridge(navigator) } diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/request/WebRequestInterceptResult.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/request/WebRequestInterceptResult.kt index 98c80426..9cf87b06 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/request/WebRequestInterceptResult.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/request/WebRequestInterceptResult.kt @@ -8,5 +8,7 @@ sealed interface WebRequestInterceptResult { data object Reject : WebRequestInterceptResult - class Modify(val request: WebRequest) : WebRequestInterceptResult + class Modify( + val request: WebRequest, + ) : WebRequestInterceptResult } diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/PlatformWebSettings.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/PlatformWebSettings.kt index cdd58662..053d1fb9 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/PlatformWebSettings.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/PlatformWebSettings.kt @@ -251,5 +251,53 @@ sealed class PlatformWebSettings { * Whether a user gesture is required to play media. The default is {@code true}. */ var mediaPlaybackRequiresUserGesture: Boolean = true, + /** + * Whether the WebView supports inspection via MacOS Safari. The default value is {@code false}. + */ + var isInspectable: Boolean = false, + ) : PlatformWebSettings() + + /** + * WasmJS web settings + */ + data class WasmJSWebSettings( + /** + * The background color of the iframe WebView. The default value is {@code null}. + * Will use WebSettings backgroundColor when null. + * + * @param backgroundColor a color value + */ + var backgroundColor: Color? = null, + /** + * Whether the iframe should have a border. The default value is {@code false}. + */ + var showBorder: Boolean = false, + /** + * The border style when showBorder is true. The default value is "1px solid #ccc". + */ + var borderStyle: String = "1px solid #ccc", + /** + * Whether the iframe should be sandboxed. The default value is {@code false}. + * When true, applies sandbox restrictions for security. + */ + var enableSandbox: Boolean = false, + /** + * Sandbox permissions when enableSandbox is true. + * The default allows scripts, same-origin, and forms. + */ + var sandboxPermissions: String = "allow-scripts allow-same-origin allow-forms", + /** + * Whether to allow fullscreen mode. The default value is {@code true}. + */ + var allowFullscreen: Boolean = true, + /** + * Custom CSS styles to apply to the iframe container. + * The default value is {@code null}. + */ + var customContainerStyle: String? = null, + /** + * Whether to enable console logging for debugging. The default value is {@code false}. + */ + var enableConsoleLogging: Boolean = false, ) : PlatformWebSettings() } diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/WebSettings.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/WebSettings.kt index d5e91950..25449b18 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/WebSettings.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/WebSettings.kt @@ -104,4 +104,9 @@ class WebSettings { * iOS platform specific settings */ val iOSWebSettings = PlatformWebSettings.IOSWebSettings() + + /** + * WasmJS platform specific settings + */ + val wasmJSWebSettings = PlatformWebSettings.WasmJSWebSettings() } diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/util/Extension.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/util/Extension.kt index e5254868..614f5557 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/util/Extension.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/util/Extension.kt @@ -1,9 +1,5 @@ package com.multiplatform.webview.util -fun Pair?.isZero(): Boolean { - return this == null || (first == 0 && second == 0) -} +fun Pair?.isZero(): Boolean = this == null || (first == 0 && second == 0) -fun Pair?.notZero(): Boolean { - return !isZero() -} +fun Pair?.notZero(): Boolean = !isZero() diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/util/KLogger.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/util/KLogger.kt index b0b1462b..0e37821b 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/util/KLogger.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/util/KLogger.kt @@ -36,8 +36,8 @@ enum class KLogSeverity { Assert, } -fun KLogSeverity.toKermitSeverity(): Severity { - return when (this) { +fun KLogSeverity.toKermitSeverity(): Severity = + when (this) { KLogSeverity.Verbose -> Severity.Verbose KLogSeverity.Debug -> Severity.Debug KLogSeverity.Info -> Severity.Info @@ -45,4 +45,3 @@ fun KLogSeverity.toKermitSeverity(): Severity { KLogSeverity.Error -> Severity.Error KLogSeverity.Assert -> Severity.Assert } -} diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt index 840e8ce9..a0ac8394 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt @@ -83,6 +83,7 @@ interface IWebView { is WebContent.File -> loadHtmlFile( content.fileName, + content.readType, ) is WebContent.Post -> @@ -117,7 +118,10 @@ interface IWebView { * * @param fileName The name of the HTML file to load. */ - suspend fun loadHtmlFile(fileName: String) + suspend fun loadHtmlFile( + fileName: String, + readType: WebViewFileReadType, + ) /** * Posts the given data to the given URL. diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/LoadingState.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/LoadingState.kt index d12679af..7db3fc47 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/LoadingState.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/LoadingState.kt @@ -18,7 +18,9 @@ sealed class LoadingState { * Describes a webview between `onPageStarted` and `onPageFinished` events, contains a * [progress] property which is updated by the webview. */ - data class Loading(val progress: Float) : LoadingState() + data class Loading( + val progress: Float, + ) : LoadingState() /** * Describes a webview that has finished loading content. diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebContent.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebContent.kt index b57c075a..8e6b026b 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebContent.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebContent.kt @@ -29,6 +29,7 @@ sealed class WebContent { data class File( val fileName: String, + val readType: WebViewFileReadType, ) : WebContent() /** @@ -61,15 +62,14 @@ sealed class WebContent { * @return the current url */ @Deprecated("Use state.lastLoadedUrl instead") - fun getCurrentUrl(): String? { - return when (this) { + fun getCurrentUrl(): String? = + when (this) { is Url -> url is Data -> baseUrl is File -> throw IllegalStateException("Unsupported") is Post -> url is NavigatorOnly -> throw IllegalStateException("Unsupported") } - } data object NavigatorOnly : WebContent() } diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt index 73b50717..8654e304 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import com.multiplatform.webview.jsbridge.WebViewJsBridge import com.multiplatform.webview.util.KLogger -import com.multiplatform.webview.util.getPlatform import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.merge @@ -91,47 +90,45 @@ fun WebView( } } - // Desktop will handle the first load by itself - if (!getPlatform().isDesktop()) { - LaunchedEffect(wv, state) { - snapshotFlow { state.content }.collect { content -> - when (content) { - is WebContent.Url -> { - state.lastLoadedUrl = content.url - wv.loadUrl(content.url, content.additionalHttpHeaders) - } - - is WebContent.Data -> { - wv.loadHtml( - content.data, - content.baseUrl, - content.mimeType, - content.encoding, - content.historyUrl, - ) - } - - is WebContent.File -> { - wv.loadHtmlFile(content.fileName) - } - - is WebContent.Post -> { - wv.postUrl( - content.url, - content.postData, - ) - } - - is WebContent.NavigatorOnly -> { - // NO-OP - } + // Handle content loading for all platforms + LaunchedEffect(wv, state) { + snapshotFlow { state.content }.collect { content -> + when (content) { + is WebContent.Url -> { + state.lastLoadedUrl = content.url + wv.loadUrl(content.url, content.additionalHttpHeaders) + } + + is WebContent.Data -> { + wv.loadHtml( + content.data, + content.baseUrl, + content.mimeType, + content.encoding, + content.historyUrl, + ) + } + + is WebContent.File -> { + wv.loadHtmlFile(content.fileName, content.readType) + } + + is WebContent.Post -> { + wv.postUrl( + content.url, + content.postData, + ) + } + + is WebContent.NavigatorOnly -> { + // NO-OP } } } } // inject the js bridge when the webview is loaded. - if (webViewJsBridge != null && !getPlatform().isDesktop()) { + if (webViewJsBridge != null) { LaunchedEffect(wv, state) { val loadingStateFlow = snapshotFlow { state.loadingState }.filter { it is LoadingState.Finished } diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewFileReadType.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewFileReadType.kt new file mode 100644 index 00000000..925894a2 --- /dev/null +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewFileReadType.kt @@ -0,0 +1,18 @@ +package com.multiplatform.webview.web + +/** + * Defines the source location for loading an HTML file into the WebView. + * + * This enum specifies whether the HTML file should be loaded from the platform-specific + * resources folder or from the Compose Multiplatform "composeResources/files" directory. + * + * - [ASSET_RESOURCES]: Loads the HTML file from the platform's `resources/assets` folder. + * + * - [COMPOSE_RESOURCE_FILES]: Loads the HTML file from the `composeResources/files` + * directory. This is the officially recommended approach for accessing file resources + * in a platform-agnostic way using `Res.readBytes()`. + */ +enum class WebViewFileReadType { + ASSET_RESOURCES, + COMPOSE_RESOURCE_FILES, +} diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt index c59a9e73..03aeac7d 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt @@ -25,7 +25,10 @@ import kotlinx.coroutines.withContext * @see [rememberWebViewNavigator] */ @Stable -class WebViewNavigator(val coroutineScope: CoroutineScope, val requestInterceptor: RequestInterceptor? = null) { +class WebViewNavigator( + val coroutineScope: CoroutineScope, + val requestInterceptor: RequestInterceptor? = null, +) { /** * Sealed class for constraining possible navigation events. */ diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewState.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewState.kt index 32a08937..bd373c60 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewState.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewState.kt @@ -25,7 +25,9 @@ import com.multiplatform.webview.util.isZero * A state holder to hold the state for the WebView. In most cases this will be remembered * using the rememberWebViewState(uri) function. */ -class WebViewState(webContent: WebContent) { +class WebViewState( + webContent: WebContent, +) { /** * The last loaded url. This is updated when a new page is loaded. */ @@ -110,6 +112,7 @@ class WebViewState(webContent: WebContent) { fun rememberWebViewState( url: String, additionalHttpHeaders: Map = emptyMap(), + extraSettings: WebSettings.() -> Unit = {}, ): WebViewState = // Rather than using .apply {} here we will recreate the state, this prevents // a recomposition loop when the webview updates the url itself. @@ -126,6 +129,7 @@ fun rememberWebViewState( url = url, additionalHttpHeaders = additionalHttpHeaders, ) + extraSettings(this.webSettings) } /** @@ -210,21 +214,76 @@ fun rememberWebViewStateWithHTMLData( }.apply { this.content = WebContent.Data( - data, baseUrl, encoding, mimeType, historyUrl, + data, + baseUrl, + encoding, + mimeType, + historyUrl, ) } /** * Creates a WebView state for HTML file loading that is remembered across Compositions. * - * @param fileName The file to load in the WebView - * Please note that the file should be placed in the commonMain/resources/assets folder. - * The fileName just need to be the relative path to the assets folder. + * @param fileName The file path or URI string to load in the WebView. The exact format and + * interpretation of this string (e.g., relative path, absolute URI) + * depend on the value of the [readType] parameter. + * @param readType Specifies the method and source location for loading the HTML file. + * This parameter is of type [WebViewFileReadType] and dictates how the + * [fileName] should be treated by the underlying platform-specific WebView + * implementation. + * + * Possible values for `readType` and their implications for `fileName`: + * - **`WebViewFileReadType.ASSET_RESOURCES` (Default)**: + * - **Android**: Expects `fileName` to be a path relative to the + * Android `assets` folder (e.g., "index.html" or "www/index.html"). + * The WebView will typically load this using a "file:///android_asset/"-based URL. + * - **iOS**: Expects `fileName` to be a path relative to the main + * bundle's resources, often within a specific assets or resources directory + * conventionally used (e.g., "assets/index.html" or a path resolved via + * `NSBundle.mainBundle.pathForResource`). The `loadHtmlFile` implementation + * constructs an appropriate `file:///` URL to this bundled resource. + * - **Desktop (JAR)**: Expects `fileName` to be a path relative to a + * predefined root within the JAR's classpath, typically an "assets" + * folder (e.g., "index.html" would be loaded from "/assets/index.html" + * within the JAR). + * + * - **`WebViewFileReadType.COMPOSE_RESOURCE_FILES`**: + * - **All Platforms**: Expects `fileName` to be the URI string + * obtained from `org.jetbrains.compose.resources.Res.getUri("files/your_file.html")`. + * This is the recommended approach for platform-agnostic file access + * using Compose Multiplatform resources. + * - **Android**: `Res.getUri()` typically returns a "file:///android_asset/composeResources/files/..." URI. + * - **iOS**: `Res.getUri()` typically returns an absolute "file:///..." URI pointing to the resource within the app bundle. + * - **Desktop (JAR)**: `Res.getUri()` typically returns a "jar:file:///path/to/your.jar!/composeResources/files/..." URI. + * The platform-specific `loadHtmlFile` implementations are responsible for correctly parsing these URIs. + * + * The default value is `WebViewFileReadType.ASSET_RESOURCES`. + * + * @see WebViewFileReadType + * @see WebContent.File */ @Composable +fun rememberWebViewStateWithHTMLFile( + fileName: String, + readType: WebViewFileReadType, +): WebViewState = + remember { + WebViewState(WebContent.File(fileName, readType)) + }.apply { + this.content = WebContent.File(fileName, readType) + } + +@Composable +@Deprecated( + "Use the overloaded " + + "rememberWebViewStateWithHTMLFile(fileName: String, readType: WebViewFileReadType) " + + "instead. Pass readType = WebViewFileReadType.ASSET_RESOURCES explicitly to make " + + "the loading source clear.", +) fun rememberWebViewStateWithHTMLFile(fileName: String): WebViewState = remember { - WebViewState(WebContent.File(fileName)) + WebViewState(WebContent.File(fileName, WebViewFileReadType.ASSET_RESOURCES)) }.apply { - this.content = WebContent.File(fileName) + this.content = WebContent.File(fileName, WebViewFileReadType.ASSET_RESOURCES) } diff --git a/webview/src/desktopMain/kotlin/com/multiplatform/webview/util/getPlatform.kt b/webview/src/desktopMain/kotlin/com/multiplatform/webview/util/getPlatform.kt index 1a7b540c..66d171de 100644 --- a/webview/src/desktopMain/kotlin/com/multiplatform/webview/util/getPlatform.kt +++ b/webview/src/desktopMain/kotlin/com/multiplatform/webview/util/getPlatform.kt @@ -1,8 +1,6 @@ package com.multiplatform.webview.util -internal actual fun getPlatform(): Platform { - return Platform.Desktop -} +internal actual fun getPlatform(): Platform = Platform.Desktop internal actual fun getPlatformVersion(): String { // TODO diff --git a/webview/src/desktopMain/kotlin/com/multiplatform/webview/util/tempDirectory.kt b/webview/src/desktopMain/kotlin/com/multiplatform/webview/util/tempDirectory.kt new file mode 100644 index 00000000..d318fd4e --- /dev/null +++ b/webview/src/desktopMain/kotlin/com/multiplatform/webview/util/tempDirectory.kt @@ -0,0 +1,19 @@ +package com.multiplatform.webview.util + +import kotlin.io.path.createTempDirectory + +val tempDirectory: java.io.File = createTempDirectory("webview-temp").toFile() + +fun addTempDirectoryRemovalHook() { + Runtime.getRuntime().addShutdownHook( + Thread { + println("Attempting to delete temp directory: ${tempDirectory.absolutePath}") + val success = tempDirectory.deleteRecursively() + if (success) { + println("✅ Temp directory deleted successfully.") + } else { + println("❌ Failed to delete temp directory.") + } + }, + ) +} diff --git a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebView.kt b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebView.kt index c5d39748..be037af6 100644 --- a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebView.kt +++ b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebView.kt @@ -3,8 +3,10 @@ package com.multiplatform.webview.web import com.multiplatform.webview.jsbridge.JsMessage import com.multiplatform.webview.jsbridge.WebViewJsBridge import com.multiplatform.webview.util.KLogger +import com.multiplatform.webview.util.tempDirectory import dev.datlag.kcef.KCEFBrowser import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.serialization.json.Json import org.cef.browser.CefBrowser import org.cef.browser.CefFrame @@ -14,6 +16,12 @@ import org.cef.handler.CefMessageRouterHandlerAdapter import org.cef.network.CefPostData import org.cef.network.CefPostDataElement import org.cef.network.CefRequest +import org.jetbrains.compose.resources.InternalResourceApi +import java.io.File +import java.io.InputStreamReader +import java.net.JarURLConnection +import java.net.URI +import java.nio.charset.StandardCharsets actual typealias NativeWebView = KCEFBrowser @@ -63,12 +71,127 @@ class DesktopWebView( "DesktopWebView loadHtml" } if (html != null) { - webView.loadHtml(html, baseUrl ?: KCEFBrowser.BLANK_URI) + try { + webView.loadHtml(html, baseUrl ?: KCEFBrowser.BLANK_URI) + } catch (e: Exception) { + KLogger.e { "DesktopWebView loadHtml error: ${e.message}" } + } + } else { + KLogger.e { "DesktopWebView loadHtml: HTML content is null" } } } - override suspend fun loadHtmlFile(fileName: String) { - // TODO + @OptIn(InternalResourceApi::class) + override suspend fun loadHtmlFile( + fileName: String, + readType: WebViewFileReadType, + ) { + var attemptedResourcePath = fileName // For logging in case of error + try { + when (readType) { + WebViewFileReadType.ASSET_RESOURCES -> { + val path = fileName.removePrefix("/") + attemptedResourcePath = "assets/$path" + val inputStream = + this::class.java.classLoader.getResourceAsStream(attemptedResourcePath) + if (inputStream == null) { + throw Exception("Resource not found: $attemptedResourcePath (for readType: $readType)") + } + + val outFile = java.io.File(tempDirectory, path.substringAfterLast("/")) + outFile.outputStream().use { output -> + inputStream.copyTo(output) + } + + val baseFolder = attemptedResourcePath.substringBeforeLast("/", "") + val basePath = if (baseFolder.isNotEmpty()) "$baseFolder/" else "" + + // Copy other assets from same folder if any + val resourceUrls = this::class.java.classLoader.getResources(basePath) + while (resourceUrls.hasMoreElements()) { + val url = resourceUrls.nextElement() + val connection = url.openConnection() + if (connection is JarURLConnection) { + val jarFile = connection.jarFile + for (entry in jarFile.entries()) { + if (entry.name.startsWith(basePath) && !entry.isDirectory) { + val file = + File(tempDirectory, entry.name.substringAfterLast("/")) + if (!file.exists()) { + jarFile.getInputStream(entry).use { input -> + file.outputStream().use { input.copyTo(it) } + } + } + } + } + } + } + + delay(200) + webView.loadURL("file://${outFile.absolutePath}") + } + + WebViewFileReadType.COMPOSE_RESOURCE_FILES -> { + // fileName is expected to be a URI string from Res.getUri(), like "jar:file:..." or "file:///..." + + val parts = fileName.split("!/") + if (parts.size != 2) { + throw Exception("Invalid JAR URI format: $fileName") + } + val pathInJar = parts[1].removePrefix("/") + attemptedResourcePath = pathInJar + + // Fix: Correct JAR URL parsing for JarURLConnection + val jarFileUrl = parts[0].removePrefix("jar:") + val jarUrl = URI("jar", "$jarFileUrl!/", null).toURL() + val jarConnection = jarUrl.openConnection() as JarURLConnection + val jarFile = jarConnection.jarFile + + for (entry in jarFile.entries()) { + if (entry.name.startsWith(pathInJar.substringBeforeLast("/")) && !entry.isDirectory) { + val file = + java.io.File(tempDirectory, entry.name.substringAfterLast("/")) + file.outputStream().use { output -> + jarFile.getInputStream(entry).copyTo(output) + } + } + } + + val htmlFile = java.io.File(tempDirectory, pathInJar.substringAfterLast("/")) + if (!htmlFile.exists()) { + throw Exception("Extracted HTML file not found: ${htmlFile.absolutePath}") + } + + delay(200) + webView.loadURL("file://${htmlFile.absolutePath}") + } + } + } catch (e: Exception) { + val errorHtml = + """ + + + + Error Loading File + + + +

Error Loading File

+

File: $fileName (ReadType: $readType)

+

Attempted Path: $attemptedResourcePath

+

Error: ${e::class.simpleName} - ${e.message}

+ ${e.cause?.let { "

Cause: ${it.message}

" } ?: ""} +
${e.stackTraceToString()}
+ + + """.trimIndent() + delay(200) + webView.loadHtml(errorHtml) + KLogger.e(e) { "DesktopWebView loadHtmlFile error for $fileName (ReadType: $readType)" } + } } override fun postUrl( @@ -164,11 +287,77 @@ class DesktopWebView( webView.client.addMessageRouter(router) } - override fun saveState(): WebViewBundle? { - return null - } + @OptIn(InternalResourceApi::class) + private fun inlineExternalResources( + htmlContent: String, + basePathInJar: String, + ): String { + var modifiedHtml = htmlContent + + try { + // Inline CSS files + val cssPattern = + """]*href\s*=\s*["']([^"']+\.css)["'][^>]*>""" + .toRegex(RegexOption.IGNORE_CASE) + modifiedHtml = + cssPattern.replace(modifiedHtml) { matchResult -> + val cssFile = matchResult.groupValues[1] + try { + val resourcePath = "$basePathInJar$cssFile".removePrefix("/") + val cssInputStream = + this::class.java.classLoader.getResourceAsStream(resourcePath) + if (cssInputStream != null) { + val cssContent = + InputStreamReader( + cssInputStream, + StandardCharsets.UTF_8, + ).use { it.readText() } + "" + } else { + KLogger.e { "DesktopWebView: CSS resource not found for inlining: $resourcePath" } + matchResult.value // Keep original if can't inline + } + } catch (e: Exception) { + KLogger.e { "DesktopWebView: Could not inline CSS $cssFile: ${e.message}" } + matchResult.value + } + } + + // Inline JS files + val jsPattern = + """]*src\s*=\s*["']([^"']+\.js)["'][^>]*>""" + .toRegex(RegexOption.IGNORE_CASE) + modifiedHtml = + jsPattern.replace(modifiedHtml) { matchResult -> + val jsFile = matchResult.groupValues[1] + try { + val resourcePath = "$basePathInJar$jsFile".removePrefix("/") + val jsInputStream = + this::class.java.classLoader.getResourceAsStream(resourcePath) + if (jsInputStream != null) { + val jsContent = + InputStreamReader( + jsInputStream, + StandardCharsets.UTF_8, + ).use { it.readText() } + "" + } else { + KLogger.e { "DesktopWebView: JS resource not found for inlining: $resourcePath" } + matchResult.value // Keep original if can't inline + } + } catch (e: Exception) { + KLogger.e { "DesktopWebView: Could not inline JS $jsFile: ${e.message}" } + matchResult.value + } + } + } catch (e: Exception) { + KLogger.e { "DesktopWebView: Error during resource inlining: ${e.message}" } + } - override fun scrollOffset(): Pair { - return Pair(0, 0) + return modifiedHtml } + + override fun saveState(): WebViewBundle? = null + + override fun scrollOffset(): Pair = Pair(0, 0) } diff --git a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebEngineExt.kt b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebEngineExt.kt index caecdc37..69d6848f 100644 --- a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebEngineExt.kt +++ b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebEngineExt.kt @@ -17,9 +17,7 @@ import kotlin.math.ln /** * Created By Kevin Zou On 2023/9/12 */ -internal fun CefBrowser.getCurrentUrl(): String? { - return this.url -} +internal fun CefBrowser.getCurrentUrl(): String? = this.url internal fun CefBrowser.addDisplayHandler(state: WebViewState) { this.client.addDisplayHandler( @@ -61,9 +59,7 @@ internal fun CefBrowser.addDisplayHandler(state: WebViewState) { override fun onTooltip( browser: CefBrowser?, text: String?, - ): Boolean { - return false - } + ) = false override fun onStatusMessage( browser: CefBrowser?, @@ -77,16 +73,12 @@ internal fun CefBrowser.addDisplayHandler(state: WebViewState) { message: String?, source: String?, line: Int, - ): Boolean { - return false - } + ) = false override fun onCursorChange( browser: CefBrowser?, cursorType: Int, - ): Boolean { - return false - } + ) = false }, ) } diff --git a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt index cfd8341b..18b43e68 100644 --- a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt +++ b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt @@ -1,16 +1,19 @@ package com.multiplatform.webview.web -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.awt.SwingPanel import com.multiplatform.webview.jsbridge.WebViewJsBridge -import compose_webview_multiplatform.webview.generated.resources.Res import dev.datlag.kcef.KCEF import dev.datlag.kcef.KCEFBrowser import dev.datlag.kcef.KCEFClient import org.cef.browser.CefRendering import org.cef.browser.CefRequestContext -import org.jetbrains.compose.resources.ExperimentalResourceApi /** * Desktop WebView implementation. @@ -74,13 +77,14 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView = param.rendering, param.transparent, ) - is WebContent.File -> + is WebContent.File -> { param.client.createBrowserWithHtml( param.fileContent, KCEFBrowser.BLANK_URI, param.rendering, param.transparent, ) + } else -> param.client.createBrowser( KCEFBrowser.BLANK_URI, @@ -93,7 +97,6 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView = /** * Desktop WebView implementation. */ -@OptIn(ExperimentalResourceApi::class) @Composable fun DesktopWebView( state: WebViewState, @@ -117,21 +120,13 @@ fun DesktopWebView( } } } - val scope = rememberCoroutineScope() - val fileContent by produceState("", state.content) { - value = - if (state.content is WebContent.File) { - val res = Res.readBytes("assets/${(state.content as WebContent.File).fileName}") - res.decodeToString().trimIndent() - } else { - "" - } - } + val scope = rememberCoroutineScope() val browser: KCEFBrowser? = - remember(client, state.webSettings, fileContent) { - client?.let { factory(WebViewFactoryParam(state, client, fileContent)) } + remember(client, state.webSettings) { + client?.let { factory(WebViewFactoryParam(state, client, "")) } } + val desktopWebView: DesktopWebView? = remember(browser) { browser?.let { DesktopWebView(browser, scope, webViewJsBridge) } diff --git a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebViewBundle.kt b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebViewBundle.desktop.kt similarity index 100% rename from webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebViewBundle.kt rename to webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebViewBundle.desktop.kt diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/cookie/IOSCookieManager.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/cookie/IOSCookieManager.kt index 2cbfa204..c1920365 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/cookie/IOSCookieManager.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/cookie/IOSCookieManager.kt @@ -77,11 +77,12 @@ object IOSCookieManager : CookieManager { override suspend fun removeCookies(url: String) = suspendCancellableCoroutine { cookieStore.getAllCookies { cookies -> - cookies?.filter { cookie -> - cookie is NSHTTPCookie && url.contains(cookie.domain) - }?.forEach { cookie -> - cookieStore.deleteCookie(cookie as NSHTTPCookie) {} - } + cookies + ?.filter { cookie -> + cookie is NSHTTPCookie && url.contains(cookie.domain) + }?.forEach { cookie -> + cookieStore.deleteCookie(cookie as NSHTTPCookie) {} + } it.resume(Unit, {}) } } diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/jsbridge/WKJsMessageHandler.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/jsbridge/WKJsMessageHandler.kt index 8e6b004d..bafa9da5 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/jsbridge/WKJsMessageHandler.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/jsbridge/WKJsMessageHandler.kt @@ -14,9 +14,10 @@ import platform.darwin.NSObject /** * A JS message handler for WKWebView. */ -class WKJsMessageHandler(private val webViewJsBridge: WebViewJsBridge) : - WKScriptMessageHandlerProtocol, - NSObject() { +class WKJsMessageHandler( + private val webViewJsBridge: WebViewJsBridge, +) : NSObject(), + WKScriptMessageHandlerProtocol { override fun userContentController( userContentController: WKUserContentController, didReceiveScriptMessage: WKScriptMessage, diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/util/Color.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/util/Color.kt index a431e9b7..c5470a45 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/util/Color.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/util/Color.kt @@ -3,11 +3,10 @@ package com.multiplatform.webview.util import androidx.compose.ui.graphics.Color import platform.UIKit.UIColor -fun Color.toUIColor(): UIColor { - return UIColor( +fun Color.toUIColor(): UIColor = + UIColor( red = red.toDouble(), green = green.toDouble(), blue = blue.toDouble(), alpha = alpha.toDouble(), ) -} diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/util/getPlatform.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/util/getPlatform.kt index 61a47109..5db4a7e2 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/util/getPlatform.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/util/getPlatform.kt @@ -2,13 +2,9 @@ package com.multiplatform.webview.util import platform.UIKit.UIDevice -internal actual fun getPlatform(): Platform { - return Platform.IOS -} +internal actual fun getPlatform(): Platform = Platform.IOS -internal actual fun getPlatformVersion(): String { - return UIDevice.currentDevice.systemVersion -} +internal actual fun getPlatformVersion(): String = UIDevice.currentDevice.systemVersion internal actual fun getPlatformVersionDouble(): Double { val systemVersion = getPlatformVersion() diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt index c2be7477..786c9246 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt @@ -15,9 +15,11 @@ import platform.Foundation.HTTPMethod import platform.Foundation.NSBundle import platform.Foundation.NSData import platform.Foundation.NSMutableURLRequest +import platform.Foundation.NSString import platform.Foundation.NSURL import platform.Foundation.create import platform.Foundation.setValue +import platform.Foundation.stringByDeletingLastPathComponent import platform.WebKit.WKWebView import platform.darwin.NSObject import platform.darwin.NSObjectMeta @@ -48,7 +50,6 @@ class IOSWebView( url: String, additionalHttpHeaders: Map, ) { - KLogger.d { "Load url: $url" } val request = NSMutableURLRequest.requestWithURL( URL = NSURL(string = url), @@ -84,10 +85,76 @@ class IOSWebView( ) } - override suspend fun loadHtmlFile(fileName: String) { - val res = NSBundle.mainBundle.resourcePath + "/compose-resources/assets/" + fileName - val url = NSURL.fileURLWithPath(res) - webView.loadFileURL(url, url) + override suspend fun loadHtmlFile( + fileName: String, + readType: WebViewFileReadType, + ) { + try { + val fileURL: NSURL + var readAccessURL: NSURL? = null + + when (readType) { + WebViewFileReadType.ASSET_RESOURCES -> { + val resourcePath = + (NSBundle.mainBundle.resourcePath ?: "") + + "/compose-resources/assets/" + fileName + fileURL = NSURL.fileURLWithPath(resourcePath) + + val parentDir = (resourcePath as NSString).stringByDeletingLastPathComponent() + if (parentDir.isNotBlank()) { + readAccessURL = NSURL.fileURLWithPath(parentDir) + } else { + readAccessURL = NSURL.fileURLWithPath(NSBundle.mainBundle.resourcePath!!) + } + } + + WebViewFileReadType.COMPOSE_RESOURCE_FILES -> { + fileURL = NSURL(string = fileName) + val readAccessURLPath = + (fileName as NSString).stringByDeletingLastPathComponent() + readAccessURL = NSURL(string = readAccessURLPath) + } + } + + if (!fileURL.isFileURL()) { + KLogger.e { + "The determined fileURL is not a valid file URL: ${fileURL.absoluteString}" + } + loadHtml( + "Error: Not a file URL: ${fileURL.absoluteString}", + ) + return + } + + val finalReadAccessURL = readAccessURL + + if (finalReadAccessURL.path.isNullOrEmpty()) { + KLogger.e { + "Critical: finalReadAccessURL is null or has an empty path. " + + "Cannot load file with proper read access for ${fileURL.absoluteString}" + } + loadHtml( + "Error: Cannot determine read access URL " + + "for ${fileURL.absoluteString}", + ) + return + } + + webView.loadFileURL(fileURL, finalReadAccessURL) + } catch (e: Exception) { + KLogger.e(e) { "Error loading HTML file: $fileName (readType: $readType)" } + val errorHtml = + """ + + Error + +

Error Loading File

+

Could not load: $fileName (readType: $readType)

+

Error: ${e.message}

+ + """.trimIndent() + loadHtml(errorHtml) + } } @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) @@ -135,7 +202,6 @@ class IOSWebView( KLogger.e { "evaluateJavaScript error: $error" } callback.invoke(error.localizedDescription()) } else { - KLogger.info { "evaluateJavaScript result: $result" } callback.invoke(result?.toString() ?: "") } } @@ -143,9 +209,6 @@ class IOSWebView( override fun injectJsBridge() { if (webViewJsBridge == null) return - KLogger.info { - "iOS WebView injectJsBridge" - } super.injectJsBridge() val callIOS = """ @@ -157,7 +220,6 @@ class IOSWebView( } override fun initJsBridge(webViewJsBridge: WebViewJsBridge) { - KLogger.info { "injectBridge" } val jsMessageHandler = WKJsMessageHandler(webViewJsBridge) webView.configuration.userContentController.apply { addScriptMessageHandler(jsMessageHandler, "iosJsBridge") diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt index 13284830..78b2e6bb 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt @@ -6,6 +6,7 @@ import com.multiplatform.webview.util.KLogger import com.multiplatform.webview.util.getPlatformVersionDouble import com.multiplatform.webview.util.notZero import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCSignatureOverride import platform.CoreGraphics.CGPointMake import platform.Foundation.HTTPMethod import platform.Foundation.NSError @@ -28,12 +29,14 @@ import platform.darwin.NSObject class WKNavigationDelegate( private val state: WebViewState, private val navigator: WebViewNavigator, -) : NSObject(), WKNavigationDelegateProtocol { +) : NSObject(), + WKNavigationDelegateProtocol { private var isRedirect = false /** * Called when the web view begins to receive web content. */ + @ObjCSignatureOverride override fun webView( webView: WKWebView, didStartProvisionalNavigation: WKNavigation?, @@ -49,6 +52,7 @@ class WKNavigationDelegate( /** * Called when the web view receives a server redirect. */ + @ObjCSignatureOverride override fun webView( webView: WKWebView, didCommitNavigation: WKNavigation?, @@ -66,6 +70,7 @@ class WKNavigationDelegate( * Called when the web view finishes loading. */ @OptIn(ExperimentalForeignApi::class) + @ObjCSignatureOverride override fun webView( webView: WKWebView, didFinishNavigation: WKNavigation?, @@ -123,7 +128,9 @@ class WKNavigationDelegate( KLogger.info { "Outer decidePolicyForNavigationAction: $url $isRedirect $decidePolicyForNavigationAction" } - if (url != null && !isRedirect && + if ( + url != null && + !isRedirect && navigator.requestInterceptor != null && decidePolicyForNavigationAction.targetFrame?.mainFrame != false ) { diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKWebViewObserver.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKWebViewObserver.kt index ece27121..32c63cdf 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKWebViewObserver.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKWebViewObserver.kt @@ -16,8 +16,10 @@ import platform.darwin.NSObject * Observer for the WKWebView's loading state */ @ExperimentalForeignApi -class WKWebViewObserver(private val state: WebViewState, private val navigator: WebViewNavigator) : - NSObject(), +class WKWebViewObserver( + private val state: WebViewState, + private val navigator: WebViewNavigator, +) : NSObject(), ObserverProtocol { override fun observeValueForKeyPath( keyPath: String?, diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt index 70493641..1a96067a 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt @@ -48,13 +48,19 @@ actual fun ActualWebView( } /** iOS WebView factory parameters: configuration created from WebSettings. */ -actual data class WebViewFactoryParam(val config: WKWebViewConfiguration) +actual data class WebViewFactoryParam( + val config: WKWebViewConfiguration, +) actual class PlatformWebViewParams /** Default WebView factory for iOS. */ @OptIn(ExperimentalForeignApi::class) -actual fun defaultWebViewFactory(param: WebViewFactoryParam) = WKWebView(frame = CGRectZero.readValue(), configuration = param.config) +actual fun defaultWebViewFactory(param: WebViewFactoryParam) = + WKWebView( + frame = CGRectZero.readValue(), + configuration = param.config, + ) /** * iOS WebView implementation. @@ -102,50 +108,53 @@ fun IOSWebView( javaScriptEnabled = state.webSettings.isJavaScriptEnabled } setValue( - state.webSettings.allowUniversalAccessFromFileURLs, + value = state.webSettings.allowUniversalAccessFromFileURLs, forKey = "allowUniversalAccessFromFileURLs", ) } - factory(WebViewFactoryParam(config)).apply { - onCreated(this) - state.viewState?.let { - this.interactionState = it - } - allowsBackForwardNavigationGestures = captureBackPresses - customUserAgent = state.webSettings.customUserAgentString - this.addProgressObservers( - observer = observer, - ) - this.navigationDelegate = navigationDelegate + factory(WebViewFactoryParam(config)) + .apply { + onCreated(this) + state.viewState?.let { + this.interactionState = it + } + allowsBackForwardNavigationGestures = captureBackPresses + customUserAgent = state.webSettings.customUserAgentString + this.addProgressObservers( + observer = observer, + ) + this.navigationDelegate = navigationDelegate - state.webSettings.let { - val backgroundColor = - (it.iOSWebSettings.backgroundColor ?: it.backgroundColor).toUIColor() - val scrollViewColor = - ( - it.iOSWebSettings.underPageBackgroundColor - ?: it.backgroundColor - ).toUIColor() - setOpaque(it.iOSWebSettings.opaque) - if (!it.iOSWebSettings.opaque) { - setBackgroundColor(backgroundColor) - scrollView.setBackgroundColor(scrollViewColor) + state.webSettings.let { + val backgroundColor = + (it.iOSWebSettings.backgroundColor ?: it.backgroundColor).toUIColor() + val scrollViewColor = + ( + it.iOSWebSettings.underPageBackgroundColor + ?: it.backgroundColor + ).toUIColor() + setOpaque(it.iOSWebSettings.opaque) + if (!it.iOSWebSettings.opaque) { + setBackgroundColor(backgroundColor) + scrollView.setBackgroundColor(scrollViewColor) + } + scrollView.pinchGestureRecognizer?.enabled = it.supportZoom } - scrollView.pinchGestureRecognizer?.enabled = it.supportZoom - } - state.webSettings.iOSWebSettings.let { - with(scrollView) { - bounces = it.bounces - scrollEnabled = it.scrollEnabled - showsHorizontalScrollIndicator = it.showHorizontalScrollIndicator - showsVerticalScrollIndicator = it.showVerticalScrollIndicator + state.webSettings.iOSWebSettings.let { + with(scrollView) { + bounces = it.bounces + scrollEnabled = it.scrollEnabled + showsHorizontalScrollIndicator = it.showHorizontalScrollIndicator + showsVerticalScrollIndicator = it.showVerticalScrollIndicator + } } + + this.setInspectable(state.webSettings.iOSWebSettings.isInspectable) + }.also { + val iosWebView = IOSWebView(it, scope, webViewJsBridge) + state.webView = iosWebView + webViewJsBridge?.webView = iosWebView } - }.also { - val iosWebView = IOSWebView(it, scope, webViewJsBridge) - state.webView = iosWebView - webViewJsBridge?.webView = iosWebView - } }, modifier = modifier, onRelease = { diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebViewBundle.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebViewBundle.ios.kt similarity index 100% rename from webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebViewBundle.kt rename to webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebViewBundle.ios.kt diff --git a/webview/src/wasmJsMain/kotlin/com/multiplatform/util/getPlatform.kt b/webview/src/wasmJsMain/kotlin/com/multiplatform/util/getPlatform.kt new file mode 100644 index 00000000..c983b714 --- /dev/null +++ b/webview/src/wasmJsMain/kotlin/com/multiplatform/util/getPlatform.kt @@ -0,0 +1,15 @@ +package com.multiplatform.webview.util + +internal actual fun getPlatform(): Platform = Platform.Desktop + +internal actual fun getPlatformVersion(): String = + // TODO + "11.0" + +internal actual fun getPlatformVersionDouble(): Double { + val systemVersion = getPlatformVersion() + val components = systemVersion.split(".") + val major = components.getOrNull(0)?.toDoubleOrNull() ?: 0.0 + val minor = components.getOrNull(1)?.toDoubleOrNull() ?: 0.0 + return major + (minor / 10.0) +} diff --git a/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/cookie/WebCookieManager.kt b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/cookie/WebCookieManager.kt new file mode 100644 index 00000000..727e4cda --- /dev/null +++ b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/cookie/WebCookieManager.kt @@ -0,0 +1,95 @@ +package com.multiplatform.webview.cookie + +/** + * Converts a timestamp to a cookie expiration date string in the browser's expected format. + */ +actual fun getCookieExpirationDate(expiresDate: Long): String = jsDateToUTCString(expiresDate) + +@JsFun("timestamp => new Date(timestamp).toUTCString()") +private external fun jsDateToUTCString(timestamp: Long): String + +/** + * Sets a cookie in the browser. + */ +private fun setDocumentCookie(cookieStr: String) = setJsCookie(cookieStr) + +@JsFun("cookieStr => { document.cookie = cookieStr; }") +private external fun setJsCookie(cookieStr: String) + +/** + * Gets all cookies from the browser. + */ +private fun getDocumentCookies(): String = getJsCookies() + +@JsFun("() => document.cookie") +private external fun getJsCookies(): String + +@Suppress(names = ["FunctionName"]) +actual fun WebViewCookieManager(): CookieManager = WasmJsCookieManager + +object WasmJsCookieManager : CookieManager { + override suspend fun setCookie( + url: String, + cookie: Cookie, + ) { + // Create the cookie string + val cookieStr = + buildString { + append("${cookie.name}=${cookie.value}") + + cookie.domain?.let { append("; domain=$it") } + cookie.path?.let { append("; path=$it") } + cookie.expiresDate?.let { append("; expires=${getCookieExpirationDate(it)}") } + cookie.maxAge?.let { append("; max-age=$it") } + cookie.sameSite?.let { append("; SameSite=$it") } + if (cookie.isSecure == true) append("; Secure") + if (cookie.isHttpOnly == true) append("; HttpOnly") + } + + // Set the cookie using the document.cookie API + setDocumentCookie(cookieStr) + } + + override suspend fun getCookies(url: String): List { + val cookiesStr = getDocumentCookies() + if (cookiesStr.isEmpty()) return emptyList() + + return cookiesStr.split(";").map { cookieStr -> + val parts = cookieStr.trim().split("=", limit = 2) + val name = parts[0] + val value = if (parts.size > 1) parts[1] else "" + + Cookie( + name = name, + value = value, + domain = null, // These additional properties aren't accessible via document.cookie + path = null, + expiresDate = null, + sameSite = null, + isSecure = null, + isHttpOnly = null, + maxAge = null, + ) + } + } + + override suspend fun removeAllCookies() { + val cookies = getCookies("") + for (cookie in cookies) { + // To delete a cookie, set it with an expired date + val expireCookie = + buildString { + append("${cookie.name}=") + append("; path=/") + append("; expires=Thu, 01 Jan 1970 00:00:00 GMT") + } + setDocumentCookie(expireCookie) + } + } + + override suspend fun removeCookies(url: String) { + // In browser context, we can't easily remove cookies for a specific URL + // So we'll use the same approach as removeAllCookies + removeAllCookies() + } +} diff --git a/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/HtmlView.kt b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/HtmlView.kt new file mode 100644 index 00000000..1f6489fa --- /dev/null +++ b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/HtmlView.kt @@ -0,0 +1,354 @@ +package com.multiplatform.webview.web + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshots.SnapshotStateObserver +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusTarget +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.round +import kotlinx.browser.document +import kotlinx.coroutines.launch +import org.w3c.dom.Element +import org.w3c.dom.events.Event + +/** + * A Composable that renders HTML content using an iframe + * + * @param state The state of the HTML view + * @param modifier The modifier for this composable + * @param navigator The navigator for HTML navigation events + * @param onCreated Callback invoked when the view is created + * @param onDispose Callback invoked when the view is disposed + */ +@Composable +fun HtmlView( + state: HtmlViewState, + modifier: Modifier = Modifier, + navigator: HtmlViewNavigator = rememberHtmlViewNavigator(), + onCreated: (Element) -> Unit = {}, + onDispose: (Element) -> Unit = {}, +) { + val scope = rememberCoroutineScope() + val element = remember { mutableStateOf(null) } + val root = LocalLayerContainer.current + val density = LocalDensity.current.density + val focusManager = LocalFocusManager.current + + val componentInfo = remember { ComponentInfo() } + val focusSwitcher = remember { FocusSwitcher(componentInfo, focusManager) } + val eventsInitialized = remember { mutableStateOf(false) } + val componentReady = remember { mutableStateOf(false) } + + Box( + modifier = + modifier.onGloballyPositioned { coordinates -> + val location = coordinates.positionInWindow().round() + val size = coordinates.size + if (componentReady.value) { + changeCoordinates( + componentInfo.component, + size.width / density, + size.height / density, + location.x / density, + location.y / density, + ) + } + }, + ) { + focusSwitcher.Content() + } + + DisposableEffect(Unit) { + componentInfo.container = document.createElement("div") + componentInfo.component = document.createElement("iframe") + componentReady.value = true + + (root as Element).insertBefore(componentInfo.container, (root as Element).firstChild) + componentInfo.container.append(componentInfo.component) + + initializingElement(componentInfo.component) + element.value = componentInfo.component + state.htmlElement = componentInfo.component + onCreated(componentInfo.component) + + componentInfo.updater = + Updater(componentInfo.component) { iframe -> + if (!eventsInitialized.value) { + eventsInitialized.value = true + + val loadCallback: (Event) -> Unit = { + state.loadingState = HtmlLoadingState.Finished() + + try { + val title = getIframeTitleJs(iframe) + if (title != null) { + state.pageTitle = title + } + + val href = getIframeUrlJs(iframe) + if (href != null && href != "about:blank") { + state.lastLoadedUrl = href + } + } catch (e: Exception) { + // Cross-origin restrictions might prevent access + } + } + + val errorCallback: (Event) -> Unit = { + state.loadingState = + HtmlLoadingState.Finished( + isError = true, + errorMessage = "Failed to load content", + ) + } + + iframe.addEventListener("load", loadCallback) + iframe.addEventListener("error", errorCallback) + + scope.launch { + navigator.handleNavigationEvents(iframe) + } + } + + when (val content = state.content) { + is HtmlContent.Url -> { + setUrlJs(iframe, content.url) + state.loadingState = HtmlLoadingState.Loading + } + + is HtmlContent.Data -> { + setHtmlContentJs(iframe, content.data) + state.loadingState = HtmlLoadingState.Loading + addContentIdentifierJs(iframe) + } + + is HtmlContent.Post -> { + // POST requests not directly supported in iframe + } + + HtmlContent.NavigatorOnly -> { + // No content update needed + } + } + + setStyleJs(iframe, "border", "none") + setStyleJs(iframe, "width", "100%") + setStyleJs(iframe, "height", "100%") + setStyleJs(iframe, "overflow", "auto") + } + + onDispose { + (root as Element).removeChild(componentInfo.container) + componentInfo.updater.dispose() + element.value?.let { onDispose(it) } + state.htmlElement = null + state.loadingState = HtmlLoadingState.Initializing + } + } + + SideEffect { + if (element.value != null) { + componentInfo.updater.update(componentInfo.component) + } + } +} + +/** + * Helper class to manage component information + */ +class ComponentInfo { + lateinit var container: Element + lateinit var component: T + lateinit var updater: Updater +} + +/** + * Helper class to manage focus switching + */ +class FocusSwitcher( + private val info: ComponentInfo, + private val focusManager: FocusManager, +) { + private val backwardRequester = FocusRequester() + private val forwardRequester = FocusRequester() + private var isRequesting = false + + fun moveBackward() { + try { + isRequesting = true + backwardRequester.requestFocus() + } finally { + isRequesting = false + } + focusManager.moveFocus(FocusDirection.Previous) + } + + fun moveForward() { + try { + isRequesting = true + forwardRequester.requestFocus() + } finally { + isRequesting = false + } + focusManager.moveFocus(FocusDirection.Next) + } + + @Composable + fun Content() { + Box( + Modifier + .focusRequester(backwardRequester) + .onFocusChanged { + if (it.isFocused && !isRequesting) { + focusManager.clearFocus(force = true) + val component = info.container.firstElementChild + if (component != null) { + requestFocus(component) + } else { + moveForward() + } + } + }.focusTarget(), + ) + Box( + Modifier + .focusRequester(forwardRequester) + .onFocusChanged { + if (it.isFocused && !isRequesting) { + focusManager.clearFocus(force = true) + + val component = info.container.lastElementChild + if (component != null) { + requestFocus(component) + } else { + moveBackward() + } + } + }.focusTarget(), + ) + } +} + +/** + * A utility class for updating a component's view in response to state changes + */ +class Updater( + private val component: T, + update: (T) -> Unit, +) { + private var isDisposed = false + + private val snapshotObserver = + SnapshotStateObserver { command -> + command() + } + + private val scheduleUpdate = { _: T -> + if (isDisposed.not()) { + performUpdate() + } + } + + var update: (T) -> Unit = update + set(value) { + if (field != value) { + field = value + performUpdate() + } + } + + private fun performUpdate() { + snapshotObserver.observeReads(component, scheduleUpdate) { + update(component) + } + } + + init { + snapshotObserver.start() + performUpdate() + } + + fun dispose() { + snapshotObserver.stop() + snapshotObserver.clear() + isDisposed = true + } +} + +/** + * Composable for displaying a URL in an HtmlView + */ +@Composable +fun HtmlViewUrl( + url: String, + modifier: Modifier = Modifier, + headers: Map = emptyMap(), + navigator: HtmlViewNavigator = rememberHtmlViewNavigator(), +) { + val state = rememberHtmlViewState() + + LaunchedEffect(url, headers) { + state.content = HtmlContent.Url(url, headers) + } + + HtmlView( + state = state, + modifier = modifier, + navigator = navigator, + onCreated = {}, + onDispose = {}, + ) +} + +/** + * Composable for displaying HTML content in an HtmlView + */ +@Composable +fun HtmlViewContent( + htmlContent: String, + modifier: Modifier = Modifier, + baseUrl: String? = null, + navigator: HtmlViewNavigator = rememberHtmlViewNavigator(), +) { + val state = rememberHtmlViewState() + + LaunchedEffect(htmlContent, baseUrl) { + state.content = HtmlContent.Data(htmlContent, baseUrl) + } + + HtmlView( + state = state, + modifier = modifier, + navigator = navigator, + onCreated = {}, + onDispose = {}, + ) +} + +/** + * Create and remember an HtmlViewState instance + */ +@Composable +fun rememberHtmlViewState(): HtmlViewState = remember { HtmlViewState() } + +// Container for HTML elements +val LocalLayerContainer = + staticCompositionLocalOf { + document.body!! + } diff --git a/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/JsInterop.kt b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/JsInterop.kt new file mode 100644 index 00000000..4b6f4ec6 --- /dev/null +++ b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/JsInterop.kt @@ -0,0 +1,197 @@ +package com.multiplatform.webview.web + +import org.w3c.dom.Element + +/** + * Check if navigation back is possible + */ +@JsFun( + "(element) => { try { return element.contentWindow && element.contentWindow.history && " + + "element.contentWindow.history.length > 1; } catch(e) { return false; } }", +) +external fun checkCanGoBackJs(element: Element): Boolean + +/** + * Check if navigation forward is possible (always returns false for iframes) + */ +@JsFun("(element) => { try { return false; } catch(e) { return false; } }") +external fun checkCanGoForwardJs(element: Element): Boolean + +/** + * Navigate back in browser history + */ +@JsFun( + "(element) => { try { if (element.contentWindow && element.contentWindow.history) { " + + "element.contentWindow.history.back(); } } catch(e) { console.error('Error going back:', e);" + + " } }", +) +external fun navigateBackJs(element: Element) + +/** + * Navigate forward in browser history + */ +@JsFun( + "(element) => { try { if (element.contentWindow && element.contentWindow.history) { " + + "element.contentWindow.history.forward(); } } catch(e) { " + + "console.error('Error going forward:', e); } }", +) +external fun navigateForwardJs(element: Element) + +/** + * Reload the current page + */ +@JsFun( + "(element) => { try { if (element.contentWindow && element.contentWindow.location) { " + + "element.contentWindow.location.reload(); } } catch(e) { " + + "console.error('Error reloading:', e); } }", +) +external fun reloadJs(element: Element) + +/** + * Stop loading the current page + */ +@JsFun( + "(element) => { try { if (element.contentWindow && element.contentWindow.stop) {" + + " element.contentWindow.stop(); } } catch(e) { " + + "console.error('Error stopping load:', e); } }", +) +external fun stopLoadingJs(element: Element) + +/** + * Set the URL of an iframe + */ +@JsFun( + "(element, url) => { try { element.src = url; } catch(e) { " + + "console.error('Error setting URL:', e); } }", +) +external fun setUrlJs( + element: Element, + url: String, +) + +/** + * Set HTML content of an iframe via srcdoc attribute + */ +@JsFun( + "(element, content) => { try { element.srcdoc = content; } catch(e) { " + + "console.error('Error setting HTML:', e); } }", +) +external fun setHtmlContentJs( + element: Element, + content: String, +) + +/** + * Evaluate JavaScript in the iframe context + */ +@JsFun( + "(element, script) => { try { return element.contentWindow && element.contentWindow.eval ?" + + " String(element.contentWindow.eval(script)) : ''; } catch(err) { " + + "return 'Error: ' + err.message; } }", +) +external fun evaluateScriptJs( + element: Element, + script: String, +): String + +/** + * Get the title from an iframe document + */ +@JsFun( + "(iframe) => { try { return iframe.contentDocument ? iframe.contentDocument.title : null; }" + + " catch(e) { return null; } }", +) +external fun getIframeTitleJs(iframe: Element): String? + +/** + * Get the current URL from an iframe + */ +@JsFun( + "(iframe) => { try { return iframe.contentWindow && iframe.contentWindow.location ? " + + "iframe.contentWindow.location.href : null; } catch(e) { return iframe.src || null; } }", +) +external fun getIframeUrlJs(iframe: Element): String? + +/** + * Set a style property on an element + */ +@JsFun("(element, property, value) => { element.style[property] = value; }") +external fun setStyleJs( + element: Element, + property: String, + value: String, +) + +/** + * Add a content identifier to an iframe for history tracking + */ +@JsFun( +""" +(iframe) => { + try { + if (iframe.contentWindow) { + const uniqueId = Math.random().toString(36).substring(2, 15); + iframe.contentWindow.history.replaceState({id: uniqueId}, '', + iframe.contentWindow.location.href); + } + } catch(e) { + console.error("Error adding content identifier:", e); + } +} +""", +) +external fun addContentIdentifierJs(iframe: Element) + +/** + * Log a message to the console + */ +@JsFun("(message) => { console.log(message); }") +external fun consoleLogJs(message: String) + +/** + * Log an info message to the console + */ +@JsFun("(message) => { console.info(message); }") +external fun consoleInfoJs(message: String) + +/** + * Log an error message to the console + */ +@JsFun("(message) => { console.error(message); }") +external fun consoleErrorJs(message: String) + +/** + * Position an element with coordinates + */ +@JsFun( + """(element, width, height, x, y) => { + element.style.width = width + 'px'; + element.style.height = height + 'px'; + element.style.left = x + 'px'; + element.style.top = y + 'px'; +}""", +) +external fun changeCoordinates( + element: Element, + width: Float, + height: Float, + x: Float, + y: Float, +) + +/** + * Initialize an element with basic styling + */ +@JsFun( + """(element) => { + element.style.position = 'absolute'; + element.style.margin = '0px'; +}""", +) +external fun initializingElement(element: Element) + +/** + * Request focus on an element + */ +@JsFun("(element) => { element.focus(); }") +external fun requestFocus(element: Element) diff --git a/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WasmJsWebView.kt b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WasmJsWebView.kt new file mode 100644 index 00000000..da5a9968 --- /dev/null +++ b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WasmJsWebView.kt @@ -0,0 +1,254 @@ +package com.multiplatform.webview.web + +import com.multiplatform.webview.jsbridge.WebViewJsBridge +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.InternalResourceApi +import org.w3c.dom.Element + +/** + * The native web view implementation for WasmJs platform. + * Uses HTML iframe element as the underlying implementation. + */ +actual class NativeWebView( + val element: Element, +) + +/** + * WebView adapter for WasmJs that implements the IWebView interface + */ +class WasmJsWebView( + private val element: Element, + override val webView: NativeWebView, + override val scope: CoroutineScope, + override val webViewJsBridge: WebViewJsBridge?, +) : IWebView { + override fun canGoBack(): Boolean = + try { + checkCanGoBackJs(element) + } catch (_: Exception) { + false + } + + override fun canGoForward(): Boolean = + try { + checkCanGoForwardJs(element) + } catch (_: Exception) { + false + } + + override fun loadUrl( + url: String, + additionalHttpHeaders: Map, + ) { + try { + setUrlJs(element, url) + if (webViewJsBridge != null) { + scope.launch { + delay(500) + injectJsBridge() + } + } + } catch (_: Exception) { + } + } + + override fun loadHtml( + html: String?, + baseUrl: String?, + mimeType: String?, + encoding: String?, + historyUrl: String?, + ) { + try { + if (html != null) { + val htmlWithBridge = + if (webViewJsBridge != null) { + injectBridgeIntoHtml(html, webViewJsBridge.jsBridgeName) + } else { + html + } + setHtmlContentJs(element, htmlWithBridge) + } + } catch (_: Exception) { + } + } + + @OptIn(InternalResourceApi::class) + override suspend fun loadHtmlFile( + fileName: String, + readType: WebViewFileReadType, + ) { + try { + val url = + when (readType) { + WebViewFileReadType.ASSET_RESOURCES -> "assets/$fileName" + WebViewFileReadType.COMPOSE_RESOURCE_FILES -> fileName + } + setUrlJs(element, url) + + if (webViewJsBridge != null) { + scope.launch { + delay(1000) + injectJsBridge() + } + } + } catch (e: Exception) { + val fallbackHtml = + """ + + + +

Failed to load file: $fileName

+

Error: ${e.message}

+ + + """.trimIndent() + loadHtml(fallbackHtml, null, null, null, null) + } + } + + override fun postUrl( + url: String, + postData: ByteArray, + ) { + loadUrl(url, emptyMap()) + } + + override fun goBack() { + try { + navigateBackJs(element) + } catch (_: Exception) { + } + } + + override fun goForward() { + try { + navigateForwardJs(element) + } catch (_: Exception) { + } + } + + override fun reload() { + try { + reloadJs(element) + } catch (_: Exception) { + } + } + + override fun stopLoading() { + try { + stopLoadingJs(element) + } catch (e: Exception) { + } + } + + override fun evaluateJavaScript( + script: String, + callback: ((String) -> Unit)?, + ) { + scope.launch { + try { + val result = evaluateScriptJs(element, script) + callback?.invoke(result) + } catch (e: Exception) { + callback?.invoke("Error: ${e.message}") + } + } + } + + override fun injectJsBridge() { + if (webViewJsBridge == null) return + super.injectJsBridge() + + val bridgeScript = createJsBridgeScript(webViewJsBridge.jsBridgeName, true) + evaluateJavaScript(bridgeScript) + + val messageHandler: (org.w3c.dom.events.Event) -> Unit = { event -> + val messageEvent = event as org.w3c.dom.MessageEvent + val iframe = element as? org.w3c.dom.HTMLIFrameElement + + if (iframe != null && + messageEvent.source == iframe.contentWindow && + messageEvent.data != null + ) { + try { + val dataString = messageEvent.data.toString() + + if (dataString.contains("kmpJsBridge")) { + val actionPattern = """action[=:][\s]*['"](.*?)['"]""".toRegex() + val paramsPattern = """params[=:][\s]*['"](.*?)['"]""".toRegex() + val callbackPattern = """callbackId[=:][\s]*(\d+)""".toRegex() + + val action = actionPattern.find(dataString)?.groupValues?.get(1) + val params = paramsPattern.find(dataString)?.groupValues?.get(1) ?: "{}" + val callbackId = + callbackPattern + .find(dataString) + ?.groupValues + ?.get(1) + ?.toIntOrNull() + ?: 0 + + if (action != null) { + val message = + com.multiplatform.webview.jsbridge.JsMessage( + callbackId = callbackId, + methodName = action, + params = params, + ) + webViewJsBridge.dispatch(message) + } + } + } catch (_: Exception) { + } + } + } + + kotlinx.browser.window.addEventListener("message", messageHandler) + webViewJsBridge.webView = this + } + + override fun initJsBridge(webViewJsBridge: WebViewJsBridge) { + // Bridge initialization is handled externally + } + + override fun saveState(): WebViewBundle? = null + + override fun scrollOffset(): Pair = Pair(0, 0) + + /** + * Inject JS bridge script into HTML content - improved version + */ + private fun injectBridgeIntoHtml( + htmlContent: String, + jsBridgeName: String, + ): String { + val bridgeScriptContent = createJsBridgeScript(jsBridgeName) + val bridgeScript = + """ + + """.trimIndent() + + if (htmlContent.contains("")) { + return htmlContent.replace("", "$bridgeScript") + } + + val headPattern = "]*>".toRegex() + val headMatch = headPattern.find(htmlContent) + if (headMatch != null) { + return htmlContent.replace(headMatch.value, "${headMatch.value}$bridgeScript") + } + + if (htmlContent.contains("") || htmlContent.contains("() + + var canGoBack by mutableStateOf(false) + internal set + + var canGoForward by mutableStateOf(false) + internal set + + /** + * Handle navigation events for the given HTML element + */ + internal suspend fun handleNavigationEvents(nativeElement: Element) { + navigationEvents.collect { event -> + when (event) { + is NavigationEvent.Back -> navigateBackJs(nativeElement) + is NavigationEvent.Forward -> navigateForwardJs(nativeElement) + is NavigationEvent.Reload -> reloadJs(nativeElement) + is NavigationEvent.LoadUrl -> { + setUrlJs(nativeElement, event.url) + } + is NavigationEvent.LoadHtml -> setHtmlContentJs(nativeElement, event.data) + is NavigationEvent.EvaluateJavaScript -> { + try { + val result = evaluateScriptJs(nativeElement, event.script) + event.callback?.invoke(result) + } catch (e: Exception) { + event.callback?.invoke("Error: ${e.message}") + } + } + is NavigationEvent.StopLoading -> stopLoadingJs(nativeElement) + } + updateNavigationState(nativeElement) + } + } + + fun navigateBack() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) } + } + + fun navigateForward() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) } + } + + fun reload() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) } + } + + fun stopLoading() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) } + } + + fun loadUrl( + url: String, + additionalHttpHeaders: Map = emptyMap(), + ) { + coroutineScope.launch { + navigationEvents.emit(NavigationEvent.LoadUrl(url, additionalHttpHeaders)) + } + } + + fun loadHtml( + data: String, + baseUrl: String? = null, + mimeType: String? = null, + encoding: String? = "utf-8", + historyUrl: String? = null, + ) { + coroutineScope.launch { + navigationEvents.emit( + NavigationEvent.LoadHtml( + data, + baseUrl, + mimeType, + encoding, + historyUrl, + ), + ) + } + } + + fun evaluateJavaScript( + script: String, + callback: ((String) -> Unit)? = null, + ) { + coroutineScope.launch { + navigationEvents.emit(NavigationEvent.EvaluateJavaScript(script, callback)) + } + } + + private fun updateNavigationState(nativeElement: Element) { + try { + canGoBack = checkCanGoBackJs(nativeElement) + canGoForward = checkCanGoForwardJs(nativeElement) + } catch (e: Exception) { + consoleErrorJs("Error updating navigation state: ${e.message}") + } + } + + private sealed class NavigationEvent { + data object Back : NavigationEvent() + + data object Forward : NavigationEvent() + + data object Reload : NavigationEvent() + + data object StopLoading : NavigationEvent() + + data class LoadUrl( + val url: String, + val additionalHttpHeaders: Map, + ) : NavigationEvent() + + data class LoadHtml( + val data: String, + val baseUrl: String?, + val mimeType: String?, + val encoding: String?, + val historyUrl: String?, + ) : NavigationEvent() + + data class EvaluateJavaScript( + val script: String, + val callback: ((String) -> Unit)?, + ) : NavigationEvent() + } +} + +/** + * Create and remember an HtmlViewNavigator instance + */ +@Composable +fun rememberHtmlViewNavigator(): HtmlViewNavigator { + val scope = rememberCoroutineScope() + return remember { HtmlViewNavigator(scope) } +} diff --git a/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WebView.wasmJs.kt b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WebView.wasmJs.kt new file mode 100644 index 00000000..f2ce7f24 --- /dev/null +++ b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WebView.wasmJs.kt @@ -0,0 +1,350 @@ +package com.multiplatform.webview.web + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.multiplatform.webview.jsbridge.WebViewJsBridge +import kotlinx.browser.document +import kotlinx.coroutines.launch +import org.w3c.dom.Element +import org.w3c.dom.HTMLIFrameElement + +/** + * Platform-specific parameters for the WebView factory in WebAssembly/JavaScript. + */ +actual class WebViewFactoryParam { + var container: Element = document.body!! + var existingElement: HTMLIFrameElement? = null +} + +/** + * Platform-specific parameters for the WebView composable in WebAssembly/JavaScript. + */ +actual class PlatformWebViewParams + +/** + * Default factory function for creating a WebView on the WebAssembly/JavaScript platform. + */ +actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView { + val iframe = param.existingElement ?: document.createElement("iframe") as HTMLIFrameElement + + iframe.style.apply { + border = "none" + width = "100%" + height = "100%" + } + + return NativeWebView(iframe) +} + +/** + * Factory function for creating a WebView with WebSettings applied. + */ +fun createWebViewWithSettings( + param: WebViewFactoryParam, + settings: com.multiplatform.webview.setting.WebSettings, +): NativeWebView { + val iframe = param.existingElement ?: document.createElement("iframe") as HTMLIFrameElement + val wasmSettings = settings.wasmJSWebSettings + + iframe.style.apply { + width = "100%" + height = "100%" + + border = if (wasmSettings.showBorder) wasmSettings.borderStyle else "none" + + val bgColor = (wasmSettings.backgroundColor ?: settings.backgroundColor) + if (bgColor != androidx.compose.ui.graphics.Color.Transparent) { + backgroundColor = "#${bgColor.value.toString(16).padStart(8, '0').substring(2)}" + } + + wasmSettings.customContainerStyle?.let { customStyle -> + cssText += "; $customStyle" + } + } + + if (wasmSettings.enableSandbox) { + iframe.setAttribute("sandbox", wasmSettings.sandboxPermissions) + } + + if (wasmSettings.allowFullscreen) { + iframe.setAttribute("allowfullscreen", "true") + } + + if (wasmSettings.enableConsoleLogging) { + consoleLogJs("WasmJS WebView created with settings") + } + + return NativeWebView(iframe) +} + +/** + * Simple state adapter for WebView state synchronization + */ +@Composable +internal fun rememberWebViewStateAdapter(commonWebViewState: WebViewState): WebViewStateAdapter = + remember(commonWebViewState) { WebViewStateAdapter(commonWebViewState) } + +internal class WebViewStateAdapter( + private val commonWebViewState: WebViewState, + private val wasmWebViewState: WasmJsWebViewState = WasmJsWebViewState(), +) { + fun syncFromCommon() { + when (val content = commonWebViewState.content) { + is WebContent.Url -> { + wasmWebViewState.url = content.url + wasmWebViewState.content = "" + } + is WebContent.Data -> { + wasmWebViewState.content = content.data + wasmWebViewState.url = "" + } + is WebContent.File -> { + wasmWebViewState.content = "" + wasmWebViewState.url = "" + } + is WebContent.Post -> { + wasmWebViewState.url = content.url + wasmWebViewState.content = "" + } + is WebContent.NavigatorOnly -> { + wasmWebViewState.url = "" + wasmWebViewState.content = "" + } + } + + commonWebViewState.lastLoadedUrl?.let { + wasmWebViewState.lastLoadedUrl = it + } + + commonWebViewState.pageTitle?.let { + wasmWebViewState.pageTitle = it + } + } + + fun syncToCommon() { + wasmWebViewState.lastLoadedUrl?.let { + commonWebViewState.lastLoadedUrl = it + } + + wasmWebViewState.pageTitle?.let { + commonWebViewState.pageTitle = it + } + + if (wasmWebViewState.isLoading) { + commonWebViewState.loadingState = LoadingState.Loading(0f) + } else { + commonWebViewState.loadingState = LoadingState.Finished + } + } + + fun getWasmWebViewState(): WasmJsWebViewState = wasmWebViewState +} + +/** + * Implementation of the WebView composable for the WebAssembly/JavaScript platform. + */ +@Composable +actual fun ActualWebView( + state: WebViewState, + modifier: Modifier, + captureBackPresses: Boolean, + navigator: WebViewNavigator, + webViewJsBridge: WebViewJsBridge?, + onCreated: (NativeWebView) -> Unit, + onDispose: (NativeWebView) -> Unit, + platformWebViewParams: PlatformWebViewParams?, + factory: (WebViewFactoryParam) -> NativeWebView, +) { + val scope = rememberCoroutineScope() + val stateAdapter = rememberWebViewStateAdapter(state) + val htmlNavigator = rememberHtmlViewNavigator() + val htmlViewState = remember { HtmlViewState() } + + LaunchedEffect(navigator, htmlNavigator) { + scope.launch { + while (true) { + kotlinx.coroutines.delay(100) + navigator.canGoBack = htmlNavigator.canGoBack + navigator.canGoForward = htmlNavigator.canGoForward + } + } + } + + LaunchedEffect(state.content) { + stateAdapter.syncFromCommon() + + when (state.content) { + is WebContent.Url -> { + htmlViewState.content = + HtmlContent.Url( + (state.content as WebContent.Url).url, + (state.content as WebContent.Url).additionalHttpHeaders, + ) + } + is WebContent.Data -> { + val data = (state.content as WebContent.Data).data + val htmlWithBridge = + if (webViewJsBridge != null) { + injectJsBridgeToHtml(data, webViewJsBridge.jsBridgeName) + } else { + data + } + + htmlViewState.content = + HtmlContent.Data( + htmlWithBridge, + (state.content as WebContent.Data).baseUrl, + ) + } + is WebContent.File -> { + val fileName = (state.content as WebContent.File).fileName + val fileReadType = (state.content as WebContent.File).readType + + val webView = state.webView + if (webView != null) { + webView.loadHtmlFile(fileName, fileReadType) + } else { + htmlViewState.loadingState = HtmlLoadingState.Loading + } + } + is WebContent.Post -> { + htmlViewState.content = + HtmlContent.Url( + (state.content as WebContent.Post).url, + ) + } + is WebContent.NavigatorOnly -> { + // No action needed + } + } + } + + LaunchedEffect(htmlViewState.lastLoadedUrl, htmlViewState.pageTitle, htmlViewState.loadingState) { + val wasmState = stateAdapter.getWasmWebViewState() + + htmlViewState.lastLoadedUrl?.let { wasmState.lastLoadedUrl = it } + htmlViewState.pageTitle?.let { wasmState.pageTitle = it } + + wasmState.isLoading = + when (htmlViewState.loadingState) { + is HtmlLoadingState.Loading -> true + is HtmlLoadingState.Finished -> false + else -> false + } + + stateAdapter.syncToCommon() + } + + HtmlView( + state = htmlViewState, + modifier = modifier, + navigator = htmlNavigator, + onCreated = { element -> + val nativeWebView = + if (state.webSettings.wasmJSWebSettings.let { + it.backgroundColor != null || + it.showBorder || + it.enableSandbox || + it.customContainerStyle != null || + it.enableConsoleLogging + } + ) { + createWebViewWithSettings( + WebViewFactoryParam().apply { + existingElement = element as? HTMLIFrameElement + }, + state.webSettings, + ) + } else { + factory( + WebViewFactoryParam().apply { + existingElement = element as? HTMLIFrameElement + }, + ) + } + + val webViewWrapper = + WasmJsWebView( + element = element, + webView = nativeWebView, + scope = scope, + webViewJsBridge = webViewJsBridge, + ) + + state.webView = webViewWrapper + + if (webViewJsBridge != null && element is HTMLIFrameElement) { + setupJsBridgeForWasm(element, webViewJsBridge, webViewWrapper) + } + + if (state.content is WebContent.File) { + val fileName = (state.content as WebContent.File).fileName + val readType = (state.content as WebContent.File).readType + scope.launch { + webViewWrapper.loadHtmlFile(fileName, readType) + } + } + + onCreated(nativeWebView) + }, + onDispose = { element -> + state.webView?.let { + onDispose(it.webView) + state.webView = null + } + }, + ) +} + +/** + * Set up the JavaScript bridge for WasmJS platform + */ +private fun setupJsBridgeForWasm( + element: HTMLIFrameElement, + webViewJsBridge: WebViewJsBridge, + webViewWrapper: WasmJsWebView, +) { + val messageHandler: (org.w3c.dom.events.Event) -> Unit = { event -> + val messageEvent = event as org.w3c.dom.MessageEvent + + if (messageEvent.source == element.contentWindow && messageEvent.data != null) { + try { + val dataString = messageEvent.data.toString() + + if (dataString.contains("kmpJsBridge") && dataString.startsWith("{")) { + val actionPattern = """"action"\s*:\s*"([^"]*)"""".toRegex() + val paramsPattern = """"params"\s*:\s*"((?:[^"\\]|\\.)*)"""".toRegex() + val callbackPattern = """"callbackId"\s*:\s*(\d+)""".toRegex() + + val actionMatch = actionPattern.find(dataString) + val paramsMatch = paramsPattern.find(dataString) + val callbackMatch = callbackPattern.find(dataString) + + if (actionMatch != null) { + val action = actionMatch.groupValues[1] + val rawParams = paramsMatch?.groupValues?.get(1) ?: "{}" + val params = rawParams.replace("\\\"", "\"").replace("\\\\", "\\") + val callbackId = callbackMatch?.groupValues?.get(1)?.toIntOrNull() ?: 0 + + val jsMessage = + com.multiplatform.webview.jsbridge.JsMessage( + callbackId = callbackId, + methodName = action, + params = params, + ) + + webViewJsBridge.dispatch(jsMessage) + } + } + } catch (e: Exception) { + consoleErrorJs("Error processing message: ${e.message}") + } + } + } + + kotlinx.browser.window.addEventListener("message", messageHandler) + webViewJsBridge.webView = webViewWrapper +} diff --git a/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WebViewBundle.kt b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WebViewBundle.kt new file mode 100644 index 00000000..07ceff5b --- /dev/null +++ b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WebViewBundle.kt @@ -0,0 +1,16 @@ +package com.multiplatform.webview.web + +/** + * Wasmjs implementation of WebViewBundle for storing WebView state. + */ +actual class WebViewBundle { + var history: List = emptyList() + var currentIndex: Int = -1 + + actual constructor() + + constructor(history: List, currentIndex: Int) { + this.history = history + this.currentIndex = currentIndex + } +} diff --git a/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WebViewJsBridge.kt b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WebViewJsBridge.kt new file mode 100644 index 00000000..c1fc6758 --- /dev/null +++ b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WebViewJsBridge.kt @@ -0,0 +1,121 @@ +package com.multiplatform.webview.web + +/** + * Creates JavaScript bridge code that can be used for communication between Kotlin and JavaScript + * @param jsBridgeName Name of the JavaScript bridge object in browser window + * @param isIife Whether to wrap the code in an Immediately Invoked Function Expression + * @return JavaScript code for bridge implementation + */ +internal fun createJsBridgeScript( + jsBridgeName: String, + isIife: Boolean = false, +): String { + val bridgeObjectCode = + """ + window.$jsBridgeName = { + _callbacks: {}, + _callbackId: 0, + + postMessage: function(methodName, params, callbackId) { + // Send as JSON string instead of object to ensure proper parsing + var messageData = JSON.stringify({ + type: 'kmpJsBridge', + action: methodName, + params: params, + callbackId: callbackId || 0 + }); + parent.postMessage(messageData, '*'); + }, + + onCallback: function(callbackId, message) { + var callback = this._callbacks[callbackId]; + if (callback) { + callback(message); + delete this._callbacks[callbackId]; + } + }, + + call: function(action, params, callback) { + var callbackId = 0; + if (callback) { + callbackId = ++this._callbackId; + this._callbacks[callbackId] = callback; + } + this.postMessage(action, params, callbackId); + return callbackId; + }, + + // Standard API as per documentation + callNative: function(methodName, params, callback) { + return this.call(methodName, params, callback); + } + }; + + // Listen for callback messages from parent + window.addEventListener('message', function(event) { + try { + var data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; + if (data && data.type === 'kmpJsBridgeCallback') { + window.$jsBridgeName.onCallback(data.callbackId, data.message); + } + } catch (e) { + console.error('Error processing callback message:', e); + } + }); + """.trimIndent() + + return if (isIife) { + """ + (function() { + $bridgeObjectCode + })(); + """.trimIndent() + } else { + bridgeObjectCode + } +} + +/** + * Helper function to inject JS bridge into HTML content + * + * @param htmlContent HTML content to inject the bridge into + * @param jsBridgeName Name of the bridge object in JavaScript + * @return HTML content with bridge injected + */ +fun injectJsBridgeToHtml( + htmlContent: String, + jsBridgeName: String, +): String { + // Only inject if it has a proper bridge implementation + // We look for specific bridge functions like callNative in the content + if (htmlContent.contains("window.$jsBridgeName") && + htmlContent.contains("$jsBridgeName.callNative") && + htmlContent.contains("$jsBridgeName._callbacks") + ) { + return htmlContent + } + + // Create bridge initialization script wrapped in script tags + val bridgeScriptContent = createJsBridgeScript(jsBridgeName) + val bridgeScript = + """ + + """.trimIndent() + + // Insert script before end of head tag + if (htmlContent.contains("")) { + return htmlContent.replace("", "$bridgeScript") + } + + // If no head tag, insert after opening body tag + if (htmlContent.contains("") || htmlContent.contains(" = emptyMap(), + ) : HtmlContent() + + /** HTML data content with optional parameters */ + data class Data( + val data: String, + val baseUrl: String? = null, + val mimeType: String? = null, + val encoding: String? = "utf-8", + val historyUrl: String? = null, + ) : HtmlContent() + + /** POST request content */ + data class Post( + val url: String, + val postData: ByteArray, + ) : HtmlContent() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as Post + + if (url != other.url) return false + if (!postData.contentEquals(other.postData)) return false + + return true + } + + override fun hashCode(): Int { + var result = url.hashCode() + result = 31 * result + postData.contentHashCode() + return result + } + } + + /** Navigation-only content (no rendering) */ + data object NavigatorOnly : HtmlContent() +} + +/** + * Loading states for HTML view + */ +sealed class HtmlLoadingState { + /** Initial state before any loading has started */ + data object Initializing : HtmlLoadingState() + + /** Content is currently being loaded */ + data object Loading : HtmlLoadingState() + + /** Loading has finished, with success or error flag */ + data class Finished( + val isError: Boolean = false, + val errorMessage: String? = null, + ) : HtmlLoadingState() +} + +/** + * State class for HtmlView component + */ +class HtmlViewState { + /** Native HTML element (iframe) */ + var htmlElement: Any? by mutableStateOf(null) + internal set + + /** Content to be displayed */ + var content: HtmlContent by mutableStateOf(HtmlContent.NavigatorOnly) + internal set + + /** Current loading state */ + var loadingState: HtmlLoadingState by mutableStateOf(HtmlLoadingState.Initializing) + internal set + + /** Last URL that was successfully loaded */ + var lastLoadedUrl: String? by mutableStateOf(null) + internal set + + /** Title of the current page */ + var pageTitle: String? by mutableStateOf(null) + internal set + + /** Error that occurred during loading */ + var error: Throwable? by mutableStateOf(null) + internal set +}