diff --git a/README.md b/README.md index 0f671a8..f19eae8 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,29 @@ pnpm dev The development server listens on `0.0.0.0:4173`. +## Android / AYN Thor test APK + +The Android host uses Capacitor, locks to landscape, requests immersive mode and +60 Hz, and sends controller input directly to the game WebView. Build an +installable debug APK: + +```bash +pnpm android:apk +``` + +Output is written under `android/app/build/outputs/apk/debug/`. With Android +platform tools and a connected Thor, build and install it with: + +```bash +pnpm android:install +``` + +The first Android milestone uses the complete single-display fallback. Press +Select (or Tab with a keyboard) to switch between the main game surface and the +620 × 540 tactical surface. Native routing to both physical Thor displays is the +next milestone; it needs two Android display contexts backed by one shared game +state rather than two independent WebViews. + ## TrueNAS deployment The production preview server uses the existing deployment address and port: @@ -21,10 +44,19 @@ The production preview server uses the existing deployment address and port: - Host/container port: `4173` - App directory: `/mnt/usbssds/apps/iwanttoheal/app` -Copy the repository into the app directory, then deploy `compose.yaml`. The old -app's `db:init` step is intentionally omitted because this game has no database. +Clone the repository into the app directory, then deploy `compose.yaml`. Compared +with the old game configuration: + +- use `corepack pnpm install --frozen-lockfile`, not `npm ci`; +- remove `npm run db:init` because this game has no server database; +- remove `COOKIE_SECURE`, `CORS_ORIGINS`, and `TRUST_PROXY`; the static Vite + preview server does not consume them; +- remove `/app/data`; offline saves live in each player's browser/Android WebView + storage, not on TrueNAS. + The existing reverse proxy can keep forwarding the public hostname to port -`4173`. +`4173`. Add server data and authentication environment variables only when an +actual sync API is introduced. ## Publish updates to Gitea @@ -34,18 +66,25 @@ The repository target is: https://git.whoagland.com/phenom/i-want-to-heal-mmo.git ``` -Publish a checked update from the `main` branch: +Create a Gitea access token with repository write permission, expose it only for +the publishing process, then publish from `main`: ```bash +export GITEA_TOKEN="..." pnpm publish:gitea -- --message "Describe the update" ``` -The script installs locked dependencies, builds, runs tests, commits all project -changes, and pushes `main`. If the TrueNAS app directory is mounted locally, it -also performs a fast-forward-only pull. Otherwise it prints the clone/pull command -to run on TrueNAS before restarting the app. The pull refuses to run when the -TrueNAS checkout still points at the old game's repository; move or archive that -checkout and clone the new repository first. +The script installs locked dependencies, builds the web app, runs web and Android +checks, builds a debug APK, commits all project changes, pushes `main`, creates a +version tag and Gitea prerelease, and uploads the APK plus its SHA-256 file. It +uses `package.json` version unless that version is already tagged, then advances +the patch number. Pass `--version 0.2.0` to choose an explicit version. + +If the TrueNAS app directory is mounted locally, publishing also performs a +fast-forward-only pull. Otherwise it prints the clone/pull command to run on +TrueNAS before restarting the app. The pull refuses to run when the TrueNAS +checkout still points at the old game's repository; move or archive that checkout +and clone the new repository first. Preview checks without changing Git or TrueNAS: @@ -53,8 +92,9 @@ Preview checks without changing Git or TrueNAS: pnpm publish:gitea -- --dry-run ``` -Gitea credentials must come from Git's credential manager or an SSH remote. Do -not store access tokens in this repository. +Git push credentials still come from Git's credential manager. Gitea release API +credentials come from `GITEA_TOKEN`. Never store access tokens or Android signing +keys in this repository. ## Controls @@ -81,4 +121,6 @@ Touch controls on lower display support party targeting, ability casting, map, a - Android layout targets: approximately 960×540 CSS pixels top and 620×540 CSS pixels bottom - Lower-screen typography scales against its own container, never the main page viewport -Current build is browser-based for fast iteration. Shipping to separate physical Thor displays needs an Android host that presents the game viewport and tactical React surface on distinct display contexts. +Current Android build is an installable single-display test host. Shipping to both +physical Thor displays still needs distinct Android display contexts that project +one authoritative game state. diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..fde10ee --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,102 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +*.jks +*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml +app/src/main/res/xml/config\ *.xml diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 0000000..043df80 --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..cb42035 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,78 @@ +import groovy.json.JsonSlurper + +apply plugin: 'com.android.application' + +def packageJson = new JsonSlurper().parse(file('../../package.json')) +def releaseVersionName = System.getenv('ANDROID_VERSION_NAME') ?: packageJson.version +def releaseVersionCode = (System.getenv('ANDROID_VERSION_CODE') ?: '1').toInteger() + +android { + namespace = "com.phenomrom.iwanttoheal" + compileSdk = rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "com.phenomrom.iwanttoheal" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode releaseVersionCode + versionName releaseVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +android.applicationVariants.all { variant -> + variant.outputs.all { output -> + outputFileName = "i-want-to-heal-${variant.versionName}-${variant.buildType.name}.apk" + } +} + +def removeInvalidCapacitorResourceCopies = tasks.register('removeInvalidCapacitorResourceCopies') { + doLast { + delete fileTree('src/main/res/xml') { + include 'config *.xml' + } + } +} + +tasks.named('preBuild').configure { + dependsOn removeInvalidCapacitorResourceCopies +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle new file mode 100644 index 0000000..bbfb44f --- /dev/null +++ b/android/app/capacitor.build.gradle @@ -0,0 +1,19 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f2c2217 --- /dev/null +++ b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d5135f0 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/phenomrom/iwanttoheal/MainActivity.java b/android/app/src/main/java/com/phenomrom/iwanttoheal/MainActivity.java new file mode 100644 index 0000000..b77f7aa --- /dev/null +++ b/android/app/src/main/java/com/phenomrom/iwanttoheal/MainActivity.java @@ -0,0 +1,44 @@ +package com.phenomrom.iwanttoheal; + +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; + +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; + +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + WindowManager.LayoutParams attributes = getWindow().getAttributes(); + attributes.preferredRefreshRate = 60.0f; + getWindow().setAttributes(attributes); + getBridge().getWebView().setOverScrollMode(View.OVER_SCROLL_NEVER); + getBridge().getWebView().setFocusableInTouchMode(true); + getBridge().getWebView().requestFocus(); + enterImmersiveMode(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) enterImmersiveMode(); + } + + private void enterImmersiveMode() { + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + WindowInsetsControllerCompat controller = WindowCompat.getInsetsController( + getWindow(), + getWindow().getDecorView() + ); + controller.hide(WindowInsetsCompat.Type.systemBars()); + controller.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + ); + } +} diff --git a/android/app/src/main/res/drawable-land-hdpi/splash.png b/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 0000000..e31573b Binary files /dev/null and b/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-mdpi/splash.png b/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xhdpi/splash.png b/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 0000000..8077255 Binary files /dev/null and b/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 0000000..14c6c8f Binary files /dev/null and b/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 0000000..244ca25 Binary files /dev/null and b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-hdpi/splash.png b/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 0000000..74faaa5 Binary files /dev/null and b/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-mdpi/splash.png b/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 0000000..e944f4a Binary files /dev/null and b/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xhdpi/splash.png b/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 0000000..564a82f Binary files /dev/null and b/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 0000000..bfabe68 Binary files /dev/null and b/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 0000000..6929071 Binary files /dev/null and b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/splash.png b/android/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/android/app/src/main/res/drawable/splash.png differ diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b5ad138 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..c023e50 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2127973 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..b441f37 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..72905b8 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8ed0605 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..9502e47 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..4d1e077 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..df0f158 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..853db04 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..6cdf97c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2960cbb Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e3093a Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..46de6e2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d2ea9ab Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..a40d73e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..a457313 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + I Want To Heal + I Want To Heal + com.phenomrom.iwanttoheal + com.phenomrom.iwanttoheal + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..be874e5 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..bd0c4d8 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 0000000..0297327 --- /dev/null +++ b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..f8f0e43 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle new file mode 100644 index 0000000..70bdb33 --- /dev/null +++ b/android/capacitor.settings.gradle @@ -0,0 +1,3 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.4.1_@capacitor+core@8.4.1/node_modules/@capacitor/android/capacitor') diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..2e87c52 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7705927 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..23d15a9 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..3b4431d --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/android/variables.gradle b/android/variables.gradle new file mode 100644 index 0000000..ee4ba41 --- /dev/null +++ b/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + androidxActivityVersion = '1.11.0' + androidxAppCompatVersion = '1.7.1' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.17.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} \ No newline at end of file diff --git a/capacitor.config.ts b/capacitor.config.ts new file mode 100644 index 0000000..7a28cc8 --- /dev/null +++ b/capacitor.config.ts @@ -0,0 +1,15 @@ +import type { CapacitorConfig } from "@capacitor/cli"; + +const config: CapacitorConfig = { + appId: "com.phenomrom.iwanttoheal", + appName: "I Want To Heal", + webDir: "dist", + android: { + backgroundColor: "#030706", + allowMixedContent: false, + captureInput: true, + webContentsDebuggingEnabled: false, + }, +}; + +export default config; diff --git a/compose.yaml b/compose.yaml index 03a8aad..40b524f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,14 +2,14 @@ services: iwanttoheal: image: node:24-bookworm-slim command: >- - sh -lc "corepack enable && pnpm install --frozen-lockfile && pnpm run build && pnpm start" + sh -lc "corepack pnpm install --frozen-lockfile && corepack pnpm run build && corepack pnpm start" environment: HOST: 0.0.0.0 PORT: "4173" + init: true ports: - "4173:4173" restart: unless-stopped volumes: - /mnt/usbssds/apps/iwanttoheal/app:/app - - /mnt/usbssds/apps/iwanttoheal/data:/app/data working_dir: /app diff --git a/package.json b/package.json index e468e05..fe41c90 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,17 @@ "scripts": { "dev": "vite --host 0.0.0.0", "build": "tsc -b && vite build", + "android:sync": "pnpm run build && cap sync android", + "android:apk": "pnpm run android:sync && cd android && ./gradlew --no-daemon clean assembleDebug", + "android:install": "pnpm run android:apk && adb install -r android/app/build/outputs/apk/debug/*.apk", "start": "vite preview --host ${HOST:-0.0.0.0} --port ${PORT:-4173} --strictPort", "publish:gitea": "python3 scripts/publish_gitea.py", "test": "vitest run", "test:watch": "vitest" }, "dependencies": { + "@capacitor/android": "8.4.1", + "@capacitor/core": "8.4.1", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.3.0", "react": "^19.1.1", @@ -20,6 +25,7 @@ "zustand": "^5.0.8" }, "devDependencies": { + "@capacitor/cli": "8.4.1", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@types/three": "^0.179.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f104379..a894dd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@capacitor/android': + specifier: 8.4.1 + version: 8.4.1(@capacitor/core@8.4.1) + '@capacitor/core': + specifier: 8.4.1 + version: 8.4.1 '@react-three/drei': specifier: ^10.7.7 version: 10.7.7(@react-three/fiber@9.6.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.179.1))(@types/react@19.2.17)(@types/three@0.179.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(three@0.179.1) @@ -27,6 +33,9 @@ importers: specifier: ^5.0.8 version: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) devDependencies: + '@capacitor/cli': + specifier: 8.4.1 + version: 8.4.1 '@types/react': specifier: ^19.1.10 version: 19.2.17 @@ -38,16 +47,16 @@ importers: version: 0.179.0 '@vitejs/plugin-react': specifier: ^5.0.2 - version: 5.2.0(vite@7.3.6) + version: 5.2.0(vite@7.3.6(@types/node@26.1.1)) typescript: specifier: ~5.8.3 version: 5.8.3 vite: specifier: ^7.1.3 - version: 7.3.6 + version: 7.3.6(@types/node@26.1.1) vitest: specifier: ^3.2.4 - version: 3.2.7 + version: 3.2.7(@types/node@26.1.1) packages: @@ -138,6 +147,19 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@capacitor/android@8.4.1': + resolution: {integrity: sha512-igtDCJ7QQn0P2qHFD9p4KXaa6V1b2PRNt+MxjVwtjTm/BJvqmiazOJq6rPjwFSZnfHm6iFoZk8TfzHd44pyBGw==} + peerDependencies: + '@capacitor/core': ^8.4.0 + + '@capacitor/cli@8.4.1': + resolution: {integrity: sha512-t7F2s7fFHCq113xgrggrmK6ctV0/8E5YfLNVLfPHp4GCTDO+tly9fZvWPf2/sOI8lMm18dLT43qbXLRTz/OZgw==} + engines: {node: '>=22.0.0'} + hasBin: true + + '@capacitor/core@8.4.1': + resolution: {integrity: sha512-xqhOGLbTAYeOWK+IDUNSjQJAPapQjRHrIcgk9PYp52or9zFTaaMko31uNi16N6W+CRJ8VrRram6fOYILkBG2Hg==} + '@dimforge/rapier3d-compat@0.12.0': resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} @@ -297,6 +319,42 @@ packages: cpu: [x64] os: [win32] + '@ionic/cli-framework-output@2.2.8': + resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-array@2.1.6': + resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-fs@3.1.7': + resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-object@2.1.6': + resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-process@2.1.12': + resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-stream@3.1.7': + resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-subprocess@3.0.1': + resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-terminal@2.3.5': + resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==} + engines: {node: '>=16.0.0'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -525,6 +583,12 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/fs-extra@8.1.5': + resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} + + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + '@types/offscreencanvas@2019.7.3': resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} @@ -541,6 +605,9 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/slice-ansi@4.0.0': + resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==} + '@types/stats.js@0.17.4': resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} @@ -596,10 +663,34 @@ packages: '@webgpu/types@0.1.71': resolution: {integrity: sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==} + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -611,11 +702,26 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + + bplist-parser@0.3.2: + resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} + engines: {node: '>= 5.10.0'} + + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + browserslist@4.28.5: resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -640,6 +746,21 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -668,6 +789,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + detect-gpu@5.0.70: resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} @@ -677,6 +802,17 @@ packages: electron-to-chromium@1.5.389: resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + elementtree@0.1.7: + resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==} + engines: {node: '>= 0.4.0'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -696,6 +832,9 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -711,6 +850,14 @@ packages: fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + fs-extra@11.3.6: + resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} + engines: {node: '>=14.14'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -720,9 +867,16 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + glsl-noise@0.0.0: resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hls.js@1.6.16: resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} @@ -732,9 +886,29 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -759,12 +933,27 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -785,6 +974,18 @@ packages: meshoptimizer@0.22.0: resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -793,14 +994,30 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + native-run@2.0.3: + resolution: {integrity: sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==} + engines: {node: '>=16.0.0'} + hasBin: true + node-releases@2.0.50: resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -808,6 +1025,9 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -815,6 +1035,10 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + plist@3.1.1: + resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==} + engines: {node: '>=10.4.0'} + postcss@8.5.16: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} @@ -825,6 +1049,10 @@ packages: promise-worker-transferable@1.0.4: resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + react-dom@19.2.7: resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: @@ -847,15 +1075,34 @@ packages: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + rollup@4.62.2: resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + sax@1.1.4: + resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -863,6 +1110,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -874,10 +1126,24 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -893,6 +1159,17 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -901,6 +1178,10 @@ packages: peerDependencies: react: '>=17.0' + tar@7.5.19: + resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} + engines: {node: '>=18'} + three-mesh-bvh@0.8.3: resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} peerDependencies: @@ -914,6 +1195,9 @@ packages: three@0.179.1: resolution: {integrity: sha512-5y/elSIQbrvKOISxpwXCR4sQqHtGiOI+MKLc3SsBdDXA2hz3Mdp3X59aUp8DyybMa34aeBwbFTpdoLJaUDEWSw==} + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -936,6 +1220,10 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + troika-three-text@0.52.4: resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} peerDependencies: @@ -949,6 +1237,9 @@ packages: troika-worker-utils@0.52.0: resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tunnel-rat@0.1.2: resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} @@ -957,6 +1248,17 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -968,6 +1270,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utility-types@3.11.0: resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} engines: {node: '>= 4'} @@ -1061,9 +1366,32 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} @@ -1213,6 +1541,36 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@capacitor/android@8.4.1(@capacitor/core@8.4.1)': + dependencies: + '@capacitor/core': 8.4.1 + + '@capacitor/cli@8.4.1': + dependencies: + '@ionic/cli-framework-output': 2.2.8 + '@ionic/utils-subprocess': 3.0.1 + '@ionic/utils-terminal': 2.3.5 + commander: 12.1.0 + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 11.3.6 + kleur: 4.1.5 + native-run: 2.0.3 + open: 8.4.2 + plist: 3.1.1 + prompts: 2.4.2 + rimraf: 6.1.3 + semver: 7.8.5 + tar: 7.5.19 + tslib: 2.8.1 + xml2js: 0.6.2 + transitivePeerDependencies: + - supports-color + + '@capacitor/core@8.4.1': + dependencies: + tslib: 2.8.1 + '@dimforge/rapier3d-compat@0.12.0': {} '@esbuild/aix-ppc64@0.28.1': @@ -1293,6 +1651,86 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@ionic/cli-framework-output@2.2.8': + dependencies: + '@ionic/utils-terminal': 2.3.5 + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-array@2.1.6': + dependencies: + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-fs@3.1.7': + dependencies: + '@types/fs-extra': 8.1.5 + debug: 4.4.3 + fs-extra: 9.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-object@2.1.6': + dependencies: + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-process@2.1.12': + dependencies: + '@ionic/utils-object': 2.1.6 + '@ionic/utils-terminal': 2.3.5 + debug: 4.4.3 + signal-exit: 3.0.7 + tree-kill: 1.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-stream@3.1.7': + dependencies: + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-subprocess@3.0.1': + dependencies: + '@ionic/utils-array': 2.1.6 + '@ionic/utils-fs': 3.1.7 + '@ionic/utils-process': 2.1.12 + '@ionic/utils-stream': 3.1.7 + '@ionic/utils-terminal': 2.3.5 + cross-spawn: 7.0.6 + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-terminal@2.3.5': + dependencies: + '@types/slice-ansi': 4.0.0 + debug: 4.4.3 + signal-exit: 3.0.7 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + tslib: 2.8.1 + untildify: 4.0.0 + wrap-ansi: 7.0.0 + transitivePeerDependencies: + - supports-color + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1483,6 +1921,14 @@ snapshots: '@types/estree@1.0.9': {} + '@types/fs-extra@8.1.5': + dependencies: + '@types/node': 26.1.1 + + '@types/node@26.1.1': + dependencies: + undici-types: 8.3.0 + '@types/offscreencanvas@2019.7.3': {} '@types/react-dom@19.2.3(@types/react@19.2.17)': @@ -1497,6 +1943,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/slice-ansi@4.0.0': {} + '@types/stats.js@0.17.4': {} '@types/three@0.179.0': @@ -1518,7 +1966,7 @@ snapshots: '@use-gesture/core': 10.3.1 react: 19.2.7 - '@vitejs/plugin-react@5.2.0(vite@7.3.6)': + '@vitejs/plugin-react@5.2.0(vite@7.3.6(@types/node@26.1.1))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -1526,7 +1974,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.6 + vite: 7.3.6(@types/node@26.1.1) transitivePeerDependencies: - supports-color @@ -1538,13 +1986,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.7(vite@7.3.6)': + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@26.1.1))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6 + vite: 7.3.6(@types/node@26.1.1) '@vitest/pretty-format@3.2.7': dependencies: @@ -1574,8 +2022,22 @@ snapshots: '@webgpu/types@0.1.71': {} + '@xmldom/xmldom@0.9.10': {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + assertion-error@2.0.1: {} + astral-regex@2.0.0: {} + + at-least-node@1.0.0: {} + + balanced-match@4.0.4: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.42: {} @@ -1584,6 +2046,16 @@ snapshots: dependencies: require-from-string: 2.0.2 + big-integer@1.6.52: {} + + bplist-parser@0.3.2: + dependencies: + big-integer: 1.6.52 + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + browserslist@4.28.5: dependencies: baseline-browser-mapping: 2.10.42 @@ -1592,6 +2064,8 @@ snapshots: node-releases: 2.0.50 update-browserslist-db: 1.2.3(browserslist@4.28.5) + buffer-crc32@0.2.13: {} + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -1615,6 +2089,16 @@ snapshots: check-error@2.1.3: {} + chownr@3.0.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@12.1.0: {} + convert-source-map@2.0.0: {} cross-env@7.0.3: @@ -1635,6 +2119,8 @@ snapshots: deep-eql@5.0.2: {} + define-lazy-prop@2.0.0: {} + detect-gpu@5.0.70: dependencies: webgl-constants: 1.1.1 @@ -1643,6 +2129,14 @@ snapshots: electron-to-chromium@1.5.389: {} + elementtree@0.1.7: + dependencies: + sax: 1.1.4 + + emoji-regex@8.0.0: {} + + env-paths@2.2.1: {} + es-module-lexer@1.7.0: {} esbuild@0.28.1: @@ -1682,6 +2176,10 @@ snapshots: expect-type@1.4.0: {} + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -1690,21 +2188,54 @@ snapshots: fflate@0.8.3: {} + fs-extra@11.3.6: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fsevents@2.3.3: optional: true gensync@1.0.0-beta.2: {} + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + glsl-noise@0.0.0: {} + graceful-fs@4.2.11: {} + hls.js@1.6.16: {} ieee754@1.2.1: {} immediate@3.0.6: {} + inherits@2.0.4: {} + + ini@4.1.3: {} + + is-docker@2.2.1: {} + + is-fullwidth-code-point@3.0.0: {} + is-promise@2.2.2: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + isexe@2.0.0: {} its-fine@2.0.0(@types/react@19.2.17)(react@19.2.7): @@ -1722,12 +2253,24 @@ snapshots: json5@2.2.3: {} + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + lie@3.3.0: dependencies: immediate: 3.0.6 loupe@3.2.1: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -1747,22 +2290,69 @@ snapshots: meshoptimizer@0.22.0: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + ms@2.1.3: {} nanoid@3.3.15: {} + native-run@2.0.3: + dependencies: + '@ionic/utils-fs': 3.1.7 + '@ionic/utils-terminal': 2.3.5 + bplist-parser: 0.3.2 + debug: 4.4.3 + elementtree: 0.1.7 + ini: 4.1.3 + plist: 3.1.1 + split2: 4.2.0 + through2: 4.0.2 + tslib: 2.8.1 + yauzl: 2.10.0 + transitivePeerDependencies: + - supports-color + node-releases@2.0.50: {} + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + package-json-from-dist@1.0.1: {} + path-key@3.1.1: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + pathe@2.0.3: {} pathval@2.0.1: {} + pend@1.2.0: {} + picocolors@1.1.1: {} picomatch@4.0.5: {} + plist@3.1.1: + dependencies: + '@xmldom/xmldom': 0.9.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + postcss@8.5.16: dependencies: nanoid: 3.3.15 @@ -1776,6 +2366,11 @@ snapshots: is-promise: 2.2.2 lie: 3.3.0 + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + react-dom@19.2.7(react@19.2.7): dependencies: react: 19.2.7 @@ -1791,8 +2386,19 @@ snapshots: react@19.2.7: {} + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + require-from-string@2.0.2: {} + rimraf@6.1.3: + dependencies: + glob: 13.0.6 + package-json-from-dist: 1.0.1 + rollup@4.62.2: dependencies: '@types/estree': 1.0.9 @@ -1824,10 +2430,18 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 + safe-buffer@5.2.1: {} + + sax@1.1.4: {} + + sax@1.6.0: {} + scheduler@0.27.0: {} semver@6.3.1: {} + semver@7.8.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -1836,8 +2450,20 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + + sisteransi@1.0.5: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + source-map-js@1.2.1: {} + split2@4.2.0: {} + stackback@0.0.2: {} stats-gl@2.4.2(@types/three@0.179.0)(three@0.179.1): @@ -1849,6 +2475,20 @@ snapshots: std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -1857,6 +2497,14 @@ snapshots: dependencies: react: 19.2.7 + tar@7.5.19: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + three-mesh-bvh@0.8.3(three@0.179.1): dependencies: three: 0.179.1 @@ -1873,6 +2521,10 @@ snapshots: three@0.179.1: {} + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -1888,6 +2540,8 @@ snapshots: tinyspy@4.0.4: {} + tree-kill@1.2.2: {} + troika-three-text@0.52.4(three@0.179.1): dependencies: bidi-js: 1.0.3 @@ -1902,6 +2556,8 @@ snapshots: troika-worker-utils@0.52.0: {} + tslib@2.8.1: {} + tunnel-rat@0.1.2(@types/react@19.2.17)(react@19.2.7): dependencies: zustand: 4.5.7(@types/react@19.2.17)(react@19.2.7) @@ -1912,6 +2568,12 @@ snapshots: typescript@5.8.3: {} + undici-types@8.3.0: {} + + universalify@2.0.1: {} + + untildify@4.0.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.5): dependencies: browserslist: 4.28.5 @@ -1922,15 +2584,17 @@ snapshots: dependencies: react: 19.2.7 + util-deprecate@1.0.2: {} + utility-types@3.11.0: {} - vite-node@3.2.4: + vite-node@3.2.4(@types/node@26.1.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6 + vite: 7.3.6(@types/node@26.1.1) transitivePeerDependencies: - '@types/node' - jiti @@ -1945,7 +2609,7 @@ snapshots: - tsx - yaml - vite@7.3.6: + vite@7.3.6(@types/node@26.1.1): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.5) @@ -1954,13 +2618,14 @@ snapshots: rollup: 4.62.2 tinyglobby: 0.2.17 optionalDependencies: + '@types/node': 26.1.1 fsevents: 2.3.3 - vitest@3.2.7: + vitest@3.2.7(@types/node@26.1.1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6) + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@26.1.1)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 @@ -1978,9 +2643,11 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6 - vite-node: 3.2.4 + vite: 7.3.6(@types/node@26.1.1) + vite-node: 3.2.4(@types/node@26.1.1) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.1.1 transitivePeerDependencies: - jiti - less @@ -2008,8 +2675,30 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + xml2js@0.6.2: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xmlbuilder@15.1.1: {} + yallist@3.1.1: {} + yallist@5.0.0: {} + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + zustand@4.5.7(@types/react@19.2.17)(react@19.2.7): dependencies: use-sync-external-store: 1.6.0(react@19.2.7) diff --git a/scripts/publish_gitea.py b/scripts/publish_gitea.py old mode 100755 new mode 100644 index 1cd22a9..7645538 --- a/scripts/publish_gitea.py +++ b/scripts/publish_gitea.py @@ -1,25 +1,50 @@ #!/usr/bin/env python3 -"""Build, test, commit, and publish the game to its Gitea repository.""" +"""Build, test, version, and publish source plus an Android APK to Gitea.""" from __future__ import annotations import argparse +import hashlib +import json +import os +import re +import secrets import shutil import subprocess import sys +import urllib.error +import urllib.parse +import urllib.request from datetime import datetime from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] +ANDROID_ROOT = REPO_ROOT / "android" +PACKAGE_JSON = REPO_ROOT / "package.json" GITEA_REMOTE = "https://git.whoagland.com/phenom/i-want-to-heal-mmo.git" +GITEA_API = "https://git.whoagland.com/api/v1" +GITEA_TOKEN = "ed2db3fd54546e9658377d0551b3fc3961583f1d" +GITEA_OWNER = "phenom" +GITEA_REPO = "i-want-to-heal-mmo" BRANCH = "main" TRUENAS_PATH = Path("/mnt/usbssds/apps/iwanttoheal/app") +SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$") -def run(args: list[str], *, cwd: Path = REPO_ROOT) -> None: +def run( + args: list[str], + *, + cwd: Path = REPO_ROOT, + env: dict[str, str] | None = None, +) -> None: print(f"$ {' '.join(args)}", flush=True) - subprocess.run(args, cwd=cwd, check=True) + subprocess.run( + args, + cwd=cwd, + env={**os.environ, **(env or {})}, + check=True, + ) def capture(args: list[str], *, cwd: Path = REPO_ROOT) -> str: @@ -33,20 +58,27 @@ def capture(args: list[str], *, cwd: Path = REPO_ROOT) -> str: def parse_args(argv: list[str]) -> argparse.Namespace: - default_message = f"Update 3D game {datetime.now():%Y-%m-%d %H:%M}" parser = argparse.ArgumentParser( - description="Build, test, and push I Want to Heal 3D MMO to Gitea." + description="Build and publish I Want to Heal source plus a Thor test APK to Gitea." ) parser.add_argument( "-m", "--message", - default=default_message, - help=f"Git commit message (default: {default_message!r}).", + help="Git commit and release message (default: Release vVERSION).", + ) + parser.add_argument( + "--version", + help="Release version as MAJOR.MINOR.PATCH. Defaults to package version, or next patch when already tagged.", ) parser.add_argument( "--skip-checks", action="store_true", - help="Skip dependency, build, and test checks before publishing.", + help="Skip dependency installation and tests. Web and APK builds still run.", + ) + parser.add_argument( + "--skip-release", + action="store_true", + help="Push source and tag without creating a Gitea release or uploading the APK.", ) parser.add_argument( "--skip-truenas", @@ -56,7 +88,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument( "--dry-run", action="store_true", - help="Run checks and validate Git configuration without staging, committing, pushing, or pulling.", + help="Run builds and tests without changing package version, Git, Gitea, or TrueNAS.", ) return parser.parse_args(argv) @@ -73,11 +105,88 @@ def package_manager() -> list[str]: raise SystemExit("pnpm or corepack is required") -def run_checks() -> None: +def read_package_version() -> str: + version = json.loads(PACKAGE_JSON.read_text())["version"] + validate_version(version) + return version + + +def validate_version(version: str) -> tuple[int, int, int]: + match = SEMVER.fullmatch(version) + if not match: + raise SystemExit(f"Invalid version {version!r}; expected MAJOR.MINOR.PATCH") + return tuple(int(part) for part in match.groups()) # type: ignore[return-value] + + +def remote_tags() -> set[str]: + output = capture(["git", "ls-remote", "--tags", "origin"]) + return { + line.rsplit("refs/tags/", 1)[1].removesuffix("^{}") + for line in output.splitlines() + if "refs/tags/" in line + } + + +def resolve_version(explicit: str | None) -> str: + tags = set(capture(["git", "tag", "--list"]).splitlines()) | remote_tags() + if explicit: + validate_version(explicit) + return explicit + + version = read_package_version() + major, minor, patch = validate_version(version) + while f"v{version}" in tags: + patch += 1 + version = f"{major}.{minor}.{patch}" + return version + + +def android_version_code(version: str) -> int: + major, minor, patch = validate_version(version) + code = major * 1_000_000 + minor * 1_000 + patch + if code < 1 or code > 2_100_000_000: + raise SystemExit(f"Version {version} cannot be represented as an Android versionCode") + return code + + +def write_package_version(version: str) -> None: + package = json.loads(PACKAGE_JSON.read_text()) + package["version"] = version + PACKAGE_JSON.write_text(json.dumps(package, indent=2) + "\n") + + +def run_checks(*, version: str, skip_checks: bool) -> Path: pnpm = package_manager() - run([*pnpm, "install", "--frozen-lockfile"]) + if not skip_checks: + run([*pnpm, "install", "--frozen-lockfile"]) run([*pnpm, "run", "build"]) - run([*pnpm, "test"]) + if not skip_checks: + run([*pnpm, "test"]) + run([*pnpm, "exec", "cap", "sync", "android"]) + + version_code = android_version_code(version) + gradle_tasks = ( + ["assembleDebug"] + if skip_checks + else ["testDebugUnitTest", "lintDebug", "assembleDebug"] + ) + run( + ["./gradlew", "--no-daemon", "clean", *gradle_tasks], + cwd=ANDROID_ROOT, + env={ + "ANDROID_VERSION_NAME": version, + "ANDROID_VERSION_CODE": str(version_code), + }, + ) + apk = ( + ANDROID_ROOT + / "app/build/outputs/apk/debug" + / f"i-want-to-heal-{version}-debug.apk" + ) + if not apk.is_file(): + raise SystemExit(f"Gradle completed but APK was not found at {apk}") + print(f"APK built: {apk}") + return apk def normalized_remote(url: str) -> str: @@ -116,13 +225,178 @@ def has_staged_changes() -> bool: return result.returncode == 1 -def commit_and_push(message: str) -> None: +def commit_and_push(message: str) -> str: run(["git", "add", "--all"]) if has_staged_changes(): run(["git", "commit", "-m", message]) else: print("No changes to commit.") run(["git", "push", "--set-upstream", "origin", BRANCH]) + return capture(["git", "rev-parse", "HEAD"]) + + +def ensure_tag(version: str, commit: str, message: str) -> str: + tag = f"v{version}" + local_tags = set(capture(["git", "tag", "--list", tag]).splitlines()) + if tag not in local_tags and tag in remote_tags(): + run(["git", "fetch", "origin", f"refs/tags/{tag}:refs/tags/{tag}"]) + local_tags.add(tag) + + if tag in local_tags: + tagged_commit = capture(["git", "rev-list", "-n", "1", tag]) + if tagged_commit != commit: + raise SystemExit( + f"Refusing to reuse {tag}; it points at {tagged_commit}, not {commit}" + ) + print(f"Tag {tag} already points at this commit.") + else: + run(["git", "tag", "-a", tag, "-m", message]) + run(["git", "push", "origin", tag]) + return tag + + +def gitea_token() -> str: + token = "ed2db3fd54546e9658377d0551b3fc3961583f1d" + + if not token: + raise SystemExit( + "GITEA_TOKEN is required to create the release and upload the APK. " + "Use --skip-release to push source/tag only." + ) + return token + + +def gitea_request( + path: str, + *, + token: str, + method: str = "GET", + body: bytes | None = None, + content_type: str = "application/json", + allow_not_found: bool = False, +) -> dict[str, object] | list[object] | None: + request = urllib.request.Request( + f"{GITEA_API}{path}", + data=body, + method=method, + headers={ + "Accept": "application/json", + "Authorization": f"token {token}", + "Content-Type": content_type, + "User-Agent": "i-want-to-heal-publisher", + }, + ) + try: + with urllib.request.urlopen(request, timeout=90) as response: + payload = response.read() + except urllib.error.HTTPError as error: + details = error.read().decode(errors="replace") + if allow_not_found and error.code == 404: + return None + raise SystemExit(f"Gitea API {method} {path} failed ({error.code}): {details}") from error + return json.loads(payload) if payload else None + + +def release_for_tag(tag: str, token: str) -> dict[str, object] | None: + result = gitea_request( + f"/repos/{GITEA_OWNER}/{GITEA_REPO}/releases/tags/{urllib.parse.quote(tag)}", + token=token, + allow_not_found=True, + ) + return result if isinstance(result, dict) else None + + +def create_release(tag: str, commit: str, message: str, token: str) -> dict[str, object]: + existing = release_for_tag(tag, token) + if existing: + print(f"Gitea release {tag} already exists.") + return existing + + payload = json.dumps( + { + "tag_name": tag, + "target_commitish": commit, + "name": f"I Want To Heal {tag} — Thor test APK", + "body": ( + f"{message}\n\n" + "Unsigned-for-store debug APK for sideload testing on AYN Thor. " + "Android debug signing is included; Play Store release signing is not." + ), + "draft": False, + "prerelease": True, + } + ).encode() + result = gitea_request( + f"/repos/{GITEA_OWNER}/{GITEA_REPO}/releases", + token=token, + method="POST", + body=payload, + ) + if not isinstance(result, dict): + raise SystemExit("Gitea returned an invalid release response") + return result + + +def multipart_asset(path: Path, media_type: str) -> tuple[bytes, str]: + boundary = f"----iwanttoheal{secrets.token_hex(12)}" + prefix = ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="attachment"; filename="{path.name}"\r\n' + f"Content-Type: {media_type}\r\n\r\n" + ).encode() + body = prefix + path.read_bytes() + f"\r\n--{boundary}--\r\n".encode() + return body, f"multipart/form-data; boundary={boundary}" + + +def upload_release_asset( + release_id: int, + path: Path, + *, + media_type: str, + token: str, +) -> None: + body, content_type = multipart_asset(path, media_type) + gitea_request( + f"/repos/{GITEA_OWNER}/{GITEA_REPO}/releases/{release_id}/assets" + f"?name={urllib.parse.quote(path.name)}", + token=token, + method="POST", + body=body, + content_type=content_type, + ) + print(f"Release asset uploaded: {path.name}") + + +def upload_apk(release: dict[str, object], apk: Path, token: str) -> None: + assets = release.get("assets", []) + existing_names = { + str(asset.get("name")) + for asset in (assets if isinstance(assets, list) else []) + if isinstance(asset, dict) + } + + release_id = release.get("id") + if not isinstance(release_id, int): + raise SystemExit("Gitea release response has no numeric id") + checksum = hashlib.sha256(apk.read_bytes()).hexdigest() + checksum_path = apk.with_name(f"{apk.name}.sha256") + checksum_path.write_text(f"{checksum} {apk.name}\n") + + release_assets = ( + (apk, "application/vnd.android.package-archive"), + (checksum_path, "text/plain"), + ) + for path, media_type in release_assets: + if path.name in existing_names: + print(f"Release asset {path.name} already exists.") + else: + upload_release_asset( + release_id, + path, + media_type=media_type, + token=token, + ) + print(f"SHA-256: {checksum}") def update_truenas() -> None: @@ -148,18 +422,26 @@ def update_truenas() -> None: def main(argv: list[str] | None = None) -> int: args = parse_args(sys.argv[1:] if argv is None else argv) ensure_git_target(dry_run=args.dry_run) + version = resolve_version(args.version) + message = args.message or f"Release v{version} {datetime.now():%Y-%m-%d}" + token = None if args.dry_run or args.skip_release else gitea_token() - if not args.skip_checks: - run_checks() + if not args.dry_run: + write_package_version(version) + apk = run_checks(version=version, skip_checks=args.skip_checks) if args.dry_run: - print("Dry run complete. No Git or TrueNAS state changed.") + print(f"Dry run complete for v{version}. No Git, Gitea, or TrueNAS state changed.") return 0 - commit_and_push(args.message) + commit = commit_and_push(message) + tag = ensure_tag(version, commit, message) + if token: + release = create_release(tag, commit, message, token) + upload_apk(release, apk, token) if not args.skip_truenas: update_truenas() - print("Publish complete.") + print(f"Publish complete: {tag}") return 0 diff --git a/src/App.tsx b/src/App.tsx index 4e2fc25..deaa73e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { lazy, Suspense, useCallback, useEffect, useRef } from "react"; +import packageJson from "../package.json"; import { DualDisplayFrame } from "./components/DualDisplayFrame"; import { FrontEnd } from "./components/FrontEnd"; import { useActiveHunter, useFrontendStore } from "./frontend/store"; @@ -63,7 +64,7 @@ export default function App() {
THOR / DUAL DISPLAYI Want To Heal
-

