From f15bf9a1e0a235e9309365efe4cc69bf1a832601 Mon Sep 17 00:00:00 2001 From: Simeon Radivoev Date: Thu, 19 Feb 2026 16:10:29 +0200 Subject: [PATCH] feat: Implemented launching and downloading of roms This is just an initial implementation lots of kings to iron out --- .gitignore | 4 +- .vscode/launch.json | 7 +- .vscode/settings.json | 10 +- README.md | 10 + bun.lock | 370 +- bunfig.toml | 3 + drizzle.config.ts | 11 + drizzle/0000_pretty_harry_osborn.sql | 65 + drizzle/meta/0000_snapshot.json | 458 + drizzle/meta/_journal.json | 13 + package.json | 23 +- scripts/generate-es-de-mapping.ts | 160 + scripts/package-bun.ts | 37 +- src/bun/api/app.ts | 76 + src/bun/api/auth.ts | 97 + src/bun/api/clients.ts | 115 +- src/bun/api/games/games.ts | 245 + src/bun/api/games/platforms.ts | 86 + .../api/games/services/launchGameService.ts | 219 + src/bun/api/games/services/statusService.ts | 171 + src/bun/api/games/services/utils.ts | 65 + src/bun/api/jobs/install-job.ts | 180 + src/bun/api/rpc.ts | 42 +- src/bun/api/schema/app.ts | 54 + src/bun/api/schema/emulators.ts | 43 + src/bun/api/secrets.ts | 133 + src/bun/api/settings.ts | 102 +- src/bun/api/system.ts | 43 + src/bun/api/task-queue.ts | 207 + src/bun/index.ts | 85 +- src/bun/types.d.ts | 19 - src/bun/types/types.d.ts | 8 + src/bun/utils.ts | 11 + src/bun/utils/browser-params.ts | 18 +- src/bun/webview/base.ts | 17 + src/bun/webview/linux.ts | 7 + .../{webview-worker.ts => webview/win32.ts} | 6 +- .../components/AnimatedBackground.tsx | 45 +- src/mainview/components/CardList.tsx | 35 +- src/mainview/components/CollectionsDetail.tsx | 8 +- src/mainview/components/ContextDialog.tsx | 107 + src/mainview/components/Filters.tsx | 6 +- src/mainview/components/GameCard.tsx | 54 +- src/mainview/components/GameList.tsx | 74 +- src/mainview/components/Header.tsx | 5 +- src/mainview/components/PlatformsList.tsx | 72 + src/mainview/components/ShortcutPrompt.tsx | 2 +- src/mainview/components/Shortcuts.tsx | 16 +- src/mainview/components/backgrounds/dots.css | 29 + src/mainview/components/backgrounds/dots.tsx | 10 + src/mainview/components/options/Button.tsx | 28 + .../components/options/OptionInput.tsx | 22 +- .../components/options/OptionSpace.tsx | 8 +- .../components/options/SettingsOption.tsx | 72 + src/mainview/contexts/ToasterContext.tsx | 76 - src/mainview/gen/routeTree.gen.ts | 145 +- src/mainview/index.css | 109 +- src/mainview/index.tsx | 17 +- .../$id.tsx => collection.$id.tsx} | 8 +- src/mainview/routes/game/$id.tsx | 241 - src/mainview/routes/game/$source.$id.tsx | 490 + src/mainview/routes/index.tsx | 129 +- src/mainview/routes/launcher.$source.$id.tsx | 58 + src/mainview/routes/platform.$source.$id.tsx | 54 + src/mainview/routes/platform/$id.tsx | 62 - src/mainview/routes/settings/about.tsx | 60 + src/mainview/routes/settings/accounts.tsx | 109 +- src/mainview/routes/settings/directories.tsx | 242 + src/mainview/routes/settings/route.tsx | 40 +- src/mainview/scripts/clientApi.ts | 22 + src/mainview/scripts/gamepads.ts | 179 +- src/mainview/scripts/spatialNavigation.ts | 49 +- src/mainview/scripts/utils.ts | 5 + src/mainview/scripts/windowEvents.ts | 6 +- src/mainview/types.d.ts | 5 + src/shared/constants.ts | 92 +- src/shared/public-types.ts | 3 + src/tests/game-launching.test.ts | 19 + src/tests/mock-roms/mock-rom.iso | 0 src/tests/preload.ts | 2 + tsconfig.json | 28 +- vendors/es-de/README.md | 4 + vendors/es-de/emulators.darwin.x64.json | 76 + vendors/es-de/emulators.darwin.x64.sqlite | Bin 0 -> 176128 bytes vendors/es-de/emulators.haiku.x64.json | 18 + vendors/es-de/emulators.haiku.x64.sqlite | Bin 0 -> 135168 bytes vendors/es-de/emulators.linux.arm.json | 55 + vendors/es-de/emulators.linux.arm.sqlite | Bin 0 -> 180224 bytes vendors/es-de/emulators.linux.x64.json | 119 + vendors/es-de/emulators.linux.x64.sqlite | Bin 0 -> 217088 bytes vendors/es-de/emulators.win32.x64.json | 119 + vendors/es-de/emulators.win32.x64.sqlite | Bin 0 -> 208896 bytes .../es-de/systems/android/es_find_rules.xml | 523 + .../es-de/systems/android/es_import_rules.xml | 29 + vendors/es-de/systems/android/es_systems.xml | 2184 ++++ vendors/es-de/systems/haiku/es_find_rules.xml | 138 + .../es-de/systems/haiku/es_import_rules.xml | 4 + vendors/es-de/systems/haiku/es_systems.xml | 1952 +++ vendors/es-de/systems/linux/es_find_rules.xml | 1492 +++ .../es-de/systems/linux/es_import_rules.xml | 47 + vendors/es-de/systems/linux/es_systems.xml | 2442 ++++ .../es-de/systems/linuxarm/es_find_rules.xml | 645 + .../systems/linuxarm/es_import_rules.xml | 47 + vendors/es-de/systems/linuxarm/es_systems.xml | 2308 ++++ vendors/es-de/systems/macos/es_find_rules.xml | 491 + .../es-de/systems/macos/es_import_rules.xml | 25 + vendors/es-de/systems/macos/es_systems.xml | 2282 ++++ vendors/es-de/systems/unix/es_find_rules.xml | 700 ++ .../es-de/systems/unix/es_import_rules.xml | 23 + vendors/es-de/systems/unix/es_systems.xml | 2342 ++++ .../es-de/systems/windows/es_find_rules.xml | 1285 ++ .../windows/es_find_rules_portable.xml | 871 ++ .../es-de/systems/windows/es_import_rules.xml | 43 + vendors/es-de/systems/windows/es_systems.xml | 2421 ++++ vendors/romm/custom-overrides.json | 11 + vendors/romm/supported-platforms.json | 10078 ++++++++++++++++ vite.config.ts | 12 +- 117 files changed, 37776 insertions(+), 1073 deletions(-) create mode 100644 bunfig.toml create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_pretty_harry_osborn.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 scripts/generate-es-de-mapping.ts create mode 100644 src/bun/api/app.ts create mode 100644 src/bun/api/auth.ts create mode 100644 src/bun/api/games/games.ts create mode 100644 src/bun/api/games/platforms.ts create mode 100644 src/bun/api/games/services/launchGameService.ts create mode 100644 src/bun/api/games/services/statusService.ts create mode 100644 src/bun/api/games/services/utils.ts create mode 100644 src/bun/api/jobs/install-job.ts create mode 100644 src/bun/api/schema/app.ts create mode 100644 src/bun/api/schema/emulators.ts create mode 100644 src/bun/api/secrets.ts create mode 100644 src/bun/api/system.ts create mode 100644 src/bun/api/task-queue.ts delete mode 100644 src/bun/types.d.ts create mode 100644 src/bun/types/types.d.ts create mode 100644 src/bun/webview/base.ts create mode 100644 src/bun/webview/linux.ts rename src/bun/{webview-worker.ts => webview/win32.ts} (62%) create mode 100644 src/mainview/components/ContextDialog.tsx create mode 100644 src/mainview/components/PlatformsList.tsx create mode 100644 src/mainview/components/backgrounds/dots.css create mode 100644 src/mainview/components/backgrounds/dots.tsx create mode 100644 src/mainview/components/options/Button.tsx create mode 100644 src/mainview/components/options/SettingsOption.tsx delete mode 100644 src/mainview/contexts/ToasterContext.tsx rename src/mainview/routes/{collection/$id.tsx => collection.$id.tsx} (75%) delete mode 100644 src/mainview/routes/game/$id.tsx create mode 100644 src/mainview/routes/game/$source.$id.tsx create mode 100644 src/mainview/routes/launcher.$source.$id.tsx create mode 100644 src/mainview/routes/platform.$source.$id.tsx delete mode 100644 src/mainview/routes/platform/$id.tsx create mode 100644 src/mainview/routes/settings/about.tsx create mode 100644 src/mainview/routes/settings/directories.tsx create mode 100644 src/mainview/scripts/clientApi.ts create mode 100644 src/shared/public-types.ts create mode 100644 src/tests/game-launching.test.ts create mode 100644 src/tests/mock-roms/mock-rom.iso create mode 100644 src/tests/preload.ts create mode 100644 vendors/es-de/README.md create mode 100644 vendors/es-de/emulators.darwin.x64.json create mode 100644 vendors/es-de/emulators.darwin.x64.sqlite create mode 100644 vendors/es-de/emulators.haiku.x64.json create mode 100644 vendors/es-de/emulators.haiku.x64.sqlite create mode 100644 vendors/es-de/emulators.linux.arm.json create mode 100644 vendors/es-de/emulators.linux.arm.sqlite create mode 100644 vendors/es-de/emulators.linux.x64.json create mode 100644 vendors/es-de/emulators.linux.x64.sqlite create mode 100644 vendors/es-de/emulators.win32.x64.json create mode 100644 vendors/es-de/emulators.win32.x64.sqlite create mode 100644 vendors/es-de/systems/android/es_find_rules.xml create mode 100644 vendors/es-de/systems/android/es_import_rules.xml create mode 100644 vendors/es-de/systems/android/es_systems.xml create mode 100644 vendors/es-de/systems/haiku/es_find_rules.xml create mode 100644 vendors/es-de/systems/haiku/es_import_rules.xml create mode 100644 vendors/es-de/systems/haiku/es_systems.xml create mode 100644 vendors/es-de/systems/linux/es_find_rules.xml create mode 100644 vendors/es-de/systems/linux/es_import_rules.xml create mode 100644 vendors/es-de/systems/linux/es_systems.xml create mode 100644 vendors/es-de/systems/linuxarm/es_find_rules.xml create mode 100644 vendors/es-de/systems/linuxarm/es_import_rules.xml create mode 100644 vendors/es-de/systems/linuxarm/es_systems.xml create mode 100644 vendors/es-de/systems/macos/es_find_rules.xml create mode 100644 vendors/es-de/systems/macos/es_import_rules.xml create mode 100644 vendors/es-de/systems/macos/es_systems.xml create mode 100644 vendors/es-de/systems/unix/es_find_rules.xml create mode 100644 vendors/es-de/systems/unix/es_import_rules.xml create mode 100644 vendors/es-de/systems/unix/es_systems.xml create mode 100644 vendors/es-de/systems/windows/es_find_rules.xml create mode 100644 vendors/es-de/systems/windows/es_find_rules_portable.xml create mode 100644 vendors/es-de/systems/windows/es_import_rules.xml create mode 100644 vendors/es-de/systems/windows/es_systems.xml create mode 100644 vendors/romm/custom-overrides.json create mode 100644 vendors/romm/supported-platforms.json diff --git a/.gitignore b/.gitignore index 9056eb9..3712023 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,6 @@ dist-* *.tar.gz settings.local.json .tanstack -artifacts \ No newline at end of file +artifacts +trace +downloads \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 4698b37..30dd986 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,7 +16,7 @@ "internalConsoleOptions": "neverOpen", "request": "attach", "name": "Attach Bun", - "url": "ws://127.0.0.1:9229/7lt63qegtr8", + "url": "ws://127.0.0.1:9229/54esztvxlfe", "localRoot": "${workspaceFolder}", "stopOnEntry": false, } @@ -24,7 +24,10 @@ "compounds": [ { "name": "Attach Debug App", - "configurations": ["Attach Bun", "Attach to Edge"], + "configurations": [ + "Attach Bun", + "Attach to Edge" + ], "stopAll": true, "preLaunchTask": "bun: dev" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 411cba2..4d8e389 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,7 +10,6 @@ "search.exclude": { "**/*.gen.ts": true, "src/mainview/gen/*": true, - }, "editor.formatOnSave": true, "[typescriptreact]": { @@ -22,6 +21,13 @@ "editor.formatOnSave": true }, "cSpell.words": [ - "elysia" + "elysia", + "elysiajs", + "gameflow", + "hackolade", + "keytar", + "norigin", + "noriginmedia", + "romm" ] } \ No newline at end of file diff --git a/README.md b/README.md index 354e079..5ecc268 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ Focused on building a simple user experience and intuitive UI. - Not tested on Mac yet - **Steam Deck Support**: Extensively tested with the steam deck. It can use flatpak installed browsers. - **Great for Controllers**: The UI is inspired by the switch and works great with joysticks and dpads. +- **Automatic Download** Downloads roms from ROMM automatically +- **Automatic Emulator Discovery** Using the configs of the excellent ES-DE to discover installed emulators and launch games. ## Screenshots @@ -47,6 +49,14 @@ Focused on building a simple user experience and intuitive UI. ``` Builds will go in `/builds/`. +4. Additional Commands: + - ```bun run mappings:generate``` converts the es-de configs into local sqlite configs with mappings to rom systems + - ```bun run drizzle:generate``` generates sqlite migrations based on the app schema + - ```bun run openapi-ts``` generated the openapi client calls from romm's API + - ```bun run package``` builds an executable + - ```bun run package:auto-prod``` builds and executable for production + + ### Tech Stack - [Bun](https://bun.com/) for the backend diff --git a/bun.lock b/bun.lock index 2fe72ea..5855171 100644 --- a/bun.lock +++ b/bun.lock @@ -9,10 +9,15 @@ "@elysiajs/cors": "^1.4.1", "@elysiajs/eden": "^1.4.6", "@elysiajs/static": "^1.4.7", - "@hackolade/keytar": "^7.9.0-7", "@rcompat/webview": "^0.18.0", + "cheerio": "^1.2.0", "conf": "^15.0.2", + "drizzle-orm": "^0.45.1", "elysia": "^1.4.22", + "get-folder-size": "^5.0.0", + "node-downloader-helper": "^2.1.10", + "node-stream-zip": "^1.15.0", + "open": "^11.0.0", "pathe": "^2.0.3", "tough-cookie": "^6.0.0", "tough-cookie-file-store": "^3.3.0", @@ -39,12 +44,16 @@ "concurrently": "^9.2.1", "cross-env": "^10.1.0", "daisyui": "^5.5.14", + "drizzle-kit": "^0.31.9", + "dts-bundle-generator": "^9.5.1", + "eden-tanstack-query": "^0.0.9", "lucide-react": "^0.563.0", "pretty-bytes": "^7.1.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-error-boundary": "^6.1.0", "react-hot-toast": "^2.6.0", + "sass-embedded": "^1.97.3", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "tailwindcss-animate": "^1.0.7", @@ -53,6 +62,7 @@ "vite": "^7.3.1", "vite-plugin-svg-icons-ng": "^1.5.2", "vite-static-assets-plugin": "^1.2.2", + "vite-tsconfig-paths": "^6.1.1", }, }, }, @@ -103,6 +113,10 @@ "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="], "@elysiajs/eden": ["@elysiajs/eden@1.4.6", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-Tsa4NwXEWg/u73vWiYZQ3L5/ecgZSxqiEjYwpS+4qBKXeTZqZKl2hcgHJSVBL+InEDMi35Xugct7qyAXE5oM4Q=="], @@ -111,69 +125,61 @@ "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "@hackolade/keytar": ["@hackolade/keytar@7.9.0-7", "", { "dependencies": { "@hackolade/keytar-darwin-arm64": "7.9.0-7", "@hackolade/keytar-darwin-x64": "7.9.0-7", "@hackolade/keytar-linux-arm64": "7.9.0-7", "@hackolade/keytar-linux-x64": "7.9.0-7", "@hackolade/keytar-win32-x64": "7.9.0-7" } }, "sha512-1U4Wfo3dbP63Dcl+SZyHgy3Q+sOdKzvZjuEu01BxDq4A/gtB/1e3Q9HouWUY37xdcTRSG1rD4iYirbQ79RN2iQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "@hackolade/keytar-darwin-arm64": ["@hackolade/keytar-darwin-arm64@7.9.0-7", "", {}, "sha512-A4rE3nAnjtJ0JaKfXoYLGDHky0JbIK2AqyZUcpSTIfVo28rBhqbfY8JPqEfgwibInKO1vjDUeizlhSOP0Q5RQA=="], - - "@hackolade/keytar-darwin-x64": ["@hackolade/keytar-darwin-x64@7.9.0-7", "", {}, "sha512-xc/B1MrTD9cfxArBVto2dL9d9noRHgT/ZDZfBlfCEfb2pmbfg83WipDKxuu0nvL2Pzs2Ob26sBbFAxQE5djWIQ=="], - - "@hackolade/keytar-linux-arm64": ["@hackolade/keytar-linux-arm64@7.9.0-7", "", {}, "sha512-G99cXS3li/mnW3qtncLAsDNPpx6Jqut1HnRgJVnO1RNotGdGU6EDcTog4pPHy7TVSwsc3QZ3Jay5wfJ0meXnSQ=="], - - "@hackolade/keytar-linux-x64": ["@hackolade/keytar-linux-x64@7.9.0-7", "", {}, "sha512-Zx2e4aSbt3Ti0727GQMohlMqBOc7KElXOpZeW+F822U67CMckulJO0D4jcRKov5Kg3eO0nqCVT0GAnvLX6xtXw=="], - - "@hackolade/keytar-win32-x64": ["@hackolade/keytar-win32-x64@7.9.0-7", "", {}, "sha512-tsSNLp5N8w7c3cauMxOpO5/ZVnEQ5TRDiAwZAQxI1TRJ3zerS7GmDbUhriZPU22d1qp4q9Fd+nQFhOe4NvJ8Lg=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@hey-api/codegen-core": ["@hey-api/codegen-core@0.6.0", "", { "dependencies": { "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-O6TRP3g1188gdpZC9yio64KkvWD97QpLjaCFnwTgykTZtzQXZbkyw+lYPVfZ7Ee+m7eLau1/jEJmQcUY9G0sLw=="], @@ -213,6 +219,34 @@ "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], + "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], + "@rcompat/assert": ["@rcompat/assert@0.6.0", "", { "dependencies": { "@rcompat/is": "^0.4.0", "@rcompat/type": "^0.9.0" } }, "sha512-V8YrttJqBNsLo9DQkXGpPt09LqfUGvwA30q8Tf+uukEhE6Nraw4jM4Nq0c5yKQF0IWv1eSoPUJyCO4W4neS5IA=="], "@rcompat/dict": ["@rcompat/dict@0.3.1", "", { "dependencies": { "@rcompat/assert": "^0.6.0", "@rcompat/is": "^0.4.2" } }, "sha512-eWZ4ACk0DpT8PS+umVlp/TmFfWAD0yqkGxfvvtfL/9fqPEh1bcCFtGMySCwmTGx/FU8sPnxwnSiZGZmN36gTBQ=="], @@ -429,6 +463,10 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], + + "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], @@ -445,6 +483,8 @@ "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], + "colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="], + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], @@ -507,19 +547,31 @@ "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + "drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="], + + "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + + "dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="], + + "eden-tanstack-query": ["eden-tanstack-query@0.0.9", "", { "peerDependencies": { "@elysiajs/eden": ">=1.0.0", "@tanstack/query-core": "^5.90.16" } }, "sha512-EYnFasVEFHFZ9aoI2TDFHU+q0gRdvnFYvX8QzzjXww4dLy0qXDNTGvywmLuiqfiICDC2y4oh2/ZpIPFkGTLNPQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.277", "", {}, "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw=="], "elysia": ["elysia@1.4.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], + "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], - "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -553,12 +605,16 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-folder-size": ["get-folder-size@5.0.0", "", { "bin": { "get-folder-size": "bin/get-folder-size.js" } }, "sha512-+fgtvbL83tSDypEK+T411GDBQVQtxv+qtQgbV+HVa/TYubqDhNd5ghH/D6cOHY9iC5/88GtOZB7WI8PXy2A3bg=="], + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], + "goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -567,8 +623,14 @@ "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "immutable": ["immutable@5.1.4", "", {}, "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA=="], + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], @@ -659,12 +721,18 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "node-downloader-helper": ["node-downloader-helper@2.1.10", "", { "bin": { "ndh": "bin/ndh" } }, "sha512-8LdieUd4Bqw/CzfZLf30h+1xSAq3riWSDfWKsPJYz8EULoWxjS1vw6BGLYFZDxQgXjDR7UmC9UpQ0oV93U98Fg=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "node-stream-zip": ["node-stream-zip@1.15.0", "", {}, "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], @@ -679,6 +747,12 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], + + "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -739,6 +813,48 @@ "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sass": ["sass@1.97.3", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg=="], + + "sass-embedded": ["sass-embedded@1.97.3", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-all-unknown": "1.97.3", "sass-embedded-android-arm": "1.97.3", "sass-embedded-android-arm64": "1.97.3", "sass-embedded-android-riscv64": "1.97.3", "sass-embedded-android-x64": "1.97.3", "sass-embedded-darwin-arm64": "1.97.3", "sass-embedded-darwin-x64": "1.97.3", "sass-embedded-linux-arm": "1.97.3", "sass-embedded-linux-arm64": "1.97.3", "sass-embedded-linux-musl-arm": "1.97.3", "sass-embedded-linux-musl-arm64": "1.97.3", "sass-embedded-linux-musl-riscv64": "1.97.3", "sass-embedded-linux-musl-x64": "1.97.3", "sass-embedded-linux-riscv64": "1.97.3", "sass-embedded-linux-x64": "1.97.3", "sass-embedded-unknown-all": "1.97.3", "sass-embedded-win32-arm64": "1.97.3", "sass-embedded-win32-x64": "1.97.3" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-eKzFy13Nk+IRHhlAwP3sfuv+PzOrvzUkwJK2hdoCKYcWGSdmwFpeGpWmyewdw8EgBnsKaSBtgf/0b2K635ecSA=="], + + "sass-embedded-all-unknown": ["sass-embedded-all-unknown@1.97.3", "", { "dependencies": { "sass": "1.97.3" }, "cpu": [ "!arm", "!x64", "!arm64", ] }, "sha512-t6N46NlPuXiY3rlmG6/+1nwebOBOaLFOOVqNQOC2cJhghOD4hh2kHNQQTorCsbY9S1Kir2la1/XLBwOJfui0xg=="], + + "sass-embedded-android-arm": ["sass-embedded-android-arm@1.97.3", "", { "os": "android", "cpu": "arm" }, "sha512-cRTtf/KV/q0nzGZoUzVkeIVVFv3L/tS1w4WnlHapphsjTXF/duTxI8JOU1c/9GhRPiMdfeXH7vYNcMmtjwX7jg=="], + + "sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.97.3", "", { "os": "android", "cpu": "arm64" }, "sha512-aiZ6iqiHsUsaDx0EFbbmmA0QgxicSxVVN3lnJJ0f1RStY0DthUkquGT5RJ4TPdaZ6ebeJWkboV4bra+CP766eA=="], + + "sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.97.3", "", { "os": "android", "cpu": "none" }, "sha512-zVEDgl9JJodofGHobaM/q6pNETG69uuBIGQHRo789jloESxxZe82lI3AWJQuPmYCOG5ElfRthqgv89h3gTeLYA=="], + + "sass-embedded-android-x64": ["sass-embedded-android-x64@1.97.3", "", { "os": "android", "cpu": "x64" }, "sha512-3ke0le7ZKepyXn/dKKspYkpBC0zUk/BMciyP5ajQUDy4qJwobd8zXdAq6kOkdiMB+d9UFJOmEkvgFJHl3lqwcw=="], + + "sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.97.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fuqMTqO4gbOmA/kC5b9y9xxNYw6zDEyfOtMgabS7Mz93wimSk2M1quQaTJnL98Mkcsl2j+7shNHxIS/qpcIDDA=="], + + "sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.97.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-b/2RBs/2bZpP8lMkyZ0Px0vkVkT8uBd0YXpOwK7iOwYkAT8SsO4+WdVwErsqC65vI5e1e5p1bb20tuwsoQBMVA=="], + + "sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.97.3", "", { "os": "linux", "cpu": "arm" }, "sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA=="], + + "sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.97.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-IP1+2otCT3DuV46ooxPaOKV1oL5rLjteRzf8ldZtfIEcwhSgSsHgA71CbjYgLEwMY9h4jeal8Jfv3QnedPvSjg=="], + + "sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.97.3", "", { "os": "linux", "cpu": "arm" }, "sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg=="], + + "sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.97.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw=="], + + "sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.97.3", "", { "os": "linux", "cpu": "none" }, "sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA=="], + + "sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.97.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw=="], + + "sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.97.3", "", { "os": "linux", "cpu": "none" }, "sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA=="], + + "sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.97.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg=="], + + "sass-embedded-unknown-all": ["sass-embedded-unknown-all@1.97.3", "", { "dependencies": { "sass": "1.97.3" }, "os": [ "!linux", "!win32", "!darwin", "!android", ] }, "sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q=="], + + "sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.97.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-RDGtRS1GVvQfMGAmVXNxYiUOvPzn9oO1zYB/XUM9fudDRnieYTcUytpNTQZLs6Y1KfJxgt5Y+giRceC92fT8Uw=="], + + "sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.97.3", "", { "os": "win32", "cpu": "x64" }, "sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -773,6 +889,10 @@ "svgo": ["svgo@3.3.2", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, "bin": "./bin/svgo" }, "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw=="], + "sync-child-process": ["sync-child-process@1.0.2", "", { "dependencies": { "sync-message-port": "^1.0.0" } }, "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA=="], + + "sync-message-port": ["sync-message-port@1.2.0", "", {}, "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], @@ -807,6 +927,8 @@ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], @@ -817,6 +939,8 @@ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="], + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], @@ -829,14 +953,22 @@ "usehooks-ts": ["usehooks-ts@3.1.1", "", { "dependencies": { "lodash.debounce": "^4.0.8" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA=="], + "varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vite-plugin-svg-icons-ng": ["vite-plugin-svg-icons-ng@1.5.2", "", { "dependencies": { "fast-glob": "^3.3.3", "fs-extra": "^11.3.2", "node-html-parser": "^7.0.1", "svgo": "^3.3.2" }, "peerDependencies": { "vite": ">=5.0.0" } }, "sha512-A68obs8XDT+q8q8dKyjrT/v0qw8h5pEBKXJ27aUXjARYeJ6MNvhIhRLLiUwnSrbn/B4TBF4UVaWRXKftAqP7+A=="], "vite-static-assets-plugin": ["vite-static-assets-plugin@1.2.2", "", { "dependencies": { "chalk": "^5.4.1", "chokidar": "^3.5.3", "minimatch": "^10.0.1" }, "peerDependencies": { "typescript": "^5.0.0", "vite": "^6.2.0" } }, "sha512-0mzHsxFa46Np5AixQcdWYLVH6eJxeok7qL7tXmxYavg/Uo0e5z+J6gavJ0TJ6dmJSe2Z+gwmDb64bCCZfg+gqA=="], + "vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -859,6 +991,8 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], @@ -889,24 +1023,184 @@ "elysia/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "sass/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "vite-static-assets-plugin/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + + "sass/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], } } diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..d253475 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,3 @@ +[test] +# Load these modules before running tests. +preload = ["./src/tests/preload.ts"] \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..7006721 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,11 @@ +import 'dotenv/config'; +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + out: './drizzle', + schema: './src/bun/api/schema/app.ts', + dialect: 'sqlite', + dbCredentials: { + url: "./games.db" + } +}); \ No newline at end of file diff --git a/drizzle/0000_pretty_harry_osborn.sql b/drizzle/0000_pretty_harry_osborn.sql new file mode 100644 index 0000000..1d5d32a --- /dev/null +++ b/drizzle/0000_pretty_harry_osborn.sql @@ -0,0 +1,65 @@ +CREATE TABLE `collections` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text +); +--> statement-breakpoint +CREATE TABLE `collections_games` ( + `collection_id` integer NOT NULL, + `game_id` integer NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`collection_id`) REFERENCES `collections`(`id`) ON UPDATE cascade ON DELETE cascade, + FOREIGN KEY (`game_id`) REFERENCES `games`(`id`) ON UPDATE cascade ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `games` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `source_id` integer, + `source` text, + `igdb_id` integer, + `name` text, + `ra_id` integer, + `path_fs` text, + `last_played` integer, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `metadata` text DEFAULT '{}', + `slug` text, + `platform_id` integer NOT NULL, + `cover` blob, + `type` text, + `summary` text, + FOREIGN KEY (`platform_id`) REFERENCES `platforms`(`id`) ON UPDATE cascade ON DELETE no action +); +--> statement-breakpoint +CREATE UNIQUE INDEX `games_source_id_unique` ON `games` (`source_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `games_igdb_id_unique` ON `games` (`igdb_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `games_ra_id_unique` ON `games` (`ra_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `games_slug_unique` ON `games` (`slug`);--> statement-breakpoint +CREATE TABLE `platforms` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `igdb_id` integer, + `igdb_slug` text, + `moby_id` integer, + `name` text NOT NULL, + `es_slug` text, + `ra_id` integer, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `slug` text NOT NULL, + `metadata` text, + `cover` blob, + `type` text, + `family_name` text +); +--> statement-breakpoint +CREATE UNIQUE INDEX `platforms_igdb_id_unique` ON `platforms` (`igdb_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `platforms_igdb_slug_unique` ON `platforms` (`igdb_slug`);--> statement-breakpoint +CREATE UNIQUE INDEX `platforms_moby_id_unique` ON `platforms` (`moby_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `platforms_es_slug_unique` ON `platforms` (`es_slug`);--> statement-breakpoint +CREATE UNIQUE INDEX `platforms_ra_id_unique` ON `platforms` (`ra_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `platforms_slug_unique` ON `platforms` (`slug`);--> statement-breakpoint +CREATE TABLE `screenshots` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `game_id` integer, + `content` blob NOT NULL, + `type` text, + FOREIGN KEY (`game_id`) REFERENCES `games`(`id`) ON UPDATE cascade ON DELETE cascade +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..8b9c6f9 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,458 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "673fe5dc-58a5-495b-8fb1-104e7945e90b", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "collections": { + "name": "collections", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "collections_games": { + "name": "collections_games", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_id": { + "name": "game_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "collections_games_collection_id_collections_id_fk": { + "name": "collections_games_collection_id_collections_id_fk", + "tableFrom": "collections_games", + "tableTo": "collections", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "collections_games_game_id_games_id_fk": { + "name": "collections_games_game_id_games_id_fk", + "tableFrom": "collections_games", + "tableTo": "games", + "columnsFrom": [ + "game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "games": { + "name": "games", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "source_id": { + "name": "source_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "igdb_id": { + "name": "igdb_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ra_id": { + "name": "ra_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path_fs": { + "name": "path_fs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_played": { + "name": "last_played", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'{}'" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "platform_id": { + "name": "platform_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cover": { + "name": "cover", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "games_source_id_unique": { + "name": "games_source_id_unique", + "columns": [ + "source_id" + ], + "isUnique": true + }, + "games_igdb_id_unique": { + "name": "games_igdb_id_unique", + "columns": [ + "igdb_id" + ], + "isUnique": true + }, + "games_ra_id_unique": { + "name": "games_ra_id_unique", + "columns": [ + "ra_id" + ], + "isUnique": true + }, + "games_slug_unique": { + "name": "games_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "games_platform_id_platforms_id_fk": { + "name": "games_platform_id_platforms_id_fk", + "tableFrom": "games", + "tableTo": "platforms", + "columnsFrom": [ + "platform_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "platforms": { + "name": "platforms", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "igdb_id": { + "name": "igdb_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "igdb_slug": { + "name": "igdb_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "moby_id": { + "name": "moby_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "es_slug": { + "name": "es_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ra_id": { + "name": "ra_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover": { + "name": "cover", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "platforms_igdb_id_unique": { + "name": "platforms_igdb_id_unique", + "columns": [ + "igdb_id" + ], + "isUnique": true + }, + "platforms_igdb_slug_unique": { + "name": "platforms_igdb_slug_unique", + "columns": [ + "igdb_slug" + ], + "isUnique": true + }, + "platforms_moby_id_unique": { + "name": "platforms_moby_id_unique", + "columns": [ + "moby_id" + ], + "isUnique": true + }, + "platforms_es_slug_unique": { + "name": "platforms_es_slug_unique", + "columns": [ + "es_slug" + ], + "isUnique": true + }, + "platforms_ra_id_unique": { + "name": "platforms_ra_id_unique", + "columns": [ + "ra_id" + ], + "isUnique": true + }, + "platforms_slug_unique": { + "name": "platforms_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "screenshots": { + "name": "screenshots", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "game_id": { + "name": "game_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "screenshots_game_id_games_id_fk": { + "name": "screenshots_game_id_games_id_fk", + "tableFrom": "screenshots", + "tableTo": "games", + "columnsFrom": [ + "game_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..2402c14 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1771508990238, + "tag": "0000_pretty_harry_osborn", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 3161070..882de28 100644 --- a/package.json +++ b/package.json @@ -12,20 +12,28 @@ "build:dev": "NODE_ENV=development bun run build", "package": "bun run build && bun run ./scripts/package-bun.ts", "package:auto-prod": "bun run build:pro && NODE_ENV=production bun run ./scripts/package-bun.ts", - "package:linux": "bun run build && TARGET=bun-linux-x64 bun run ./scripts/package-bun.ts", + "package:linux": "bun run build && NODE_ENV=development TARGET=bun-linux-x64 bun run ./scripts/package-bun.ts", "openapi-ts": "bun run ./scripts/romm/openapi-ts.ts", - "run:build-action": "act workflow_dispatch --artifact-server-path artifacts --env ACTIONS_RUNTIME_TOKEN=foo -W .github/workflows/build.yml", - "hmr": "vite --port 5173" + "run:build-action": "act workflow_dispatch --artifact-server-path artifacts --env ACTIONS_RUNTIME_TOKEN=foo -W .forgejo/workflows/build.yml", + "hmr": "vite --port 5173", + "drizzle:generate": "bunx drizzle-kit generate", + "test": "bun test", + "mappings:generate": "bun run drizzle-kit generate --dialect=sqlite --schema=./src/bun/api/schema/emulators.ts --out=./scripts/drizzle/es-de && bun run ./scripts/generate-es-de-mapping.ts" }, "dependencies": { "@auth/core": "^0.34.3", "@elysiajs/cors": "^1.4.1", "@elysiajs/eden": "^1.4.6", "@elysiajs/static": "^1.4.7", - "@hackolade/keytar": "^7.9.0-7", "@rcompat/webview": "^0.18.0", + "cheerio": "^1.2.0", "conf": "^15.0.2", + "drizzle-orm": "^0.45.1", "elysia": "^1.4.22", + "get-folder-size": "^5.0.0", + "node-downloader-helper": "^2.1.10", + "node-stream-zip": "^1.15.0", + "open": "^11.0.0", "pathe": "^2.0.3", "tough-cookie": "^6.0.0", "tough-cookie-file-store": "^3.3.0", @@ -52,12 +60,16 @@ "concurrently": "^9.2.1", "cross-env": "^10.1.0", "daisyui": "^5.5.14", + "drizzle-kit": "^0.31.9", + "dts-bundle-generator": "^9.5.1", + "eden-tanstack-query": "^0.0.9", "lucide-react": "^0.563.0", "pretty-bytes": "^7.1.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-error-boundary": "^6.1.0", "react-hot-toast": "^2.6.0", + "sass-embedded": "^1.97.3", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "tailwindcss-animate": "^1.0.7", @@ -65,6 +77,7 @@ "usehooks-ts": "^3.1.1", "vite": "^7.3.1", "vite-plugin-svg-icons-ng": "^1.5.2", - "vite-static-assets-plugin": "^1.2.2" + "vite-static-assets-plugin": "^1.2.2", + "vite-tsconfig-paths": "^6.1.1" } } \ No newline at end of file diff --git a/scripts/generate-es-de-mapping.ts b/scripts/generate-es-de-mapping.ts new file mode 100644 index 0000000..559f6d0 --- /dev/null +++ b/scripts/generate-es-de-mapping.ts @@ -0,0 +1,160 @@ +import fs from 'node:fs/promises'; +import * as cheerio from 'cheerio'; +import { getSupportedPlatformsEndpointApiPlatformsSupportedGet } from '../src/clients/romm'; +import customMappings from '../vendors/romm/custom-overrides.json'; +import { Database } from "bun:sqlite"; +import * as schema from '../src/bun/api/schema/emulators'; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; +import { drizzle } from "drizzle-orm/bun-sqlite"; + +/** get all latest supported romm platforms */ +const rommPlatforms = await getSupportedPlatformsEndpointApiPlatformsSupportedGet({ baseUrl: "https://demo.romm.app" }); + +/** a matrix for supported platforms and architectures */ +const platforms: [NodeJS.Platform, NodeJS.Architecture][] = [['linux', 'x64'], ['win32', 'x64'], ['darwin', 'x64'], ['haiku', 'x64'], ['linux', 'arm']]; + +/** Save client minimal info for emulator names and descriptions */ +await Promise.all(platforms.map(async ([platform, arch]) => +{ + const rules = await Bun.file(`./vendors/es-de/systems/${mapSystem(platform, arch)}/es_find_rules.xml`).arrayBuffer(); + const $r = cheerio.load(Buffer.from(rules)); + const es_emulators = $r('ruleList emulator'); + + const emulators = Object.fromEntries(es_emulators.toArray().map(system => + { + const $system = $r(system); + const key = $system.attr('name'); + const comment = $system.contents().toArray().find((node) => node.type === 'comment'); + return [key, comment?.data.trim() ?? key]; + })); + + await Bun.write(`./vendors/es-de/emulators.${platform}.${arch}.json`, JSON.stringify(emulators, null, 3)); +})); + +/** Delete old databases, we recreate them each time */ +await Promise.all(platforms.map(async ([platform, arch]) => +{ + const sqlitePath = `./vendors/es-de/emulators.${platform}.${arch}.sqlite`; + if (await fs.exists(sqlitePath)) + await fs.rm(sqlitePath); +})); + +await Promise.all(platforms.map(async ([platform, arch]) => +{ + const systems = await Bun.file(`./vendors/es-de/systems/${mapSystem(platform, arch)}/es_systems.xml`).arrayBuffer(); + const $s = cheerio.load(Buffer.from(systems)); + const rules = await Bun.file(`./vendors/es-de/systems/${mapSystem(platform, arch)}/es_find_rules.xml`).arrayBuffer(); + const $r = cheerio.load(Buffer.from(rules)); + + const sqlitePath = `./vendors/es-de/emulators.${platform}.${arch}.sqlite`; + const sqlite = new Database(sqlitePath, { create: true, readwrite: true }); + const db = drizzle(sqlite, { schema }); + migrate(db, { migrationsFolder: "./scripts/drizzle/es-de" }); + + /** Save the ruleset for emulators */ + await db.insert(schema.emulators).values($r('ruleList emulator').toArray().map(s => + { + const $emulator = $r(s); + const $systempath = $emulator.find('rule[type=systempath] entry'); + const $staticpath = $emulator.find('rule[type=staticpath] entry'); + const $corepath = $emulator.find('rule[type=corepath] entry'); + const $androidpackage = $emulator.find('rule[type=androidpackage] entry'); + const $winregistrypath = $emulator.find('rule[type=winregistrypath] entry'); + + const emulatorName = $emulator.attr('name'); + const emulator: typeof schema.emulators.$inferInsert = { + name: emulatorName!, + systempath: $systempath.toArray().map(p => $r(p).text()), + staticpath: $staticpath.toArray().map(p => $r(p).text()), + corepath: $corepath.toArray().map(p => $r(p).text()), + androidpackage: $androidpackage.toArray().map(p => $r(p).text()), + winregistrypath: $winregistrypath.toArray().map(p => $r(p).text()), + }; + return emulator; + })); + + /** Save the systems like ps2 or psp */ + await Promise.all($s(`systemList system`).toArray().map(async s => + { + const name = $s(s).find("name").text(); + const fullname = $s(s).find("fullname").text(); + const rommMapping = rommPlatforms.data?.find(p => + p.slug === (customMappings as any)[name] || + p.slug === name || + p.igdb_slug === name || + p.hltb_slug === name || + p.moby_slug === name || + p.display_name === fullname + ); + + const system: typeof schema.systems.$inferInsert = { + name, + fullname, + path: $s(s).find("path").text(), + extension: $s(s).find("extension").text().replaceAll('.', '').split(' ') + }; + + /** Store mappings to all other sources for easy reference */ + db.transaction(async (tx) => + { + await tx.insert(schema.systems).values(system); + if (rommMapping) + { + const sources: [string, keyof typeof rommMapping | null, keyof typeof rommMapping | null][] = [ + ['ra', 'ra_id', null], + ['ss', 'ss_id', null], + ['hltb', null, 'hltb_slug'], + ['moby', 'moby_id', 'moby_slug'], + ['launchbox', 'launchbox_id', null], + ['sgdb', 'sgdb_id', null], + ['tgdb', 'tgdb_id', null], + ['hasheous', 'hasheous_id', null], + ['flashpoint', 'flashpoint_id', null], + ['romm', null, 'slug'], + ['igdb', 'igdb_id', 'igdb_slug'] + ]; + + await tx.insert(schema.systemMappings) + .values(sources.map(([source, sourceId, sourceSlug]) => ({ + source, + sourceId: sourceId ? rommMapping[sourceId] as number : null, + sourceSlug: sourceSlug ? rommMapping[sourceSlug] as string : null, + system: system.name + } satisfies typeof schema.systemMappings.$inferInsert)) + .filter(m => m.sourceId !== null || m.sourceSlug !== null)); + } + }); + + await db.insert(schema.commands).values($s(s).find("command").toArray().map(c => + { + const command: typeof schema.commands.$inferInsert = { + label: $s(c).attr('label'), + command: $s(c).text(), + system: system.name + }; + + return command; + })); + })); +})); + +/** map from bun platform to es-de folder naming */ +function mapSystem (platform: NodeJS.Platform, arch: NodeJS.Architecture) +{ + let system: string | undefined = undefined; + if (platform === 'darwin') + { + system = 'macos'; + } else if (platform === 'win32') + { + system = 'windows'; + } else if (platform === 'linux' && arch === 'arm') + { + system = 'linuxarm'; + } + else + { + system = platform; + } + return system; +} \ No newline at end of file diff --git a/scripts/package-bun.ts b/scripts/package-bun.ts index 4fefbf7..484f56f 100644 --- a/scripts/package-bun.ts +++ b/scripts/package-bun.ts @@ -1,8 +1,10 @@ import fs from "node:fs/promises"; import path, { } from "node:path"; import os from "node:os"; +import { Glob } from "bun"; -const buildSubDir = process.env.BUILD_DIR ?? `./build/${os.platform()}`; +const system = getPlatform(); +const buildSubDir = process.env.BUILD_DIR ?? `./build/${system.platform}`; const compileOption: Bun.CompileBuildOptions = { outfile: "gameflow", @@ -19,7 +21,7 @@ if (process.env.TARGET) } await Bun.build({ - entrypoints: ["./src/bun/index.ts", "./src/bun/webview-worker.ts"], + entrypoints: ["./src/bun/index.ts", `./src/bun/webview/${system.platform}.ts`], metafile: true, compile: compileOption, outdir: buildSubDir, @@ -27,8 +29,8 @@ await Bun.build({ define: { "process.env.IS_BINARY": "true" }, - minify: true, - sourcemap: "linked", + minify: process.env.NODE_ENV !== 'development', + sourcemap: process.env.NODE_ENV === 'development' ? 'inline' : "linked", target: 'bun', format: 'esm', loader: { @@ -52,7 +54,32 @@ await Bun.build({ build.onEnd(async () => { await fs.cp('./dist', `${buildSubDir}/dist`, { recursive: true }); + await fs.cp('./drizzle', `${buildSubDir}/drizzle`, { recursive: true }); + await fs.cp(`./vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, `${buildSubDir}/vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, { recursive: true }); }); }, }] -}); \ No newline at end of file +}); + +function getPlatform () +{ + if (process.env.TARGET) + { + const arch = process.env.TARGET.includes('arm') ? 'arm' : 'x64'; + let platform = os.platform(); + if (platform.includes('windows')) + { + platform = 'win32'; + } else if (platform.includes('darwin')) + { + platform = 'darwin'; + } else + { + platform = 'linux'; + } + return { platform, arch }; + } else + { + return { platform: os.platform(), arch: os.arch() }; + } +} \ No newline at end of file diff --git a/src/bun/api/app.ts b/src/bun/api/app.ts new file mode 100644 index 0000000..465418e --- /dev/null +++ b/src/bun/api/app.ts @@ -0,0 +1,76 @@ + +import { TaskQueue } from "./task-queue"; +import { Database } from "bun:sqlite"; +import { CookieJar } from 'tough-cookie'; +import FileCookieStore from 'tough-cookie-file-store'; +import path from 'node:path'; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import Conf from "conf"; +import projectPackage from '~/package.json'; +import { SERVER_URL, SettingsSchema, SettingsType } from "../../shared/constants"; +import { client } from "@clients/romm/client.gen"; +import * as schema from "./schema/app"; +import * as emulatorSchema from "./schema/emulators"; +import { login, logout } from "./auth"; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import { ActiveGame } from "../types/types"; +import EventEmitter from "node:events"; +import { ErrorLike } from "bun"; + +export const config = new Conf({ + projectName: projectPackage.name, + projectSuffix: 'bun', + schema: Object.fromEntries(Object.entries(SettingsSchema.shape).map(([key, schema]) => [key, schema.toJSONSchema() as any])) as any, + defaults: SettingsSchema.parse({}), +}); +export const customEmulators = new Conf>({ + projectName: projectPackage.name, + projectSuffix: 'bun', + configName: 'custom-emulators', + rootSchema: { + "type": "object", + "additionalProperties": { + "type": "string" + } + } +}); + +console.log("Config Path Located At: ", config.path); +console.log("Custom Emulator Paths Located At: ", customEmulators.path); +const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json')); +console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath); +export const jar = new CookieJar(fileCookieStore); +await fs.mkdir(config.get('downloadPath'), { recursive: true }); +const sqlite = new Database(path.join(config.get('downloadPath'), 'db.sqlite'), { create: true, readwrite: true }); +export const db = drizzle(sqlite, { schema }); +migrate(db, { migrationsFolder: "./drizzle" }); +const emulatorsSqlite = new Database(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`, { readonly: true }); +export const emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema }); +export const taskQueue = new TaskQueue(); +config.onDidChange('rommAddress', v => client.setConfig({ baseUrl: v })); +await login(); +export let activeGame: ActiveGame | undefined; +export function setActiveGame (game: ActiveGame) +{ + if (activeGame) throw new Error("Only one active game at a time"); + return activeGame = game; +} +export const events = new EventEmitter(); +events.addListener('activegameexit', () => activeGame = undefined); +console.log("Logging In to Romm"); + +export async function cleanup () +{ + await taskQueue.close(); + sqlite.close(); + await logout(); + emulatorsSqlite.close(); +} + +interface AppEventMap +{ + activegameexit: [{ subprocess: Bun.Subprocess, exitCode: number | null, signalCode: number | null, error?: ErrorLike; }]; + exitapp: []; +} \ No newline at end of file diff --git a/src/bun/api/auth.ts b/src/bun/api/auth.ts new file mode 100644 index 0000000..03001dd --- /dev/null +++ b/src/bun/api/auth.ts @@ -0,0 +1,97 @@ +import Elysia, { status } from "elysia"; +import { config, db, jar } from "./app"; +import z from "zod"; +import { client } from "@clients/romm/client.gen"; +import { loginApiLoginPost } from "@clients/romm"; +import secrets from '../api/secrets'; + +export default new Elysia() + .post('/login', async ({ body: { host, username, password } }) => + { + if (config.has('rommAddress') && config.has('rommUser')) + { + await logout(); + const oldRommAddress = config.get('rommAddress'); + if (oldRommAddress) + { + const cookies = await jar.getCookies(oldRommAddress); + cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key)); + } + } + + config.set('rommAddress', host); + config.set('rommUser', username); + + await secrets.set({ service: 'gameflow', name: 'romm', value: password }); + await login(); + + return status(200); + }, { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) }) + .get('/login', async () => + { + const credentials = await secrets.get({ service: 'gameflow', name: 'romm' }); + return { hasPassword: !!credentials }; + }, { response: z.object({ hasPassword: z.boolean() }) }) + .post('/logout', async () => + { + await secrets.delete({ service: 'gameflow', name: 'romm' }); + await logout(); + const rommAddress = config.get('rommAddress'); + if (rommAddress) + { + const cookies = await jar.getCookies(rommAddress); + cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key)); + } + return status(200); + }, { response: z.any() }); + +async function updateClient () +{ + client.setConfig({ + baseUrl: config.get('rommAddress'), headers: { + cookie: await jar.getCookieString(config.get('rommAddress') ?? '') + } + }); +} + +export async function logout () +{ + if (!config.has('rommAddress')) + { + return; + } + const rommAddress = config.get('rommAddress'); + if (rommAddress) + { + console.log("Logging Out of ROMM"); + try + { + await loginApiLoginPost({ + baseUrl: rommAddress, headers: { + 'cookie': await jar.getCookieString(rommAddress) + } + }); + } catch (error) + { + console.error("Failed to logout of ROMM ", error); + } + } +} + +export async function login () +{ + if (!config.has('rommAddress') || !config.has('rommUser')) + { + return; + } + const rommAddress = config.get('rommAddress'); + const rommUser = config.get('rommUser'); + if (rommAddress && rommUser) + { + console.log("Logging In to ROMM"); + const password = await secrets.get({ service: 'gameflow', name: "romm" }); + const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` }); + loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress)); + await updateClient(); + } +} \ No newline at end of file diff --git a/src/bun/api/clients.ts b/src/bun/api/clients.ts index e16ea15..b444eb6 100644 --- a/src/bun/api/clients.ts +++ b/src/bun/api/clients.ts @@ -1,97 +1,12 @@ import z from "zod"; -import { config } from "./settings"; -import Elysia, { status } from "elysia"; -import keytar from '@hackolade/keytar'; -import { loginApiLoginPost } from "../../clients/romm"; -import { CookieJar } from 'tough-cookie'; -import FileCookieStore from 'tough-cookie-file-store'; -import path from 'node:path'; +import Elysia from "elysia"; +import { config, jar } from "./app"; +import games from "./games/games"; +import platforms from "./games/platforms"; +import auth from "./auth"; -const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json')); -console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath); -const jar = new CookieJar(fileCookieStore); -await login(); - -export async function logout () -{ - if (!config.has('rommAddress')) - { - return; - } - const rommAddress = config.get('rommAddress'); - if (rommAddress) - { - console.log("Logging Out of ROMM"); - try - { - await loginApiLoginPost({ - baseUrl: rommAddress, headers: { - 'cookie': await jar.getCookieString(rommAddress) - } - }); - } catch (error) - { - console.error("Failed to logout of ROMM ", error); - } - } -} - -async function login () -{ - if (!config.has('rommAddress') || !config.has('rommUser')) - { - return; - } - const rommAddress = config.get('rommAddress'); - const rommUser = config.get('rommUser'); - if (rommAddress && rommUser) - { - console.log("Logging In to ROMM"); - const password = await keytar.getPassword('romm', 'gameflow'); - const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` }); - loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress)); - } -} - -export const romm = new Elysia({ prefix: "/romm" }) - .post('/login', async ({ body: { host, username, password } }) => - { - if (config.has('rommAddress') && config.has('rommUser')) - { - await logout(); - const oldRommAddress = config.get('rommAddress'); - if (oldRommAddress) - { - const cookies = await jar.getCookies(oldRommAddress); - cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key)); - } - } - - config.set('rommAddress', host); - config.set('rommUser', username); - - await keytar.setPassword('romm', 'gameflow', password); - await login(); - - return status(200); - }, { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) }) - .get('/login', async () => - { - const credentials = await keytar.getPassword('romm', 'gameflow'); - return { hasPassword: !!credentials }; - }, { response: z.object({ hasPassword: z.boolean() }) }) - .post('/logout', async () => - { - await keytar.deletePassword('romm', 'gameflow'); - await logout(); - const rommAddress = config.get('rommAddress'); - if (rommAddress) - { - const cookies = await jar.getCookies(rommAddress); - cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key)); - } - return status(200); - }) +export default new Elysia({ prefix: "/api/romm" }) + .use([games, platforms, auth]) .all("/*", async ({ request, params, set }) => { if (!config.has('rommAddress') && !config.get('rommAddress')) @@ -119,19 +34,6 @@ export const romm = new Elysia({ prefix: "/romm" }) redirect: 'manual', // avoid ROMM redirects }); - /* - if (rommResponse.status === 403 && config.has('rommUser')) - { - await login(); - headers.set('cookie', await jar.getCookieString(rommUrl.href)); - rommResponse = await fetch(url, { - method: request.method, - headers, - body: await request.arrayBuffer(), - redirect: 'manual', // avoid ROMM redirects - }); - }*/ - set.status = rommResponse.status; rommResponse.headers.forEach((value, key) => { @@ -139,4 +41,5 @@ export const romm = new Elysia({ prefix: "/romm" }) }); return new Response(rommResponse.body, { status: rommResponse.status }); - }).on('stop', logout); \ No newline at end of file + }, { response: z.instanceof(Response) }); + diff --git a/src/bun/api/games/games.ts b/src/bun/api/games/games.ts new file mode 100644 index 0000000..156faf4 --- /dev/null +++ b/src/bun/api/games/games.ts @@ -0,0 +1,245 @@ +import Elysia, { status } from "elysia"; +import { activeGame, config, db, events, setActiveGame, taskQueue } from "../app"; +import { and, eq, getTableColumns } from "drizzle-orm"; +import z from "zod"; +import * as schema from "../schema/app"; +import fs from "node:fs/promises"; +import { FrontEndGameType, FrontEndGameTypeDetailed } from "@shared/constants"; +import { getRomApiRomsIdGet, getRomsApiRomsGet } from "@clients/romm"; +import { InstallJob } from "../jobs/install-job"; +import path from "node:path"; +import { calculateSize, checkInstalled, convertRomToFrontend, convertRomToFrontendDetailed, getLocalGameMatch } from "./services/utils"; +import buildStatusResponse, { getValidLaunchCommandsForGame } from "./services/statusService"; +import { errorToResponse } from "elysia/adapter/bun/handler"; + +export default new Elysia() + .get('/game/local/:id/cover', async ({ params: { id }, set }) => + { + const coverBlob = await db.query.games.findFirst({ columns: { cover: true, cover_type: true }, where: eq(schema.games.id, id) }); + if (!coverBlob || !coverBlob.cover) + { + return status(404); + } + if (coverBlob.cover_type) + { + set.headers["content-type"] = coverBlob.cover_type; + } + return status(200, coverBlob.cover); + }, { response: { 200: z.instanceof(Buffer), 404: z.any() }, params: z.object({ id: z.coerce.number() }) }) + .get('/screenshot/:id', async ({ params: { id }, set }) => + { + const screenshot = await db.query.screenshots.findFirst({ where: eq(schema.screenshots.id, id), columns: { content: true, type: true } }); + if (screenshot) + { + if (screenshot.type) + { + set.headers["content-type"] = screenshot.type; + } + return screenshot.content; + + } + + return status(404); + }, { params: z.object({ id: z.coerce.number() }) }) + .get("/game/local/:id/installed", async ({ params: { id } }) => + { + const data = await db.query.games.findFirst({ where: eq(schema.games.id, id) }); + if (data && data.path_fs) + { + return { installed: await fs.exists(data.path_fs) }; + } + + return { installed: false }; + }, { + params: z.object({ id: z.number() }), + response: z.object({ installed: z.boolean() }) + }).get('/games', async ({ query: { platform_id, collection_id } }) => + { + const where: any[] = []; + if (platform_id) + { + where.push(eq(schema.games.id, platform_id)); + } + + const games: FrontEndGameType[] = []; + + const localGames = await db.select({ + platform_display_name: schema.platforms.name, + id: schema.games.id, + last_played: schema.games.last_played, + created_at: schema.games.created_at, + platform_id: schema.games.platform_id, + slug: schema.games.slug, + name: schema.games.name, + path_fs: schema.games.path_fs, + source_id: schema.games.source_id, + source: schema.games.source + }).from(schema.games).leftJoin(schema.platforms, eq(schema.games.platform_id, schema.platforms.id)).where(and(...where)); + + const localGamesSet = new Set(localGames.map(g => g.source_id)); + games.push(...localGames.map(g => + { + const game: FrontEndGameType = { + ...g, + platform_display_name: g.platform_display_name ?? "Local", + id: { id: g.id, source: 'local' }, + updated_at: g.created_at, + path_cover: `/api/romm/game/local/${g.id}/cover`, + source_id: g.source_id, + source: g.source, + path_platform_cover: `/api/romm/platform/local/${g.platform_id}/cover` + }; + return game; + })); + + const rommGames = await getRomsApiRomsGet({ query: { platform_ids: platform_id ? [platform_id] : undefined, collection_id }, throwOnError: true }); + games.push(...rommGames.data.items.filter(g => !localGamesSet.has(g.id)).map(g => + { + return convertRomToFrontend(g); + })); + + return { games }; + }, { + query: z.object({ platform_id: z.coerce.number().optional(), collection_id: z.coerce.number().optional() }), + }) + .get('/game/:source/:id', async ({ params: { source, id } }) => + { + async function getLocalGameDetailed (match: any) + { + const localGames = await db.select({ + platform_display_name: schema.platforms.name, + ...getTableColumns(schema.games) + }).from(schema.games).where(match).leftJoin(schema.platforms, eq(schema.games.platform_id, schema.platforms.id)); + if (localGames.length > 0) + { + const screenshots = await db.query.screenshots.findMany({ where: eq(schema.screenshots.game_id, localGames[0].id), columns: { id: true } }); + const exists = await checkInstalled(localGames[0].path_fs); + const fileSize = await calculateSize(localGames[0].path_fs); + const game: FrontEndGameTypeDetailed = { + ...localGames[0], + path_cover: `/api/romm/game/local/${localGames[0].id}/cover`, + updated_at: localGames[0].created_at, + id: { id: localGames[0].id, source: 'local' }, + path_platform_cover: `/api/romm/platform/local/${localGames[0].platform_id}/cover`, + fs_size_bytes: fileSize ?? null, + paths_screenshots: screenshots.map(s => `/api/romm/screenshot/${s.id}`), + local: true, + missing: !exists + }; + return game; + } + + return undefined; + } + + if (source === 'local') + { + + const localGame = await getLocalGameDetailed(eq(schema.games.id, id)); + if (localGame) return localGame; + return status('Not Found'); + } + else + { + + const localGame = await getLocalGameDetailed(getLocalGameMatch(id, source)); + if (localGame) return localGame; + + const rom = await getRomApiRomsIdGet({ path: { id } }); + if (rom.data) + { + const romGame = convertRomToFrontendDetailed(rom.data); + return romGame; + } + + return status("Not Found", rom.response); + } + + }, { + params: z.object({ source: z.string(), id: z.coerce.number() }) + }) + .get('/status/:source/:id', async ({ params: { source, id }, set }) => + { + set.headers["content-type"] = 'text/event-stream'; + set.headers["cache-control"] = 'no-cache'; + set.headers['connection'] = 'keep-alive'; + return buildStatusResponse(source, id); + }, { + response: z.any(), + params: z.object({ id: z.coerce.number(), source: z.string() }), + query: z.object({ isLocal: z.boolean().optional() }) + }) + .delete('/game/:source/:id', async ({ params: { source, id } }) => + { + const deleted = await db.delete(schema.games).where(getLocalGameMatch(id, source)).returning({ path_fs: schema.games.path_fs }); + const downloadPath = config.get('downloadPath'); + await Promise.all(deleted.filter(d => !!d.path_fs).map(async d => + { + await fs.rm(path.join(downloadPath, d.path_fs!), { recursive: true, force: true }); + })); + + return status(deleted.length > 0 ? 'OK' : 'Not Modified'); + }, { + params: z.object({ id: z.coerce.number(), source: z.string() }), + }) + .post('/game/:source/:id/install', async ({ params: { id, source } }) => + { + if (!taskQueue.hasActive()) + { + taskQueue.enqueue(`install-rom-${source}-${id}`, new InstallJob(id)); + return status(200); + } else + { + return status('Not Implemented'); + } + }, { + params: z.object({ id: z.coerce.number(), source: z.string() }), + response: z.any() + }) + .post('/game/:source/:id/play', async ({ params: { id, source }, set }) => + { + const validCommand = await getValidLaunchCommandsForGame(source, id); + if (validCommand) + { + if (validCommand instanceof Error) + { + return errorToResponse(validCommand, set); + } + else + { + + if (activeGame && activeGame.process.killed === false) + { + return status('Conflict', `${activeGame.name} currently running`); + } + + const localGame = await db.query.games.findFirst({ + where: eq(schema.games.id, validCommand.gameId), columns: { + name: true + + } + }); + + const game = setActiveGame({ + process: Bun.spawn({ + cmd: validCommand.command.command.split(' '), onExit (subprocess, exitCode, signalCode, error) + { + events.emit('activegameexit', { subprocess, exitCode, signalCode, error }); + }, + }), + name: localGame?.name ?? "Unknown", + gameId: validCommand.gameId, + command: validCommand.command.command + }); + + await game.process.exited; + if (game.process.exitCode && game.process.exitCode > 0) + { + return status('Internal Server Error'); + } + return status('OK'); + } + } + }, { + params: z.object({ id: z.coerce.number(), source: z.string() }), + }); \ No newline at end of file diff --git a/src/bun/api/games/platforms.ts b/src/bun/api/games/platforms.ts new file mode 100644 index 0000000..c342059 --- /dev/null +++ b/src/bun/api/games/platforms.ts @@ -0,0 +1,86 @@ +import Elysia, { status } from "elysia"; +import { getPlatformApiPlatformsIdGet, getPlatformsApiPlatformsGet } from "@clients/romm"; +import z from "zod"; +import { count, eq, getTableColumns, notInArray } from "drizzle-orm"; +import { db } from "../app"; +import { FrontEndPlatformType } from "@shared/constants"; +import * as schema from "../schema/app"; + +export default new Elysia() + .get('/platforms', async () => + { + const platforms: FrontEndPlatformType[] = []; + let rommPlatformsSet: Set | undefined; + const { data: rommPlatforms } = await getPlatformsApiPlatformsGet(); + if (rommPlatforms) + { + const frontEndPlatforms = rommPlatforms.map(p => + { + const platform: FrontEndPlatformType = { + slug: p.slug, + name: p.display_name, + family_name: p.family_name, + path_cover: `/api/romm/assets/platforms/${p.slug}.svg`, + game_count: p.rom_count, + updated_at: new Date(p.updated_at), + id: { source: 'romm', id: p.id }, + source: null, + source_id: null + }; + + return platform; + }); + rommPlatformsSet = new Set(rommPlatforms.map(p => p.slug)); + platforms.push(...frontEndPlatforms); + } + + const localPlatforms = await db.select({ ...getTableColumns(schema.platforms), game_count: count(schema.games.id) }) + .from(schema.platforms) + .leftJoin(schema.games, eq(schema.games.platform_id, schema.platforms.id)) + .groupBy(schema.platforms.id) + .where(notInArray(schema.platforms.slug, Array.from(rommPlatformsSet ?? []))); + platforms.push(...localPlatforms.map(p => + { + const platform: FrontEndPlatformType = { + slug: p.slug, + name: p.name, + family_name: p.family_name, + path_cover: `/api/romm/platform/local/${p.id}/cover`, + game_count: p.game_count, + updated_at: p.created_at, + id: { source: 'local', id: p.id }, + source: null, + source_id: null + }; + + return platform; + })); + + return { platforms }; + }).get('/platforms/:source/:id', async ({ params: { source, id } }) => + { + const rommPlatform = await getPlatformApiPlatformsIdGet({ path: { id } }); + if (rommPlatform.data) + { + return rommPlatform.data; + } + + return status("Not Found", rommPlatform.response); + }, { params: z.object({ source: z.string(), id: z.coerce.number() }) }).get('/platform/local/:id/cover', async ({ params: { id }, set }) => + { + const coverBlob = await db.query.platforms.findFirst({ + columns: { + cover: true, cover_type: true + + }, where: eq(schema.platforms.id, id) + }); + if (!coverBlob || !coverBlob.cover) + { + return status(404); + } + if (coverBlob.cover_type) + { + set.headers["content-type"] = coverBlob.cover_type; + } + return status(200, coverBlob.cover); + }, { response: { 200: z.instanceof(Buffer), 404: z.any() }, params: z.object({ id: z.coerce.number() }) }); \ No newline at end of file diff --git a/src/bun/api/games/services/launchGameService.ts b/src/bun/api/games/services/launchGameService.ts new file mode 100644 index 0000000..4f87e66 --- /dev/null +++ b/src/bun/api/games/services/launchGameService.ts @@ -0,0 +1,219 @@ +import path, { basename, dirname } from 'node:path'; +import { which } from 'bun'; +import fs from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import * as schema from '../../schema/emulators'; +import { eq } from 'drizzle-orm'; +import { config, emulatorsDb } from '../../app'; +import os from 'node:os'; + +export const varRegex = /%([^%]+)%/g; + +interface CommandEntry +{ + label?: string; + command: string; + valid: boolean; + emulator?: string; +} + +export async function getValidLaunchCommands (data: { + systemSlug: string; + gamePath: string; + customEmulatorConfig: { + get: (id: string) => string | undefined, + has: (id: string) => boolean, + }; +}): Promise +{ + + const system = await emulatorsDb.query.systems.findFirst({ with: { commands: true }, where: eq(schema.systems.name, data.systemSlug) }); + + if (!system) + { + throw new Error(`Could not find system '${data.systemSlug}'`); + } + + if (!system.extension || system.extension.length <= 0) + { + throw new Error(`No extensions listed for system '${data.systemSlug}'`); + } + + const downloadPath = config.get('downloadPath'); + const gamePath = path.join(downloadPath, data.gamePath); + + const validFiles: string[] = []; + if (!existsSync(gamePath)) + { + throw new Error(`Provided rom path is missing: '${gamePath}'`); + } + + const gamePathStat = await fs.stat(gamePath); + + const extensionList = system.extension.join(','); + + if (gamePathStat.isDirectory()) + { + for await (const file of fs.glob(path.join(gamePath, `/**/*.{${extensionList}}`))) + { + validFiles.push(file); + } + + if (validFiles.length <= 0) + { + throw new Error(`Could not find valid rom file. Must be a file that ends in '${extensionList}'`); + } + } else + { + if (system.extension.some(e => gamePath.toLocaleLowerCase().endsWith(e.toLocaleLowerCase()))) + { + validFiles.push(gamePath); + } + else + { + throw new Error(`Invalid Rom File. Must be a file that ends in '${extensionList}'`); + } + } + + const formattedCommands = await Promise.all(system.commands.map(async command => + { + const label = command.label; + const cmd = command.command; + + const matches = cmd.match(varRegex); + if (matches) + { + let emulator: string | undefined = undefined; + const varList = await Promise.all(matches.map(async (value) => + { + if (value.startsWith("%EMULATOR_")) + { + const emulatorName = value.substring("%EMULATOR_".length, value.length - 1); + let exec = await findExec(emulatorName); + if (data.customEmulatorConfig.has(emulatorName)) + { + exec = data.customEmulatorConfig.get(emulatorName); + } + + emulator = emulatorName; + return [value, exec]; + } + + const key = value.substring(1, value.length - 1); + return [value, process.env[key]]; + })); + const vars = Object.fromEntries(varList); + vars['%ROM%'] = validFiles[0]; + vars['%ESPATH%'] = config.get('downloadPath'); + + // missing variable + const invalid = Object.entries(vars).find(c => c[1] === undefined); + + const command = cmd.replace(varRegex, (s) => vars[s] ?? ''); + return { label: label ?? undefined, command, valid: !invalid, emulator } satisfies CommandEntry; + } + })); + + return formattedCommands.filter(c => !!c); +} + +export async function findExec (emulatorName: string) +{ + const emulator = await emulatorsDb.query.emulators.findFirst({ where: eq(schema.emulators.name, emulatorName) }); + if (!emulator) + { + throw new Error(`Could not find emulator ${emulatorName}`); + } + if (os.platform() === 'win32') + { + const regValues = emulator.winregistrypath; + if (regValues.length > 0) + { + for (const node of regValues) + { + const registryValue = await readRegistryValue(node); + if (registryValue) + { + return registryValue; + } + } + + } + } + + const systempaths = emulator.systempath; + if (systempaths.length > 0) + { + const systemPath = await resolveSystemPath(systempaths); + if (systemPath) + { + return systemPath; + } + } + + const staticPaths = emulator.staticpath; + if (staticPaths.length > 0) + { + const staticPath = await resolveStaticPath(staticPaths); + if (staticPath) + { + return staticPath; + } + } +} + +async function readRegistryValue (text: string) +{ + const params = text.split('|'); + const key = dirname(params[0]); + const value = basename(params[0]); + const bin = params.length > 1 ? params[1] : undefined; + + const proc = Bun.spawn({ + cmd: ["reg", "QUERY", key, "/v", value], + stdout: "pipe", + stderr: "pipe", + }); + + const output = await new Response(proc.stdout).text(); + await proc.exited; + + if (!output.includes(value)) return null; + + const lines = output.split("\n"); + for (const line of lines) + { + if (line.includes(value)) + { + const parts = line.trim().split(/\s{4,}/); + return bin ? path.join(parts[2], bin) : parts[2]; // registry value + } + } + + return null; +} + +async function resolveStaticPath (entries: string[]) +{ + for (const entry of entries) + { + for await (const match of fs.glob(entry)) + { + return match; + } + } + return null; +} + +async function resolveSystemPath (entries: string[]) +{ + for (const entry of entries) + { + try + { + const found = which(entry); + return found; + } catch { } + } + return null; +} \ No newline at end of file diff --git a/src/bun/api/games/services/statusService.ts b/src/bun/api/games/services/statusService.ts new file mode 100644 index 0000000..8037c25 --- /dev/null +++ b/src/bun/api/games/services/statusService.ts @@ -0,0 +1,171 @@ +import { GameInstallProgress, GameStatusType, } from "@shared/constants"; +import { activeGame, customEmulators, db, events, taskQueue } from "../../app"; +import { getValidLaunchCommands } from "./launchGameService"; +import * as schema from '../../schema/app'; +import { eq } from "drizzle-orm"; +import { getErrorMessage } from "@/bun/utils"; +import { getLocalGameMatch } from "./utils"; + +class CommandSearchError extends Error +{ + constructor(status: GameStatusType, message: string) + { + super(message); + this.name = status; + } +} + +export async function getLocalGame (source: string, id: number) +{ + const localGames = await db.select({ id: schema.games.id, path_fs: schema.games.path_fs, platform_slug: schema.platforms.es_slug }) + .from(schema.games) + .where(getLocalGameMatch(id, source)) + .leftJoin(schema.platforms, eq(schema.games.platform_id, schema.platforms.id)); + + if (localGames.length > 0) + { + return localGames[0]; + } + + return undefined; +} + +export async function getValidLaunchCommandsForGame (source: string, id: number) +{ + const localGame = await getLocalGame(source, id); + if (localGame) + { + if (localGame.platform_slug) + { + if (localGame.path_fs) + { + try + { + const commands = await getValidLaunchCommands({ systemSlug: localGame.platform_slug, customEmulatorConfig: customEmulators, gamePath: localGame.path_fs }); + const validCommand = commands.find(c => c.valid); + if (validCommand) + { + return { command: validCommand, gameId: localGame.id, source: source, sourceId: id }; + + } + else + { + return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator).join(',')}`); + } + } catch (error) + { + console.error(error); + return new CommandSearchError('error', getErrorMessage(error)); + } + + } else + { + return new CommandSearchError('error', 'Missing Path'); + } + } + else + { + return new CommandSearchError('error', 'Missing Platform'); + } + + } + + return undefined; +} + +export default async function buildStatusResponse (source: string, id: number) +{ + let cleanup: (() => void) | undefined; + return new Response(new ReadableStream({ + async start (controller) + { + function enqueue (data: GameInstallProgress, event?: 'error' | 'refresh') + { + const evntString = event ? `event: ${event}\n` : ''; + controller.enqueue(`${evntString}data: ${JSON.stringify(data)}\n\n`); + } + + const sourceId = `${source}-${id}`; + + async function sendLatests () + { + const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(id, source), columns: { id: true } }); + const activeTask = taskQueue.findJob(`install-rom-${source}-${id}`); + if (activeTask) + { + enqueue({ + progress: activeTask.progress, + status: activeTask.state as any + }); + + } else if (activeGame && activeGame.gameId === localGame?.id) + { + enqueue({ status: 'playing' as GameStatusType, details: 'Playing' }); + } + else + { + const validCommand = await getValidLaunchCommandsForGame(source, id); + if (validCommand) + { + if (validCommand instanceof Error) + { + enqueue({ status: validCommand.name as GameStatusType, error: validCommand.message }); + } + else + { + enqueue({ status: 'installed', details: validCommand.command.label }); + } + + } else + { + enqueue({ status: 'install', details: 'Install' }); + } + } + } + + await sendLatests(); + + const dispose: Function[] = []; + const handleActiveExit = async () => + { + await sendLatests(); + }; + events.on('activegameexit', handleActiveExit); + dispose.push(() => events.off('activegameexit', handleActiveExit)); + dispose.push(taskQueue.on('progress', ({ id, progress, state }) => + { + if (id.endsWith(sourceId)) + { + enqueue({ progress, status: state as any }); + } + })); + dispose.push(taskQueue.on('completed', ({ id }) => + { + if (id.endsWith(sourceId)) + { + enqueue({}, 'refresh'); + } + })); + dispose.push(taskQueue.on('error', ({ id, error }) => + { + if (id.endsWith(sourceId)) + { + enqueue({ + status: 'error', + error: error + }, 'error'); + } + })); + + cleanup = () => + { + dispose.forEach(f => f()); + }; + }, + cancel (reason) + { + cleanup?.(); + cleanup = undefined; + }, + })); +} \ No newline at end of file diff --git a/src/bun/api/games/services/utils.ts b/src/bun/api/games/services/utils.ts new file mode 100644 index 0000000..7f682aa --- /dev/null +++ b/src/bun/api/games/services/utils.ts @@ -0,0 +1,65 @@ +import getFolderSize from "get-folder-size"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { config } from "../../app"; +import { and, eq } from "drizzle-orm"; +import * as schema from "../../schema/app"; +import { FrontEndGameType, FrontEndGameTypeDetailed } from "@shared/constants"; +import { DetailedRomSchema, SimpleRomSchema } from "@clients/romm"; + +export async function calculateSize (installPath: string | null) +{ + if (!installPath) return null; + return (await getFolderSize(path.join(config.get('downloadPath'), installPath))).size; +} + +export async function checkInstalled (installPath: string | null) +{ + if (!installPath) return false; + return fs.exists(path.join(config.get('downloadPath'), installPath)); +} + +export function getLocalGameMatch (id: number, source: string) +{ + return source !== 'local' ? and(eq(schema.games.source_id, id), eq(schema.games.source, source)) : eq(schema.games.id, id); +} + +export function convertRomToFrontend (rom: SimpleRomSchema): FrontEndGameType +{ + const game: FrontEndGameType = { + id: { id: rom.id, source: 'romm' }, + path_cover: `/api/romm${rom.path_cover_large}`, + last_played: rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null, + updated_at: new Date(rom.updated_at), + slug: rom.slug, + platform_id: rom.platform_id, + platform_display_name: rom.platform_display_name, + name: rom.name, + path_fs: null, + path_platform_cover: `/api/romm/assets/platforms/${rom.platform_slug}.svg`, + source: null, + source_id: null + }; + + return game; +} + +export function convertRomToFrontendDetailed (rom: DetailedRomSchema) +{ + const detailed: FrontEndGameTypeDetailed = { + ...convertRomToFrontend(rom), + summary: rom.summary, + fs_size_bytes: rom.fs_size_bytes, + paths_screenshots: rom.merged_screenshots.map(s => `/api/romm${s}`), + local: false, + missing: rom.missing_from_fs + }; + if (rom.merged_ra_metadata?.achievements) + { + detailed.achievements = { + unlocked: rom.merged_ra_metadata.achievements?.map(a => a.num_awarded).length, + total: rom.merged_ra_metadata.achievements.length + }; + } + return detailed; +} \ No newline at end of file diff --git a/src/bun/api/jobs/install-job.ts b/src/bun/api/jobs/install-job.ts new file mode 100644 index 0000000..90e0a25 --- /dev/null +++ b/src/bun/api/jobs/install-job.ts @@ -0,0 +1,180 @@ +import { IJob, JobContext } from "../task-queue"; +import { mkdir } from 'node:fs/promises'; +import { eq, or } from 'drizzle-orm'; +import fs from 'node:fs/promises'; +import { DownloaderHelper } from 'node-downloader-helper'; +import StreamZip from 'node-stream-zip'; +import * as schema from "../schema/app"; +import * as emulatorSchema from "../schema/emulators"; +import path from 'node:path'; +import { getPlatformApiPlatformsIdGet, getRomApiRomsIdGet } from "@clients/romm"; +import { config, db, emulatorsDb, jar } from "../app"; + +interface JobConfig +{ + dryRun?: boolean; + dryDownload?: boolean; +} + +export class InstallJob implements IJob +{ + public id: number; + + public config?: JobConfig; + + constructor(id: number, config?: JobConfig) + { + this.id = id; + this.config = config; + } + + public async start (cx: JobContext) + { + cx.setProgress(0, 'download'); + fs.mkdir(config.get('downloadPath'), { recursive: true }); + + if (this.config?.dryRun !== true) + { + const downloadPath = config.get('downloadPath'); + + if (this.config?.dryDownload !== true) + { + // download files for rom + const downloadUrl = new URL(`${config.get('rommAddress')}/api/roms/download`); + downloadUrl.searchParams.set('rom_ids', String(this.id)); + const downloader = new DownloaderHelper(downloadUrl.href, downloadPath, { + headers: { + cookie: await jar.getCookieString(config.get('rommAddress') ?? '') + }, + fileName: `${this.id}.zip`, + // Romm doesn't support resume download + override: true + }); + + cx.abortSignal.addEventListener('abort', downloader.stop); + + downloader.on('progress.throttled', e => + { + cx.setProgress(e.progress, 'download'); + }); + + downloader.on('error', (e) => + { + cx.abort(e); + }); + const finishPromise = new Promise(resolve => + { + downloader.on("end", ({ filePath }) => resolve(filePath)); + }); + + await downloader.start().catch(err => console.error(err)); + const zipFilePath = await finishPromise; + + cx.setProgress(0, 'extract'); + + const zip = new StreamZip.async({ file: zipFilePath }); + const totalCount = await zip.entriesCount; + let extractCount = 0; + zip.on('extract', async (entry, file) => + { + console.log(`Extracted ${entry.name} to ${file}`); + cx.setProgress(extractCount / totalCount * 100, 'extract'); + extractCount++; + }); + await zip.extract(null, downloadPath); + await zip.close(); + + await fs.rm(zipFilePath); + } + + const rom = (await getRomApiRomsIdGet({ path: { id: this.id }, throwOnError: true })).data; + const romPlatform = (await getPlatformApiPlatformsIdGet({ path: { id: rom.platform_id }, throwOnError: true })).data; + + if (this.config?.dryDownload === true) + { + rom.files.length; + await mkdir(path.join(downloadPath, rom.fs_path, rom.fs_name), { recursive: true }); + } + + // pre-fetch screenshots + const screenshots = await Promise.all(rom.merged_screenshots.map(s => fetch(`${config.get('rommAddress')}${s}`))); + + const rommAddress = config.get('rommAddress'); + const coverResponse = await fetch(`${rommAddress}${rom.path_cover_large}`); + + if (cx.abortSignal.aborted) return; + + await db.transaction(async (tx) => + { + // Search for existing platform + const platformSearch = []; + if (romPlatform.igdb_id) platformSearch.push(eq(schema.platforms.igdb_id, romPlatform.igdb_id)); + if (romPlatform.igdb_slug) platformSearch.push(eq(schema.platforms.igdb_slug, romPlatform.igdb_slug)); + if (romPlatform.ra_id) platformSearch.push(eq(schema.platforms.ra_id, romPlatform.ra_id)); + if (romPlatform.slug) platformSearch.push(eq(schema.platforms.slug, romPlatform.slug)); + if (romPlatform.moby_id) platformSearch.push(eq(schema.platforms.moby_id, romPlatform.moby_id)); + + const esPlatform = await emulatorsDb + .select({ slug: emulatorSchema.systems.name, romm_slug: emulatorSchema.systemMappings.sourceSlug }) + .from(emulatorSchema.systems) + .leftJoin(emulatorSchema.systemMappings, eq(emulatorSchema.systemMappings.source, 'romm')) + .where(eq(emulatorSchema.systemMappings.sourceSlug, romPlatform.slug)); + + const existingPlatform = await tx.query.platforms.findFirst({ where: or(...platformSearch) }); + let platformId: number; + if (!existingPlatform) + { + // Create new local platform + const platformCover = await fetch(`${rommAddress}/assets/platforms/${romPlatform.slug.toLocaleLowerCase()}.svg`); + const platform: typeof schema.platforms.$inferInsert = { + slug: romPlatform.slug, + igdb_id: romPlatform.igdb_id, + igdb_slug: romPlatform.igdb_slug, + ra_id: romPlatform.ra_id, + cover: Buffer.from(await platformCover.arrayBuffer()), + cover_type: platformCover.headers.get('content-type'), + name: romPlatform.name, + family_name: romPlatform.family_name, + es_slug: esPlatform.length > 0 ? esPlatform[0].slug : undefined + }; + // TODO: add ES slug once I have better way to query ES + const [{ id }] = await tx.insert(schema.platforms).values(platform).returning({ id: schema.platforms.id }); + platformId = id; + } else + { + platformId = existingPlatform.id; + } + + // create the rom + const game: typeof schema.games.$inferInsert = { + source_id: rom.id, + source: 'romm', + slug: rom.slug, + path_fs: path.join(rom.fs_path, rom.fs_name), + last_played: rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null, + platform_id: platformId, + igdb_id: rom.igdb_id, + ra_id: rom.ra_id, + summary: rom.summary, + name: rom.name, + cover: Buffer.from(await coverResponse.arrayBuffer()), + cover_type: coverResponse.headers.get('content-type') + }; + + // Save screenshots and update database + const [{ id }] = await tx.insert(schema.games).values(game).returning({ id: schema.games.id }); + await tx.insert(schema.screenshots).values(await Promise.all(screenshots.map(async (response) => + { + const screenshot: typeof schema.screenshots.$inferInsert = { + game_id: id, + content: Buffer.from(await response.arrayBuffer()), + type: response.headers.get('content-type') + }; + + return screenshot; + }))); + }); + } + + } +} \ No newline at end of file diff --git a/src/bun/api/rpc.ts b/src/bun/api/rpc.ts index eb2e933..87e0503 100644 --- a/src/bun/api/rpc.ts +++ b/src/bun/api/rpc.ts @@ -1,16 +1,17 @@ -import { RPC_PORT } from "../../shared/constants"; -import { settings } from "./settings"; -import { romm } from "./clients"; -import Elysia from "elysia"; import { cors } from "@elysiajs/cors"; +import Elysia from "elysia"; +import { RPC_PORT } from "../../shared/constants"; import { host } from "../utils"; +import clients from "./clients"; +import { settings } from "./settings"; +import { system } from "./system"; -const api = new Elysia({ prefix: "/api", serve: {} }) - .use(cors()) - .use(romm) - .use(settings); +const api = new Elysia({ serve: {} }) + .use([cors(), clients, settings, system]); -export type AppType = typeof api; +export type RommAPIType = typeof clients; +export type SettingsAPIType = typeof settings; +export type SystemAPIType = typeof system; export function RunAPIServer () { @@ -19,24 +20,11 @@ export function RunAPIServer () apiServer: api.listen({ port: RPC_PORT, hostname: host, - development: process.env.NODE_ENV === 'development', - fetch (req, server) - { - if (server.upgrade(req, { - data: undefined - })) - { - return; - } - return api.fetch(req); - }, - websocket: { - message (ws, message) - { + development: process.env.NODE_ENV === 'development' + }), + async cleanup () + { - - }, - } - }) + } }; } \ No newline at end of file diff --git a/src/bun/api/schema/app.ts b/src/bun/api/schema/app.ts new file mode 100644 index 0000000..d4e7315 --- /dev/null +++ b/src/bun/api/schema/app.ts @@ -0,0 +1,54 @@ +import { sql } from "drizzle-orm"; +import { integer, text, sqliteTable, blob } from "drizzle-orm/sqlite-core"; + +export const games = sqliteTable('games', { + id: integer('id').primaryKey({ autoIncrement: true }), + source_id: integer('source_id').unique(), + source: text("source"), + igdb_id: integer("igdb_id").unique(), + name: text("name"), + ra_id: integer('ra_id').unique(), + path_fs: text("path_fs"), + last_played: integer("last_played", { mode: 'timestamp' }), + created_at: integer("created_at", { mode: 'timestamp' }).default(sql`(unixepoch())`).notNull(), + metadata: text("metadata", { mode: 'json' }).default(sql`'{}'`), + slug: text("slug").unique(), + platform_id: integer("platform_id").references(() => platforms.id, { onUpdate: 'cascade' }).notNull(), + cover: blob("cover", { mode: 'buffer' }), + cover_type: text('type'), + summary: text("summary") +}); + +export const platforms = sqliteTable('platforms', { + id: integer("id").primaryKey({ autoIncrement: true }), + igdb_id: integer("igdb_id").unique(), + igdb_slug: text("igdb_slug").unique(), + moby_id: integer("moby_id").unique(), + name: text("name").notNull(), + es_slug: text('es_slug').unique(), + ra_id: integer('ra_id').unique(), + created_at: integer("created_at", { mode: 'timestamp' }).default(sql`(unixepoch())`).notNull(), + slug: text("slug").unique().notNull(), + metadata: text("metadata", { mode: 'json' }), + cover: blob("cover", { mode: 'buffer' }), + cover_type: text('type'), + family_name: text("family_name") +}); + +export const collections_games = sqliteTable('collections_games', { + collection_id: integer('collection_id').notNull().references(() => collections.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + game_id: integer('game_id').notNull().references(() => games.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + created_at: integer("created_at", { mode: 'timestamp' }).default(sql`(unixepoch())`).notNull(), +}); + +export const collections = sqliteTable('collections', { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text('name') +}); + +export const screenshots = sqliteTable('screenshots', { + id: integer("id").primaryKey({ autoIncrement: true }), + game_id: integer('game_id').references(() => games.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + content: blob('content', { mode: 'buffer' }).notNull(), + type: text('type') +}); \ No newline at end of file diff --git a/src/bun/api/schema/emulators.ts b/src/bun/api/schema/emulators.ts new file mode 100644 index 0000000..67262c4 --- /dev/null +++ b/src/bun/api/schema/emulators.ts @@ -0,0 +1,43 @@ +import { relations, sql } from "drizzle-orm"; +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const emulators = sqliteTable('emulators', { + name: text().primaryKey().unique(), + systempath: text({ mode: 'json' }).notNull().$type().default(sql`(json_array())`), + staticpath: text({ mode: 'json' }).notNull().$type().default(sql`(json_array())`), + corepath: text({ mode: 'json' }).notNull().$type().default(sql`(json_array())`), + androidpackage: text({ mode: 'json' }).notNull().$type().default(sql`(json_array())`), + winregistrypath: text({ mode: 'json' }).notNull().$type().default(sql`(json_array())`), +}); + +export const systems = sqliteTable('systems', { + name: text().primaryKey().unique(), + fullname: text(), + path: text(), + extension: text({ mode: 'json' }).notNull().$type().default(sql`(json_array())`) +}); + +export const systemsRelations = relations(systems, ({ many }) => +({ + commands: many(commands) +})); + +export const systemMappings = sqliteTable('systemMappings', { + source: text(), + sourceSlug: text(), + sourceId: integer(), + system: text().notNull().references(() => systems.name) +}); + +export const commands = sqliteTable('commands', { + system: text().references(() => systems.name, { onDelete: 'cascade', onUpdate: 'cascade' }), + label: text(), + command: text().notNull() +}); + +export const commandsRelations = relations(commands, ({ one }) => ({ + author: one(systems, { + fields: [commands.system], + references: [systems.name], + }), +})); \ No newline at end of file diff --git a/src/bun/api/secrets.ts b/src/bun/api/secrets.ts new file mode 100644 index 0000000..e37dab8 --- /dev/null +++ b/src/bun/api/secrets.ts @@ -0,0 +1,133 @@ +import Conf from "conf"; +import projectPackage from '~/package.json'; +import crypto from 'node:crypto'; +import fs from 'node:fs/promises'; +import os from 'node:os'; + +let secrets: ISecrets; + +interface ISecrets +{ + set (data: { service: string, name: string, value: string; }): Promise; + get (data: { service: string, name: string; }): Promise; + delete (data: { service: string, name: string; }): Promise; +} + +class BunSecrets implements ISecrets +{ + public set (data: { service: string, name: string, value: string; }) + { + return Bun.secrets.set(data); + } + + public get (data: { service: string, name: string; }) + { + return Bun.secrets.get(data); + } + + public delete (data: { service: string, name: string; }) + { + return Bun.secrets.delete(data); + } +} + +class FallbackSecrets implements ISecrets +{ + config: Conf>; + machineKey?: Buffer; + + constructor() + { + this.config = new Conf>({ + projectName: projectPackage.name, + projectSuffix: 'bun', + configFileMode: 0o600, + configName: 'secrets' + }); + console.log("Secrets Store Located at: ", this.config.path); + } + + async getMachineKey () + { + if (!this.machineKey) + { + + let raw: string; + try + { + raw = await fs.readFile("/etc/machine-id", 'utf-8'); + } catch (error) + { + raw = [ + os.homedir(), + os.userInfo().username, + os.platform(), + os.arch(), + os.cpus().map(c => c.model).join(','), + String(os.totalmem()) + ].filter(Boolean).join("|"); + } + this.machineKey = crypto.createHash('sha256').update(raw.trim()).digest(); + } + + return this.machineKey; + } + + public async set ({ service, name, value }: { service: string, name: string, value: string; }) + { + const iv = crypto.randomBytes(16); + const key = await this.getMachineKey(); + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + const encrypted = Buffer.concat([ + iv, + cipher.update(value, "utf-8"), + cipher.final() + ]); + return this.config.set(`${service}-${name}`, encrypted.toString('base64')); + } + + public async get ({ service, name }: { service: string, name: string; }) + { + const rawBase = this.config.get(`${service}-${name}`); + if (!rawBase) + { + return null; + } + try + { + const key = await this.getMachineKey(); + const raw = Buffer.from(rawBase, 'base64'); + + const iv = raw.subarray(0, 16); + const ciphertext = raw.subarray(16); + const cipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + const data = Buffer.concat([cipher.update(ciphertext), cipher.final()]).toString("utf-8"); + + return data; + } catch (error) + { + console.error(error); + return null; + } + } + + public async delete ({ service, name }: { service: string, name: string; }) + { + this.config.delete(`${service}-${name}`); + return true; + } +} + +/* +try +{ + await Bun.secrets.get({ service: 'test', name: 'test' }); + secrets = new BunSecrets(); +} catch +{ + secrets = new FallbackSecrets(); +}*/ + +secrets = new FallbackSecrets(); + +export default secrets; \ No newline at end of file diff --git a/src/bun/api/settings.ts b/src/bun/api/settings.ts index 118d41c..2914194 100644 --- a/src/bun/api/settings.ts +++ b/src/bun/api/settings.ts @@ -1,18 +1,95 @@ import z from "zod"; -import { SettingsSchema, SettingsType } from "../../shared/constants"; -import Conf from "conf"; -import projectPackage from '../../../package.json'; +import { SettingsSchema } from "@shared/constants"; import Elysia from "elysia"; +import { config, customEmulators, db, emulatorsDb } from "./app"; +import * as appSchema from './schema/app'; +import { findExec } from "./games/services/launchGameService"; +import * as emulatorSchema from "./schema/emulators"; +import { eq, inArray } from 'drizzle-orm'; +import fs from 'node:fs/promises'; -export const config = new Conf({ - projectName: projectPackage.name, - projectSuffix: 'bun', - schema: Object.fromEntries(Object.entries(SettingsSchema.shape).map(([key, schema]) => [key, schema.toJSONSchema() as any])) as any, - defaults: SettingsSchema.parse({}), -}); -console.log("Config Path Located At: ", config.path); +export const settings = new Elysia({ prefix: '/api/settings' }) + .get('/emulators/automatic', async () => + { + const localGames = await db.select({ es_slug: appSchema.platforms.es_slug, platform_id: appSchema.platforms.id }) + .from(appSchema.games) + .leftJoin(appSchema.platforms, eq(appSchema.games.platform_id, appSchema.platforms.id)) + .groupBy(appSchema.platforms.es_slug); -export const settings = new Elysia({ prefix: '/settings' }) + const platformLookup = new Map(localGames.map(g => [g.es_slug, g.platform_id])); + + const commands = await emulatorsDb + .select({ command: emulatorSchema.commands.command, system_slug: emulatorSchema.systems.name }) + .from(emulatorSchema.commands).where(inArray(emulatorSchema.commands.system, Array.from(new Set(localGames.filter(g => g.es_slug).map(s => s.es_slug!))))) + .leftJoin(emulatorSchema.systems, eq(emulatorSchema.systems.name, emulatorSchema.commands.system)); + + + const emulatorCounts: Record = {}; + const emulators = commands + .flatMap(command => + { + const matches = command.command.match(/(?<=%EMULATOR_)[\w-]+(?=%)/); + if (!matches) + { + return undefined; + } + + matches.forEach(m => + { + emulatorCounts[m] = (emulatorCounts[m] ?? 0) + 1; + }); + + return matches?.map(m => [m, command.system_slug] as [string, string]); + } + ).filter(c => !!c); + const uniqueEmulators = new Map(emulators); + + return await Promise.all(Array.from(uniqueEmulators.entries()).map(async ([emulator, system_slug]) => + { + let execPath: string | undefined; + if (customEmulators.has(emulator)) + { + execPath = customEmulators.get(emulator); + } else + { + execPath = await findExec(emulator); + } + + let platform: number | null | undefined = null; + if (emulatorCounts[emulator] <= 1) + { + platform = platformLookup.get(system_slug); + } + + return { emulator: emulator, path: execPath, exists: !!execPath && await fs.exists(execPath), path_cover: platform ? `/api/romm/platform/local/${platform}/cover` : null }; + })); + }, { + response: z.array(z.object({ emulator: z.string(), path: z.string().optional(), exists: z.boolean(), path_cover: z.string().nullable() })) + }) + .put('/emulators/custom/:id', async ({ params: { id }, body: { value } }) => + { + return customEmulators.set(id, value); + }, + { + body: z.object({ value: z.string() }) + }) + .delete('/emulators/custom/:id', async ({ params: { id } }) => + { + return customEmulators.delete(id); + }) + .get('/emulators/custom/:id', async ({ params: { id } }) => + { + return customEmulators.get(id); + }, + { + response: z.string() + }) + .get('/emulators/custom', async () => + { + return Object.keys(customEmulators.store); + }, { + response: z.array(z.string()) + }) .get("/:id", async ({ params: { id } }) => { const value = config.get(id); @@ -25,5 +102,6 @@ export const settings = new Elysia({ prefix: '/settings' }) config.set(id, value); }, { params: z.object({ id: z.keyof(SettingsSchema) }), - body: z.object({ value: z.any() }) + body: z.object({ value: z.any() }), }); + diff --git a/src/bun/api/system.ts b/src/bun/api/system.ts new file mode 100644 index 0000000..a210382 --- /dev/null +++ b/src/bun/api/system.ts @@ -0,0 +1,43 @@ +import Elysia from "elysia"; +import open from 'open'; +import z from "zod"; +import os from 'node:os'; +import { events } from "./app"; +import { isSteamDeckGameMode } from "../utils"; + +// steam://open/keyboard?XPosition=%i&YPosition=%i&Width=%i&Height=%i&Mode=%d +export const system = new Elysia({ prefix: '/api/system' }) + .post('/show_keyboard', async () => + { + if (isSteamDeckGameMode()) + { + open('steam://open/keyboard'); + } + }) + .get('/info', () => + { + return { + homeDir: os.homedir(), + user: os.userInfo().username, + arch: os.arch(), + platform: os.platform(), + hostname: os.hostname(), + steamDeck: process.env.SteamDeck, + machine: os.machine() + }; + }) + .post('/exit', () => + { + if (process.env.PUBLIC_ACCESS) + { + return; + } + + events.emit('exitapp'); + }) + .post('/open', async ({ query: { url } }) => + { + open(url); + }, { + query: z.object({ url: z.url() }) + }); \ No newline at end of file diff --git a/src/bun/api/task-queue.ts b/src/bun/api/task-queue.ts new file mode 100644 index 0000000..39b6623 --- /dev/null +++ b/src/bun/api/task-queue.ts @@ -0,0 +1,207 @@ + +import EventEmitter from 'node:events'; + +export class TaskQueue +{ + private activeQueue: { context: JobContext, promise?: Promise; }[] = []; + private queue?: { context: JobContext, promise?: Promise; }[] = []; + private events?: EventEmitter = new EventEmitter(); + + public enqueue (id: string, job: IJob): Promise + { + this.disposeSafeguard(); + if (!this.queue || !this.events) throw new Error("Queue disposed"); + const context = new JobContext(id, this.events, job); + this.queue.push({ context }); + return this.processQueue(); + } + + private processQueue (): Promise + { + if (!this.queue) return Promise.resolve(); + const top = this.queue.pop(); + if (top) + { + const promise = top.context.start(); + top.promise = promise; + const index = this.queue.length; + this.activeQueue.push(top); + promise.finally(() => + { + this.activeQueue.splice(index, 1); + setTimeout(this.processQueue); + }); + return promise; + } + return Promise.resolve(); + } + + private disposeSafeguard () + { + if (!this.queue) throw new Error("Queue disposed"); + } + + public hasActive () + { + return this.activeQueue.length > 0; + } + + public waitForJob (id: string): Promise + { + return this.queue?.find(j => j.context.id === id)?.promise ?? Promise.resolve(); + } + + public findJob (id: string): IPublicJob | undefined + { + return this.queue?.find(j => j.context.id === id)?.context; + } + + public on (event: E, listener: E extends keyof EventsList ? EventsList[E] extends unknown[] ? (...args: EventsList[E]) => void : never : never): () => void + { + this.events?.on(event, listener); + return () => this.events?.removeListener(event, listener); + } + + public once (event: E, listener: E extends keyof EventsList ? EventsList[E] extends unknown[] ? (...args: EventsList[E]) => void : never : never) + { + this.events?.once(event, listener); + } + + public async close () + { + this.queue = []; + this.activeQueue.forEach(c => c.context.abort()); + return Promise.all(this.activeQueue.map(c => c.promise)); + } +} + +export interface EventsList +{ + progress: [e: ProgressEvent]; + abort: [e: AbortEvent]; + completed: [e: CompletedEvent]; + error: [e: ErrorEvent]; + ended: [e: BaseEvent]; +} + +interface BaseEvent +{ + id: string; + job: IJob; +} + +interface ErrorEvent extends BaseEvent +{ + error: unknown; +} + +interface AbortEvent extends BaseEvent +{ + reason?: any; +} + +interface ProgressEvent extends BaseEvent +{ + progress: number; + state?: string; +} + +interface CompletedEvent extends BaseEvent +{ + +} + +export interface IJob +{ + start (context: JobContext): Promise; +} + +export type JobStatus = 'completed' | 'error' | 'running' | 'waiting' | 'aborted'; + +export interface IPublicJob +{ + progress: number; + state?: string; + status: JobStatus; + job: any; +} + +export class JobContext implements IPublicJob +{ + private m_id: string; + private m_progress: number = 0; + private m_state?: string; + private running: boolean = false; + private aborted: boolean = false; + private completed: boolean = false; + private error?: any; + private events: EventEmitter; + private abortController: AbortController; + private m_job: IJob; + + constructor(id: string, events: EventEmitter, job: IJob) + { + this.m_id = id; + this.m_job = job; + this.abortController = new AbortController(); + this.abortController.signal.addEventListener('abort', () => + { + this.aborted = true; + this.events.emit('abort', { id: this.m_id, reason: this.abortController.signal.reason, job: this.m_job } satisfies AbortEvent); + }); + this.events = events; + } + + public async start (): Promise + { + try + { + await this.m_job.start(this); + this.completed = true; + this.events.emit('completed', { id: this.m_id, job: this.m_job }); + + } catch (error) + { + console.error(error); + this.events.emit('error', { id: this.m_id, error }); + this.error = error; + } finally + { + this.running = false; + this.events.emit('ended', { id: this.m_id, job: this.m_job }); + } + } + + public get status (): JobStatus + { + if (this.completed) return 'completed'; + if (this.error) return 'error'; + if (this.aborted) return 'aborted'; + if (this.running) return 'running'; + return 'waiting'; + } + + public get id () { return this.m_id; } + + public get job () { return this.m_job; } + + public get abortSignal () { return this.abortController.signal; } + + public get progress () { return this.m_progress; } + + public get state () { return this.m_state; } + + public setProgress (progress: number, state?: string) + { + this.m_progress = progress; + if (state) + this.m_state = state; + this.events.emit('progress', { id: this.m_id, progress, state: state ?? this.m_state, job: this.m_job }); + } + + public abort (reason?: any) + { + this.error = reason; + this.abortController.abort(reason); + } +} \ No newline at end of file diff --git a/src/bun/index.ts b/src/bun/index.ts index c3dfbc8..eba7b37 100644 --- a/src/bun/index.ts +++ b/src/bun/index.ts @@ -2,6 +2,8 @@ import { RunBunServer } from './server'; import { RunAPIServer } from './api/rpc'; import { spawnBrowser } from './utils/browser-spawner'; import { BuildParams } from './utils/browser-params'; +import { cleanup as appCleanup, events } from './api/app'; +import os from 'node:os'; const api = RunAPIServer(); let bunServer: { stop: () => void; url: URL; } | undefined; @@ -13,43 +15,80 @@ if (!Bun.env.PUBLIC_ACCESS) async function cleanup () { + await appCleanup(); bunServer?.stop(); await api.apiServer.stop(); + await api.cleanup(); process.exit(0); } -try +if (Bun.env.FORCE_BROWSER) { - const webviewWorker = new Worker(process.env.IS_BINARY ? "./webview-worker.ts" : new URL("./webview-worker", import.meta.url).href, { + await runBrowser(); +} else +{ + try + { + await runWebview(); + } catch (error) + { + await runBrowser(); + } +} + +async function runWebview () +{ + const webviewWorker = new Worker(Bun.env.IS_BINARY ? `./webview/${os.platform()}.ts` : new URL(`./webview/${os.platform()}`, import.meta.url).href, { smol: true, }); - webviewWorker.addEventListener('error', console.error); - await new Promise(resolve => webviewWorker.addEventListener('close', resolve)); + + await new Promise((resolve, reject) => + { + webviewWorker.addEventListener('error', e => + { + console.error(e.message); + reject(e.error); + }); + + webviewWorker.addEventListener('message', (e) => + { + if (e.data === 'destroyed') + { + resolve(true); + } + }); + + events.on('exitapp', () => + { + resolve(true); + }); + }); await cleanup(); } -catch (error) + +async function runBrowser () { - console.error(error); - const browserParams = await BuildParams(); - if (!browserParams) { console.error("Could not find valid browser"); - process.exit(); - } + await cleanup(); + } else + { + const browser = spawnBrowser({ + browser: browserParams.browser.type, + args: browserParams.args, + env: browserParams.env, + detached: false, + execPath: browserParams.browser.path, + source: browserParams.browser.source, + ipc (message) + { + console.log(message); + }, + onExit: cleanup + }); - const browser = spawnBrowser({ - browser: browserParams.browser.type, - args: browserParams.args, - env: browserParams.env, - detached: true, - execPath: browserParams.browser.path, - source: browserParams.browser.source, - ipc (message) - { - console.log(message); - }, - onExit: cleanup - }); + events.on('exitapp', () => browser.kill(15)); + } } \ No newline at end of file diff --git a/src/bun/types.d.ts b/src/bun/types.d.ts deleted file mode 100644 index fa77aaa..0000000 --- a/src/bun/types.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare const IS_BINARY: string; - -declare module 'download-chromium' { - export default function download ({ - platform, - revision = '499413', - log = false, - onProgress = undefined, - installPath = '{__dirname}/.local-chromium' }: { - platform?: 'linux' | 'mac' | 'win32' | 'win64', - revision?: string, - log?: boolean, - installPath?: string, - onProgress?: (percent: number, transferred: number, total: number) => void; - }): Promise - { - - }; -} \ No newline at end of file diff --git a/src/bun/types/types.d.ts b/src/bun/types/types.d.ts new file mode 100644 index 0000000..d97f781 --- /dev/null +++ b/src/bun/types/types.d.ts @@ -0,0 +1,8 @@ +declare const IS_BINARY: string; + +export type ActiveGame = { + process: Bun.Subprocess; + gameId: number; + name: string; + command: string; +}; \ No newline at end of file diff --git a/src/bun/utils.ts b/src/bun/utils.ts index acd0053..d6465d2 100644 --- a/src/bun/utils.ts +++ b/src/bun/utils.ts @@ -16,4 +16,15 @@ export function checkRunning (pid: number) { return error.code === 'EPERM'; } +} + +export function getErrorMessage (error: unknown): string +{ + if (error instanceof Error) return error.message; + return String(error); +} + +export function isSteamDeckGameMode () +{ + return !!Bun.env.SteamDeck; } \ No newline at end of file diff --git a/src/bun/utils/browser-params.ts b/src/bun/utils/browser-params.ts index 4afa59f..0a4c0ce 100644 --- a/src/bun/utils/browser-params.ts +++ b/src/bun/utils/browser-params.ts @@ -2,8 +2,8 @@ import { SERVER_URL } from "../../shared/constants"; import os from 'node:os'; import path, { dirname } from 'node:path'; import { getBrowserPath } from "./get-browser"; -import { config } from "../api/settings"; -import { host } from "../utils"; +import { host, isSteamDeckGameMode } from "../utils"; +import { config } from "../api/app"; export async function BuildParams () { @@ -42,7 +42,15 @@ export async function BuildParams () args.push('--disable-component-update'); args.push('--allow-insecure-localhost'); args.push('--auto-accept-camera-and-microphone-capture'); - args.push(`--window-size=${config.get('windowSize.width')},${config.get('windowSize.height')}`); + + if (isSteamDeckGameMode()) + { + args.push('--kiosk'); + } else + { + args.push(`--window-size=${config.get('windowSize.width')},${config.get('windowSize.height')}`); + } + args.push('--password-store=basic'); args.push('--block-new-web-contents'); args.push('--bwsi'); @@ -82,8 +90,8 @@ export async function BuildParams () if (os.platform() === 'linux') { - args.push("--disable-web-security"); - args.push("--no-sandbox"); + //args.push("--disable-web-security"); + //args.push("--no-sandbox"); } } diff --git a/src/bun/webview/base.ts b/src/bun/webview/base.ts new file mode 100644 index 0000000..69d279b --- /dev/null +++ b/src/bun/webview/base.ts @@ -0,0 +1,17 @@ +import { SERVER_URL } from "@/shared/constants"; +import Webview from "@rcompat/webview"; +import { host } from "../utils"; + +export default function (webview: Webview) +{ + self.addEventListener('message', (e) => + { + console.log("Terminate"); + if (e.data === 'exit') + { + webview.destroy(); + } + }); + webview.navigate(SERVER_URL(host)); + webview.run(); +} \ No newline at end of file diff --git a/src/bun/webview/linux.ts b/src/bun/webview/linux.ts new file mode 100644 index 0000000..8fb8a9f --- /dev/null +++ b/src/bun/webview/linux.ts @@ -0,0 +1,7 @@ +import Webview from "@rcompat/webview"; +import platform from "@rcompat/webview/linux-x64"; +import webviewWorkerBase from "./base"; + +console.log("Launching Webview"); +const webview = new Webview({ debug: import.meta.env.NODE_ENV === 'development', platform }); +webviewWorkerBase(webview); \ No newline at end of file diff --git a/src/bun/webview-worker.ts b/src/bun/webview/win32.ts similarity index 62% rename from src/bun/webview-worker.ts rename to src/bun/webview/win32.ts index 4eafaac..a60197e 100644 --- a/src/bun/webview-worker.ts +++ b/src/bun/webview/win32.ts @@ -1,9 +1,7 @@ import Webview from "@rcompat/webview"; import platform from "@rcompat/webview/windows-x64"; -import { SERVER_URL } from "../shared/constants"; -import { host } from "./utils"; +import webviewWorkerBase from "./base"; console.log("Launching Webview"); const webview = new Webview({ debug: import.meta.env.NODE_ENV === 'development', platform }); -webview.navigate(SERVER_URL(host)); -webview.run(); \ No newline at end of file +webviewWorkerBase(webview); \ No newline at end of file diff --git a/src/mainview/components/AnimatedBackground.tsx b/src/mainview/components/AnimatedBackground.tsx index dec7ce8..ee254d3 100644 --- a/src/mainview/components/AnimatedBackground.tsx +++ b/src/mainview/components/AnimatedBackground.tsx @@ -1,4 +1,5 @@ -import React, { createContext, Ref, useContext, useState } from 'react'; +import classNames from 'classnames'; +import React, { createContext, JSX, Ref, useContext, useEffect, useState } from 'react'; import { twMerge } from 'tailwind-merge'; import { useSessionStorage } from 'usehooks-ts'; @@ -13,15 +14,23 @@ export function AnimatedBackground (data: { animated?: boolean, }) { - const [lastBackgroundUrl, setLastBackgroundUrl] = data.backgroundUrl ? useSessionStorage( + const blurBackground = true; + const animateBackground = true; + + const [lastBackgroundUrl, setLastBackgroundUrl] = data.backgroundKey ? useSessionStorage( `${data.backgroundKey!}-last`, data.backgroundUrl, ) : useState(); - const [backgroundUrl, setBackgroundUrl] = data.backgroundUrl ? useSessionStorage( + const [backgroundUrl, setBackgroundUrl] = data.backgroundKey ? useSessionStorage( data.backgroundKey!, data.backgroundUrl, - ) : useState(data.backgroundUrl); + ) : useState(); + + useEffect(() => + { + setBackgroundUrl(data.backgroundUrl); + }, [data.backgroundUrl]); function handleSetBackground (url: string) { @@ -36,6 +45,20 @@ export function AnimatedBackground (data: { color-mix(in srgb, var(--color-base-100) 80%, transparent) ), url('${url}') center / cover`; + let backgroundElements: JSX.Element | undefined = undefined; + if (true) + { + backgroundElements =
+
+
+
+
+
+
+
+
; + } + return (
{!!lastBackgroundUrl &&
} {!!backgroundUrl &&
} -
- {data.animated &&
-
-
-
-
-
-
-
-
-
+ {blurBackground &&
} + {data.animated && animateBackground &&
+ {backgroundElements}
} {data.children}
diff --git a/src/mainview/components/CardList.tsx b/src/mainview/components/CardList.tsx index 939cacd..39f9618 100644 --- a/src/mainview/components/CardList.tsx +++ b/src/mainview/components/CardList.tsx @@ -3,17 +3,16 @@ import FocusContext, useFocusable, } from "@noriginmedia/norigin-spatial-navigation"; -import { GameMeta } from "../../shared/constants"; -import GameCard, { GameCardSkeleton } from "./GameCard"; -import { JSX, useEffect, useMemo, useState } from "react"; -import { useLocalStorage } from "usehooks-ts"; -import { useScrollSave } from "../scripts/utils"; +import { FrontEndId, GameMeta } from "../../shared/constants"; +import GameCard, { GameCardParams } from "./GameCard"; +import { JSX, useState } from "react"; import classNames from "classnames"; +import { twMerge } from "tailwind-merge"; export interface GameMetaExtra extends GameMeta { - preview?: JSX.Element; - badge?: JSX.Element; + preview?: GameCardParams['preview']; + badges?: JSX.Element[]; focusKey: string; } @@ -22,8 +21,9 @@ export function CardList (data: { type?: string; games: GameMetaExtra[]; grid?: boolean; - onSelectGame?: (id: number) => void; - onGameFocus?: (id: number) => void; + onSelectGame?: (id: string) => void; + onGameFocus?: (id: string) => void; + className?: string; }) { const { ref, focusKey } = useFocusable({ @@ -32,7 +32,7 @@ export function CardList (data: { function BuildGame (g: GameMetaExtra, i: number) { - let preview: JSX.Element | string | undefined = g.preview; + let preview: GameCardParams['preview'] = g.preview; if (!preview && g.previewUrl) { preview = g.previewUrl; @@ -48,11 +48,17 @@ export function CardList (data: { subtitle={g.subtitle ?? ""} onFocus={() => { + g.onFocus?.(); data.onGameFocus?.(g.id); + (document.querySelector(":root") as HTMLElement).style.setProperty('--selected-card-offset', `${i}s`); + }} + onAction={() => + { + g.onSelect?.(); + data.onSelectGame?.(g.id); }} - onAction={() => data.onSelectGame?.(g.id)} preview={preview} - badge={g.badge} + badges={g.badges} id={g.id} /> ); @@ -64,8 +70,9 @@ export function CardList (data: { id={`card-list-${data.id}`} ref={ref} save-child-focus="session" - className={classNames("my-6 items-center justify-center-safe h-(--game-card-height) ", - data.grid ? "card-grid h-fit gap-5" : 'card-list gap-6' + className={twMerge("my-6 items-center justify-center-safe h-(--game-card-height) ", + data.grid ? "card-grid h-fit gap-5" : 'card-list gap-6', + data.className )} onKeyDown={(e) => { diff --git a/src/mainview/components/CollectionsDetail.tsx b/src/mainview/components/CollectionsDetail.tsx index 080a2ad..095ed68 100644 --- a/src/mainview/components/CollectionsDetail.tsx +++ b/src/mainview/components/CollectionsDetail.tsx @@ -3,9 +3,7 @@ import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-naviga import { HeaderUI } from './Header'; import { GameList, GameListFilter } from './GameList'; import { Search, Settings2 } from 'lucide-react'; -import ShortcutPrompt from './ShortcutPrompt'; -import { selfFocusSmart } from '../scripts/utils'; -import { JSX, Suspense, useEffect, useState } from 'react'; +import { JSX, Suspense } from 'react'; import Shortcuts from './Shortcuts'; import { AutoFocus } from './AutoFocus'; @@ -21,7 +19,7 @@ export interface CollectionsDetailParams export function CollectionsDetail (data: CollectionsDetailParams) { - const focusKey = `game-list-${data.id}-${data.filters.platformIds?.join()}-${data.filters.collectionId}`; + const focusKey = `game-list-${data.id}-${data.filters.platformId}-${data.filters.collectionId}`; const { ref, focusSelf } = useFocusable({ focusKey, preferredChildFocusKey: `${focusKey}-list`, @@ -46,7 +44,7 @@ export function CollectionsDetail (data: CollectionsDetailParams)
{data.footer}
- + diff --git a/src/mainview/components/ContextDialog.tsx b/src/mainview/components/ContextDialog.tsx new file mode 100644 index 0000000..6e13528 --- /dev/null +++ b/src/mainview/components/ContextDialog.tsx @@ -0,0 +1,107 @@ +import { FocusContext, FocusDetails, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; +import classNames from "classnames"; +import { createContext, JSX, useContext, useEffect } from "react"; +import { twMerge } from "tailwind-merge"; +import { useEventListener } from "usehooks-ts"; +import { X } from "lucide-react"; + +const ContextDialogContext = createContext({} as { + close: () => void, + id: string; +}); + +export function ContextList (data: { options: DialogEntry[]; className?: string; showCloseButton?: boolean; }) +{ + const context = useContext(ContextDialogContext); + return
    + {data.options.map(o => )} + {data.showCloseButton !== false && } action={context.close} id="close" content="Close" />} +
; +} + +export function OptionElement (data: DialogEntry & { onFocus?: () => void; className?: string; }) +{ + const context = useContext(ContextDialogContext); + const handleFocus = () => + { + (ref.current as HTMLElement).scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + data.onFocus?.(); + }; + const handleAction = data.action ? () => data.action?.({ close: context.close, focus: focusSelf }) : undefined; + const { ref, focused, focusSelf } = useFocusable({ + focusKey: `${context.id}-list-option-${data.id}`, + onEnterPress: handleAction, + onFocus: handleFocus + }); + const colors = { + primary: classNames("hover:bg-primary/40", { "bg-primary text-primary-content": focused }), + secondary: classNames("hover:bg-secondary/40", { "bg-secondary text-secondary-content": focused }), + accent: classNames("hover:bg-accent/40", { "bg-accent text-accent-content": focused }), + info: classNames("hover:bg-info/40", { "bg-info text-info-content": focused }), + warning: classNames("hover:bg-warning/40", { "bg-warning text-warning-content": focused }), + error: classNames("hover:bg-error/40", { "bg-error text-error-content": focused }) + }; + return
  • +

    + {data.icon} + {data.content} +

    +
  • ; +} + +export interface DialogEntry +{ + id: string, + content: string | JSX.Element; + icon?: string | JSX.Element; + type: 'primary' | 'secondary' | 'accent' | 'info' | 'warning' | 'error'; + action?: (ctx: { close: () => void, focus: (focusDetails?: FocusDetails | undefined) => void; }) => void; +} + +export function ContextDialog (data: { id: string, children: any | any[], open: boolean, close: () => void; }) +{ + const { ref, focusKey, focusSelf } = useFocusable({ focusable: data.open, focusKey: `${data.id}-context-dialog`, isFocusBoundary: true }); + useEffect(() => + { + if (data.open) + { + focusSelf(); + } + }, [data.open]); + + useEventListener('cancel', (e) => + { + if (data.open) + { + e.stopPropagation(); + data.close(); + } + }, ref); + + return + { + if (data.open) data.close(); + }}> + + +
    e.stopPropagation()} + > + {data.children} +
    +
    +
    +
    ; +} \ No newline at end of file diff --git a/src/mainview/components/Filters.tsx b/src/mainview/components/Filters.tsx index 898a49d..f5d0f0f 100644 --- a/src/mainview/components/Filters.tsx +++ b/src/mainview/components/Filters.tsx @@ -27,10 +27,10 @@ function FilterCat ( className={classNames( "flex px-4 h-12 items-center justify-center rounded-full transition-all", { - "bg-primary text-primary-content drop-shadow-sm cursor-default": + "bg-base-content px-3 text-base-300 drop-shadow cursor-default": focused || data.active, - "ring-base-content ring-7": focused, - "hover:bg-base-300 cursor-pointer": !focused, + "ring-primary ring-7": focused, + "hover:bg-base-content/40 cursor-pointer": !focused, }, )} > diff --git a/src/mainview/components/GameCard.tsx b/src/mainview/components/GameCard.tsx index 857b805..5085f2a 100644 --- a/src/mainview/components/GameCard.tsx +++ b/src/mainview/components/GameCard.tsx @@ -16,23 +16,30 @@ export function GameCardSkeleton () ); } -export default function GameCard (data: { +export interface GameCardParams +{ title: string; type?: string; - subtitle: string; - preview?: string | JSX.Element; + subtitle: string | JSX.Element; + preview?: string | JSX.Element | ((p: { focused: boolean; }) => JSX.Element); focusKey: string; index: number; - id: number; - badge?: JSX.Element; - onFocus?: (id: number) => void; + id: string; + badges?: JSX.Element[]; + className?: string; + onFocus?: (id: string) => void; + onBlur?: (id: string) => void; onAction?: () => void; -}) + clickFocuses?: boolean; +} + +export default function GameCard (data: GameCardParams) { const { ref, focused, focusSelf } = useFocusable({ focusKey: data.focusKey, onFocus: () => data.onFocus?.(data.id), onEnterPress: () => data.onAction?.(), + onBlur: () => data.onBlur?.(data.id) }); useEffect(() => @@ -59,33 +66,48 @@ export default function GameCard (data: { }} onFocus={focusSelf} onDoubleClick={data.onAction} - onClick={focused ? data.onAction : focusSelf} + onClick={() => + { + focusSelf(); + data.onAction?.(); + }} className={twMerge( - `game-card game-card-height flex flex-col justify-end`, + `game-card game-card-height flex flex-col justify-end z-5`, 'max-h-(--game-card-height) min-w-(--game-card-width) w-(--game-card-width)', "overflow-hidden transition-all duration-200 drop-shadow-lg cursor-pointer", focused ? - `animate-wiggle ring-7 bg-base-content text-base-300 ring-primary drop-shadow-xl drop-shadow-base-300/60 scale-102 z-10` : - "bg-base-300 text-base-content", + `focused animate-wiggle ring-7 bg-base-content text-base-300 ring-primary drop-shadow-xl drop-shadow-black/30 scale-102 z-10` : + "bg-base-300 hover:bg-base-100 hover:scale-102 text-base-content", classNames({ "h-(--game-card-height)": typeof data.preview === "string" - }) + }), + data.className )} >
    {typeof data.preview === "string" ? ( - + ) : ( - data.preview + typeof data.preview === 'function' ? data.preview({ focused }) : data.preview )}
    -
    {data.badge}
    +
    + {data.badges?.map(b => +
    + {b} +
    ) + } +
    {data.title}
    {data.subtitle}
    - + ); } diff --git a/src/mainview/components/GameList.tsx b/src/mainview/components/GameList.tsx index 79d75aa..904370c 100644 --- a/src/mainview/components/GameList.tsx +++ b/src/mainview/components/GameList.tsx @@ -1,15 +1,15 @@ -import { keepPreviousData, useQuery, useSuspenseQuery } from "@tanstack/react-query"; -import { getRomsApiRomsGetOptions } from "../../clients/romm/@tanstack/react-query.gen"; +import { useSuspenseQuery } from "@tanstack/react-query"; import { GameMetaExtra, CardList } from "./CardList"; -import { DefaultRommStaleTime, RPC_URL } from "../../shared/constants"; +import { FrontEndId, RPC_URL } from "../../shared/constants"; import { useLocation, useNavigate } from "@tanstack/react-router"; -import { Suspense, useEffect } from "react"; import { SaveSource } from "../scripts/spatialNavigation"; -import { gamesQueryOptions } from "../query-options"; +import { rommApi } from "../scripts/clientApi"; +import { HardDrive } from "lucide-react"; +import { JSX } from "react"; export interface GameListFilter { - platformIds?: number[]; + platformId?: number; collectionId?: number; } @@ -19,30 +19,39 @@ export interface GameListParams filters?: GameListFilter, grid?: boolean, setBackground?: (url: string) => void; - onGameSelect?: (id: number) => void; + onGameSelect?: (id: FrontEndId) => void; + className?: string; } export function GameList (data: GameListParams) { - const games = useSuspenseQuery(gamesQueryOptions(data.filters)); + const games = useSuspenseQuery({ + queryKey: ['games', data.filters ?? 'all'], + queryFn: () => rommApi.api.romm.games.get({ + query: { + platform_id: data.filters?.platformId, + collection_id: data.filters?.collectionId + } + }).then(d => d.data) + }); const navigator = useNavigate(); const location = useLocation(); - const handleFocus = (id: number) => + const handleFocus = (id: FrontEndId) => { - const game = games.data?.items.find((g) => g.id === id); + const game = games.data?.games.find((g) => g.id === id); if (game) { data.setBackground?.( - `${RPC_URL(__HOST__)}/api/romm${game.path_cover_small}`, + `${RPC_URL(__HOST__)}${game.path_cover}`, ); } }; - function handleDefaultSelect (id: number) + function handleDefaultSelect (id: FrontEndId, source: string | null, sourceId: number | null) { - SaveSource('details', location.pathname); - navigator({ to: '/game/$id', params: { id: String(id) }, viewTransition: { types: ['zoom-in'] } }); + SaveSource('details'); + navigator({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source }, viewTransition: { types: ['zoom-in'] } }); }; return ( @@ -51,23 +60,34 @@ export function GameList (data: GameListParams) id={data.id} type="game" grid={data.grid} - games={games.data.items.sort( - (a, b) => - Date.parse(b.rom_user.last_played ?? b.updated_at) - - Date.parse(a.rom_user.last_played ?? a.updated_at), - ) + className={data.className} + games={games.data?.games .map( (g) => - ({ - id: g.id, + { + const badges: JSX.Element[] = []; + if (g.id.source === 'local') + { + badges.push(); + } + + return { + id: `game-${g.id.source}-${g.id.id}`, focusKey: g.slug ?? `game-${g.id}`, title: g.name ?? "", - subtitle: g.platform_display_name ?? "", - previewUrl: `${RPC_URL(__HOST__)}/api/romm${g.path_cover_large}`, - }) satisfies GameMetaExtra, - )} - onGameFocus={handleFocus} - onSelectGame={id => data.onGameSelect ? data.onGameSelect(id) : handleDefaultSelect(id)} + subtitle: ( +
    + {!!g.path_platform_cover && } +

    {g.platform_display_name}

    +
    + ), + previewUrl: `${RPC_URL(__HOST__)}${g.path_cover}`, + badges: badges, + onSelect: () => data.onGameSelect ? data.onGameSelect(g.id) : handleDefaultSelect(g.id, g.source, g.source_id), + onFocus: () => handleFocus(g.id) + } satisfies GameMetaExtra; + }, + ) ?? []} /> ); diff --git a/src/mainview/components/Header.tsx b/src/mainview/components/Header.tsx index 65ce4e6..bd44d24 100644 --- a/src/mainview/components/Header.tsx +++ b/src/mainview/components/Header.tsx @@ -50,7 +50,6 @@ function HeaderAvatar (data: { id={data.id} ref={ref} onClick={data.onSelect} - style={{ viewTransitionName: data.id }} className={classNames( `avatar indicator ring-base-100 ring-offset-base-100 size-14 rounded-full flex items-center justify-center`, bgColors[data.type ?? "none"], @@ -92,6 +91,7 @@ export interface HeaderButton id: string; icon: JSX.Element; external?: boolean; + action?: () => void; } export interface HeaderAccount @@ -135,7 +135,7 @@ export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAcc ], action: () => { - SaveSource('settings', location.pathname); + SaveSource('settings'); navigate({ to: '/settings/accounts', viewTransition: { types: ['zoom-in'] }, search: { focus: 'rommAddress' } }); }, status: user.data ? "status-success" : 'status-error', @@ -182,6 +182,7 @@ export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAcc id={b.id} icon={b.icon} external={b.external} + action={b.action} />)}
    diff --git a/src/mainview/components/PlatformsList.tsx b/src/mainview/components/PlatformsList.tsx new file mode 100644 index 0000000..2490264 --- /dev/null +++ b/src/mainview/components/PlatformsList.tsx @@ -0,0 +1,72 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; +import { getPlatformsApiPlatformsGetOptions } from "../../clients/romm/@tanstack/react-query.gen"; +import { DefaultRommStaleTime, GameMeta, RPC_URL } from "../../shared/constants"; +import { CardList, GameMetaExtra } from "./CardList"; +import classNames from "classnames"; +import { rommApi } from "../scripts/clientApi"; + +export function PlatformsList (data: { id: string, setBackground: (url: string) => void; className?: string; }) +{ + const navigate = useNavigate(); + const { data: platforms } = useSuspenseQuery( + { + queryKey: ['platform', 'all'], + queryFn: async () => + { + const { data, error } = await rommApi.api.romm.platforms.get(); + if (error) throw error; + return data.platforms; + }, + refetchOnWindowFocus: false, + staleTime: DefaultRommStaleTime, + }); + + return ( + a.updated_at.getTime() - b.updated_at.getTime()) + .map((g) => ({ + id: g.slug, + focusKey: g.slug, + title: g.name, + subtitle: g.family_name ?? "", + previewUrl: "", + badges: [( + {g.game_count} + )], + onFocus: () => data.setBackground( + `https://picsum.photos/id/${10 + g.slug.length}/1920/1080.webp`, + ), + onSelect: () => + { + navigate({ to: `/platform/${g.source ?? g.id.source}/${g.source_id ?? g.id.id}`, viewTransition: { types: ['zoom-in'] } }); + }, + preview: + ({ focused }) =>
    + +
    + , + } satisfies GameMetaExtra))} + onSelectGame={(id) => + { + + }} + /> + ); +} \ No newline at end of file diff --git a/src/mainview/components/ShortcutPrompt.tsx b/src/mainview/components/ShortcutPrompt.tsx index 065b2bc..8c7bedf 100644 --- a/src/mainview/components/ShortcutPrompt.tsx +++ b/src/mainview/components/ShortcutPrompt.tsx @@ -14,7 +14,7 @@ export default function ShortcutPrompt (data: { void; +} + +export default function Shortcuts (data: { shortcuts: Shortcut[]; }) { return (
    - - - - + {data.shortcuts.map((s, i) => )}
    ); } diff --git a/src/mainview/components/backgrounds/dots.css b/src/mainview/components/backgrounds/dots.css new file mode 100644 index 0000000..8506642 --- /dev/null +++ b/src/mainview/components/backgrounds/dots.css @@ -0,0 +1,29 @@ +.ball { + border-radius: 50%; + animation: bounce 0.6s 32 alternate; +} + +.ball:nth-child(1) { + background: var(--color-accent); + animation-delay: 0s; +} + +.ball:nth-child(2) { + background: var(--color-secondary); + animation-delay: 0.2s; +} + +.ball:nth-child(3) { + background: var(--color-primary); + animation-delay: 0.4s; +} + +@keyframes bounce { + from { + transform: translateY(0); + } + + to { + transform: translateY(-30px); + } +} \ No newline at end of file diff --git a/src/mainview/components/backgrounds/dots.tsx b/src/mainview/components/backgrounds/dots.tsx new file mode 100644 index 0000000..ce6e0af --- /dev/null +++ b/src/mainview/components/backgrounds/dots.tsx @@ -0,0 +1,10 @@ +import './dots.css'; + +export default function DotsLoading () +{ + return
    +
    +
    +
    +
    ; +} \ No newline at end of file diff --git a/src/mainview/components/options/Button.tsx b/src/mainview/components/options/Button.tsx new file mode 100644 index 0000000..93d8e77 --- /dev/null +++ b/src/mainview/components/options/Button.tsx @@ -0,0 +1,28 @@ + +import { twMerge } from "tailwind-merge"; +import +{ + useFocusable, +} from "@noriginmedia/norigin-spatial-navigation"; +import classNames from "classnames"; + +export function Button (data: { id: string, children?: any, className?: string, disabled?: boolean, type: "reset" | "button" | "submit" | undefined; } & InteractParams & FocusParams) +{ + const { ref, focused } = useFocusable({ + focusKey: data.id, + onEnterPress: data.onAction, + onFocus: data.onFocus, + focusable: !data.disabled + }); + return ; +} \ No newline at end of file diff --git a/src/mainview/components/options/OptionInput.tsx b/src/mainview/components/options/OptionInput.tsx index 1dd78e2..4f6cef1 100644 --- a/src/mainview/components/options/OptionInput.tsx +++ b/src/mainview/components/options/OptionInput.tsx @@ -1,7 +1,9 @@ import classNames from "classnames"; -import { ChangeEventHandler, FocusEventHandler, HTMLInputTypeAttribute, JSX, useRef } from "react"; +import { ChangeEventHandler, FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef } from "react"; import { twMerge } from "tailwind-merge"; import { useOptionContext } from "./OptionSpace"; +import { useFocusable } from "@noriginmedia/norigin-spatial-navigation"; +import { systemApi } from "../../scripts/clientApi"; export function OptionInput (data: { name: string; @@ -11,10 +13,18 @@ export function OptionInput (data: { icon?: JSX.Element; value?: string; defaultValue?: string; + autocomplete?: HTMLInputAutoCompleteAttribute; onBlur?: FocusEventHandler; onChange?: ChangeEventHandler; }) { + const { ref, focused } = useFocusable({ + focusKey: data.name, onEnterPress: () => + { + inputRef.current?.focus(); + systemApi.api.system.show_keyboard.post(); + } + }); const inputRef = useRef(null); const option = useOptionContext({ onOptionEnterPress () @@ -24,10 +34,11 @@ export function OptionInput (data: { }); return ( -