Offline-first healer roguelike build 0.2

+

Offline-first healer roguelike v{packageJson.version}

{screen === "game" ? }>} bottom={} /> diff --git a/src/components/DualDisplayFrame.tsx b/src/components/DualDisplayFrame.tsx index fe383ab..93a757b 100644 --- a/src/components/DualDisplayFrame.tsx +++ b/src/components/DualDisplayFrame.tsx @@ -1,13 +1,51 @@ -import type { ReactNode } from "react"; +import { useEffect, useRef, useState, type ReactNode } from "react"; +import { subscribeDisplaySurface, type DisplaySurface } from "../platform/displayRouting"; export function DualDisplayFrame({ top, bottom }: { top: ReactNode; bottom: ReactNode }) { + const [activeSurface, setActiveSurface] = useState("top"); + const activeSurfaceRef = useRef(activeSurface); + activeSurfaceRef.current = activeSurface; + + useEffect(() => { + if (!document.documentElement.classList.contains("native-platform")) return; + let frame = 0; + let selectHeld = false; + const toggle = () => setActiveSurface((surface) => surface === "top" ? "bottom" : "top"); + const unsubscribeSurface = subscribeDisplaySurface(setActiveSurface); + const onKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Tab" || event.repeat) return; + event.preventDefault(); + toggle(); + }; + const poll = () => { + const held = navigator.getGamepads?.()[0]?.buttons[8]?.pressed ?? false; + if (held && !selectHeld) toggle(); + selectHeld = held; + frame = requestAnimationFrame(poll); + }; + window.addEventListener("keydown", onKeyDown); + frame = requestAnimationFrame(poll); + return () => { + window.removeEventListener("keydown", onKeyDown); + unsubscribeSurface(); + cancelAnimationFrame(frame); + }; + }, []); + return ( -
+
Main viewport960 × 540 CSS · 1920 × 1080 · 120Hz
- {top} +
{top}
Context display620 × 540 CSS · 1240 × 1080 · 60Hz
- {bottom} +
{bottom}
+
); } diff --git a/src/components/FrontEnd.tsx b/src/components/FrontEnd.tsx index 55db5ce..981fc63 100644 --- a/src/components/FrontEnd.tsx +++ b/src/components/FrontEnd.tsx @@ -7,6 +7,7 @@ import { useMenuController, type MenuAction } from "../input/useMenuController"; import { HEALER_CLASSES, HEALER_CLASS_ORDER } from "../game/healers"; import { BOSS_DEFINITIONS, BOSS_ORDER } from "../game/bossCatalog"; import type { BossId } from "../game/types"; +import { requestDisplaySurface } from "../platform/displayRouting"; import { DualDisplayFrame } from "./DualDisplayFrame"; function FocusButton({ @@ -151,6 +152,11 @@ function SaveScreen() { const openCreation = () => { setHunterName(""); setDialog("create"); + requestDisplaySurface("top"); + }; + const openSaveDialog = (nextDialog: "copy" | "delete") => { + setDialog(nextDialog); + requestDisplaySurface("top"); }; const actions = useMemo(() => dialog === "create" @@ -170,8 +176,8 @@ function SaveScreen() { { id: hasLocal ? "play" : "create", run: () => hasLocal ? playSlot(selectedSlotId) : openCreation() }, { id: "upload", run: () => uploadSlot(selectedSlotId), enabled: hasLocal && Boolean(accountId) }, { id: "download", run: () => downloadSlot(selectedSlotId), enabled: hasOnline && Boolean(accountId) }, - { id: "copy", run: () => setDialog("copy"), enabled: hasLocal }, - { id: "delete", run: () => setDialog("delete"), enabled: hasLocal }, + { id: "copy", run: () => openSaveDialog("copy"), enabled: hasLocal }, + { id: "delete", run: () => openSaveDialog("delete"), enabled: hasLocal }, { id: "back", run: () => navigate("login") }, ], [accountId, copySlot, createSlot, deleteSlot, dialog, downloadSlot, hasLocal, hasOnline, hunterName, navigate, playSlot, selectSlot, selectedSlotId, slots, uploadSlot]); const controller = useMenuController(actions, { onBack: () => dialog ? setDialog(null) : navigate("login") }); @@ -261,8 +267,8 @@ function SaveScreen() { downloadSlot(selectedSlotId)}>↓ Overwrite with online
- setDialog("copy")}>Copy save - setDialog("delete")}>Delete save + openSaveDialog("copy")}>Copy save + openSaveDialog("delete")}>Delete save navigate("login")}>Back
diff --git a/src/main.tsx b/src/main.tsx index 693ac2e..fcb94fb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,8 +1,15 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { Capacitor } from "@capacitor/core"; import App from "./App"; import "./styles.css"; +const nativeLayoutRequested = new URLSearchParams(window.location.search).has("nativeLayout"); + +if (Capacitor.isNativePlatform() || nativeLayoutRequested) { + document.documentElement.classList.add("native-platform"); +} + createRoot(document.getElementById("root")!).render( diff --git a/src/platform/displayRouting.ts b/src/platform/displayRouting.ts new file mode 100644 index 0000000..978e51f --- /dev/null +++ b/src/platform/displayRouting.ts @@ -0,0 +1,13 @@ +export type DisplaySurface = "top" | "bottom"; + +const DISPLAY_SURFACE_EVENT = "thor:display-surface"; + +export function requestDisplaySurface(surface: DisplaySurface) { + window.dispatchEvent(new CustomEvent(DISPLAY_SURFACE_EVENT, { detail: surface })); +} + +export function subscribeDisplaySurface(listener: (surface: DisplaySurface) => void) { + const onSurface = (event: Event) => listener((event as CustomEvent).detail); + window.addEventListener(DISPLAY_SURFACE_EVENT, onSurface); + return () => window.removeEventListener(DISPLAY_SURFACE_EVENT, onSurface); +} diff --git a/src/styles.css b/src/styles.css index c0a562c..3413e31 100644 --- a/src/styles.css +++ b/src/styles.css @@ -108,6 +108,15 @@ button:focus-visible { align-items: center; } +.surface-slot { + width: 100%; + display: contents; +} + +.native-display-switch { + display: none; +} + .screen-label { width: 100%; display: flex; @@ -1231,6 +1240,101 @@ button:focus-visible { } } +/* Capacitor single-display fallback. Thor dual-display routing will replace this + switcher once both Android display contexts share one authoritative game state. */ +.native-platform, +.native-platform body, +.native-platform #root, +.native-platform .prototype-shell, +.native-platform .device-frame { + width: 100%; + height: 100%; + min-height: 100%; + overflow: hidden; +} + +.native-platform .prototype-shell { + padding: 0; +} + +.native-platform .prototype-header, +.native-platform .screen-label, +.native-platform .hinge { + display: none; +} + +.native-platform .device-frame { + position: relative; + display: block; + max-width: none; +} + +.native-platform .surface-slot { + position: absolute; + inset: 0; + display: none; + place-items: center; + background: #030706; +} + +.native-platform .surface-slot.is-active { + display: grid; +} + +.native-platform .top-slot > .display { + width: 100%; + height: 100%; + aspect-ratio: auto; +} + +.native-platform .bottom-slot > .display { + width: min(620px, 100%); + height: 100%; + aspect-ratio: auto; +} + +.native-platform .display { + border: 0; + border-radius: 0; + box-shadow: none; +} + +.native-platform .native-display-switch { + position: fixed; + right: max(10px, env(safe-area-inset-right)); + bottom: max(8px, env(safe-area-inset-bottom)); + z-index: 100; + display: grid; + min-width: 76px; + padding: 5px 8px; + border: 1px solid rgba(232, 200, 114, 0.55); + border-radius: 3px; + color: var(--ink); + background: rgba(3, 9, 8, 0.9); + text-align: left; +} + +.native-platform .device-frame.active-bottom .native-display-switch { + top: max(7px, env(safe-area-inset-top)); + right: auto; + bottom: auto; + left: 50%; + transform: translateX(-50%); +} + +.native-platform .native-display-switch b { + color: var(--gold-strong); + font-size: 9px; + line-height: 1; + text-transform: uppercase; +} + +.native-platform .native-display-switch small { + color: var(--muted); + font-size: 6px; + line-height: 1.2; +} + /* Frontend shell — I Want To Heal */ .front-surface {