diff --git a/.config/appimage/AppRun b/.config/appimage/AppRun deleted file mode 100644 index 7320ecc..0000000 --- a/.config/appimage/AppRun +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exec "$APPDIR/usr/bin/{{BINARY_NAME}}" "$@" \ No newline at end of file diff --git a/.config/appimage/com.simeonradivoev.gameflow-deck.appdata.xml b/.config/appimage/com.simeonradivoev.gameflow-deck.appdata.xml deleted file mode 100644 index 1d4cc0f..0000000 --- a/.config/appimage/com.simeonradivoev.gameflow-deck.appdata.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - {{APP_ID}} - CC0-1.0 - {{LICENSE}} - {{APP_NAME}} - Retro gaming frontend designed for handheld and controllers - - Simeon Radivoev - - -

A Cross-Platform Retro gaming frontend designed for handheld and controllers. Focused on building a simple user experience and intuitive UI.

-
- - Game - - - always - - {{APP_ID}}.desktop - https://github.com/simeonradivoev/gameflow-deck - https://github.com/simeonradivoev/gameflow-deck/issues - https://github.com/sponsors/simeonradivoev - - - https://media.githubusercontent.com/media/simeonradivoev/gameflow-deck/master/.github/screenshots/yObFD2LySH.jpg - - - Game Details - https://media.githubusercontent.com/media/simeonradivoev/gameflow-deck/master/.github/screenshots/3nhuKCK6E3.jpg - - - The Settings Panel - https://media.githubusercontent.com/media/simeonradivoev/gameflow-deck/master/.github/screenshots/GL7SkQbHIY.png - - - Emulator Details - https://media.githubusercontent.com/media/simeonradivoev/gameflow-deck/master/.github/screenshots/xNj7scPEDQ.png - - - Gameflow Store - https://media.githubusercontent.com/media/simeonradivoev/gameflow-deck/master/.github/screenshots/CpBLzTNM6N.png - - - -{{{RELEASES}}} - - - - {{APP_ID}}.desktop - gameflow - -
\ No newline at end of file diff --git a/.config/appimage/com.simeonradivoev.gameflow-deck.desktop b/.config/appimage/com.simeonradivoev.gameflow-deck.desktop deleted file mode 100644 index dddf26d..0000000 --- a/.config/appimage/com.simeonradivoev.gameflow-deck.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -X-AppImage-Name={{APP_NAME}} -X-AppImage-Version={{VERSION}} -X-AppImage-Arch={{ARCH}} -Name={{APP_NAME}} -Comment={{DESCRIPTION}} -Exec=gameflow -Icon=gameflow -Type=Application -Categories=Game; \ No newline at end of file diff --git a/.config/flatpak/com.simeonradivoev.gameflow-deck.json b/.config/flatpak/com.simeonradivoev.gameflow-deck.json index ebe1efa..ccdc833 100644 --- a/.config/flatpak/com.simeonradivoev.gameflow-deck.json +++ b/.config/flatpak/com.simeonradivoev.gameflow-deck.json @@ -1,36 +1,23 @@ { "app-id": "com.simeonradivoev.gameflow-deck", - "runtime": "org.freedesktop.Platform", - "runtime-version": "25.08", - "sdk": "org.freedesktop.Sdk", + "runtime": "org.kde.Platform", + "runtime-version": "6.10", + "sdk": "org.kde.Sdk", "command": "/app/bin/gameflow", + "base": "io.qt.qtwebengine.BaseApp", + "base-version": "6.10", "finish-args": [ "--share=ipc", "--share=network", "--socket=pulseaudio", "--socket=wayland", - "--socket=inherit-wayland-socket", "--socket=x11", - "--socket=fallback-x11", - "--socket=session-bus", - "--socket=system-bus", "--device=all", "--filesystem=host", "--filesystem=home", - "--filesystem=~/.steam/steam:rw", - "--filesystem=~/.steam:rw", - "--filesystem=~/.var/app/com.valvesoftware.Steam:rw", - "--filesystem=/run/udev:ro", - "--filesystem=/run/media", - "--filesystem=xdg-documents", - "--filesystem=xdg-desktop", - "--filesystem=xdg-run/gamescope-0:rw", "--env=PKG_CONFIG_LIBDIR=/app/lib", "--env=FLATPAK_BUILD=true", - "--allow=devel", - "--talk-name=org.freedesktop.portal.OpenURI", - "--talk-name=org.freedesktop.Flatpak", - "--talk-name=org.a11y.Bus" + "--allow=devel" ], "modules": [ { @@ -42,6 +29,7 @@ "mkdir -p /app/lib", "install -Dm644 256x256.png /app/share/icons/hicolor/256x256/apps/com.simeonradivoev.gameflow-deck.png", "install -Dm644 com.simeonradivoev.gameflow-deck.desktop /app/share/applications/com.simeonradivoev.gameflow-deck.desktop", + "mv libvips-cpp.so.* /app/lib", "mv * /app/share/gameflow/", "mv /app/share/gameflow/gameflow /app/bin", "mv /app/share/gameflow/bun /app/bin", @@ -51,15 +39,15 @@ "sources": [ { "type": "dir", - "path": "../../build/linux" + "path": "../build/linux" }, { "type": "file", - "path": "com.simeonradivoev.gameflow-deck.desktop" + "path": "../flatpak/com.simeonradivoev.gameflow-deck.desktop" }, { "type": "file", - "path": "../../src/mainview/public/256x256.png" + "path": "../src/mainview/public/256x256.png" }, { "type": "script", @@ -84,22 +72,23 @@ "only-arches": [ "aarch64" ] + }, + { + "type": "file", + "path": "../node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3", + "only-arches": [ + "x86_64" + ] } ] }, { - "name": "NW.js", - "buildsystem": "simple", - "build-commands": [ - "mkdir -p /app/bin/nw", - "mv * /app/bin/nw", - "chmod +x /app/bin/nw/nw" - ], + "name": "webview", + "buildsystem": "cmake-ninja", "sources": [ { - "type": "archive", - "url": "https://dl.nwjs.io/v0.110.1/nwjs-v0.110.1-linux-x64.tar.gz", - "sha256": "d9a9ed2255e9ee87c9dd1860d9c7a479cea5279dcd80d3e80e23b083d325554a" + "type": "dir", + "path": "../flatpak/webview" } ] } diff --git a/.config/flatpak/webview/CMakeLists.txt b/.config/flatpak/webview/CMakeLists.txt new file mode 100644 index 0000000..511252a --- /dev/null +++ b/.config/flatpak/webview/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.16) + +project(SimpleWebView LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Required for Qt WebEngine +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +find_package(Qt6 REQUIRED COMPONENTS + Core + Widgets + WebEngineWidgets, + Gamepad +) + +add_executable(webview + main.cpp +) + +target_link_libraries(webview PRIVATE + Qt6::Core + Qt6::Widgets + Qt6::WebEngineWidgets, + Qt6::Gamepad +) + + +# Install binary into Flatpak prefix (/app) +install(TARGETS webview + RUNTIME DESTINATION bin +) \ No newline at end of file diff --git a/.config/flatpak/webview/main.cpp b/.config/flatpak/webview/main.cpp new file mode 100644 index 0000000..d0982e7 --- /dev/null +++ b/.config/flatpak/webview/main.cpp @@ -0,0 +1,14 @@ +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + if (argc < 2) { return 1; } + QWebEngineView view; + view.setUrl(QUrl(argv[1])); + view.showFullScreen(); + return app.exec(); +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index 01c3dfe..0a0c0bd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,6 +4,3 @@ *.gif filter=lfs diff=lfs merge=lfs -text *.webp filter=lfs diff=lfs merge=lfs -text *.svg filter=lfs diff=lfs merge=lfs -text -*.wav filter=lfs diff=lfs merge=lfs -text -*.mp3 filter=lfs diff=lfs merge=lfs -text -*.ogg filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/.github/screenshots/3d screenshot.png b/.github/screenshots/3d screenshot.png deleted file mode 100644 index 6510ea1..0000000 --- a/.github/screenshots/3d screenshot.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8b246781b29fd3ef53ace2639229cb2149b791bf82977cb481f3b4f343f8776d -size 246650 diff --git a/.github/screenshots/3nhuKCK6E3.jpg b/.github/screenshots/3nhuKCK6E3.jpg new file mode 100644 index 0000000..7f70d9c --- /dev/null +++ b/.github/screenshots/3nhuKCK6E3.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a33280e455034d34c0b2dfa111cff8fe179f97c2a39f7cd0c99b71b1957eda4f +size 1070602 diff --git a/.github/screenshots/3nhuKCK6E3.png b/.github/screenshots/3nhuKCK6E3.png deleted file mode 100644 index 5feae4b..0000000 --- a/.github/screenshots/3nhuKCK6E3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5be6551346cd1067ab4bbb172828a801c4e26f1c9cac9ce35e1e712356df05bb -size 1970162 diff --git a/.github/screenshots/6wz3gW8c2h.png b/.github/screenshots/6wz3gW8c2h.png deleted file mode 100644 index 6ebc8de..0000000 --- a/.github/screenshots/6wz3gW8c2h.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:06131801cba199fa267b532a2f26b841d2b856cdd1bb9428e08879981e7b36c8 -size 1932256 diff --git a/.github/screenshots/EWPHmIBEE5.png b/.github/screenshots/EWPHmIBEE5.png index 90e6416..6fd9f40 100644 --- a/.github/screenshots/EWPHmIBEE5.png +++ b/.github/screenshots/EWPHmIBEE5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0eeee2b3d31fbb4ea49bb38a2634088fa0a54d6ce5d41f7bf39f419d518b802e -size 850435 +oid sha256:a03b0cd56b78f51f8bc4c304b8b14fa611748b9efa192ef26283e732beff90c1 +size 643600 diff --git a/.github/screenshots/GL7SkQbHIY.png b/.github/screenshots/GL7SkQbHIY.png index 28544a3..2cdbe12 100644 --- a/.github/screenshots/GL7SkQbHIY.png +++ b/.github/screenshots/GL7SkQbHIY.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a22580330264a0ad2d4a6f758ad26c18ed9a0a17cbe1254dbbad01e959b205f8 -size 110988 +oid sha256:bf2d692f8ccf3a1c5f9addf26052a34a74c332474fbd8c5bbc7923208407a748 +size 86214 diff --git a/.github/screenshots/MMeJxl4IXr.png b/.github/screenshots/MMeJxl4IXr.png deleted file mode 100644 index f8cd130..0000000 --- a/.github/screenshots/MMeJxl4IXr.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2ba59fccc691e8f43d4eff532d88edfab411d9abb68d0566fad1c167b7b17bdd -size 183846 diff --git a/.github/screenshots/Pkazk0RufB.png b/.github/screenshots/Pkazk0RufB.png index 3025b0d..ceced8a 100644 --- a/.github/screenshots/Pkazk0RufB.png +++ b/.github/screenshots/Pkazk0RufB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9db331ad2d2cf2fb2525560baf5970f8faa6c8ff6ae885d9eedd65560af8fffd -size 2035855 +oid sha256:2dd9859c9495af93872534a913a78597d235f8fb723fe685aa1aeab9283e028b +size 1986843 diff --git a/.github/screenshots/iunZbvYEGp-ezgif.com-optimize.gif b/.github/screenshots/iunZbvYEGp-ezgif.com-optimize.gif deleted file mode 100644 index bd842a9..0000000 --- a/.github/screenshots/iunZbvYEGp-ezgif.com-optimize.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22e018fb97f05c24294fd3b8fe088ca755760b81d3b46dd9f04b5f52f98f34da -size 2693240 diff --git a/.github/screenshots/mockup-1777308293568.png b/.github/screenshots/mockup-1777308293568.png deleted file mode 100644 index 93558db..0000000 --- a/.github/screenshots/mockup-1777308293568.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e478586834f41ec41e85ab76bec7ca244c2521446a40442ba3de0ea97888fa17 -size 2007401 diff --git a/.github/screenshots/rBY2mgTLy0.png b/.github/screenshots/rBY2mgTLy0.png deleted file mode 100644 index 653ddab..0000000 --- a/.github/screenshots/rBY2mgTLy0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eca786ab7f525bcfe663f82995fc9e91234ddd5df726528b0926eaf32ed8ad8e -size 110966 diff --git a/.github/screenshots/xNj7scPEDQ.png b/.github/screenshots/xNj7scPEDQ.png index 4813a47..d50d6aa 100644 --- a/.github/screenshots/xNj7scPEDQ.png +++ b/.github/screenshots/xNj7scPEDQ.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c26e4b9c7f690c49f9625ea2b8c2a82f03a24a0236c53dc02c158f7222c2519d -size 1805877 +oid sha256:4a234b8d4624ccfd677c698c1e33eb7c0b757dc13f1403fd8bc6d37ed9e6ff02 +size 1673960 diff --git a/.github/screenshots/yObFD2LySH.jpg b/.github/screenshots/yObFD2LySH.jpg index f540a83..00d761f 100644 --- a/.github/screenshots/yObFD2LySH.jpg +++ b/.github/screenshots/yObFD2LySH.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f813f98ae41c6d383dcc5e9d6ea693b6701dddc5a03f73cbd1ed990b8532710 -size 1274712 +oid sha256:46e473f90661400fec49d87a972a3324cd4fb18f5b8c670aa5b606462f98fbfe +size 1194459 diff --git a/.github/screenshots/zEQxtzhPGx.png b/.github/screenshots/zEQxtzhPGx.png deleted file mode 100644 index 1b18477..0000000 --- a/.github/screenshots/zEQxtzhPGx.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e3bf4df252866dd15f3c1c6efcfc648fbd9d320e47cf2d64551137497d229d6 -size 1324186 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6ff5ea..84ec3e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,7 +83,7 @@ jobs: with: type: "zip" directory: ${{ github.workspace }} - filename: "Gameflow-win32-x64.zip" + filename: "Gameflow-Windows.zip" path: "canary-build-Windows" - name: Publish Release @@ -96,4 +96,4 @@ jobs: omitBodyDuringUpdate: true replacesArtifacts: true token: ${{ secrets.GITHUB_TOKEN }} - artifacts: "${{ github.workspace }}/canary-build-*/*.AppImage,${{ github.workspace }}/Gameflow-*.zip" + artifacts: "${{ github.workspace }}/canary-build-*/*.AppImage,${{ github.workspace }}/Gameflow-Windows.zip" diff --git a/.gitignore b/.gitignore index 880e27d..a89abd1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,5 @@ downloads gameflow-deck.code-workspace .env.local src/tests/mock-roms/db.sqlite -src/tests/mock-roms/store src/tests/mock-config -bin -.config/flatpak/repo -xenia.log \ No newline at end of file +bin \ No newline at end of file diff --git a/.versionrc b/.versionrc deleted file mode 100644 index fa5c461..0000000 --- a/.versionrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "packageFiles": [ - { - "filename": "package.json", - "type": "json" - } - ], - "bumpFiles": [ - { - "filename": "package.json", - "type": "json" - }, - { - "filename": "src/packages/gameflow-sdk/package.json", - "type": "json" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index b63a222..2c6da05 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,22 +6,17 @@ "files.watcherExclude": { "**/*.gen.*": true, "src/mainview/gen/*": true, - "**/build": true, - "**/.config/flatpack/repo/**": true, - "**/.flatpak-builder/**": true, }, "search.exclude": { "**/*.gen.*": true, - "**/.flatpak-builder": true, - "**/.config/flatpack/repo/**": true, - "**/build": true, + ".flatpak-builder/**/*": true, "src/mainview/gen/*": true, }, "npm.scriptRunner": "bun", "npm.exclude": [ "**/.flatpak-builder/**/*", "**/build/flatpack/**", - "**/.config/flatpack/repo/**", + "**/flatpack/repo/**", ], "editor.formatOnSave": true, "[typescriptreact]": { diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a70168..8e45981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,52 +1,6 @@ # Changelog -All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. - -## [1.6.0](https://github.com/simeonradivoev/gameflow-deck/compare/v1.5.0...v1.6.0) (2026-05-09) - - -### Features - -* Implemented public plugin system accessible from the store. ([38cb752](https://github.com/simeonradivoev/gameflow-deck/commit/38cb7525527b5ad4f6eb284cdad0001fd87eaf7e)) - -## [1.5.0](https://github.com/simeonradivoev/gameflow-deck/compare/v1.4.0...v1.5.0) (2026-05-05) - - -### Features - -* Implemented local game import (with a wizard) ([06b7e40](https://github.com/simeonradivoev/gameflow-deck/commit/06b7e4074da23afdec3b2ff97f84a9e1486944d2)) - - -### Bug Fixes - -* Navigation blocking now working with focuesed input fields ([4da717c](https://github.com/simeonradivoev/gameflow-deck/commit/4da717c26d9840febd48ee87a6a493a3e1acc6b9)) - -## [1.4.0](https://github.com/simeonradivoev/gameflow-deck/compare/v1.3.0...v1.4.0) (2026-04-26) - - -### Features - -* Added more ways to detect duplicates ([05fafce](https://github.com/simeonradivoev/gameflow-deck/commit/05fafced07c853deb656d7c17d05184c42ee507c)) -* added update notes and moved update to own tab ([cf84f40](https://github.com/simeonradivoev/gameflow-deck/commit/cf84f40a174b8f242ca58fb6fe02eefab46ff442)) -* Added way to update the local games from romm when IDs change based on IGDB or Retro Achievement ID ([4806f34](https://github.com/simeonradivoev/gameflow-deck/commit/4806f3487a577ab8e7c66907e5b640d95ab8a46c)), closes [#2](https://github.com/simeonradivoev/gameflow-deck/issues/2) -* Bundled NW.js with appimages ([813785f](https://github.com/simeonradivoev/gameflow-deck/commit/813785f4f3d292a87cc4a6b86dc152c43572d2c8)) -* Implemented audio effects ([edbc390](https://github.com/simeonradivoev/gameflow-deck/commit/edbc390d144bf44da35d0f5383ec36eb25c34d1b)) -* Implemented dolphin integration ([a69147a](https://github.com/simeonradivoev/gameflow-deck/commit/a69147a4f73cf626b92622a8ee22b54f538d41a9)) -* Implemented emulator launching ([09b8b9c](https://github.com/simeonradivoev/gameflow-deck/commit/09b8b9c6f850cea3b897308925faf9be02cefa1a)), closes [#1](https://github.com/simeonradivoev/gameflow-deck/issues/1) -* Implemented emulator versions and updating ([34db717](https://github.com/simeonradivoev/gameflow-deck/commit/34db717ec5cbcf8b1ae54fbda33bf9a78f01bd17)) -* Implemented filtering and searching ([444d8c4](https://github.com/simeonradivoev/gameflow-deck/commit/444d8c4c278c6032b37f44a884cb6d7bf0b54c85)) -* implemented haptics ([54dd925](https://github.com/simeonradivoev/gameflow-deck/commit/54dd9256e361877d0950a84061d9402616706352)) -* Implemented romm saves for dolphin and xenia ([7948bd2](https://github.com/simeonradivoev/gameflow-deck/commit/7948bd24fabfc01b7be358f06fcd58c8795826c7)) - - -### Bug Fixes - -* Fixed a bunch of issues on linux ([6aacec2](https://github.com/simeonradivoev/gameflow-deck/commit/6aacec2c0de253a71599e261e07aff53055cdb1e)) -* Fixed emulator details buttons not showing ([04d5856](https://github.com/simeonradivoev/gameflow-deck/commit/04d5856f7d71c944c82877d2a1457facea4b6d31)) -* Fixed tests ([c09fbd3](https://github.com/simeonradivoev/gameflow-deck/commit/c09fbd3dc88891227eda2b9f3bd9ac45621c00ea)) -* logins now refresh on plugins load ([7bd0ebd](https://github.com/simeonradivoev/gameflow-deck/commit/7bd0ebdcca1843076911547ec1098cbaae9e2414)) -* Made self update work on windows ([ae196e1](https://github.com/simeonradivoev/gameflow-deck/commit/ae196e11d616b9813dba11f64e7c844077686db8)) -* Made store downloads extract in their own folder ([764691f](https://github.com/simeonradivoev/gameflow-deck/commit/764691fc8610fafebc93a69ca24f74bcac42a898)) +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. ## [1.3.0](https://github.com/simeonradivoev/gameflow-deck/compare/v1.2.1...v1.3.0) (2026-03-31) diff --git a/README.md b/README.md index 0e5864e..b3578f1 100644 --- a/README.md +++ b/README.md @@ -4,78 +4,50 @@ A Cross-Platform open source Retro gaming frontend designed for handheld and con Focused on building a simple user experience and intuitive UI as a curated community driven experience. > [!WARNING] -> This app is actively in development, it is constantly changing and improving. +> This app is actively in development, it doesn't have most of its major features implemented yet. > It will have an opinionated design and will be used as an experiment in discovering a good UX. -## Community - -Join us on Discord, where you can ask questions, submit ideas and get help. - -[![](https://invidget.switchblade.xyz/R9KakhY67d)](https://discord.gg/R9KakhY67d) - ## Features ### Integrations - **[ROMM](https://github.com/rommapp/romm)** - download, sync and update roms and platforms. - - Show Achievements and sync playtime. - - Experimental save syncing - **[Emulator JS](https://github.com/EmulatorJS/EmulatorJS)** - play your games with emulator js right within the app. Uses RetroArch cores. -- **[RClone](https://github.com/rclone/rclone)** - sync saves between devices or cloud. Some Emulators and store games support it. -- **[UMU](https://github.com/Open-Wine-Components/umu-launcher)** - UMU Launcher for playing windows games on linux without needing steam. (Only used for store games for now) ### Store -- **Emulators** - (WIP) Download and install emulators and automatically configure them from a list of supported in the store. Some even come with advanced features like cloud saves. -- **Free Curated Games** - Download free curated games and homebrew roms without ever leaving the app +- **Emulators** - (WIP) Download and install emulators and automatically configure them +- **Free Curated Games** - Download free curreted games and homebrew roms without ever leaving the app ### Others - **Cross Platform** - Can run on multiple platforms. Built with web technologies and bun backend. - **Steam Deck Support** - Extensively tested with the steam deck. It can use flatpak installed browsers. -- **Lightweight** - It uses the window's webview as a frontend, reducing build size and ram usage. +- **Lightweight** - It uses the existing system browser to launch the front end, so no need to include a whole web browser. - On Windows it first uses webview2 then your browser - - On linux it does ship with NW.js to work on most distros. A big one is the steam deck missing WebKitGTK. + - On linux it uses WebKitGTK or a browser even from flatpak - Not tested on Mac yet - **Great for Controllers** - The UI is inspired by the switch and works great with joysticks and dpads. - **Automatic Downloads** - Downloads roms from ROMM automatically -- **Automatic Emulator Discovery** - Using the configs of the excellent ES-DE to discover installed emulators and launch roms. You can bring your existing configurations. +- **Automatic Emulator Discovery** - Using the configs of the excellent ES-DE to discover installed emulators and launch games. - Easy fallback configuration with built in file browser. - **Responsive Layout** - Optimized mainly for the steam deck with responsive layout support and dynamic switching of inputs. -- **Cloud/Device Save Sync** - For supported games and emulators. -- **Dark and Light** - Dark and light themes for your preference. ## Screenshots - - - - - - - - - - + + + + + + ## Goals - I want to build an open and free platform where you can play and discover new hidden gems from the past. - I plan to add a free store where you can download all your needed emulators, the goal is to not have to leave the UI for anything. - I really want to add matrix chat support in the app for engaging with your favorite community. Having access to so many nodejs libraries would make it quite straight forward. -- I'm sick of closed source and private store fronts, and want a way to share community curated free experiences. I'm also sick of the profit driven nature of games and promotions. -- Being self contained, I want to avoid writing as little as possible to system and contain and manage settings in a custom changeable directory. This was mainly a side-effect of having the low storage steam deck and always running out of space on my internal hard drive. - -## Usage - -There are currently 2 ways of getting games. One is logging in through romm and importing your games from there. The other is the store (it's a bit limited right now). I might add local import of roms since IGDB login is already implemented. - -The app created a default folder in your home folder. You can move it. It stores everything there. From downloaded roms, emulators and configs. - -## Existing Setups - -The game should work pretty well with existing emulators one has installed. It uses the ES-DE config to find installed emulators. Only downside is more advanced integrations won't work, as they are mainly used for store emulators where the app has more control over, plus I don't want to mess up existing setups. -But given it's an existing setup, say from emudeck it won't matter much as it's already configured say for the steam deck. +- I'm sick of closed source and private store fronts, and want a way to share community currated free experiences. I'm also sick of the profit driven nature of games and promotions. ## Development @@ -106,17 +78,6 @@ But given it's an existing setup, say from emudeck it won't matter much as it's - `bun run openapi-ts` generated the openapi client calls from romm's API - `bun run package:windows` builds an package to be distributed on windows - `bun run package:linux` builds an AppImage to be distributed on linux - - `bun run test` run tests - - `bun run download:chromium` downloads degoogled chromium to use as the frontend - - `bun run download:nwjs` downloads NW.js to use as a frontend. - -## Plugins - -To create a plugin create a new npm project and install: -`bun i --peer @simeonradivoev/gameflow-sdk` - -Then publish the package to npmjs with a tag `gameflow-plugin` to appear in the UI. -For more info check the [SDK README](./scripts/sdk/README.md) ### Tech Stack @@ -129,10 +90,3 @@ For more info check the [SDK README](./scripts/sdk/README.md) - [elysia](https://elysiajs.com/) for the APIs - [webview](https://github.com/webview/webview) for launching existing system webviews instead of full browser if possible. - [emulatorjs](https://emulatorjs.org/) for playing lots of roms inside the app without having to deal with external emulators - -### Credits - -- UI Sounds - - [CC BY 4.0 - Credit: JC Sounds](https://opengameart.org/content/jc-sounds-ui-utility-pack-vol-1) - - [Sounds by: Chhoff](https://chhoffmusic.itch.io/classic-ui-sfx) - - [UI Sound Effects by lolurio](https://lolurio.itch.io/lolurios-free-cozy-ui-sfx) diff --git a/bun.lock b/bun.lock index f005808..6188919 100644 --- a/bun.lock +++ b/bun.lock @@ -7,153 +7,106 @@ "dependencies": { "7zip-bin": "^5.2.0", "@auth/core": "^0.34.3", - "@elysiajs/cors": "^1.4.2", - "@elysiajs/eden": "^1.4.9", - "@jimp/wasm-webp": "^1.6.1", - "@phalcode/ts-igdb-client": "^1.0.26", + "@elysiajs/cors": "^1.4.1", + "@elysiajs/eden": "^1.4.6", + "@jimp/wasm-webp": "^1.6.0", "cheerio": "^1.2.0", - "conf": "^15.1.0", - "drizzle-orm": "^0.45.2", - "elysia": "^1.4.28", - "fs-extra": "^11.3.5", + "conf": "^15.0.2", + "drizzle-orm": "^0.45.1", + "elysia": "^1.4.22", + "fs-extra": "^11.3.3", "get-folder-size": "^5.0.0", "ini": "^6.0.0", - "jimp": "^1.6.1", + "jimp": "^1.6.0", "mustache": "^4.2.0", "node-7z": "^3.0.0", "node-disk-info": "^1.3.0", - "node-downloader-helper": "^2.1.11", + "node-downloader-helper": "^2.1.10", "node-stream-zip": "^1.15.0", - "node-unrar-js": "^2.0.2", "open": "^11.0.0", - "p-queue": "^9.2.0", "pathe": "^2.0.3", - "slugify": "^1.6.9", - "smol-toml": "^1.6.1", - "systeminformation": "^5.31.6", - "tapable": "^2.3.3", - "tough-cookie": "^6.0.1", + "systeminformation": "^5.31.5", + "tapable": "^2.3.0", + "tough-cookie": "^6.0.0", "tough-cookie-file-store": "^3.3.0", + "ts-igdb-client": "^0.4.2", "unzip-stream": "^0.3.4", "webview-bun": "^2.4.0", - "zod": "^4.4.3", + "zod": "^4.3.6", }, "devDependencies": { - "@ap0nia/eden": "^1.6.1", + "@ap0nia/eden": "^1.0.0-next.22", "@ap0nia/eden-tanstack-query": "^1.0.0-next.22", "@emulatorjs/emulatorjs": "^4.2.3", - "@hey-api/openapi-ts": "^0.91.1", - "@noriginmedia/norigin-spatial-navigation": "^3.1.0", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.3.0", - "@tanstack/react-form": "^1.32.0", - "@tanstack/react-query": "^5.100.10", - "@tanstack/react-query-devtools": "^5.100.10", - "@tanstack/react-query-persist-client": "^5.100.10", - "@tanstack/react-router": "^1.169.2", - "@tanstack/react-router-devtools": "^1.166.13", - "@tanstack/react-router-ssr-query": "^1.166.12", - "@tanstack/router-plugin": "^1.167.35", - "@tanstack/zod-adapter": "^1.166.9", + "@hey-api/openapi-ts": "^0.91.0", + "@noriginmedia/norigin-spatial-navigation": "^2.3.0", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-form": "^1.28.0", + "@tanstack/react-query": "^5.90.20", + "@tanstack/react-query-devtools": "^5.91.3", + "@tanstack/react-router": "^1.157.16", + "@tanstack/react-router-devtools": "^1.154.12", + "@tanstack/react-router-ssr-query": "^1.157.17", + "@tanstack/router-plugin": "^1.157.16", + "@tanstack/zod-adapter": "^1.162.4", "@types/adm-zip": "^0.5.8", - "@types/audiosprite": "^0.7.3", "@types/bun": "latest", "@types/fs-extra": "^11.0.4", - "@types/howler": "^2.2.12", "@types/ini": "^4.1.1", - "@types/json-schema": "^7.0.15", "@types/mustache": "^4.2.6", "@types/node-7z": "^2.1.11", - "@types/rclone.js": "^0.6.3", - "@types/react": "^19.2.14", + "@types/react": "^19.2.9", "@types/react-dom": "^19.2.3", "@types/unzip-stream": "^0.3.4", - "@vitejs/plugin-react": "^5.2.0", - "adm-zip": "^0.5.17", + "@vitejs/plugin-react": "^5.1.2", + "adm-zip": "^0.5.16", "animate.css": "^4.1.1", "app-builder-bin": "^5.0.0-alpha.13", - "audiosprite": "^0.7.2", "babel-plugin-react-compiler": "^1.0.0", "classnames": "^2.5.1", - "commit-and-tag-version": "^12.7.3", "concurrently": "^9.2.1", "cross-env": "^10.1.0", - "daisyui": "^5.5.19", - "drizzle-kit": "^0.31.10", + "daisyui": "^5.5.14", + "drizzle-kit": "^0.31.9", + "dts-bundle-generator": "^9.5.1", "eden-tanstack-query": "^0.0.9", - "howler": "^2.2.4", - "idb-keyval": "^6.2.2", "lucide-react": "^0.563.0", "pretty-bytes": "^7.1.0", - "pretty-ms": "^9.3.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "react-error-boundary": "^6.1.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-error-boundary": "^6.1.0", "react-hot-toast": "^2.6.0", - "react-markdown": "^10.1.0", - "react-qr-code": "^2.0.21", - "sass-embedded": "^1.99.0", - "tailwind-merge": "^3.6.0", - "tailwindcss": "^4.3.0", + "react-qr-code": "^2.0.18", + "sass-embedded": "^1.97.3", + "standard-version": "^9.5.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "usehooks-ts": "^3.1.1", - "vite": "^7.3.3", - "vite-plugin-svg-icons-ng": "^1.9.1", + "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", }, }, - "src/packages/gameflow-sdk": { - "name": "@simeonradivoev/gameflow-sdk", - "version": "1.6.0", - "bin": { - "gameflow-build": "build.ts", - }, - "peerDependencies": { - "7zip-bin": "^5.2.0", - "@auth/core": "^0.34.3", - "cheerio": "^1.2.0", - "conf": "^15.1.0", - "drizzle-orm": "^0.45.2", - "elysia": "^1.4.28", - "fs-extra": "^11.3.5", - "get-folder-size": "^5.0.0", - "ini": "^6.0.0", - "jimp": "^1.6.1", - "mustache": "^4.2.0", - "node-7z": "^3.0.0", - "node-disk-info": "^1.3.0", - "node-downloader-helper": "^2.1.11", - "node-stream-zip": "^1.15.0", - "node-unrar-js": "^2.0.2", - "open": "^11.0.0", - "p-queue": "^9.2.0", - "pathe": "^2.0.3", - "slugify": "^1.6.9", - "smol-toml": "^1.6.1", - "tapable": "^2.3.3", - "unzip-stream": "^0.3.4", - "zod": "^4.4.3", - }, - }, }, "packages": { "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], - "@ap0nia/eden": ["@ap0nia/eden@1.6.1", "", { "dependencies": { "elysia": "1.2.15" } }, "sha512-jlsUyh4PsYNnMcPuQ3IJq0hhDNnyRNGYx+MSAJlcgKs4En9qrokLorSbTRvVjA1Mdx4VdzEADcPn99Kbph0SOw=="], + "@ap0nia/eden": ["@ap0nia/eden@1.0.0-next.22", "", { "peerDependencies": { "elysia": "^1.3.1" } }, "sha512-9iH09koK29Yuem80fz8nCt9iHVcJqxUo2QHAr4psI02PhvL70n6aWVo/hlHyYXwOSsSgRQlLl1vPmiulFOUFoA=="], "@ap0nia/eden-tanstack-query": ["@ap0nia/eden-tanstack-query@1.0.0-next.22", "", {}, "sha512-eSQ98G4TYzrAdsfRekrvqIrTqrAUFy+YpibZ5fj5KL6/R6FcrS2U2F51iML98baXT4MTpOJARY9p+7x0hiA8Qw=="], "@auth/core": ["@auth/core@0.34.3", "", { "dependencies": { "@panva/hkdf": "^1.1.1", "@types/cookie": "0.6.0", "cookie": "0.6.0", "jose": "^5.1.3", "oauth4webapi": "^2.10.4", "preact": "10.11.3", "preact-render-to-string": "5.2.3" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^7" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-jMjY/S0doZnWYNV90x0jmU3B+UcrsfGYnukxYrRbj0CVvGI/MX3JbHsxSrx2d4mbnXaUsqJmAcDfoQWA6r0lOw=="], - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], @@ -173,7 +126,7 @@ "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], @@ -195,9 +148,9 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - "@elysiajs/cors": ["@elysiajs/cors@1.4.2", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-FTCcbH35brTLigF1W7BYySRZomgI/dBEMK9BgK9RP9Nez7zmpGh4koL/Yr1BFv8nYz7CfhRvcM8d/c+XnwMaVQ=="], + "@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="], - "@elysiajs/eden": ["@elysiajs/eden@1.4.9", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-3CKVD4ycVjB8nCNssfmhnUuq3SzSHkUES3v5PNCFr9LxIrx39/HVRAZ8z2sLxrFqzUs48dCBZaxoZzJ5UUVHDA=="], + "@elysiajs/eden": ["@elysiajs/eden@1.4.6", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-Tsa4NwXEWg/u73vWiYZQ3L5/ecgZSxqiEjYwpS+4qBKXeTZqZKl2hcgHJSVBL+InEDMi35Xugct7qyAXE5oM4Q=="], "@emulatorjs/core-81": ["@emulatorjs/core-81@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-oPQEqjpR3z7Yedte4u3sOXDZ4NXAykNcbENjYcB+x3QshF8I+3MQCo8kINOT2lsqqgx91WR4kmEaYQqU39YsDA=="], @@ -357,13 +310,13 @@ "@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.1", "", { "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-khTIpxhKEAqmRmeLUnAFJQs4Sbg9RPokovJk9rRcC8B5MWH1j3/BRSqfpAIiJUBDU1+nbVg2RVCV+eQ174cdvw=="], + "@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=="], "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-gRbyyTjzpFVNmbD+Upn3w4dWV+bCXGJbff3A7leDO/tfNxSz1xIb6Ad/5UKtvEW9kDt/2Uyc3XkFZ6rpafvbfQ=="], - "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.91.1", "", { "dependencies": { "@hey-api/codegen-core": "0.6.1", "@hey-api/json-schema-ref-parser": "1.2.3", "@hey-api/shared": "0.1.1", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-d16WR35UtthK/ihAIwJaKxrj/zvb5LbYwtVJCyZFFMin2qzDU8Y3Lpk78ensAykrLoaDLzpd0iIyt9JCP5Qmww=="], + "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.91.0", "", { "dependencies": { "@hey-api/codegen-core": "0.6.0", "@hey-api/json-schema-ref-parser": "1.2.3", "@hey-api/shared": "0.1.0", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-AHkd982HsPz1XpqRm59URwJyJqTzyzzC30EAp07b/0M9KojjneCPxm8FnvFnXLRTMyKgcOymMsYXuLzJ9mpMHA=="], - "@hey-api/shared": ["@hey-api/shared@0.1.1", "", { "dependencies": { "@hey-api/codegen-core": "0.6.1", "@hey-api/json-schema-ref-parser": "1.2.3", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "cross-spawn": "7.0.6", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-/irgNGXw9TL5aKB3S7jCLgh07vgDFkYjSjz7vEWO9xEe6MUhx76zSFzkPspk2UrLghYayvmaKPf1ky4XjNI9ZQ=="], + "@hey-api/shared": ["@hey-api/shared@0.1.0", "", { "dependencies": { "@hey-api/codegen-core": "0.6.0", "@hey-api/json-schema-ref-parser": "1.2.3", "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "cross-spawn": "7.0.6", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-qEDMSBWEEWxcBU5XHacjCCnFOVq1YWPPR3owURVep60I7ejfSG5OINxM4eF+p3KJGMcZduzzfq9pd1grStHZBg=="], "@hey-api/types": ["@hey-api/types@0.1.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-mZaiPOWH761yD4GjDQvtjS2ZYLu5o5pI1TVSvV/u7cmbybv51/FVtinFBeaE1kFQCKZ8OQpn2ezjLBJrKsGATw=="], @@ -375,63 +328,63 @@ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "@jimp/core": ["@jimp/core@1.6.1", "", { "dependencies": { "@jimp/file-ops": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^21.3.3", "mime": "3" } }, "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A=="], + "@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="], - "@jimp/diff": ["@jimp/diff@1.6.1", "", { "dependencies": { "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "pixelmatch": "^5.3.0" } }, "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ=="], + "@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="], - "@jimp/file-ops": ["@jimp/file-ops@1.6.1", "", {}, "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w=="], + "@jimp/file-ops": ["@jimp/file-ops@1.6.0", "", {}, "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ=="], - "@jimp/js-bmp": ["@jimp/js-bmp@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "bmp-ts": "^1.0.9" } }, "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ=="], + "@jimp/js-bmp": ["@jimp/js-bmp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "bmp-ts": "^1.0.9" } }, "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw=="], - "@jimp/js-gif": ["@jimp/js-gif@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w=="], + "@jimp/js-gif": ["@jimp/js-gif@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g=="], - "@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "jpeg-js": "^0.4.4" } }, "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg=="], + "@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "jpeg-js": "^0.4.4" } }, "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA=="], - "@jimp/js-png": ["@jimp/js-png@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "pngjs": "^7.0.0" } }, "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA=="], + "@jimp/js-png": ["@jimp/js-png@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "pngjs": "^7.0.0" } }, "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg=="], - "@jimp/js-tiff": ["@jimp/js-tiff@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "utif2": "^4.1.0" } }, "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw=="], + "@jimp/js-tiff": ["@jimp/js-tiff@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "utif2": "^4.1.0" } }, "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw=="], - "@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ=="], + "@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA=="], - "@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/utils": "1.6.1" } }, "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ=="], + "@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw=="], - "@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ=="], + "@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw=="], - "@jimp/plugin-color": ["@jimp/plugin-color@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A=="], + "@jimp/plugin-color": ["@jimp/plugin-color@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA=="], - "@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-blit": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg=="], + "@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ=="], - "@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-crop": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg=="], + "@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA=="], - "@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg=="], + "@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang=="], - "@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g=="], + "@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q=="], - "@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1" } }, "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew=="], + "@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0" } }, "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ=="], - "@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA=="], + "@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA=="], - "@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA=="], + "@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg=="], - "@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/js-bmp": "1.6.1", "@jimp/js-jpeg": "1.6.1", "@jimp/js-png": "1.6.1", "@jimp/js-tiff": "1.6.1", "@jimp/plugin-color": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "any-base": "^1.1.0" } }, "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A=="], + "@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "any-base": "^1.1.0" } }, "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q=="], - "@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A=="], + "@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA=="], - "@jimp/plugin-print": ["@jimp/plugin-print@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/js-jpeg": "1.6.1", "@jimp/js-png": "1.6.1", "@jimp/plugin-blit": "1.6.1", "@jimp/types": "1.6.1", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w=="], + "@jimp/plugin-print": ["@jimp/plugin-print@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/types": "1.6.0", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A=="], - "@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.1", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww=="], + "@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.0", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg=="], - "@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/types": "1.6.1", "zod": "^3.23.8" } }, "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg=="], + "@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA=="], - "@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-crop": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg=="], + "@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw=="], - "@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/plugin-color": "1.6.1", "@jimp/plugin-hash": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "zod": "^3.23.8" } }, "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q=="], + "@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w=="], - "@jimp/types": ["@jimp/types@1.6.1", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w=="], + "@jimp/types": ["@jimp/types@1.6.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg=="], - "@jimp/utils": ["@jimp/utils@1.6.1", "", { "dependencies": { "@jimp/types": "1.6.1", "tinycolor2": "^1.6.0" } }, "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw=="], + "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], - "@jimp/wasm-webp": ["@jimp/wasm-webp@1.6.1", "", { "dependencies": { "@jsquash/webp": "^1.4.0", "zod": "^3.23.8" } }, "sha512-t+Wqkde4xQHP/UZ4bDiDo3pbhFz32E7FvQCUkuFdJDmEDl6gPCs6LQiQVBmumUQYTeVLiLtLzlM9j8s7yF0sXQ=="], + "@jimp/wasm-webp": ["@jimp/wasm-webp@1.6.0", "", { "dependencies": { "@jsquash/webp": "^1.4.0", "zod": "^3.23.8" } }, "sha512-P0zUpK6n2XIAn8bt0F6rhSn1+FgteBTrL+TBb6Oqw8v5qEDJoNYkd6LlfZYN8YwtRBTBdZ8GFnWsg2Sar+qOkA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -449,8 +402,6 @@ "@jsquash/webp": ["@jsquash/webp@1.5.0", "", { "dependencies": { "wasm-feature-detect": "^1.2.11" } }, "sha512-KggLoj2MnRSfIqTeKe1EmbljTX2vuV7mh79k89PCL1pyqiDULcPM1L47twxXt0hkb68F70bXiL31MxsuoZtKFw=="], - "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="], - "@node-minify/clean-css": ["@node-minify/clean-css@9.0.1", "", { "dependencies": { "@node-minify/utils": "9.0.1", "clean-css": "5.3.3" } }, "sha512-GHTMmjGloRvNzqdG7foI0iZeS2QmuYCQvdASJP9sCKjkpH45bygODpXPYKnlzUEpQgYvPK9Q3GxqYnVY9SdoqA=="], "@node-minify/core": ["@node-minify/core@9.0.2", "", { "dependencies": { "@node-minify/utils": "9.0.1", "glob": "10.3.3", "mkdirp": "3.0.1" } }, "sha512-FNhv29Wom6wKrrFKaeAfmZqz7TX5A1E6P+bpd0VIc+DYWMLUIhAViS8riaZg3A1oD0s06s+5BG2Fg7RqMKiKHw=="], @@ -459,11 +410,13 @@ "@node-minify/utils": ["@node-minify/utils@9.0.1", "", { "dependencies": { "gzip-size": "6.0.0" } }, "sha512-aC1+mhKTP3IMa2VcuGl3ui92LO/7CPQWldNGzu3BVGKiMNJ70AKJW/R6huuYCSuQyHDGM9oFwiVClsZnFxn67g=="], - "@noriginmedia/norigin-spatial-navigation": ["@noriginmedia/norigin-spatial-navigation@3.1.0", "", { "dependencies": { "@noriginmedia/norigin-spatial-navigation-core": "^3.1.0", "@noriginmedia/norigin-spatial-navigation-react": "^3.1.0" } }, "sha512-KPge4ocpDFde7cpZ2aqrPrKmxOxkue983NsfpmE/vX4k2l+Ik8UkucCWGqkcy81TXkEyRhdsYwFTRePNB5qUCg=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - "@noriginmedia/norigin-spatial-navigation-core": ["@noriginmedia/norigin-spatial-navigation-core@3.1.0", "", { "dependencies": { "lodash-es": "^4.17.21" } }, "sha512-AFxJHurTqy+I3NLnaXsLUBa9FZjUryMNFEdLpPrITSqDjk525aINeLMOK1PN7WTiK5xpHL0pbpw0+uVOfWgp4w=="], + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - "@noriginmedia/norigin-spatial-navigation-react": ["@noriginmedia/norigin-spatial-navigation-react@3.1.0", "", { "dependencies": { "@noriginmedia/norigin-spatial-navigation-core": "^3.1.0", "lodash-es": "^4.17.21" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-F2PIqzTnlYbbc+oRdIQfBf7e1VcA1uhyjze4uOal8FHI8tZs1U8nomH84+2KcM6G3EM/XGexgQsPy5f5dtrmUA=="], + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@noriginmedia/norigin-spatial-navigation": ["@noriginmedia/norigin-spatial-navigation@2.3.0", "", { "dependencies": { "lodash": "^4.17.21" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-gR//N45NnKz1h0/AVknkfg7QnNATETdgXUUD3EKPxuQPyhk7NhsphODzRamyvjYaxsU6VbY/szcUlzBWWBkNMw=="], "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], @@ -495,13 +448,9 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], - "@phalcode/ts-apicalypse": ["@phalcode/ts-apicalypse@1.0.26", "", { "dependencies": { "axios": "^1.11.0" } }, "sha512-RdqkuunEYu63hRs4tYZ6FLTC17ynC6AJ/YUppRGSIyr6pm5pI/vB1qlEaeUr/f4JJsNmbFwGjnMJdXvoP1LmWA=="], - - "@phalcode/ts-igdb-client": ["@phalcode/ts-igdb-client@1.0.26", "", { "dependencies": { "@phalcode/ts-apicalypse": "^1.0.26", "axios": "^1.11.0" } }, "sha512-ITBazxhafHDBVJFI6THrLOT8OuO4zhD9pOeKQUFJ80soKhBevvbJz3tzkt24fF783Hoqaja8rWmGSwcN04d5gA=="], - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], @@ -553,99 +502,91 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="], - "@simeonradivoev/gameflow-sdk": ["@simeonradivoev/gameflow-sdk@workspace:src/packages/gameflow-sdk"], - "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], - "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], - "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="], + "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="], - "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="], + "@tanstack/form-core": ["@tanstack/form-core@1.28.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.7.7" } }, "sha512-MX3YveB6SKHAJ2yUwp+Ca/PCguub8bVEnLcLUbFLwdkSRMkP0lMGdaZl+F0JuEgZw56c6iFoRyfILhS7OQpydA=="], - "@tanstack/form-core": ["@tanstack/form-core@1.32.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.9.1" } }, "sha512-Tn5VRDSjyqjmaet2tJMuEWDRFyrCaon03vxXPlSSaiSs6C/N7lCIwGCXJbZXEUq1kTj8jYN9qyXHbsz4LQHcow=="], - - "@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="], + "@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="], "@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="], - "@tanstack/query-core": ["@tanstack/query-core@5.100.10", "", {}, "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], - "@tanstack/query-devtools": ["@tanstack/query-devtools@5.100.10", "", {}, "sha512-3DmJf25hDPus5IpVvp6ujXv6bKV2zPzI9vpbAmpJigsL/H6DPvPjmf7/Q9yVKEke//8fgeQ45abjgnLuyYxAiw=="], + "@tanstack/query-devtools": ["@tanstack/query-devtools@5.93.0", "", {}, "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg=="], - "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.100.10", "", { "dependencies": { "@tanstack/query-core": "5.100.10" } }, "sha512-O9Pey40DhTTDBABS0bHr+KNL5/VMf6PrqjexS8WoDDtnkaoWM+y0MSe0V9E5W+BwvkjM33mB3aYcCxa175gZTQ=="], + "@tanstack/react-form": ["@tanstack/react-form@1.28.0", "", { "dependencies": { "@tanstack/form-core": "1.28.0", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-ibLcf5QkTogV0Ly944CuqGxWTpHyreNA4Cy8Wtky7zE9wtE3HVapQt4/hUuXo51zihfTkv5URiXpoTSKF5Xosg=="], - "@tanstack/react-form": ["@tanstack/react-form@1.32.0", "", { "dependencies": { "@tanstack/form-core": "1.32.0", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6WP5SQTA6/H9crCpvpq3ZppYWqtrdE5NjOy6ebABi6uAQPqhfTzrdjS9t40mCZCFtGI5585OhJV6zBP/KN2zcw=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="], - "@tanstack/react-query": ["@tanstack/react-query@5.100.10", "", { "dependencies": { "@tanstack/query-core": "5.100.10" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q=="], + "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.3", "", { "dependencies": { "@tanstack/query-devtools": "5.93.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.20", "react": "^18 || ^19" } }, "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA=="], - "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.100.10", "", { "dependencies": { "@tanstack/query-devtools": "5.100.10" }, "peerDependencies": { "@tanstack/react-query": "^5.100.10", "react": "^18 || ^19" } }, "sha512-zes0+o9ef5rAZXJ9f/SeaLs2nufJaeVkZkl/Or9NGrWVF41kL9Od9ED9nCwtQlgiF2VGtrzhEw5AU/igAO+aAg=="], + "@tanstack/react-router": ["@tanstack/react-router@1.157.16", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.157.16", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-xwFQa7S7dhBhm3aJYwU79cITEYgAKSrcL6wokaROIvl2JyIeazn8jueWqUPJzFjv+QF6Q8euKRlKUEyb5q2ymg=="], - "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.100.10", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.100.10" }, "peerDependencies": { "@tanstack/react-query": "^5.100.10", "react": "^18 || ^19" } }, "sha512-EImacngLXYEtzlrIPf8IAqKN3foS7cmSj4GWqsHJvc7K+8fy2c3s7mdV8oTJeii/TvrzO4X9fcnXi6tUHMIOHA=="], + "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.154.12", "", { "dependencies": { "@tanstack/router-devtools-core": "1.154.12" }, "peerDependencies": { "@tanstack/react-router": "^1.154.12", "@tanstack/router-core": "^1.154.12", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-TcGe7pmeVjk1zD58eMR87GG9OXMx6LDGz5QopmJS4LafvK2hvuaht+eKBnZlCvKLPlXu5juwHT4u+2bYdn6sqQ=="], - "@tanstack/react-router": ["@tanstack/react-router@1.169.2", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.169.2", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-OJM7Kguc7ERnweaNRWsyWgIKcl3z23rD1B4jaxjzd9RGdnzpt2HfrWa9rggbT0Hfzhfo4D2ZmsfoTme035tniQ=="], + "@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.157.17", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.157.16" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-IrLi+cNLOAiTShuwGUi9WCNWXMG4993fnyXnWMC53M64rhUngPRFCAQiA05BavE/d7bifC6WKDTt1ZhC1Pewaw=="], - "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.166.13", "", { "dependencies": { "@tanstack/router-devtools-core": "1.167.3" }, "peerDependencies": { "@tanstack/react-router": "^1.168.15", "@tanstack/router-core": "^1.168.11", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-6yKRFFJrEEOiGp5RAAuGCYsl81M4XAhJmLcu9PKj+HZle4A3dsP60lwHoqQYWHMK9nKKFkdXR+D8qxzxqtQbEA=="], + "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], - "@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.166.12", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.168.0" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-yDUIoEh+PimAcWmk/2BE0EkI8TwLVeToNzoIuwahmTtBUR+ptZPWbtiPjudO8JZ0BhT3odHtuOn1eBOK0/4NAQ=="], + "@tanstack/router-core": ["@tanstack/router-core@1.157.16", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-eJuVgM7KZYTTr4uPorbUzUflmljMVcaX2g6VvhITLnHmg9SBx9RAgtQ1HmT+72mzyIbRSlQ1q0fY/m+of/fosA=="], - "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], + "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.154.12", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.154.12", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-lvnP9cqknvSSkUjqQRVn61TcBhq72hCFFOzMwdFdFPTO8nMEXvYE6ZZJiXtivwcvsKmO6XVFLMXuJr/928gNkw=="], - "@tanstack/router-core": ["@tanstack/router-core@1.169.2", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-5sm0DJF1A7Mz+9gy4Gz/lLovNailK3yot4vYvz9MkBUPw26uLnhQiR8hSCYxucjE0wD6Mdlc5l+Z0/XTlZ7xHw=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.157.16", "", { "dependencies": { "@tanstack/router-core": "1.157.16", "@tanstack/router-utils": "1.154.7", "@tanstack/virtual-file-routes": "1.154.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-Ae2M00VTFjjED7glSCi/mMLENRzhEym6NgjoOx7UVNbCC/rLU/5ASDe5VIlDa8QLEqP5Pj088Gi51gjmRuICvQ=="], - "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.167.3", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.168.11", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.157.16", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.157.16", "@tanstack/router-generator": "1.157.16", "@tanstack/router-utils": "1.154.7", "@tanstack/virtual-file-routes": "1.154.7", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.157.16", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-YQg7L06xyCJAYyrEJNZGAnDL8oChILU+G/eSDIwEfcWn5iLk+47x1Gcdxr82++47PWmOPhzuTo8edDQXWs7kAA=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.166.42", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.169.2", "@tanstack/router-utils": "1.161.8", "@tanstack/virtual-file-routes": "1.161.7", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^3.24.2" } }, "sha512-2qBWC0t78r6b3vI+AbnvCZcFAvbYBDlLuWZrTjQbcjUmwG3qyeQp983tJyDuj9wb5//adG1tgAGXZkJ3aDwdBg=="], + "@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.157.16", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-YuwNG4jdtn+r90yyti8yP27IKaVoflWmRezqnj0gyJxpRauBkK7MVLvWSNbJadnk88b9H+rdtNOF2k3owGaong=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.35", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.169.2", "@tanstack/router-generator": "1.166.42", "@tanstack/router-utils": "1.161.8", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^3.0.0", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.169.2", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-UAScU5VAzLYVY4FML/Cbc5S5TucT4I8Ata05yozGOe4ZfepTKRffA5xWLtD2N+ov5svdv0KTX/kqlZnYPe28mA=="], + "@tanstack/router-utils": ["@tanstack/router-utils@1.154.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-61bGx32tMKuEpVRseu2sh1KQe8CfB7793Mch/kyQt0EP3tD7X0sXmimCl3truRiDGUtI0CaSoQV1NPjAII1RBA=="], - "@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.168.0", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-5yBUAF1d9z2kOFKoz1spvpvkMSTmRnRXEwi+bGKfrXYmt7CfHu3Pk8KUFMln67uQoKQ9VTkcd5tLkjJVrZ2/AQ=="], + "@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="], - "@tanstack/router-utils": ["@tanstack/router-utils@1.161.8", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-xyiLWEKjfBAVhauDSSjXxyf7s8elU6SM+V050sbkofvGmIIvkwPFtDsX7Gvwh14kBd6iCwAT+RiPvXTxAptY0Q=="], + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.154.7", "", {}, "sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg=="], - "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], - - "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="], - - "@tanstack/zod-adapter": ["@tanstack/zod-adapter@1.166.9", "", { "peerDependencies": { "@tanstack/react-router": ">=1.43.2", "zod": "^3.23.8" } }, "sha512-HHllQ/CKGi8YBbftv6OmzojtHM6Rk4UszAFICAgUMbwiqtKqjlIZQ/7mv2IPNxBb8YlOQgzyQ4jz2UTEXIi6YA=="], + "@tanstack/zod-adapter": ["@tanstack/zod-adapter@1.162.4", "", { "peerDependencies": { "@tanstack/react-router": ">=1.43.2", "zod": "^3.23.8" } }, "sha512-sO4n2o9F7gZKHZb/nW/fMcDaeVcbFZ2a7zCA+GkaHJwRmhKKlQQ0dae9pc8wOMMG+QkfH1Wysq+tg2RNvm/kpg=="], "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@types/adm-zip": ["@types/adm-zip@0.5.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q=="], + "@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="], - "@types/audiosprite": ["@types/audiosprite@0.7.3", "", {}, "sha512-P4rUuHPt2kWPMqyObfh1SfqS2H/ZuTxByh00ecuI2tOdvP5b8NznuBeQgemDXV9v8b4pewFPB9G3BuYRONqD7A=="], + "@types/adm-zip": ["@types/adm-zip@0.5.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -655,36 +596,24 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], - "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], - "@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="], - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - - "@types/howler": ["@types/howler@2.2.12", "", {}, "sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g=="], - "@types/ini": ["@types/ini@4.1.1", "", {}, "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="], - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/mustache": ["@types/mustache@4.2.6", "", {}, "sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw=="], "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], @@ -693,29 +622,25 @@ "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - "@types/rclone.js": ["@types/rclone.js@0.6.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-BssKAAVRY//fxGKso8SatyOwiD7X0toDofNnVxZlIXmN7UHrn2UBTxldNAjgUvWA91qJyeEPfKmeJpZVhLugXg=="], - - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@types/unzip-stream": ["@types/unzip-stream@0.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-ud0vtsNRF+joUCyvNMyo0j5DKX2Lh/im+xVgRzBEsfHhQYZ+i4fKTveova9XxLzt6Jl6G0e/0mM4aC0gqZYSnA=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "add-stream": ["add-stream@1.0.0", "", {}, "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ=="], - "adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="], + "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -727,7 +652,7 @@ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], @@ -743,26 +668,26 @@ "arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], - "async": ["async@0.9.2", "", {}, "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw=="], + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="], - "audiosprite": ["audiosprite@0.7.2", "", { "dependencies": { "async": "~0.9.0", "glob": "^6.0.4", "mkdirp": "^0.5.0", "optimist": "~0.6.1", "underscore": "~1.8.3", "winston": "~1.0.0" }, "bin": { "audiosprite": "./cli.js" } }, "sha512-9Z6UwUuv4To5nUQNRIw5/Q3qA7HYm0ANzoW5EDGPEsU2oIRVgmIlLlm9YZfpPKoeUxt54vMStl2/762189VmJw=="], - "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], - "axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="], + "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ=="], @@ -783,11 +708,13 @@ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -803,19 +730,9 @@ "caniuse-lite": ["caniuse-lite@1.0.30001765", "", {}, "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ=="], - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="], - "chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - - "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], - - "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - - "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "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=="], @@ -833,24 +750,18 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "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=="], - "colors": ["colors@1.0.3", "", {}, "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw=="], - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], - "commit-and-tag-version": ["commit-and-tag-version@12.7.3", "", { "dependencies": { "chalk": "^2.4.2", "conventional-changelog": "4.0.0", "conventional-changelog-config-spec": "2.1.0", "conventional-changelog-conventionalcommits": "6.1.0", "conventional-recommended-bump": "7.0.1", "detect-indent": "^6.1.0", "detect-newline": "^3.1.0", "dotgitignore": "^2.1.0", "fast-xml-parser": "^5.5.6", "figures": "^3.2.0", "find-up": "^5.0.0", "git-semver-tags": "^5.0.1", "semver": "^7.7.2", "yaml": "^2.6.0", "yargs": "^17.7.2" }, "bin": { "commit-and-tag-version": "bin/cli.js" } }, "sha512-rbauuCDU98yEHMy/LrNNu8HLTuGv7C2kN/3GXC59L18aJGii0eiryCESb1SEHXNFem2/2ngWG/Pq6qaCqw3aCw=="], - "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], @@ -859,51 +770,51 @@ "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=="], - "conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="], + "conf": ["conf@15.0.2", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw=="], "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "conventional-changelog": ["conventional-changelog@4.0.0", "", { "dependencies": { "conventional-changelog-angular": "^6.0.0", "conventional-changelog-atom": "^3.0.0", "conventional-changelog-codemirror": "^3.0.0", "conventional-changelog-conventionalcommits": "^6.0.0", "conventional-changelog-core": "^5.0.0", "conventional-changelog-ember": "^3.0.0", "conventional-changelog-eslint": "^4.0.0", "conventional-changelog-express": "^3.0.0", "conventional-changelog-jquery": "^4.0.0", "conventional-changelog-jshint": "^3.0.0", "conventional-changelog-preset-loader": "^3.0.0" } }, "sha512-JbZjwE1PzxQCvm+HUTIr+pbSekS8qdOZzMakdFyPtdkEWwFvwEJYONzjgMm0txCb2yBcIcfKDmg8xtCKTdecNQ=="], + "conventional-changelog": ["conventional-changelog@3.1.25", "", { "dependencies": { "conventional-changelog-angular": "^5.0.12", "conventional-changelog-atom": "^2.0.8", "conventional-changelog-codemirror": "^2.0.8", "conventional-changelog-conventionalcommits": "^4.5.0", "conventional-changelog-core": "^4.2.1", "conventional-changelog-ember": "^2.0.9", "conventional-changelog-eslint": "^3.0.9", "conventional-changelog-express": "^2.0.6", "conventional-changelog-jquery": "^3.0.11", "conventional-changelog-jshint": "^2.0.9", "conventional-changelog-preset-loader": "^2.3.4" } }, "sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ=="], - "conventional-changelog-angular": ["conventional-changelog-angular@6.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg=="], + "conventional-changelog-angular": ["conventional-changelog-angular@5.0.13", "", { "dependencies": { "compare-func": "^2.0.0", "q": "^1.5.1" } }, "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA=="], - "conventional-changelog-atom": ["conventional-changelog-atom@3.0.0", "", {}, "sha512-pnN5bWpH+iTUWU3FaYdw5lJmfWeqSyrUkG+wyHBI9tC1dLNnHkbAOg1SzTQ7zBqiFrfo55h40VsGXWMdopwc5g=="], + "conventional-changelog-atom": ["conventional-changelog-atom@2.0.8", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw=="], - "conventional-changelog-codemirror": ["conventional-changelog-codemirror@3.0.0", "", {}, "sha512-wzchZt9HEaAZrenZAUUHMCFcuYzGoZ1wG/kTRMICxsnW5AXohYMRxnyecP9ob42Gvn5TilhC0q66AtTPRSNMfw=="], + "conventional-changelog-codemirror": ["conventional-changelog-codemirror@2.0.8", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw=="], "conventional-changelog-config-spec": ["conventional-changelog-config-spec@2.1.0", "", {}, "sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ=="], - "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@6.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw=="], + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@4.6.3", "", { "dependencies": { "compare-func": "^2.0.0", "lodash": "^4.17.15", "q": "^1.5.1" } }, "sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g=="], - "conventional-changelog-core": ["conventional-changelog-core@5.0.2", "", { "dependencies": { "add-stream": "^1.0.0", "conventional-changelog-writer": "^6.0.0", "conventional-commits-parser": "^4.0.0", "dateformat": "^3.0.3", "get-pkg-repo": "^4.2.1", "git-raw-commits": "^3.0.0", "git-remote-origin-url": "^2.0.0", "git-semver-tags": "^5.0.0", "normalize-package-data": "^3.0.3", "read-pkg": "^3.0.0", "read-pkg-up": "^3.0.0" } }, "sha512-RhQOcDweXNWvlRwUDCpaqXzbZemKPKncCWZG50Alth72WITVd6nhVk9MJ6w1k9PFNBcZ3YwkdkChE+8+ZwtUug=="], + "conventional-changelog-core": ["conventional-changelog-core@4.2.4", "", { "dependencies": { "add-stream": "^1.0.0", "conventional-changelog-writer": "^5.0.0", "conventional-commits-parser": "^3.2.0", "dateformat": "^3.0.0", "get-pkg-repo": "^4.0.0", "git-raw-commits": "^2.0.8", "git-remote-origin-url": "^2.0.0", "git-semver-tags": "^4.1.1", "lodash": "^4.17.15", "normalize-package-data": "^3.0.0", "q": "^1.5.1", "read-pkg": "^3.0.0", "read-pkg-up": "^3.0.0", "through2": "^4.0.0" } }, "sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg=="], - "conventional-changelog-ember": ["conventional-changelog-ember@3.0.0", "", {}, "sha512-7PYthCoSxIS98vWhVcSphMYM322OxptpKAuHYdVspryI0ooLDehRXWeRWgN+zWSBXKl/pwdgAg8IpLNSM1/61A=="], + "conventional-changelog-ember": ["conventional-changelog-ember@2.0.9", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A=="], - "conventional-changelog-eslint": ["conventional-changelog-eslint@4.0.0", "", {}, "sha512-nEZ9byP89hIU0dMx37JXQkE1IpMmqKtsaR24X7aM3L6Yy/uAtbb+ogqthuNYJkeO1HyvK7JsX84z8649hvp43Q=="], + "conventional-changelog-eslint": ["conventional-changelog-eslint@3.0.9", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA=="], - "conventional-changelog-express": ["conventional-changelog-express@3.0.0", "", {}, "sha512-HqxihpUMfIuxvlPvC6HltA4ZktQEUan/v3XQ77+/zbu8No/fqK3rxSZaYeHYant7zRxQNIIli7S+qLS9tX9zQA=="], + "conventional-changelog-express": ["conventional-changelog-express@2.0.6", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ=="], - "conventional-changelog-jquery": ["conventional-changelog-jquery@4.0.0", "", {}, "sha512-TTIN5CyzRMf8PUwyy4IOLmLV2DFmPtasKN+x7EQKzwSX8086XYwo+NeaeA3VUT8bvKaIy5z/JoWUvi7huUOgaw=="], + "conventional-changelog-jquery": ["conventional-changelog-jquery@3.0.11", "", { "dependencies": { "q": "^1.5.1" } }, "sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw=="], - "conventional-changelog-jshint": ["conventional-changelog-jshint@3.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-bQof4byF4q+n+dwFRkJ/jGf9dCNUv4/kCDcjeCizBvfF81TeimPZBB6fT4HYbXgxxfxWXNl/i+J6T0nI4by6DA=="], + "conventional-changelog-jshint": ["conventional-changelog-jshint@2.0.9", "", { "dependencies": { "compare-func": "^2.0.0", "q": "^1.5.1" } }, "sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA=="], - "conventional-changelog-preset-loader": ["conventional-changelog-preset-loader@3.0.0", "", {}, "sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA=="], + "conventional-changelog-preset-loader": ["conventional-changelog-preset-loader@2.3.4", "", {}, "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g=="], - "conventional-changelog-writer": ["conventional-changelog-writer@6.0.1", "", { "dependencies": { "conventional-commits-filter": "^3.0.0", "dateformat": "^3.0.3", "handlebars": "^4.7.7", "json-stringify-safe": "^5.0.1", "meow": "^8.1.2", "semver": "^7.0.0", "split": "^1.0.1" }, "bin": { "conventional-changelog-writer": "cli.js" } }, "sha512-359t9aHorPw+U+nHzUXHS5ZnPBOizRxfQsWT5ZDHBfvfxQOAik+yfuhKXG66CN5LEWPpMNnIMHUTCKeYNprvHQ=="], + "conventional-changelog-writer": ["conventional-changelog-writer@5.0.1", "", { "dependencies": { "conventional-commits-filter": "^2.0.7", "dateformat": "^3.0.0", "handlebars": "^4.7.7", "json-stringify-safe": "^5.0.1", "lodash": "^4.17.15", "meow": "^8.0.0", "semver": "^6.0.0", "split": "^1.0.0", "through2": "^4.0.0" }, "bin": { "conventional-changelog-writer": "cli.js" } }, "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ=="], - "conventional-commits-filter": ["conventional-commits-filter@3.0.0", "", { "dependencies": { "lodash.ismatch": "^4.4.0", "modify-values": "^1.0.1" } }, "sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q=="], + "conventional-commits-filter": ["conventional-commits-filter@2.0.7", "", { "dependencies": { "lodash.ismatch": "^4.4.0", "modify-values": "^1.0.0" } }, "sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA=="], - "conventional-commits-parser": ["conventional-commits-parser@4.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^1.0.1", "meow": "^8.1.2", "split2": "^3.2.2" }, "bin": { "conventional-commits-parser": "cli.js" } }, "sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg=="], + "conventional-commits-parser": ["conventional-commits-parser@3.2.4", "", { "dependencies": { "JSONStream": "^1.0.4", "is-text-path": "^1.0.1", "lodash": "^4.17.15", "meow": "^8.0.0", "split2": "^3.0.0", "through2": "^4.0.0" }, "bin": { "conventional-commits-parser": "cli.js" } }, "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q=="], - "conventional-recommended-bump": ["conventional-recommended-bump@7.0.1", "", { "dependencies": { "concat-stream": "^2.0.0", "conventional-changelog-preset-loader": "^3.0.0", "conventional-commits-filter": "^3.0.0", "conventional-commits-parser": "^4.0.0", "git-raw-commits": "^3.0.0", "git-semver-tags": "^5.0.0", "meow": "^8.1.2" }, "bin": { "conventional-recommended-bump": "cli.js" } }, "sha512-Ft79FF4SlOFvX4PkwFDRnaNiIVX7YbmqGU0RwccUaiGvgp3S0a8ipR2/Qxk31vclDNM+GSdJOVs2KrsUCjblVA=="], + "conventional-recommended-bump": ["conventional-recommended-bump@6.1.0", "", { "dependencies": { "concat-stream": "^2.0.0", "conventional-changelog-preset-loader": "^2.3.4", "conventional-commits-filter": "^2.0.7", "conventional-commits-parser": "^3.2.0", "git-raw-commits": "^2.0.8", "git-semver-tags": "^4.1.1", "meow": "^8.0.0", "q": "^1.5.1" }, "bin": { "conventional-recommended-bump": "cli.js" } }, "sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], - "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -917,19 +828,15 @@ "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], - "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + "css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "cycle": ["cycle@1.0.3", "", {}, "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA=="], - - "daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="], + "daisyui": ["daisyui@5.5.14", "", {}, "sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg=="], "dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="], @@ -945,8 +852,6 @@ "decamelize-keys": ["decamelize-keys@1.1.1", "", { "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" } }, "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg=="], - "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], - "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -957,8 +862,6 @@ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], @@ -967,8 +870,6 @@ "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], @@ -985,9 +886,11 @@ "dotgitignore": ["dotgitignore@2.1.0", "", { "dependencies": { "find-up": "^3.0.0", "minimatch": "^3.0.4" } }, "sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA=="], - "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], + "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.2", "", { "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-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], + "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=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -999,7 +902,7 @@ "electron-to-chromium": ["electron-to-chromium@1.5.277", "", {}, "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw=="], - "elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "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-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="], + "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=="], @@ -1009,7 +912,7 @@ "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], + "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=="], @@ -1027,33 +930,35 @@ "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=="], "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="], "exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], - "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "fast-xml-builder": ["fast-xml-builder@1.1.9", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw=="], - - "fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -1061,7 +966,7 @@ "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], - "file-type": ["file-type@21.3.4", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g=="], + "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -1075,7 +980,7 @@ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - "fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], + "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -1099,15 +1004,15 @@ "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=="], - "git-raw-commits": ["git-raw-commits@3.0.0", "", { "dependencies": { "dargs": "^7.0.0", "meow": "^8.1.2", "split2": "^3.2.2" }, "bin": { "git-raw-commits": "cli.js" } }, "sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw=="], + "git-raw-commits": ["git-raw-commits@2.0.11", "", { "dependencies": { "dargs": "^7.0.0", "lodash": "^4.17.15", "meow": "^8.0.0", "split2": "^3.0.0", "through2": "^4.0.0" }, "bin": { "git-raw-commits": "cli.js" } }, "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A=="], "git-remote-origin-url": ["git-remote-origin-url@2.0.0", "", { "dependencies": { "gitconfiglocal": "^1.0.0", "pify": "^2.3.0" } }, "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw=="], - "git-semver-tags": ["git-semver-tags@5.0.1", "", { "dependencies": { "meow": "^8.1.2", "semver": "^7.0.0" }, "bin": { "git-semver-tags": "cli.js" } }, "sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA=="], + "git-semver-tags": ["git-semver-tags@4.1.1", "", { "dependencies": { "meow": "^8.0.0", "semver": "^6.0.0" }, "bin": { "git-semver-tags": "cli.js" } }, "sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA=="], "gitconfiglocal": ["gitconfiglocal@1.0.0", "", { "dependencies": { "ini": "^1.3.2" } }, "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ=="], - "glob": ["glob@6.0.4", "", { "dependencies": { "inflight": "^1.0.4", "inherits": "2", "minimatch": "2 || 3", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A=="], + "glob": ["glob@10.3.3", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.0.3", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { "glob": "dist/cjs/src/bin.js" } }, "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1121,7 +1026,7 @@ "gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="], - "handlebars": ["handlebars@4.7.9", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ=="], + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], "hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="], @@ -1133,20 +1038,12 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], - "howler": ["howler@2.2.4", "", {}, "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="], - "html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="], - "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], @@ -1155,35 +1052,23 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], - "immutable": ["immutable@5.1.5", "", {}, "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A=="], + "immutable": ["immutable@5.1.4", "", {}, "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA=="], "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - - "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], - - "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], - - "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], @@ -1193,8 +1078,6 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], @@ -1203,7 +1086,7 @@ "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], "is-text-path": ["is-text-path@1.0.1", "", { "dependencies": { "text-extensions": "^1.0.0" } }, "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w=="], @@ -1215,11 +1098,9 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="], - "jackspeak": ["jackspeak@2.3.6", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ=="], - "jimp": ["jimp@1.6.1", "", { "dependencies": { "@jimp/core": "1.6.1", "@jimp/diff": "1.6.1", "@jimp/js-bmp": "1.6.1", "@jimp/js-gif": "1.6.1", "@jimp/js-jpeg": "1.6.1", "@jimp/js-png": "1.6.1", "@jimp/js-tiff": "1.6.1", "@jimp/plugin-blit": "1.6.1", "@jimp/plugin-blur": "1.6.1", "@jimp/plugin-circle": "1.6.1", "@jimp/plugin-color": "1.6.1", "@jimp/plugin-contain": "1.6.1", "@jimp/plugin-cover": "1.6.1", "@jimp/plugin-crop": "1.6.1", "@jimp/plugin-displace": "1.6.1", "@jimp/plugin-dither": "1.6.1", "@jimp/plugin-fisheye": "1.6.1", "@jimp/plugin-flip": "1.6.1", "@jimp/plugin-hash": "1.6.1", "@jimp/plugin-mask": "1.6.1", "@jimp/plugin-print": "1.6.1", "@jimp/plugin-quantize": "1.6.1", "@jimp/plugin-resize": "1.6.1", "@jimp/plugin-rotate": "1.6.1", "@jimp/plugin-threshold": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1" } }, "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw=="], + "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -1251,29 +1132,29 @@ "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -1283,8 +1164,6 @@ "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], - "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], - "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], "lodash.defaultsdeep": ["lodash.defaultsdeep@4.6.1", "", {}, "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA=="], @@ -1299,8 +1178,6 @@ "lodash.negate": ["lodash.negate@3.0.2", "", {}, "sha512-JGJYYVslKYC0tRMm/7igfdHulCjoXjoganRNWM8AgS+RXfOvFnPkOveDhPI65F9aAypCX9QEEQoBqWf7Q6uAeA=="], - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -1313,69 +1190,15 @@ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], - - "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], - - "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], - - "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], - - "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], - - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], - - "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], - - "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - - "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="], "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], "meow": ["meow@8.1.2", "", { "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.0", "read-pkg-up": "^7.0.1", "redent": "^3.0.0", "trim-newlines": "^3.0.0", "type-fest": "^0.18.0", "yargs-parser": "^20.2.3" } }, "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q=="], - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], - - "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], - - "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], - - "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], - - "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], - - "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], - - "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], - - "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], - - "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], - - "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], - - "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], - - "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], - - "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], - - "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], - - "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], - - "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], - - "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], - - "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], - - "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], - - "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], @@ -1419,18 +1242,18 @@ "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - "node-downloader-helper": ["node-downloader-helper@2.1.11", "", { "bin": { "ndh": "bin/ndh" } }, "sha512-882fH2C9AWdiPCwz/2beq5t8FGMZK9Dx8TJUOIxzMCbvG7XUKM5BuJwN5f0NKo4SCQK6jR4p2TPm54mYGdGchQ=="], + "node-downloader-helper": ["node-downloader-helper@2.1.10", "", { "bin": { "ndh": "bin/ndh" } }, "sha512-8LdieUd4Bqw/CzfZLf30h+1xSAq3riWSDfWKsPJYz8EULoWxjS1vw6BGLYFZDxQgXjDR7UmC9UpQ0oV93U98Fg=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], "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=="], - "node-unrar-js": ["node-unrar-js@2.0.2", "", {}, "sha512-hLNmoJzqaKJnod8yiTVGe9hnlNRHotUi0CreSv/8HtfRi/3JnRC8DvsmKfeGGguRjTEulhZK6zXX5PXoVuDZ2w=="], - "normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -1449,24 +1272,16 @@ "omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "opener": ["opener@1.5.2", "", { "bin": { "opener": "bin/opener-bin.js" } }, "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="], - "optimist": ["optimist@0.6.1", "", { "dependencies": { "minimist": "~0.0.1", "wordwrap": "~0.0.2" } }, "sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g=="], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "p-queue": ["p-queue@9.2.0", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g=="], - - "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], @@ -1477,12 +1292,8 @@ "parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="], - "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - "parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - "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=="], @@ -1491,10 +1302,6 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], - - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], @@ -1505,6 +1312,8 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], + "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -1517,16 +1326,12 @@ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], - "pkginfo": ["pkginfo@0.3.1", "", {}, "sha512-yO5feByMzAp96LtP58wvPKSbaKAi/1C4kV9XpTctr6EepnP6F33RBNOiVrdz9BrPA98U2BMFsTNHo44TWcbQ2A=="], - "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], "portfinder": ["portfinder@1.0.38", "", { "dependencies": { "async": "^3.2.6", "debug": "^4.3.6" } }, "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], - "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], "preact": ["preact@10.11.3", "", {}, "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg=="], @@ -1539,37 +1344,37 @@ "pretty-format": ["pretty-format@3.8.0", "", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="], - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + "q": ["q@1.5.1", "", {}, "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="], "qr.js": ["qr.js@0.0.0", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="], "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], - "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], - "react-error-boundary": ["react-error-boundary@6.1.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="], + "react-error-boundary": ["react-error-boundary@6.1.0", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-02k9WQ/mUhdbXir0tC1NiMesGzRPaCsJEWU/4bcFrbY1YMZOtHShtZP6zw0SJrBWA/31H0KT9/FgdL8+sPKgHA=="], "react-hot-toast": ["react-hot-toast@2.6.0", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg=="], "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], - - "react-qr-code": ["react-qr-code@2.0.21", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-xaywjo0eaF4S3LOz6ns5eoPbM2E+q9HYl4VATYpxK4bBniOhQ9noY2RJ9G4SnZFhUwzx63FUT6KdHzfKgUwyuQ=="], + "react-qr-code": ["react-qr-code@2.0.18", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg=="], "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], @@ -1579,75 +1384,79 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], - "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], - - "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], - "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "sass": ["sass@1.99.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q=="], + "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.99.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", "immutable": "^5.1.5", "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.99.0", "sass-embedded-android-arm": "1.99.0", "sass-embedded-android-arm64": "1.99.0", "sass-embedded-android-riscv64": "1.99.0", "sass-embedded-android-x64": "1.99.0", "sass-embedded-darwin-arm64": "1.99.0", "sass-embedded-darwin-x64": "1.99.0", "sass-embedded-linux-arm": "1.99.0", "sass-embedded-linux-arm64": "1.99.0", "sass-embedded-linux-musl-arm": "1.99.0", "sass-embedded-linux-musl-arm64": "1.99.0", "sass-embedded-linux-musl-riscv64": "1.99.0", "sass-embedded-linux-musl-x64": "1.99.0", "sass-embedded-linux-riscv64": "1.99.0", "sass-embedded-linux-x64": "1.99.0", "sass-embedded-unknown-all": "1.99.0", "sass-embedded-win32-arm64": "1.99.0", "sass-embedded-win32-x64": "1.99.0" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-gF/juR1aX02lZHkvwxdF80SapkQeg2fetoDF6gIQkNbSw5YEUFspMkyGTjPjgZSgIHuZpy+Wz4PlebKnLXMjdg=="], + "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.99.0", "", { "dependencies": { "sass": "1.99.0" }, "cpu": [ "!arm", "!x64", "!arm64", ] }, "sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw=="], + "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.99.0", "", { "os": "android", "cpu": "arm" }, "sha512-EHvJ0C7/VuP78Qr6f8gIUVUmCqIorEQpw2yp3cs3SMg02ZuumlhjXvkTcFBxHmFdFR23vTNk1WnhY6QSeV1nFQ=="], + "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.99.0", "", { "os": "android", "cpu": "arm64" }, "sha512-fNHhdnP23yqqieCbAdym4N47AleSwjbNt6OYIYx4DdACGdtERjQB4iOX/TaKsW034MupfF7SjnAAK8w7Ptldtg=="], + "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.99.0", "", { "os": "android", "cpu": "none" }, "sha512-4zqDFRvgGDTL5vTHuIhRxUpXFoh0Cy7Gm5Ywk19ASd8Settmd14YdPRZPmMxfgS1GH292PofV1fq1ifiSEJWBw=="], + "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.99.0", "", { "os": "android", "cpu": "x64" }, "sha512-Uk53k/dGYt04RjOL4gFjZ0Z9DH9DKh8IA8WsXUkNqsxerAygoy3zqRBS2zngfE9K2jiOM87q+1R1p87ory9oQQ=="], + "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.99.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u61/7U3IGLqoO6gL+AHeiAtlTPFwJK1+964U8gp45ZN0hzh1yrARf5O1mivXv8NnNgJvbG2wWJbiNZP0lG/lTg=="], + "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.99.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-j/kkk/NcXdIameLezSfXjgCiBkVcA+G60AXrX768/3g0miK1g7M9dj7xOhCb1i7/wQeiEI3rw2LLuO63xRIn4A=="], + "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.99.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d4IjJZrX2+AwB2YCy1JySwdptJECNP/WfAQLUl8txI3ka8/d3TUI155GtelnoZUkio211PwIeFvvAeZ9RXPQnw=="], + "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.99.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-btNcFpItcB56L40n8hDeL7sRSMLDXQ56nB5h2deddJx1n60rpKSElJmkaDGHtpkrY+CTtDRV0FZDjHeTJddYew=="], + "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.99.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2gvHOupgIw3ytatXT4nFUow71LFbuOZPEwG+HUzcNQDH8ue4Ez8cr03vsv5MDv3lIjOKcXwDvWD980t18MwkoQ=="], + "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.99.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hi2bt/IrM5P4FBKz6EcHAlniwfpoz9mnTdvSd58y+avA3SANM76upIkAdSayA8ZGwyL3gZokru1AKDPF9lJDNw=="], + "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.99.0", "", { "os": "linux", "cpu": "none" }, "sha512-mKqGvVaJ9rHMqyZsF0kikQe4NO0f4osb67+X6nLhBiVDKvyazQHJ3zJQreNefIE36yL2sjHIclSB//MprzaQDg=="], + "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.99.0", "", { "os": "linux", "cpu": "x64" }, "sha512-huhgOMmOc30r7CH7qbRbT9LerSEGSnWuS4CYNOskr9BvNeQp4dIneFufNRGZ7hkOAxUM8DglxIZJN/cyAT95Ew=="], + "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.99.0", "", { "os": "linux", "cpu": "none" }, "sha512-mevFPIFAVhrH90THifxLfOntFmHtcEKOcdWnep2gJ0X4DVva4AiVIRlQe/7w9JFx5+gnDRE1oaJJkzuFUuYZsA=="], + "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.99.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9k7IkULqIZdCIVt4Mboryt6vN8Mjmm3EhI1P3mClU5y5i3wLK5ExC3cbVWk047KsID/fvB1RLslqghXJx5BoxA=="], + "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.99.0", "", { "dependencies": { "sass": "1.99.0" }, "os": [ "!linux", "!win32", "!darwin", "!android", ] }, "sha512-P7MxiUtL/XzGo3PX0CaB8lNNEFLQWKikPA8pbKytx9ZCLZSDkt2NJcdAbblB/sqMs4AV3EK2NadV8rI/diq3xg=="], + "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.99.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8whpsW7S+uO8QApKfQuc36m3P9EISzbVZOgC79goob4qGy09u8Gz/rYvw8h1prJDSjltpHGhOzBE6LDz7WvzVw=="], + "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.99.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ipuOv1R2K4MHeuCEAZGpuUbAgma4gb0sdacyrTjJtMOy/OY9UvWfVlwErdB09KIkp4fPDpQJDJfvYN6bC8jeNg=="], + "sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.97.3", "", { "os": "win32", "cpu": "x64" }, "sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw=="], - "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -1655,9 +1464,9 @@ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], + "seroval": ["seroval@1.4.2", "", {}, "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ=="], - "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + "seroval-plugins": ["seroval-plugins@1.4.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1677,24 +1486,18 @@ "simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="], - "slugify": ["slugify@1.6.9", "", {}, "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg=="], - - "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], - "socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="], "socket.io-adapter": ["socket.io-adapter@2.5.6", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" } }, "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ=="], "socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="], - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], @@ -1707,7 +1510,7 @@ "split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], - "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + "standard-version": ["standard-version@9.5.0", "", { "dependencies": { "chalk": "^2.4.2", "conventional-changelog": "3.1.25", "conventional-changelog-config-spec": "2.1.0", "conventional-changelog-conventionalcommits": "4.6.3", "conventional-recommended-bump": "6.1.0", "detect-indent": "^6.0.0", "detect-newline": "^3.1.0", "dotgitignore": "^2.1.0", "figures": "^3.1.0", "find-up": "^5.0.0", "git-semver-tags": "^4.0.0", "semver": "^7.1.1", "stringify-package": "^1.0.1", "yargs": "^16.0.0" }, "bin": { "standard-version": "bin/cli.js" } }, "sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1715,7 +1518,7 @@ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "stringify-package": ["stringify-package@1.0.1", "", {}, "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1725,49 +1528,45 @@ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], - "strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], - "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], - "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], - - "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "svg-icon-baker": ["svg-icon-baker@2.0.1", "", { "dependencies": { "css-tree": "^3.2.1", "fast-xml-builder": "^1.1.5", "fast-xml-parser": "^5.7.2", "svgo": "^4.0.1" } }, "sha512-1QWVlle2fSUra129CEKpo5sn4hLGa0KCd7v8kX+PkD8e1x8fAQXB5rkc8J8mXTHe6iSzcOS7j4Y2K+q9RaUoNQ=="], - - "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], + "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=="], - "systeminformation": ["systeminformation@5.31.6", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA=="], + "systeminformation": ["systeminformation@5.31.5", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], - "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], - "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], - "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - "terser": ["terser@5.36.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w=="], + "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], "text-extensions": ["text-extensions@1.9.0", "", {}, "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ=="], "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], - "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], + "through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], @@ -1783,7 +1582,7 @@ "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], - "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], "tough-cookie-file-store": ["tough-cookie-file-store@3.3.0", "", { "dependencies": { "tough-cookie": "^6.0.0" } }, "sha512-FbO/cOi/jp4wweo8soVNG/ZjDsgpBZWqaxWwu7gRKvsjg/Qt44kStp87VLfJnin749DlTbZDYvV1wuSr5jly2g=="], @@ -1791,11 +1590,11 @@ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - "trim-newlines": ["trim-newlines@3.0.1", "", {}, "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw=="], - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "ts-apicalypse": ["ts-apicalypse@0.4.2", "", { "dependencies": { "axios": "^1.4.0" } }, "sha512-A02KFDFZHYTft0fTkxr5AERLb//XK/qjvGxrX8uNCwZAvFpLI/4TrOVxbq6kV2caZXWAn8eZZfRzVn1xd/cSMw=="], + + "ts-igdb-client": ["ts-igdb-client@0.4.2", "", { "dependencies": { "axios": "^1.4.0", "ts-apicalypse": "^0.4.2" } }, "sha512-VGQbIyy75GbHW0WGGHBXsdJzuj8wOW/jiWURmQWltpkFjCTMSqqx9gTPSUUcJ0W2kdlWcyMU4TDDHo4N3Fij7A=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], @@ -1813,29 +1612,15 @@ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], - "underscore": ["underscore@1.8.3", "", {}, "sha512-5WsVTFcH1ut/kkhAaHf4PVgI8c7++GiVcpCGxPouI6ZVjsqPnSDf8h/8HtVqc0t4fzRXwnMK70EcZeAs3PIddg=="], - "undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], - "union": ["union@0.5.0", "", { "dependencies": { "qs": "^6.4.0" } }, "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA=="], - "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], - - "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], - - "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - - "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], - - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="], + "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], "unzip-stream": ["unzip-stream@0.3.4", "", { "dependencies": { "binary": "^0.3.0", "mkdirp": "^0.5.1" } }, "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw=="], @@ -1857,13 +1642,9 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + "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=="], - "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - - "vite": ["vite@7.3.3", "", { "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-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], - - "vite-plugin-svg-icons-ng": ["vite-plugin-svg-icons-ng@1.9.1", "", { "dependencies": { "svg-icon-baker": "2.0.1", "tinyglobby": "^0.2.16" }, "peerDependencies": { "vite": ">=5.0.0" } }, "sha512-g00nlit2havo0VRxpLiPkeJfMYt0DL/RO8X5HHop72rbMEZB5H1Fk7qXLWbTIO2/PkwJ8zSq0+h28ItaE1YQHQ=="], + "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=="], @@ -1885,16 +1666,12 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "winston": ["winston@1.0.2", "", { "dependencies": { "async": "~1.0.0", "colors": "1.0.x", "cycle": "1.0.x", "eyes": "0.1.x", "isstream": "0.1.x", "pkginfo": "0.3.x", "stack-trace": "0.0.x" } }, "sha512-BLxJH3KCgJ2paj2xKYTQLpxdKr9URPDDDLJnRVcbud7izT+m8Xzt5Rod6mnNgEcfT0fRvhEy2Cj3cEnnQpa6qA=="], - - "wordwrap": ["wordwrap@0.0.3", "", {}, "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw=="], + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], @@ -1911,42 +1688,18 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - - "@ap0nia/eden/elysia": ["elysia@1.2.15", "", { "dependencies": { "@sinclair/typebox": "^0.34.15", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-/oUSNb83jIWAGi6uSmbQ7Uy0RSJ9NimbVToSLnYS8jjsGId3zgdHqprsdf4rIMInOmEM8skjsFhZ4x8C5AB6+w=="], - - "@babel/core/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - - "@babel/core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/generator/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/parser/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - - "@babel/template/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], - - "@babel/template/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], - - "@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], - - "@babel/traverse/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], - - "@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], - "@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=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -1955,6 +1708,8 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@jimp/core/file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], + "@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], "@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -1991,53 +1746,41 @@ "@jimp/wasm-webp/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@node-minify/core/glob": ["glob@10.3.3", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.0.3", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { "glob": "dist/cjs/src/bin.js" } }, "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw=="], - "@node-minify/core/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], - "@tailwindcss/node/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + "@node-minify/terser/terser": ["terser@5.36.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + "@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.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tanstack/router-generator/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], - "@tanstack/router-generator/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + "@tanstack/router-core/@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@tanstack/router-utils/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - - "@tanstack/router-utils/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], - - "@types/babel__core/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], - - "@types/babel__template/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "babel-dead-code-elimination/@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], - - "babel-dead-code-elimination/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], - "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "compare-func/dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], - "concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "conventional-changelog-writer/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], @@ -2051,13 +1794,17 @@ "engine.io/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "get-pkg-repo/through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], + "get-pkg-repo/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + "git-semver-tags/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "gitconfiglocal/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "handlebars/wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + "handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], @@ -2065,14 +1812,8 @@ "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "http-proxy/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], - - "http-server/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], - "is-core-module/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], - "load-json-file/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], "meow/read-pkg-up": ["read-pkg-up@7.0.1", "", { "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", "type-fest": "^0.8.1" } }, "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg=="], @@ -2081,14 +1822,10 @@ "meow/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], - "minimist-options/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], - "optimist/minimist": ["minimist@0.0.10", "", {}, "sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw=="], - - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -2097,46 +1834,36 @@ "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], - "portfinder/async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - "read-pkg/normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], "read-pkg-up/find-up": ["find-up@2.1.0", "", { "dependencies": { "locate-path": "^2.0.0" } }, "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ=="], + "readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "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=="], + + "standard-version/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "standard-version/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + "string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "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=="], - "through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - - "tough-cookie-file-store/tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], - "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-plugin-svg-icons-ng/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], - "vite-static-assets-plugin/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "winston/async": ["async@1.0.0", "", {}, "sha512-5mO7DX4CbJzp9zjaFXusQQ4tzKJARjNB1Ih1pVBi8wkbmXy/xzIDgEMXxWePLzt2OdFwaxfneIlT1nCiXubrPQ=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], - - "@ap0nia/eden/elysia/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - - "@ap0nia/eden/elysia/memoirist": ["memoirist@0.3.1", "", {}, "sha512-lmk4Z45IuVZPT67nxAdD3rAsNExxMEBFXgCeJGJnoLkYOjmZnJ8Hmi+MGdl9oLKtAENFAAgG8FvV3Z8BNiqy8w=="], - "@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=="], @@ -2187,38 +1914,28 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "@node-minify/core/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "@jimp/core/file-type/strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], - "@tanstack/router-utils/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "@jimp/core/file-type/token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], - "babel-dead-code-elimination/@babel/core/@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], - - "babel-dead-code-elimination/@babel/core/@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], - - "babel-dead-code-elimination/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@node-minify/terser/terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "concurrently/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], "dotgitignore/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "get-pkg-repo/through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "get-pkg-repo/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], "get-pkg-repo/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "http-server/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "http-server/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "meow/read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "meow/read-pkg-up/read-pkg": ["read-pkg@5.2.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", "parse-json": "^5.0.0", "type-fest": "^0.6.0" } }, "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg=="], @@ -2233,7 +1950,13 @@ "sass/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "standard-version/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "standard-version/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "standard-version/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + + "standard-version/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], @@ -2287,8 +2010,6 @@ "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], - "vite-plugin-svg-icons-ng/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "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=="], @@ -2341,19 +2062,11 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], - "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "@node-minify/core/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "concurrently/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "dotgitignore/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], "dotgitignore/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - "http-server/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "get-pkg-repo/through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "meow/read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], @@ -2367,16 +2080,12 @@ "read-pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - "wrap-ansi-cjs/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "standard-version/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "concurrently/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "standard-version/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "dotgitignore/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "http-server/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "meow/read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "meow/read-pkg-up/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], @@ -2385,6 +2094,8 @@ "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@1.3.0", "", { "dependencies": { "p-try": "^1.0.0" } }, "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q=="], + "standard-version/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "meow/read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "read-pkg-up/find-up/locate-path/p-locate/p-limit/p-try": ["p-try@1.0.0", "", {}, "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww=="], diff --git a/drizzle/0002_flowery_rocket_raccoon.sql b/drizzle/0002_flowery_rocket_raccoon.sql deleted file mode 100644 index 5f7942e..0000000 --- a/drizzle/0002_flowery_rocket_raccoon.sql +++ /dev/null @@ -1,31 +0,0 @@ -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_games` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `source_id` text, - `source` text, - `igdb_id` integer, - `name` text, - `ra_id` integer, - `path_fs` text, - `main_glob` text, - `last_played` integer, - `created_at` integer DEFAULT (unixepoch()) NOT NULL, - `metadata` text DEFAULT '{}' NOT NULL, - `slug` text, - `platform_id` integer NOT NULL, - `cover` blob, - `type` text, - `summary` text, - `version` text, - `version_source` text, - `version_system` text, - FOREIGN KEY (`platform_id`) REFERENCES `platforms`(`id`) ON UPDATE cascade ON DELETE no action -); ---> statement-breakpoint -INSERT INTO `__new_games`("id", "source_id", "source", "igdb_id", "name", "ra_id", "path_fs", "main_glob", "last_played", "created_at", "metadata", "slug", "platform_id", "cover", "type", "summary", "version", "version_source", "version_system") SELECT "id", "source_id", "source", "igdb_id", "name", "ra_id", "path_fs", NULL, "last_played", "created_at", "metadata", "slug", "platform_id", "cover", "type", "summary", NULL, NULL, NULL FROM `games`;--> statement-breakpoint -DROP TABLE `games`;--> statement-breakpoint -ALTER TABLE `__new_games` RENAME TO `games`;--> statement-breakpoint -PRAGMA foreign_keys=ON;--> 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`); \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json deleted file mode 100644 index fb182f2..0000000 --- a/drizzle/meta/0002_snapshot.json +++ /dev/null @@ -1,479 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "40569ae5-facd-4680-bd48-fe70c5abf498", - "prevId": "6dc00b41-64d7-4cb3-ad90-f9e8e35af643", - "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": "text", - "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 - }, - "main_glob": { - "name": "main_glob", - "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": true, - "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 - }, - "version": { - "name": "version", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "version_source": { - "name": "version_source", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "version_system": { - "name": "version_system", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "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 index 0df44e3..c181729 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,13 +15,6 @@ "when": 1772998956867, "tag": "0001_outstanding_silk_fever", "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1776111721964, - "tag": "0002_flowery_rocket_raccoon", - "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 913be66..79ddcec 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,17 @@ { "name": "com.simeonradivoev.gameflow-deck", "displayName": "Gameflow", - "author": { - "name": "Simeon Radivoev", - "email": "work@simeonradivoev.com", - "url": "https://simeonradivoev.com" - }, - "version": "1.6.0", + "version": "1.3.0", "description": "Game Launcher", "icon": "./src/mainview/assets/icon.svg", "main": "./src/bun/index.ts", "bin": "gameflow", - "license": "AGPL-3.0", "repository": { "type": "git", "url": "https://github.com/simeonradivoev/gameflow-deck" }, "packageManager": "bun@1.3.9", "type": "module", - "workspaces": [ - "./src/packages/gameflow-sdk" - ], "scripts": { "dev": "NODE_ENV=development bun run build:vite && conc 'bun run ./scripts/dev.ts'", "dev:hmr": "PUBLIC_ACCESS=true conc -k 'bun run hmr' 'bun run ./scripts/dev.ts'", @@ -30,8 +21,8 @@ "build:prod:vite": "NODE_ENV=production bun run build:vite", "build:dev:vite": "NODE_ENV=development bun run build:vite", "build": "bun run build:vite && bun run ./scripts/package-bun.ts", - "build:non-compiled": "bun run build:vite && NON_COMPILED=true bun run ./scripts/package-bun.ts", "build:prod": "NODE_ENV=production bun run build", + "build:prod:dynamic": "NODE_ENV=production NON_COMPILED=true bun run build", "build:linux": "TARGET=bun-linux-x64 bun run build", "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 .forgejo/workflows/build.yml", @@ -42,121 +33,100 @@ "flatpak:generate-sources": "bun run ./scripts/generate-flatpak-sources.ts", "flatpak:override": "flatpak override org.flatpak.Builder --filesystem=host --device=all", "flatpak:restore": "flatpak override --reset --user org.flatpak.Builder", - "flatpak:build": "FLATPAK_BUILD=true NODE_ENV=production NON_COMPILED=true bun run build && flatpak run org.flatpak.Builder ../gameflow-flatpak/build/flatpak .config/flatpak/com.simeonradivoev.gameflow-deck.json --repo=.config/flatpak/repo --state-dir=../gameflow-flatpak/state --force-clean", + "flatpak:build": "flatpak run org.flatpak.Builder build/flatpak flatpak/com.simeonradivoev.gameflow-deck.json --repo=.config/flatpak/repo --force-clean", "flatpak:install": "bun run flatpak:build && flatpak --user install --reinstall \"$PWD/.config/flatpak/repo\" com.simeonradivoev.gameflow-deck", "build:prod:appimage": "bun run build:prod && bun run ./scripts/build-appimage.ts", "build:dev:appimage": "bun run build && bun run ./scripts/build-appimage.ts", - "version:generate": "commit-and-tag-version --sign", + "version:generate": "standard-version --sign", "package:Linux": "bun run build:prod:appimage", "package:Windows": "bun run build:prod", - "download:chromium": "bun scripts/download-chromium.ts --out=./bin/chromium", - "download:nwjs": "bun scripts/download-nw.ts", - "build:audiosprites": "bun ./scripts/generate-audio-sprites.ts", - "tsc": "tsc --noEmit", - "publish:sdk": "bun publish --cwd ./src/packages/gameflow-sdk/ --access public" + "download:chromium": "bun scripts/download-chromium.ts --out=./bin/chromium" }, "dependencies": { "7zip-bin": "^5.2.0", "@auth/core": "^0.34.3", - "@elysiajs/cors": "^1.4.2", - "@elysiajs/eden": "^1.4.9", - "@jimp/wasm-webp": "^1.6.1", - "@phalcode/ts-igdb-client": "^1.0.26", + "@elysiajs/cors": "^1.4.1", + "@elysiajs/eden": "^1.4.6", + "@jimp/wasm-webp": "^1.6.0", "cheerio": "^1.2.0", - "conf": "^15.1.0", - "drizzle-orm": "^0.45.2", - "elysia": "^1.4.28", - "fs-extra": "^11.3.5", + "conf": "^15.0.2", + "drizzle-orm": "^0.45.1", + "elysia": "^1.4.22", + "fs-extra": "^11.3.3", "get-folder-size": "^5.0.0", "ini": "^6.0.0", - "jimp": "^1.6.1", + "jimp": "^1.6.0", "mustache": "^4.2.0", "node-7z": "^3.0.0", "node-disk-info": "^1.3.0", - "node-downloader-helper": "^2.1.11", + "node-downloader-helper": "^2.1.10", "node-stream-zip": "^1.15.0", - "node-unrar-js": "^2.0.2", "open": "^11.0.0", - "p-queue": "^9.2.0", "pathe": "^2.0.3", - "slugify": "^1.6.9", - "smol-toml": "^1.6.1", - "systeminformation": "^5.31.6", - "tapable": "^2.3.3", - "tough-cookie": "^6.0.1", + "systeminformation": "^5.31.5", + "tapable": "^2.3.0", + "tough-cookie": "^6.0.0", "tough-cookie-file-store": "^3.3.0", + "ts-igdb-client": "^0.4.2", "unzip-stream": "^0.3.4", "webview-bun": "^2.4.0", - "zod": "^4.4.3" - }, - "overrides": { - "@tanstack/router-generator": { - "zod": "^3.23.8" - } + "zod": "^4.3.6" }, "devDependencies": { - "@ap0nia/eden": "^1.6.1", + "@ap0nia/eden": "^1.0.0-next.22", "@ap0nia/eden-tanstack-query": "^1.0.0-next.22", "@emulatorjs/emulatorjs": "^4.2.3", - "@hey-api/openapi-ts": "^0.91.1", - "@noriginmedia/norigin-spatial-navigation": "^3.1.0", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.3.0", - "@tanstack/react-form": "^1.32.0", - "@tanstack/react-query": "^5.100.10", - "@tanstack/react-query-devtools": "^5.100.10", - "@tanstack/react-query-persist-client": "^5.100.10", - "@tanstack/react-router": "^1.169.2", - "@tanstack/react-router-devtools": "^1.166.13", - "@tanstack/react-router-ssr-query": "^1.166.12", - "@tanstack/router-plugin": "^1.167.35", - "@tanstack/zod-adapter": "^1.166.9", + "@hey-api/openapi-ts": "^0.91.0", + "@noriginmedia/norigin-spatial-navigation": "^2.3.0", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-form": "^1.28.0", + "@tanstack/react-query": "^5.90.20", + "@tanstack/react-query-devtools": "^5.91.3", + "@tanstack/react-router": "^1.157.16", + "@tanstack/react-router-devtools": "^1.154.12", + "@tanstack/react-router-ssr-query": "^1.157.17", + "@tanstack/router-plugin": "^1.157.16", + "@tanstack/zod-adapter": "^1.162.4", "@types/adm-zip": "^0.5.8", - "@types/audiosprite": "^0.7.3", "@types/bun": "latest", "@types/fs-extra": "^11.0.4", "@types/howler": "^2.2.12", "@types/ini": "^4.1.1", - "@types/json-schema": "^7.0.15", "@types/mustache": "^4.2.6", "@types/node-7z": "^2.1.11", - "@types/rclone.js": "^0.6.3", - "@types/react": "^19.2.14", + "@types/react": "^19.2.9", "@types/react-dom": "^19.2.3", "@types/unzip-stream": "^0.3.4", - "@vitejs/plugin-react": "^5.2.0", - "adm-zip": "^0.5.17", + "@vitejs/plugin-react": "^5.1.2", + "adm-zip": "^0.5.16", "animate.css": "^4.1.1", "app-builder-bin": "^5.0.0-alpha.13", - "audiosprite": "^0.7.2", "babel-plugin-react-compiler": "^1.0.0", "classnames": "^2.5.1", - "commit-and-tag-version": "^12.7.3", "concurrently": "^9.2.1", "cross-env": "^10.1.0", - "daisyui": "^5.5.19", - "drizzle-kit": "^0.31.10", + "daisyui": "^5.5.14", + "drizzle-kit": "^0.31.9", + "dts-bundle-generator": "^9.5.1", "eden-tanstack-query": "^0.0.9", "howler": "^2.2.4", - "idb-keyval": "^6.2.2", "lucide-react": "^0.563.0", "pretty-bytes": "^7.1.0", - "pretty-ms": "^9.3.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "react-error-boundary": "^6.1.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-error-boundary": "^6.1.0", "react-hot-toast": "^2.6.0", - "react-markdown": "^10.1.0", - "react-qr-code": "^2.0.21", - "sass-embedded": "^1.99.0", - "tailwind-merge": "^3.6.0", - "tailwindcss": "^4.3.0", + "react-qr-code": "^2.0.18", + "sass-embedded": "^1.97.3", + "standard-version": "^9.5.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "usehooks-ts": "^3.1.1", - "vite": "^7.3.3", - "vite-plugin-svg-icons-ng": "^1.9.1", + "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" } -} \ No newline at end of file +} diff --git a/scripts/build-appimage.ts b/scripts/build-appimage.ts index c2f07f5..852df2e 100644 --- a/scripts/build-appimage.ts +++ b/scripts/build-appimage.ts @@ -4,11 +4,11 @@ import fs from 'node:fs/promises'; import { appBuilderPath, } from 'app-builder-bin'; import path from 'node:path'; import { ensureDir } from "fs-extra"; -import mustache from "mustache"; const APP_DIR = process.env.BUILD_DIR ?? `./build/${process.platform}`; const BINARY_NAME = pkg.bin; const ICON = "./src/mainview/public/256x256.png"; +const DESKTOP = "./flatpak/com.simeonradivoev.gameflow-deck.desktop"; const TMP_FOLDER = "."; const APP_NAME = pkg.displayName ?? pkg.name; @@ -27,45 +27,24 @@ await fs.rename(path.join(APPDIR, `usr`, 'share', BINARY_NAME), path.join(APPDIR await fs.rename(path.join(APPDIR, `usr`, 'share', `libwebview-${process.arch}.so`), path.join(APPDIR, `usr`, 'lib', `libwebview-${process.arch}.so`)); await fs.rename(path.join(APPDIR, `usr`, 'share', `7za`), path.join(APPDIR, `usr`, 'bin', `7za`)); -if (!await fs.exists('./bin/nw/nw')) -{ - await import('./download-nw'); -} +await fs.writeFile(path.join(APPDIR, `${APP_ID}.desktop`), `[Desktop Entry] +Version=${pkg.version} +X-AppImage-Name=${APP_NAME} +X-AppImage-Version=${pkg.version} +X-AppImage-Arch=${process.arch} +Name=${APP_NAME} +Comment=${pkg.description} +Exec=${APP_ID}.AppImage +Icon=.DirIcon +Type=Application +Categories=Game; +`); -await ensureDir(path.join(APPDIR, `usr`, 'lib', 'nw')); -await fs.cp('./bin/nw', path.join(APPDIR, `usr`, 'lib', 'nw'), { recursive: true }); -await fs.symlink(path.join(APPDIR, `usr`, 'lib', 'nw', 'nw'), path.join(APPDIR, `usr`, `bin`, 'nw')); - -const templateVars = { - APP_NAME, - VERSION: pkg.version, - ARCH: process.arch, - DESCRIPTION: pkg.description, - APP_ID, - BINARY_NAME, - LICENSE: pkg.license -}; - -const desktopFileTemplate = await fs.readFile('./.config/appimage/com.simeonradivoev.gameflow-deck.desktop', 'utf8'); - -const raw = await $`git tag --sort=-version:refname`.text().then(d => d.trim()); -const tags = raw.split('\n').filter(t => t.match(/^\d+\.\d+\.\d+$/)); -console.log("tags", tags); - -console.log(">>> Updating Release History..."); -const releases = await Promise.all(tags.map(async tag => -{ - const date = await $`git log -1 --format=%as ${tag}`.text().then(d => d.trim()); - const version = tag.replace(/^v/, ''); - return ` `; -})); - -const appStreamTemplate = await fs.readFile('./.config/appimage/com.simeonradivoev.gameflow-deck.appdata.xml', 'utf8'); -await ensureDir(path.join(APPDIR, 'usr', 'share', 'metainfo')); -await fs.writeFile(path.join(APPDIR, 'usr', 'share', 'metainfo', `${APP_ID}.appdata.xml`), mustache.render(appStreamTemplate, { ...templateVars, RELEASES: releases })); - -const appRunTemplate = await fs.readFile(`./.config/appimage/AppRun`, 'utf8'); -await Bun.write(path.join(APPDIR, "AppRun"), mustache.render(appRunTemplate, templateVars)); +await Bun.write(path.join(APPDIR, "AppRun"), `#!/bin/bash +APPDIR="$(dirname "$(readlink -f "$0")")" +APPIMAGE=true +exec "$APPDIR/usr/bin/${BINARY_NAME}" "$@" +`); await $`chmod +x ${APPDIR}/AppRun`; console.log(">>> Building AppImage..."); @@ -73,7 +52,7 @@ const config = { productName: pkg.displayName, productFilename: pkg.name, executableName: BINARY_NAME, - desktopEntry: mustache.render(desktopFileTemplate, templateVars), + desktopEntry: DESKTOP, icons: [ { file: ICON, @@ -88,7 +67,7 @@ const config = { // Remove the build dir, mainly to help with CIs await fs.rm(APP_DIR, { recursive: true }); await ensureDir(APP_DIR); -const OUTPUT = path.resolve(APP_DIR, `${APP_NAME}-${process.platform}-${process.arch}.AppImage`); +const OUTPUT = path.resolve(APP_DIR, `${APP_NAME}.AppImage`); const STAGE = path.resolve(TMP_FOLDER, `${APP_ID}.stage`); await ensureDir(STAGE); @@ -107,9 +86,8 @@ const proc = Bun.spawn([ }); const code = await proc.exited; -await fs.rm(STAGE, { recursive: true, force: true }); await fs.rm(APPDIR, { recursive: true, force: true }); - +await fs.rm(STAGE, { recursive: true, force: true }); if (code !== 0) process.exit(code); console.log(`\n Done!`); \ No newline at end of file diff --git a/scripts/dev.ts b/scripts/dev.ts index 1331f36..ef3ad70 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -2,44 +2,50 @@ import EventEmitter from "events"; import browser from '../src/bun/browser'; import { tmpdir } from "os"; import path from "path"; -import { watch } from "fs"; -import { sleep } from "bun"; +import { createInterface } from "readline"; +import { Readable } from "stream"; const events = new EventEmitter(); const abortController = new AbortController(); -let restarting = false; process.env.WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = "--remote-debugging-port=9222"; process.env.NODE_ENV = "development"; +let retries = 0; + function spawnServer () { - const s = Bun.spawn(["bun", '--install=fallback', "run", "--inspect=127.0.0.1:9228/fixed-session", "./src/bun/index.ts"], { + const s = Bun.spawn(["bun", '--watch', '--install=fallback', "run", "--inspect=127.0.0.1:9228/fixed-session", "./src/bun/index.ts"], { env: { ...process.env, HEADLESS: "true", }, - stdout: 'inherit', - stderr: 'inherit', - stdin: 'inherit', + stdout: "pipe", + stderr: "inherit", + stdin: "pipe", signal: abortController.signal, - killSignal: 'SIGKILL', - ipc (message, subprocess, handle) - { - if (message === 'focus') - { - events.emit('focus'); - } else if (message === 'exitapp') - { - events.emit('exitapp'); - } - }, + killSignal: 'SIGUSR1', onExit (subprocess, exitCode, signalCode) { - if (!restarting) + if (exitCode === 1 && retries <= 3) + { + server = spawnServer(); + retries++; + } else { - console.log("Existing Dev With", exitCode); process.exit(); } + + } + }); + const rl = createInterface({ input: Readable.fromWeb(s.stdout as any) }); + rl.on('line', e => + { + if (e === 'focus') + { + events.emit('focus'); + } else + { + console.log(e); } }); return s; @@ -50,10 +56,9 @@ function spawnBrowser () try { - return browser(events, { + return browser(events, process.env.FORCE_BROWSER === "true", { configPath: path.join(tmpdir(), 'gameflow'), - isSteamDeckGameMode: false, - forceBrowser: process.env.FORCE_BROWSER === "true" + isSteamDeckGameMode: false }); } catch (error) { @@ -61,44 +66,13 @@ function spawnBrowser () }; } -async function restart () -{ - if (server) - { - restarting = true; - server.kill(); - await server.exited; - server = undefined; - console.log("Old Server stopped"); - } - - server = spawnServer(); - await sleep(1000); - console.log("New Server started"); - restarting = false; -} - -watch("./src/bun", { recursive: true }, (event, filename) => -{ - if (restarting) return; - console.log(`[watcher] ${event}: ${filename} — restarting...`); - restart(); -}); - -watch("./src/packages", { recursive: true }, (event, filename) => -{ - if (restarting) return; - console.log(`[watcher] ${event}: ${filename} — restarting...`); - restart(); -}); - -let server: Bun.Subprocess | undefined = spawnServer(); +let server = spawnServer(); if (!process.env.HEADLESS) { spawnBrowser()?.then(async e => { - if (!server) return; - abortController.abort(); - await server.exited; + console.log("Sending exit Signal to server"); + await server.stdin.write('shutdown\n'); + await server.stdin.flush(); }); } \ No newline at end of file diff --git a/scripts/download-nw.ts b/scripts/download-nw.ts deleted file mode 100644 index 7d318b6..0000000 --- a/scripts/download-nw.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ensureDir, remove } from "fs-extra"; -import StreamZip from "node-stream-zip"; -import { spawnSync } from "node:child_process"; -import fs from 'node:fs/promises'; - -const VERSION = "0.110.1"; - -const platformMap: Record = { - "win32": "win", - "darwin": "osx" -}; -const extMap: Record = { - "win32": "zip", - "linux": "tar.gz", - "darwin": "zip" -}; - -console.log("Removing old download"); -await remove('./bin/nw'); - -const downloadUrl = `https://dl.nwjs.io/v${VERSION}/nwjs-sdk-v${VERSION}-${platformMap[process.platform] ?? process.platform}-${process.arch}.${extMap[process.platform]}`; - -console.log("Starting NW download from", downloadUrl); -const response = await fetch(downloadUrl); -if (!response.ok) throw new Error(response.statusText); -const downlodPath = `./bin/nw.${extMap[process.platform]}`; -await ensureDir('./bin'); -await Bun.write(downlodPath, response); -console.log("Downloaded NW to", downlodPath); - -if (downlodPath.endsWith('.zip')) -{ - await extractZip(downlodPath, './bin'); -} -else if (downlodPath.endsWith(".tar.gz")) -{ - const result = spawnSync("tar", ["-xvf", downlodPath, "-C", './bin'], { stdio: "inherit" }); - if (result.status !== 0) console.error("tar extraction failed"); -} - -console.log('Renaming to nw'); -await fs.rename(`./bin/nwjs-sdk-v${VERSION}-${platformMap[process.platform] ?? process.platform}-${process.arch}`, './bin/nw'); -await fs.rm(downlodPath); - -async function extractZip (src: string, outDir: string) -{ - console.log(`Extracting zip -> ${outDir}`); - const zip = new StreamZip.async({ file: src }); - const entries = await zip.entries(); - const total = Object.keys(entries).length; - await zip.extract(null, outDir); - await zip.close(); - console.log(`Extracted ${total} files.`); -} \ No newline at end of file diff --git a/scripts/drizzle/es-de/0000_sparkling_banshee.sql b/scripts/drizzle/es-de/0000_sparkling_banshee.sql deleted file mode 100644 index c86dd0e..0000000 --- a/scripts/drizzle/es-de/0000_sparkling_banshee.sql +++ /dev/null @@ -1,34 +0,0 @@ -CREATE TABLE `commands` ( - `system` text, - `label` text, - `command` text NOT NULL, - FOREIGN KEY (`system`) REFERENCES `systems`(`name`) ON UPDATE cascade ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `emulators` ( - `name` text PRIMARY KEY NOT NULL, - `fullname` text, - `systempath` text DEFAULT (json_array()) NOT NULL, - `staticpath` text DEFAULT (json_array()) NOT NULL, - `corepath` text DEFAULT (json_array()) NOT NULL, - `androidpackage` text DEFAULT (json_array()) NOT NULL, - `winregistrypath` text DEFAULT (json_array()) NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX `emulators_name_unique` ON `emulators` (`name`);--> statement-breakpoint -CREATE TABLE `systemMappings` ( - `source` text, - `sourceSlug` text, - `sourceId` integer, - `system` text NOT NULL, - FOREIGN KEY (`system`) REFERENCES `systems`(`name`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `systems` ( - `name` text PRIMARY KEY NOT NULL, - `fullname` text, - `path` text, - `extension` text DEFAULT (json_array()) NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX `systems_name_unique` ON `systems` (`name`); \ No newline at end of file diff --git a/scripts/drizzle/es-de/meta/0000_snapshot.json b/scripts/drizzle/es-de/meta/0000_snapshot.json deleted file mode 100644 index 5c40c18..0000000 --- a/scripts/drizzle/es-de/meta/0000_snapshot.json +++ /dev/null @@ -1,234 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "b4ee710f-eaa5-4bbb-9e69-13d490c7142c", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "commands": { - "name": "commands", - "columns": { - "system": { - "name": "system", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "label": { - "name": "label", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "command": { - "name": "command", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "commands_system_systems_name_fk": { - "name": "commands_system_systems_name_fk", - "tableFrom": "commands", - "tableTo": "systems", - "columnsFrom": [ - "system" - ], - "columnsTo": [ - "name" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "emulators": { - "name": "emulators", - "columns": { - "name": { - "name": "name", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "fullname": { - "name": "fullname", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "systempath": { - "name": "systempath", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(json_array())" - }, - "staticpath": { - "name": "staticpath", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(json_array())" - }, - "corepath": { - "name": "corepath", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(json_array())" - }, - "androidpackage": { - "name": "androidpackage", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(json_array())" - }, - "winregistrypath": { - "name": "winregistrypath", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(json_array())" - } - }, - "indexes": { - "emulators_name_unique": { - "name": "emulators_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "systemMappings": { - "name": "systemMappings", - "columns": { - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sourceSlug": { - "name": "sourceSlug", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sourceId": { - "name": "sourceId", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "system": { - "name": "system", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "systemMappings_system_systems_name_fk": { - "name": "systemMappings_system_systems_name_fk", - "tableFrom": "systemMappings", - "tableTo": "systems", - "columnsFrom": [ - "system" - ], - "columnsTo": [ - "name" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "systems": { - "name": "systems", - "columns": { - "name": { - "name": "name", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "fullname": { - "name": "fullname", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "path": { - "name": "path", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "extension": { - "name": "extension", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(json_array())" - } - }, - "indexes": { - "systems_name_unique": { - "name": "systems_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/scripts/drizzle/es-de/meta/_journal.json b/scripts/drizzle/es-de/meta/_journal.json deleted file mode 100644 index 65dbc00..0000000 --- a/scripts/drizzle/es-de/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1776039605377, - "tag": "0000_sparkling_banshee", - "breakpoints": true - } - ] -} \ No newline at end of file diff --git a/scripts/generate-audio-sprites.ts b/scripts/generate-audio-sprites.ts deleted file mode 100644 index 24b65df..0000000 --- a/scripts/generate-audio-sprites.ts +++ /dev/null @@ -1,30 +0,0 @@ -import audioSprite from 'audiosprite'; -import path from 'node:path'; -import { soundMap } from '../src/mainview/scripts/audio/audioConstants'; - -var allFiles = await Array.fromAsync(new Bun.Glob('*.{ogg,wav}').scan({ cwd: './src/sounds' })); -const files = Object.values(soundMap).map(v => -{ - const existingFile = allFiles.find(f => f.startsWith(v.key)); - if (!existingFile) throw new Error(`Could not find file for sound ${v.key}`); - const filePath = path.join(path.resolve('./src/sounds'), existingFile); - return filePath; -}); -console.log("Loaded", files.join(",")); - -await new Promise((resolve) => -{ - audioSprite(files, - { - output: path.resolve('./src/mainview/assets/sounds'), - path: path.resolve('./src/sounds'), - format: 'howler', - export: 'ogg' - }, - async function (err, obj: any) - { - if (err) return console.error(err); - delete obj.urls; - Bun.file('./src/mainview/assets/sounds.json').write(JSON.stringify(obj, null, 2)).then(r => resolve(true)); - }); -}); \ No newline at end of file diff --git a/scripts/generate-es-de-mapping.ts b/scripts/generate-es-de-mapping.ts index 8e8fa74..115c19f 100644 --- a/scripts/generate-es-de-mapping.ts +++ b/scripts/generate-es-de-mapping.ts @@ -96,18 +96,12 @@ await Promise.all(platforms.map(async ([platform, arch]) => }); const rommMapping = rommPlatforms.data?.find(p => - { - const custom = (customMappings as any)[name]; - if (Array.isArray(custom) && custom.some(m => m === p.slug)) - { - return true; - } - - return p.slug === custom || - p.slug === name || - p.igdb_slug === name || - p.display_name === fullname; - } + 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 mappings: { diff --git a/scripts/generate-flatpak-sources.ts b/scripts/generate-flatpak-sources.ts index 75c6084..6217e30 100644 --- a/scripts/generate-flatpak-sources.ts +++ b/scripts/generate-flatpak-sources.ts @@ -1,5 +1,6 @@ import { $ } from "bun"; +const lockfile = Bun.argv[2] ?? "bun.lockb"; const output = Bun.argv[3] ?? ".config/flatpak/sources.gen.json"; const text = await $`bun ./bun.lockb --hash: 0000000000000000-0000000000000000-0000000000000000-0000000000000000`.text(); diff --git a/src/bun/api/app.ts b/src/bun/api/app.ts index 4695726..350607e 100644 --- a/src/bun/api/app.ts +++ b/src/bun/api/app.ts @@ -1,5 +1,5 @@ -import { TaskQueue, AppEventMap } from "@simeonradivoev/gameflow-sdk"; +import { TaskQueue } from "./task-queue"; import { Database } from "bun:sqlite"; import { CookieJar } from 'tough-cookie'; import FileCookieStore from 'tough-cookie-file-store'; @@ -8,7 +8,7 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator"; import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite"; import Conf from "conf"; import projectPackage from '~/package.json'; -import { SettingsType, SettingsSchema } from '@simeonradivoev/gameflow-sdk/shared'; +import { SettingsSchema, SettingsType } from "@shared/constants"; import { client } from "@clients/romm/client.gen"; import * as schema from "@schema/app"; import cacheSchema from "@schema/cache"; @@ -18,12 +18,13 @@ import EventEmitter from "node:events"; import { appPath } from "../utils"; import { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite"; import { ensureDir } from "fs-extra"; +import UpdateStoreJob from "./jobs/update-store"; +import { getStoreFolder } from "./store/services/gamesService"; import { PluginManager } from "./plugins/plugin-manager"; import registerPlugins from "./plugins/register-plugins"; import controls from './controls/controls'; import { RunAPIServer } from "./rpc"; import { RunBunServer } from "../server"; -import ReloadPluginsJob from "./jobs/reload-plugins-job"; export let config: Conf; export let customEmulators: Conf>; @@ -42,8 +43,6 @@ export let events: EventEmitter; let controlsHandle: { cleanup: () => void; }; let api: { cleanup: () => Promise; }; let bunServer: { cleanup: () => Promise; } | undefined; -let cleannedUp = false; -let cleaningUp = false; export async function load () { @@ -57,7 +56,6 @@ export async function load () windowSize: { width: 1280, height: 800 } }), }); - customEmulators = new Conf>({ projectName: projectPackage.name, projectSuffix: 'bun', @@ -74,7 +72,7 @@ export async function load () console.log("Config Path Located At: ", config.path); console.log("Custom Emulator Paths Located At: ", customEmulators.path); console.log("App Directory is ", process.env.APPDIR); - console.log("Cache Path is ", cachePath); + console.log("Store Directory is ", getStoreFolder()); cachePath = path.join(os.tmpdir(), 'gameflow', 'cache.sqlite'); fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json')); @@ -86,21 +84,18 @@ export async function load () emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema }); await reloadDatabase(); plugins = new PluginManager(); - api = await RunAPIServer(); await registerPlugins(plugins); - taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); + api = await RunAPIServer(); controlsHandle = await controls(); if (!process.env.PUBLIC_ACCESS) bunServer = await RunBunServer(); config.onDidChange('downloadPath', () => reloadDatabase()); config.onDidChange('rommAddress', v => client.setConfig({ baseUrl: v })); + taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob()); } export async function cleanup () { - if (cleaningUp) throw new Error("Already Cleaning Up"); - cleaningUp = true; - if (cleannedUp) throw new Error("Already Cleaned Up. Skipping"); console.log("Cleaning Up"); await bunServer?.cleanup(); await api.cleanup(); @@ -113,14 +108,6 @@ export async function cleanup () config._closeWatcher(); customEmulators._closeWatcher(); console.log("Finished Cleaning Up"); - cleannedUp = true; -} - -/** Reset the cleanup flags. This is mainly used by tests since they run the same app. */ -export async function resetCleanup () -{ - cleaningUp = false; - cleannedUp = false; } export async function reloadDatabase () @@ -133,7 +120,6 @@ export async function reloadDatabase () db = drizzle(sqlite, { schema }); cache = drizzle(cacheSqlite, { schema: cacheSchema }); migrate(db!, { migrationsFolder: appPath("./drizzle") }); - sqlite.run("PRAGMA foreign_keys = ON;"); await cache.run(` CREATE TABLE IF NOT EXISTS item_cache ( key TEXT PRIMARY KEY, diff --git a/src/bun/api/auth.ts b/src/bun/api/auth.ts index 2cd6e3f..b171ed0 100644 --- a/src/bun/api/auth.ts +++ b/src/bun/api/auth.ts @@ -1,5 +1,5 @@ import Elysia, { status } from "elysia"; -import { config, events, plugins, taskQueue } from "./app"; +import { config, events, jar, plugins, taskQueue } from "./app"; import z from "zod"; import { getCurrentUserApiUsersMeGet, tokenApiTokenPost, UserSchema } from "@clients/romm"; import secrets from '../api/secrets'; @@ -46,7 +46,67 @@ export default new Elysia() return status(res.status, res.statusText); }) - .get('/login/twitch', checkLoginAndRefreshTwitch) + .get('/login/twitch', async () => + { + const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); + if (!access_token) + { + return status('Not Found', "Not Logged In"); + } + + const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${access_token}` } }); + if (res.ok) + { + return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; }; + } + + if (!process.env.TWITCH_CLIENT_ID) + { + return status("Not Found", "Twitch Client ID not set"); + } + + const refresh_token = await secrets.get({ service: 'gamflow_twitch', name: "refresh_token" }); + if (!refresh_token) + { + return status("Not Found", "Refresh Token Not Found"); + } + + // refresh token + const refreshResponse = await fetch('https://id.twitch.tv/oauth2/token', { + method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ + client_id: process.env.TWITCH_CLIENT_ID, + access_token, + grant_type: "refresh_token", + refresh_token + }) + }); + + if (refreshResponse.ok) + { + const data: { + access_token: string, + refresh_token: string, + token_type: string; + expires_in: number; + } = await refreshResponse.json(); + + await secrets.set({ service: 'gamflow_twitch', name: 'access_token', value: data.access_token }); + await secrets.set({ service: 'gamflow_twitch', name: 'refresh_token', value: data.refresh_token }); + await secrets.set({ service: 'gamflow_twitch', name: 'expires_in', value: new Date(new Date().getTime() + data.expires_in).toString() }); + + await plugins.hooks.auth.loginComplete.promise({ service: 'twitch' }); + + events.emit('notification', { message: "Twitch Refresh Successful", type: 'success' }); + + const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${data.access_token}` } }); + if (res.ok) + { + return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; }; + } + } + + return status(400, res.statusText); + }) .post('/login/romm/qr', async () => { if (taskQueue.hasActiveOfType(LoginJob)) @@ -63,7 +123,47 @@ export default new Elysia() return data.data as UserSchema; }) .post('/login/romm', async ({ body }) => tryLoginAndSave(body), { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) }) - .get('/login/romm', checkLoginAndRefreshRomm, + .get('/login/romm', async () => + { + const access_token = await secrets.get({ service: 'gameflow', name: 'romm_access_token' }); + if (!access_token) + { + return { hasLogin: false }; + } + + const expires_in = await secrets.get({ service: 'gameflow', name: "romm_expires_in" }); + if (expires_in) + { + const date = new Date(expires_in); + if (date > new Date()) + { + return { hasLogin: true }; + } + } + + const refresh_token = await secrets.get({ service: 'gameflow', name: "romm_refresh_token" }); + if (!refresh_token) + { + return { hasLogin: false }; + } + + const refreshResponse = await tokenApiTokenPost({ body: { grant_type: "refresh_token", refresh_token: refresh_token } }); + + if (refreshResponse.response.ok && refreshResponse.data) + { + await secrets.set({ service: 'gameflow', name: 'romm_access_token', value: refreshResponse.data.access_token }); + if (refreshResponse.data.refresh_token) + await secrets.set({ service: 'gameflow', name: 'romm_refresh_token', value: refreshResponse.data.refresh_token }); + await secrets.set({ service: 'gameflow', name: 'romm_expires_in', value: new Date(new Date().getTime() + refreshResponse.data.expires * 1000).toString() }); + + await plugins.hooks.auth.loginComplete.promise({ service: 'romm' }); + + events.emit('notification', { message: "Romm Refresh Successful", type: 'success' }); + return { hasLogin: true }; + } + + return status(refreshResponse.response.status, refreshResponse.response.statusText) as any; + }, { response: z.object({ hasLogin: z.boolean() }) }) .post('/logout/romm', async () => { @@ -74,115 +174,6 @@ export default new Elysia() }, { response: z.any() }); -export async function checkLoginAndRefreshTwitch () -{ - const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); - if (!access_token) - { - return status('Not Found', "Not Logged In"); - } - - const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${access_token}` } }); - if (res.ok) - { - return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; }; - } - - if (!process.env.TWITCH_CLIENT_ID) - { - return status("Not Found", "Twitch Client ID not set"); - } - - const refresh_token = await secrets.get({ service: 'gamflow_twitch', name: "refresh_token" }); - if (!refresh_token) - { - return status("Not Found", "Refresh Token Not Found"); - } - - // refresh token - const refreshResponse = await fetch('https://id.twitch.tv/oauth2/token', { - method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ - client_id: process.env.TWITCH_CLIENT_ID, - access_token, - grant_type: "refresh_token", - refresh_token - }) - }); - - if (refreshResponse.ok) - { - const data: { - access_token: string, - refresh_token: string, - token_type: string; - expires_in: number; - } = await refreshResponse.json(); - - await secrets.set({ service: 'gamflow_twitch', name: 'access_token', value: data.access_token }); - await secrets.set({ service: 'gamflow_twitch', name: 'refresh_token', value: data.refresh_token }); - await secrets.set({ service: 'gamflow_twitch', name: 'expires_in', value: new Date(new Date().getTime() + data.expires_in).toString() }); - - await plugins.hooks.auth.loginComplete.promise({ service: 'twitch' }); - - events.emit('notification', { message: "Twitch Refresh Successful", type: 'success' }); - - const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${data.access_token}` } }); - if (res.ok) - { - return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; }; - } - } - - return status(400, res.statusText); -} - -export async function checkLoginAndRefreshRomm () -{ - //TODO: move to plugin logic - if (plugins.plugins['com.simeonradivoev.gameflow.romm'].config?.get('clientApiToken')) - { - return { hasLogin: true }; - } - - const access_token = await secrets.get({ service: 'gameflow', name: 'romm_access_token' }); - if (!access_token) - { - return { hasLogin: false }; - } - - const expires_in = await secrets.get({ service: 'gameflow', name: "romm_expires_in" }); - if (expires_in) - { - const date = new Date(expires_in); - if (date > new Date()) - { - return { hasLogin: true }; - } - } - - const refresh_token = await secrets.get({ service: 'gameflow', name: "romm_refresh_token" }); - if (!refresh_token) - { - return { hasLogin: false }; - } - - const refreshResponse = await tokenApiTokenPost({ body: { grant_type: "refresh_token", refresh_token: refresh_token } }); - - if (refreshResponse.response.ok && refreshResponse.data) - { - await secrets.set({ service: 'gameflow', name: 'romm_access_token', value: refreshResponse.data.access_token }); - if (refreshResponse.data.refresh_token) - await secrets.set({ service: 'gameflow', name: 'romm_refresh_token', value: refreshResponse.data.refresh_token }); - await secrets.set({ service: 'gameflow', name: 'romm_expires_in', value: new Date(new Date().getTime() + refreshResponse.data.expires * 1000).toString() }); - - await plugins.hooks.auth.loginComplete.promise({ service: 'romm' }); - - events.emit('notification', { message: "Romm Refresh Successful", type: 'success' }); - return { hasLogin: true }; - } - - return status(refreshResponse.response.status, refreshResponse.response.statusText) as any; -} export async function tryLoginAndSave ({ host, username, password }: { host: string, username: string, password: string; }) { @@ -190,7 +181,7 @@ export async function tryLoginAndSave ({ host, username, password }: { host: str body: { password, username, - scope: 'me.read roms.read platforms.read assets.read assets.write firmware.read roms.user.read collections.read me.write roms.user.write' + scope: 'me.read roms.read platforms.read assets.read firmware.read roms.user.read collections.read me.write roms.user.write' }, baseUrl: host }); diff --git a/src/bun/api/cache.ts b/src/bun/api/cache.ts index 04abd1e..941ba7a 100644 --- a/src/bun/api/cache.ts +++ b/src/bun/api/cache.ts @@ -1,9 +1,7 @@ import { eq } from "drizzle-orm"; import { cache } from "./app"; import cacheSchema from "@schema/cache"; -import { GithubReleaseSchema } from '@simeonradivoev/gameflow-sdk/shared'; -import PQueue from "p-queue"; -import z from "zod"; +import { GithubReleaseSchema } from "@/shared/constants"; export const CACHE_KEYS = { ROM_PLATFORMS: 'rom-platforms', @@ -11,21 +9,17 @@ export const CACHE_KEYS = { STORE_GAME_MANIFEST: 'store-game-manifest' } as const; -// we aggressively cache github data so burst of calls is fine. -export const githubRequestQueue = new PQueue({ intervalCap: 60, interval: 1000 * 60 * 60, strict: true }); - -export async function getOrCached (key: string, getter: (lastValue: T | undefined) => Promise, options?: { expireMs?: number; force?: boolean; }): Promise +export async function getOrCached (key: string, getter: () => Promise, options?: { expireMs?: number; }): Promise { const cached = await cache.query.item_cache.findFirst({ where: eq(cacheSchema.item_cache.key, key) }); const updated_at = new Date(); - if (cached && cached.expire_at > updated_at && !options?.force) + if (cached && cached.expire_at > updated_at) { return cached.data as T; } - const data = await getter(cached?.data as T); - if (data === undefined) return data; + const data = await getter(); const expire_at = options?.expireMs ? new Date(updated_at.getTime() + options.expireMs) : new Date(updated_at.getTime() + 24 * 60 * 60 * 1000); @@ -40,15 +34,12 @@ export async function getOrCached (key: string, getter: (lastValue: T | undef return data; } -export async function getOrCachedGithubRelease (path: string, forceCheck?: boolean) +export async function getOrCachedGithubRelease (path: string) { - return getOrCached>(`github-release-${path}`, () => githubRequestQueue.add(async () => + return getOrCached(`github-release-${path}`, async () => { - const response = await fetch(`https://api.github.com/repos/${path}/releases/latest`, { - method: "GET" - }); + const response = await fetch(`https://api.github.com/repos/${path}/releases/latest`, { method: "GET" }); if (!response.ok) throw new Error(response.statusText); - const release = await GithubReleaseSchema.parseAsync(await response.json()); - return release; - }), { expireMs: 1000 * 60 * 60, force: forceCheck }); + return GithubReleaseSchema.parseAsync(await response.json()); + }); } \ No newline at end of file diff --git a/src/bun/api/clients.ts b/src/bun/api/clients.ts index 470faf8..7d117d3 100644 --- a/src/bun/api/clients.ts +++ b/src/bun/api/clients.ts @@ -5,10 +5,9 @@ import games from "./games/games"; import platforms from "./games/platforms"; import auth from "./auth"; import collections from "./games/collections"; -import emulatorjs from "./emulatorjs/emulatorjs"; export default new Elysia({ prefix: "/api/romm" }) - .use([games, platforms, collections, auth, emulatorjs]) + .use([games, platforms, collections, auth]) .all("/*", async ({ request, set }) => { set.headers["cross-origin-resource-policy"] = 'cross-origin'; diff --git a/src/bun/api/controls/controls.ts b/src/bun/api/controls/controls.ts index cc3c455..4aa417a 100644 --- a/src/bun/api/controls/controls.ts +++ b/src/bun/api/controls/controls.ts @@ -14,8 +14,8 @@ export default async function Initialize () const launchGameTask = taskQueue.findJob(LaunchGameJob.id, LaunchGameJob); if (launchGameTask) { - taskQueue.waitForJob(LaunchGameJob.id).then(() => setTimeout(() => events.emit('focus'), 300)); launchGameTask.abort('exit'); + taskQueue.waitForJob(LaunchGameJob.id).then(() => setTimeout(() => events.emit('focus'), 300)); } else { events.emit('focus'); diff --git a/src/bun/api/controls/windows.ts b/src/bun/api/controls/windows.ts index 2621c26..40fc7d9 100644 --- a/src/bun/api/controls/windows.ts +++ b/src/bun/api/controls/windows.ts @@ -72,6 +72,7 @@ export class GamepadWindows implements IGamepadBackend private index: number; private buffer = new ArrayBuffer(16); private view = new DataView(this.buffer); + private prevButtons = 0; private currButtons = 0; constructor(index = 0) { this.index = index; } diff --git a/src/bun/api/drives.ts b/src/bun/api/drives.ts index 99452d8..2df0dd8 100644 --- a/src/bun/api/drives.ts +++ b/src/bun/api/drives.ts @@ -1,7 +1,6 @@ import si from 'systeminformation'; import fs from 'node:fs'; import os from "node:os"; -import { Drive } from '@simeonradivoev/gameflow-sdk/shared'; async function getAccess (path: string) { diff --git a/src/bun/api/emulatorjs/emulatorjs.ts b/src/bun/api/emulatorjs/emulatorjs.ts index d733d34..c8018de 100644 --- a/src/bun/api/emulatorjs/emulatorjs.ts +++ b/src/bun/api/emulatorjs/emulatorjs.ts @@ -1,12 +1,4 @@ // ES-DE to emulator JS mapping - -import Elysia, { status } from "elysia"; -import z from "zod"; -import path from 'node:path'; -import { config, events, plugins } from "../app"; -import { getLocalGame, updateLocalLastPlayed } from "../games/services/statusService"; -import { SaveFileChange } from "@simeonradivoev/gameflow-sdk/shared"; - // TODO: use the retroarch cores based on ES-DE export const cores: Record = { "atari5200": "atari5200", @@ -51,57 +43,4 @@ export const cores: Record = { "plus4": "plus4", "vic20": "vic20", "dos": "dos" -}; - -export default new Elysia({ prefix: '/emulatorjs' }) - .put('/save', async ({ body: { save, screenshot } }) => - { - await Bun.write(path.join(config.get('downloadPath'), 'saves', "EMULATORJS", save.name), save); - }, { - body: z.object({ - save: z.file(), - screenshot: z.file().optional() - }) - }).get('/load', async ({ query: { filePath } }) => - { - return Bun.file(path.join(config.get('downloadPath'), 'saves', "EMULATORJS", filePath)); - }, { query: z.object({ filePath: z.string() }) }) - .post('/post_play/:source/:id', async ({ params: { source, id }, body: { save } }) => - { - const localGame = await getLocalGame(source, id); - if (!localGame) return status("Not Found"); - - const changedSaveFiles: Record = {}; - if (save) - { - const savesPath = path.join(config.get('downloadPath'), 'saves', "EMULATORJS"); - const saveFile = path.join(savesPath, save.name); - await Bun.write(saveFile, save); - changedSaveFiles.gameflow = { subPath: save.name, cwd: savesPath, shared: false }; - events.emit('notification', { message: "Save Backed Up", type: "success", icon: "save" }); - } - await updateLocalLastPlayed(localGame.id); - await plugins.hooks.games.postPlay.promise({ - source, - id, - saveFolderSlots: { 'emulatorjs': { cwd: path.join(config.get('downloadPath'), "saves", "EMULATORJS") } }, - gameInfo: { platformSlug: localGame?.platform.slug }, - changedSaveFiles: [], - validChangedSaveFiles: changedSaveFiles, - command: { - id: "EMULATORJS", - command: "", - emulator: "EMULATORJS", - valid: true, - metadata: { - romPath: localGame?.path_fs ?? undefined, - emulatorBin: undefined, - emulatorDir: undefined - } - } - }); - }, { - body: z.object({ - save: z.file().optional() - }) - }); \ No newline at end of file +}; \ No newline at end of file diff --git a/src/bun/api/games/collections.ts b/src/bun/api/games/collections.ts index ae31430..6728845 100644 --- a/src/bun/api/games/collections.ts +++ b/src/bun/api/games/collections.ts @@ -1,6 +1,5 @@ import Elysia, { status } from "elysia"; import { plugins } from "../app"; -import { FrontEndCollection } from "@simeonradivoev/gameflow-sdk/shared"; export default new Elysia() .get('/collections', async () => diff --git a/src/bun/api/games/games.ts b/src/bun/api/games/games.ts index d922b36..9666d44 100644 --- a/src/bun/api/games/games.ts +++ b/src/bun/api/games/games.ts @@ -1,29 +1,25 @@ import Elysia, { status } from "elysia"; import { config, db, emulatorsDb, plugins, taskQueue } from "../app"; -import { and, desc, eq, getTableColumns, inArray, like, sql } from "drizzle-orm"; +import { and, eq, getTableColumns, inArray, sql } from "drizzle-orm"; import z from "zod"; import * as schema from "@schema/app"; import fs from "node:fs/promises"; -import { SERVER_URL } from "@shared/constants"; -import { CommandEntry, DownloadLookupEntry, DownloadsLookupFilterValues, GameListFilterSchema } from '@simeonradivoev/gameflow-sdk/shared'; +import { GameListFilterSchema, SERVER_URL } from "@shared/constants"; import { InstallJob } from "../jobs/install-job"; import path from "node:path"; -import { convertLocalToFrontend, getLocalGameMatch, getSourceGameDetailed } from "./services/utils"; -import buildStatusResponse, { customUpdate, fixSource, getValidLaunchCommandsForGame, update, validateGameSource } from "./services/statusService"; +import { convertLocalToFrontend, convertStoreToFrontend, getLocalGameMatch, getSourceGameDetailed } from "./services/utils"; +import buildStatusResponse, { getValidLaunchCommandsForGame } from "./services/statusService"; import { errorToResponse } from "elysia/adapter/bun/handler"; -import { launchCommand } from "./services/launchGameService"; +import { getEmulatorsForSystem, launchCommand } from "./services/launchGameService"; import { getErrorMessage, SeededRandom } from "@/bun/utils"; import { defaultFormats, defaultPlugins } from 'jimp'; import { createJimp } from "@jimp/core"; import webp from "@jimp/wasm-webp"; import * as emulatorSchema from '@schema/emulators'; -import { buildStoreFrontendEmulatorSystems, getStoreEmulatorPackage } from "../store/services/gamesService"; +import { buildStoreFrontendEmulatorSystems, getShuffledStoreGames, getStoreEmulatorPackage, getStoreGameFromPath, getStoreGameManifest } from "../store/services/gamesService"; +import { convertStoreEmulatorToFrontend } from "../store/services/emulatorsService"; import { host } from "@/bun/utils/host"; import { LaunchGameJob } from "../jobs/launch-game-job"; -import { cores } from "../emulatorjs/emulatorjs"; -import { findEmulatorPluginIntegration } from "../store/services/emulatorsService"; -import { ImportJob } from "../jobs/import-job"; -import { EmulatorSourceEntryType, EmulatorSystem, FrontEndFilterLists, FrontEndFilterSets, FrontEndGameType, FrontEndGameTypeDetailedEmulator, FrontEndGameTypeWithIds, FrontEndId, GameLookup } from "@simeonradivoev/gameflow-sdk/shared"; // A custom jimp that supports webp const Jimp = createJimp({ @@ -61,15 +57,8 @@ async function processImage (img: string | Buffer | ArrayBuffer, { blur, width, if (typeof img === 'string') { - const res = await fetch(img); - - return new Response(res.body, { - status: res.status, - headers: { - "Content-Type": res.headers.get("Content-Type") ?? "image/jpeg", - "Cache-Control": "public, max-age=86400", - }, - }); + const rommFetch = await fetch(img); + return rommFetch; } return img; @@ -146,142 +135,97 @@ export default new Elysia() { const games: FrontEndGameType[] = []; - const where: any[] = []; - let localGamesSet: Set | undefined; - - if (query.platform_slug) + if (query.source === 'store') { - where.push(eq(schema.platforms.slug, query.platform_slug)); - } else if (query.platform_id && query.platform_source === 'local') - { - where.push(eq(schema.platforms.id, query.platform_id)); - } - else if (query.platform_id && query.platform_source) - { - const platform = await plugins.hooks.games.platformLookup.promise({ source: query.platform_source, id: query.platform_id ? String(query.platform_id) : undefined }); - if (platform) - { - where.push(eq(schema.platforms.slug, platform?.slug)); - } - } - - if (query.search) - { - where.push(like(schema.games.name, query.search)); - } - - if (query.source) - { - where.push(eq(schema.games.source, query.source)); - } - - const ordering: any[] = []; - - if (query.orderBy) - { - switch (query.orderBy) - { - case 'added': - ordering.push(desc(schema.games.created_at)); - break; - case 'activity': - ordering.push(sql`MAX(COALESCE(${schema.games.created_at}, '1970-01-01'), COALESCE(${schema.games.last_played}, '1970-01-01')) DESC`); - break; - case 'name': - ordering.push(desc(schema.games.name)); - break; - case "release": - ordering.push(sql`json_extract(${schema.games.metadata}, '$.first_release_date') DESC NULLS LAST`); - break; - } - } - - const localGames = await db.select({ - ...getTableColumns(schema.games), - platform: schema.platforms, - screenshotIds: sql`coalesce(json_group_array(${schema.screenshots.id}),json('[]'))`.mapWith(d => JSON.parse(d) as number[]), - }) - .from(schema.games) - .leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id)) - .leftJoin(schema.screenshots, eq(schema.screenshots.game_id, schema.games.id)) - .groupBy(schema.games.id) - .orderBy(...ordering) - .where(and(...where)); - - localGamesSet = new Set( - localGames.filter(g => !!g.source_id && !!g.source).map(g => `${g.source}@${g.source_id}`) - .concat(localGames.filter(g => !!g.igdb_id).map(g => `igdb@${g.igdb_id}`)) - ); - - function localGameExistsPredicate (game: { id: FrontEndId, igdb_id?: number | null, ra_id?: number | null; }) - { - if (localGamesSet?.has(`${game.id.source}@${game.id.id}`)) return true; - if (game.igdb_id && localGamesSet?.has(`igdb@${game.igdb_id}`)) return true; - if (game.ra_id && localGamesSet?.has(`ra@${game.ra_id}`)) return true; - return false; - } - - if (query.collection_id) - { - // Collections are just a remote thing for now. - const remoteGames: FrontEndGameTypeWithIds[] = []; - await plugins.hooks.games.fetchGames.promise({ query, games: remoteGames }).catch(e => console.error(e)); - games.push(...remoteGames.map(g => - { - if (localGameExistsPredicate(g)) + const shuffledGames = await getShuffledStoreGames(); + set.headers['x-max-items'] = shuffledGames.length; + const storeGames = await Promise.all(shuffledGames + .slice(query.offset ?? 0, Math.min((query.offset ?? 0) + (query.limit ?? 50), shuffledGames.length)) + .map(async (e) => { - return convertLocalToFrontend(localGames.find(g => localGameExistsPredicate({ id: { id: g.source_id ?? '', source: g.source ?? '' }, igdb_id: g.igdb_id, ra_id: g.ra_id }))!); - } - else - { - return g; - } - })); + const system = path.dirname(e.path); + const id = path.basename(e.path, path.extname(e.path)); + const localGame = await db.select({ + ...getTableColumns(schema.games), + platform: schema.platforms, + screenshotIds: sql`coalesce(json_group_array(${schema.screenshots.id}),json('[]'))`.mapWith(d => JSON.parse(d) as number[]), + }) + .from(schema.games) + .leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id)) + .leftJoin(schema.screenshots, eq(schema.screenshots.game_id, schema.games.id)) + .groupBy(schema.games.id) + .where(and(eq(schema.games.source, 'store'), eq(schema.games.source_id, `${system}@${id}`))); + + if (localGame.length > 0) return convertLocalToFrontend(localGame[0]); + + const storeGame = await getStoreGameFromPath(e.path); + + return convertStoreToFrontend(system, id, storeGame); + })); + games.push(...storeGames.filter(g => g !== undefined)); } else { - games.push(...localGames.slice(query.offset, query.limit !== undefined ? ((query.offset ?? 0) + query.limit) : undefined).filter(g => + const where: any[] = []; + let localGamesSet: Set | undefined; + + if (query.platform_slug) { - if (query.genres && query.genres.length > 0) + where.push(eq(schema.platforms.slug, query.platform_slug)); + } else if (query.platform_id && query.platform_source === 'local') + { + where.push(eq(schema.platforms.id, query.platform_id)); + } + else if (query.platform_id && query.platform_source) + { + const platform = await plugins.hooks.games.platformLookup.promise({ source: query.platform_source, id: String(query.platform_id) }); + if (platform) { - if (!g.metadata) return false; - if (!g.metadata.genres) return false; - if (query.genres.some(genre => !g.metadata?.genres?.includes(genre))) return false; + where.push(eq(schema.platforms.slug, platform?.slug)); } + } - return true; - }).map(g => + if (query.source) { - return convertLocalToFrontend(g); - })); + where.push(eq(schema.games.source, query.source)); + } - if (query.localOnly !== true) + const localGames = await db.select({ + ...getTableColumns(schema.games), + platform: schema.platforms, + screenshotIds: sql`coalesce(json_group_array(${schema.screenshots.id}),json('[]'))`.mapWith(d => JSON.parse(d) as number[]), + }) + .from(schema.games) + .leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id)) + .leftJoin(schema.screenshots, eq(schema.screenshots.game_id, schema.games.id)) + .groupBy(schema.games.id) + .where(and(...where)); + + localGamesSet = new Set(localGames.filter(g => !!g.source_id && !!g.source).map(g => `${g.source}@${g.source_id}`)); + + if (!query.collection_id) { - const remoteGames: FrontEndGameTypeWithIds[] = []; - const remoteGameSet = new Set(); - await plugins.hooks.games.fetchGames.promise({ query, games: remoteGames }).catch(e => console.error(e)); - games.push(...remoteGames.filter(g => + games.push(...localGames.slice(query.offset, query.limit ? query.offset ?? 0 + query.limit : undefined).map(g => { - if (localGameExistsPredicate(g)) - { - return false; - } + return convertLocalToFrontend(g); + })); - if (g.igdb_id) + const remoteGames: FrontEndGameType[] = []; + await plugins.hooks.games.fetchGames.promise({ query, games: remoteGames }).catch(e => console.error(e)); + games.push(...remoteGames.filter(g => !localGamesSet?.has(`${g.id.source}@${g.id.id}`))); + } else + { + const remoteGames: FrontEndGameType[] = []; + await plugins.hooks.games.fetchGames.promise({ query, games: remoteGames }).catch(e => console.error(e)); + games.push(...remoteGames.map(g => + { + if (localGamesSet?.has(`${g.id.source}@${g.id.id}`)) { - const igdbId = `igdb@${g.igdb_id}`; - if (remoteGameSet.has(igdbId)) return false; - remoteGameSet.add(igdbId); - } - - if (g.ra_id) + return convertLocalToFrontend(localGames.find(l => l.source === g.id.source && l.source_id === g.id.id)!); + } else { - const raId = `ra@${g.ra_id}`; - if (remoteGameSet.has(raId)) return false; - remoteGameSet.add(raId); + return g; } - - return true; })); } } @@ -299,9 +243,6 @@ export default new Elysia() case 'name': games.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')); break; - case "release": - games.sort((a, b) => (b.metadata.first_release_date?.getTime() ?? 0) - (a.metadata.first_release_date?.getTime() ?? 0)); - break; } } @@ -310,55 +251,27 @@ export default new Elysia() }, { query: GameListFilterSchema, }) - .get('/games/filters', async ({ query: { source } }) => - { - const filterSets: FrontEndFilterSets = { - age_ratings: new Set(), - player_counts: new Set(), - languages: new Set(), - companies: new Set(), - genres: new Set() - }; - - let filter: any = undefined; - if (source) filter = eq(schema.games.source, source); - const local_metadata = await db.query.games.findMany({ columns: { metadata: true }, where: filter }); - - local_metadata.forEach(game => - { - game.metadata.age_ratings?.forEach(r => filterSets.age_ratings.add(r)); - game.metadata.genres?.forEach(r => filterSets.genres.add(r)); - game.metadata.companies?.forEach(r => filterSets.companies.add(r)); - - if (game.metadata.player_count) - filterSets.player_counts.add(game.metadata.player_count); - }); - - await plugins.hooks.games.fetchFilters.promise({ filters: filterSets, source }); - - const filters: FrontEndFilterLists = { - age_ratings: Array.from(filterSets.age_ratings), - player_counts: Array.from(filterSets.player_counts), - languages: Array.from(filterSets.languages), - companies: Array.from(filterSets.companies), - genres: Array.from(filterSets.genres) - }; - - return filters; - }, { - query: z.object({ source: z.string().optional() }) - }) .get('/rom/:source/:id', async ({ params: { id, source } }) => { - const filePaths = await plugins.hooks.games.fetchRomFiles.promise({ source, id }); + const localGame = await db.query.games.findFirst({ + where: getLocalGameMatch(id, source), + columns: { path_fs: true } + }); - if (!filePaths || filePaths.length <= 0) + if (!localGame?.path_fs) { - return status("Not Found", "No Valid Roms Found"); + return status("Not Found"); } - return Bun.file(filePaths[0]); + const downloadPath = config.get('downloadPath'); + const path_fs = path.join(downloadPath, localGame.path_fs); + const stats = await fs.stat(path_fs); + if (stats.isDirectory()) + { + return status("Not Found", "Rom is a folder"); + } + return Bun.file(path_fs); }, { params: z.object({ source: z.string(), id: z.string() }) }) @@ -373,61 +286,38 @@ export default new Elysia() const systemMapping = await emulatorsDb.query.systemMappings.findFirst({ where: and(eq(emulatorSchema.systemMappings.sourceSlug, sourceData.platform_slug), eq(emulatorSchema.systemMappings.source, 'romm')) }); if (systemMapping) { - const emulatorNames: string[] = []; - await plugins.hooks.emulators.findEmulatorForSystem.promise({ system: systemMapping.system, emulators: emulatorNames }); + const emulatorNames = await getEmulatorsForSystem(systemMapping.system); + const emulators = await Promise.all(emulatorNames.map(n => getStoreEmulatorPackage(n).then(e => ({ name: n, data: e })))); - sourceData.emulators = (await Promise.all(emulatorNames.map(async name => + sourceData.emulators = await Promise.all(emulators.map(async ({ name, data }) => { - if (name === 'EMULATORJS') + if (data) + { + const systems = await buildStoreFrontendEmulatorSystems(data); + return { ...await convertStoreEmulatorToFrontend(data, 0, systems), store_exists: true }; + } + else if (name === 'EMULATORJS') { return { name: 'EMULATORJS', validSources: [{ binPath: SERVER_URL(host), type: 'embedded', exists: true }], - logo: 'https://emulatorjs.org/logo/EmulatorJS.png', - systems: await Promise.all(Object.keys(cores).map(async c => - { - const mapping = await emulatorsDb.query.systemMappings.findFirst({ - where (fields, operators) - { - return operators.and(operators.eq(fields.source, "romm"), operators.eq(fields.system, c)); - }, columns: { sourceSlug: true } - }); - const system: EmulatorSystem = { - id: c, - name: c, - iconUrl: `/api/romm/image/romm/assets/platforms/${mapping?.sourceSlug}.svg` - }; - return system; - })), + logo: `/api/romm/image?url=${encodeURIComponent('https://emulatorjs.org/logo/EmulatorJS.png')}`, + systems: [], + gameCount: 0 + } satisfies FrontEndGameTypeDetailedEmulator; + } + else + { + return { + name: name, + logo: "", + systems: [], gameCount: 0, - source: 'local', - integrations: [] + validSources: [] } satisfies FrontEndGameTypeDetailedEmulator; } - const foundEmulator = await plugins.hooks.store.fetchEmulator.promise({ id: name }); - - const execPaths: EmulatorSourceEntryType[] = []; - await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: name, sources: execPaths }); - const integrations = findEmulatorPluginIntegration(id, execPaths); - - if (foundEmulator) - { - foundEmulator.validSources = execPaths; - foundEmulator.integrations = integrations; - return foundEmulator; - } - - return { - name: name, - logo: "", - source: 'local', - systems: [], - gameCount: 0, - validSources: execPaths, - integrations: integrations - } satisfies FrontEndGameTypeDetailedEmulator; - }))).filter(e => !!e); + })); } } @@ -454,18 +344,17 @@ export default new Elysia() }, { params: z.object({ id: z.string(), source: z.string() }), }) - .post('/game/:source/:id/install', async ({ params: { id, source }, body }) => + .post('/game/:source/:id/install', async ({ params: { id, source } }) => { if (!taskQueue.findJob(InstallJob.query({ source, id }), InstallJob)) { - return taskQueue.enqueue(InstallJob.query({ source, id }), new InstallJob(id, source, body)); + return taskQueue.enqueue(InstallJob.query({ source, id }), new InstallJob(id, source)); } else { return status('Not Implemented'); } }, { params: z.object({ id: z.string(), source: z.string() }), - body: z.object({ downloadId: z.string().optional() }).optional(), response: z.any() }) .delete('/game/:source/:id/install', async ({ params: { id, source } }) => @@ -481,56 +370,7 @@ export default new Elysia() params: z.object({ id: z.string(), source: z.string() }), response: z.any() }) - .get('/game/:source/:id/validate', async ({ params: { id, source } }) => - { - const valid = await validateGameSource(source, id); - return { valid: valid.valid, reason: valid.reason }; - }) - .post('/game/:source/:id/fix_source', async ({ params: { id, source } }) => - { - return fixSource(source, id); - }) - .post('/game/:source/:id/update', async ({ params: { id, source } }) => - { - return update(source, id); - }) - .post('/game/:source/:id/update', async ({ params: { id, source }, body }) => - { - return customUpdate(source, id, body.source, body.id); - }, { body: z.object({ source: z.string(), id: z.string() }) }) - .get('/lookup', async ({ query: { search } }) => - { - const matches = new Map(); - await plugins.hooks.games.gameLookup.promise(matches, { search }); - return { hadMatchers: matches.size > 0, matches: Array.from(matches.values()).flatMap(m => m) }; - }, { - query: z.object({ search: z.string() }) - }) - .get('/lookup/:source/:id', async ({ params: { source, id } }) => - { - const matches = new Map(); - await plugins.hooks.games.gameLookup.promise(matches, { source, id }); - return Array.from(matches.values()).flatMap(m => m); - }) - .get('/game/:source/:id/commands', async ({ params: { id, source }, set }) => - { - const validCommands = await getValidLaunchCommandsForGame(source, id); - if (validCommands instanceof Error) - { - return errorToResponse(validCommands, set); - } - return validCommands as { - commands: CommandEntry[]; - gameId: FrontEndId; - source?: string; - sourceId?: string; - } | undefined; - }, { - response: z.object({ - commands: z.custom().array() - }) - }) - .post('/game/:source/:id/play', async ({ params: { id, source }, body: { command_id }, set }) => + .post('/game/:source/:id/play', async ({ params: { id, source }, body, set }) => { const validCommands = await getValidLaunchCommandsForGame(source, id); if (validCommands) @@ -543,11 +383,11 @@ export default new Elysia() { try { - const validCommand = command_id ? validCommands.commands.find(c => c.id === command_id) : validCommands.commands[0]; + const validCommand = body.command_id ? validCommands.commands.find(c => c.id === body.command_id) : validCommands.commands[0]; if (validCommand) { // launch command waits for the game to exit, we don't want that. - await launchCommand(validCommand, validCommands.gameId, validCommands.source, validCommands.sourceId); + await launchCommand(validCommand, source, id, validCommands.gameId); return { type: 'application', command: null }; } else { @@ -588,6 +428,8 @@ export default new Elysia() const emulator = await getStoreEmulatorPackage(id); if (!emulator) return status("Not Found"); const systems = await buildStoreFrontendEmulatorSystems(emulator); + const systemsIdSet = new Set(systems.map(s => s.id)); + const games: FrontEndGameType[] = []; @@ -614,6 +456,28 @@ export default new Elysia() await plugins.hooks.games.fetchRecommendedGamesForEmulator.promise({ emulator, systems, games: remoteGames }); games.push(...remoteGames.filter(g => !localGamesSet?.has(`${g.id.source}@${g.id.id}`))); + const gamesManifest = await getStoreGameManifest(); + const storeGames = await Promise.all(gamesManifest + .filter(g => systemsIdSet.has(path.dirname(g.path))) + .map(async (e) => + { + const system = path.dirname(e.path); + const id = path.basename(e.path, path.extname(e.path)); + + const localGame = await db.query.games.findFirst({ columns: { id: true }, where: and(eq(schema.games.source, 'store'), eq(schema.games.source_id, `${system}@${id}`)) }); + + if (localGame) + { + return undefined; + } + + const storeGame = await getStoreGameFromPath(e.path); + + return convertStoreToFrontend(system, id, storeGame); + })); + + games.push(...storeGames.filter(g => g !== undefined).slice(0, 3)); + return games; }) .get('/recommended/games/game/:source/:id', async ({ params: { source, id } }) => @@ -621,10 +485,10 @@ export default new Elysia() const sourceData = await getSourceGameDetailed(source, id); if (!sourceData) return status("Not Found"); - const sourceCompaniesSet = new Set(sourceData.metadata.companies); - const sourceGenresSet = new Set(sourceData.metadata.genres); - + const sourceCompaniesSet = new Set(sourceData.companies); + const sourceGenresSet = new Set(sourceData.genres); + const esSystem = sourceData.platform_slug ? await emulatorsDb.query.systemMappings.findFirst({ where: and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.sourceSlug, sourceData.platform_slug)), columns: { system: true } }) : undefined; const games: (FrontEndGameType & { metadata?: any; })[] = []; @@ -635,9 +499,37 @@ export default new Elysia() const localGamesSourceSet = new Set(localGames.filter(g => g.source).map(g => `${g.source}@${g.source_id}`)); - games.push(...localGames.map(g => convertLocalToFrontend(g))); + games.push(...localGames.map(g => ({ ...convertLocalToFrontend(g), metadata: g.metadata }))); + const shuffledGames = await getShuffledStoreGames(); + const storeGames = await Promise.all(shuffledGames + .filter(g => + { + const system = path.dirname(g.path); + const id = path.basename(g.path, path.extname(g.path)); + if (localGamesSourceSet.has(`${system}@${id}`)) + return false; + + if (esSystem) + { + if (path.dirname(g.path) === esSystem.system) return true; + } + + return false; + }) + .map(async (e) => + { + const system = path.dirname(e.path); + const id = path.basename(e.path, path.extname(e.path)); + const storeGame = await getStoreGameFromPath(e.path); + return convertStoreToFrontend(system, id, storeGame); + })); + + if (storeGames) + { + games.push(...storeGames.slice(0, 3)); + } const remoteGames: (FrontEndGameType & { metadata?: any; })[] = []; plugins.hooks.games.fetchRecommendedGamesForGame.promise({ @@ -690,57 +582,4 @@ export default new Elysia() rankedGames.sort((lhs, rhs) => rhs.rank - lhs.rank); return rankedGames.map(g => g.game).slice(0, 10); - }) - .post('/add/custom', async ({ body: { source, id, platformId, gamePath } }) => - { - if (taskQueue.hasActiveOfType(ImportJob)) return status("Conflict", "Import Job Already Running"); - const data = await taskQueue.enqueue(ImportJob.query({ source, id }), new ImportJob(source, id, gamePath, platformId), { - throwOnCancel: true - - }); - return { source: 'local', id: data.localId }; - }, { - body: z.object({ - source: z.string(), - id: z.string(), - gamePath: z.string(), - platformId: z.number() - }) - }).get('/downloads/lookup', async ({ query: { search, page, rows, orderBy, sortDirection, source } }) => - { - const matches = new Map(); - await plugins.hooks.games.downloadsLookup.promise(matches, { search, page, rows, orderBy, sortDirection, source }); - const allValues = Array.from(matches.values()); - return { hadMatchers: matches.size > 0, matches: allValues.flatMap(m => m.items), totalCount: allValues.reduce((p, c) => p + c.count, 0) }; - }, { - query: z.object({ - search: z.string().optional(), - page: z.coerce.number().optional(), - rows: z.coerce.number().optional(), - orderBy: z.string().optional(), - sortDirection: z.literal(["desc", "asc"]).optional(), - source: z.string().optional() - }) - }).get('/download/lookup/:source/:id', async ({ params: { source, id } }) => - { - const match = await plugins.hooks.games.downloadLookup.promise({ source, id }); - if (!match) return status("Not Found"); - return match; - }).get('/download/file/info', async ({ query: { file_url } }) => - { - const response = await fetch(file_url, { method: "HEAD" }); - if (!response.ok) return status('Internal Server Error', response.statusText); - return { size: Number(response.headers.get('content-length')), content_type: response.headers.get('content-type') }; - }, { - query: z.object({ file_url: z.url() }) - }).get('/download/lookup/filters', async () => - { - const filters: DownloadsLookupFilterValues = { - source: [], - orderBy: [] - }; - - await plugins.hooks.games.downloadsLookupFilters.promise({ filters }); - - return filters; }); \ No newline at end of file diff --git a/src/bun/api/games/platforms.ts b/src/bun/api/games/platforms.ts index 10aaf42..a33e155 100644 --- a/src/bun/api/games/platforms.ts +++ b/src/bun/api/games/platforms.ts @@ -1,10 +1,8 @@ import Elysia, { status } from "elysia"; import z from "zod"; -import { and, count, eq, getTableColumns, not, notExists, or } from "drizzle-orm"; -import { config, db, plugins } from "../app"; +import { and, count, eq, getTableColumns, not } from "drizzle-orm"; +import { db, plugins } from "../app"; import * as schema from "@schema/app"; -import { findPlatform } from "./services/utils"; -import { FrontEndPlatformType } from "@simeonradivoev/gameflow-sdk/shared"; export default new Elysia() .get('/platforms', async () => @@ -93,11 +91,9 @@ export default new Elysia() { const remotePlatform = await plugins.hooks.games.fetchPlatform.promise({ source, id }); if (!remotePlatform) return status("Not Found"); - const local = await db.query.platforms.findFirst({ where: or(eq(schema.platforms.slug, remotePlatform?.slug), eq(schema.platforms.name, remotePlatform?.name)) }); - return { ...remotePlatform, hasLocal: !!local }; + return remotePlatform; } - }, { params: z.object({ source: z.string(), id: z.string() }) }) - .get('/platform/local/:id/cover', async ({ params: { id }, set }) => + }, { params: z.object({ source: z.string(), id: z.string() }) }).get('/platform/local/:id/cover', async ({ params: { id }, set }) => { set.headers["cross-origin-resource-policy"] = 'cross-origin'; @@ -116,70 +112,4 @@ export default new Elysia() 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() }) }) - .post('/platform/:source/:id/update', async ({ params: { source, id } }) => - { - const where: any[] = []; - if (source === 'local') - { - where.push(eq(schema.platforms.id, Number(id))); - } else - { - const remotePlatform = await plugins.hooks.games.fetchPlatform.promise({ source, id }); - if (remotePlatform) - { - where.push(eq(schema.platforms.slug, remotePlatform.slug)); - } - } - - const localPlatform = await db.query.platforms.findFirst({ - where: or(...where) - - }); - if (!localPlatform) return status("Not Found"); - - const platformLookup = await plugins.hooks.games.platformLookup.promise({ - slug: localPlatform.slug - }); - let platformCover = await fetch(`${config.get('rommAddress') ?? 'https://demo.romm.app'}/assets/platforms/${localPlatform.slug}.svg`); - if (!platformCover.ok && platformLookup?.url_logo) - { - platformCover = await fetch(platformLookup.url_logo); - } - - await db.update(schema.platforms).set({ - name: platformLookup?.name, - cover: Buffer.from(await platformCover.arrayBuffer()), - cover_type: platformCover.headers.get('content-type'), - }).where(eq(schema.platforms.id, localPlatform.id)); - }) - .delete('/platform/local/:id', async ({ params: { id } }) => - { - const deleted = await db.delete(schema.platforms).where(and(eq(schema.platforms.id, Number(id)), - notExists( - db - .select() - .from(schema.games) - .where(eq(schema.games.platform_id, Number(id))) - ))).returning(); - if (deleted.length <= 0) return status("Not Found"); - }) - .get('/platform/lookup/match/:source/:id', async ({ params: { source, id } }) => - { - const platformLookup = await plugins.hooks.games.platformLookup.promise({ source, id }); - if (!platformLookup) return status("Not Found"); - const match = await findPlatform({ - system_slug: platformLookup.slug, - platform: { - source_slug: platformLookup.slug, - source_id: Number(id), - source: source, - name: platformLookup.name - } - }); - return { details: platformLookup, match }; - }, { - detail: { - description: "Find matches of remote platform lookups. Returns the operations for each platform if it were to be imported. If platform locally exists. Will a new local platform be created from say romm. Unknown is returned if no match is found." - } - }); \ No newline at end of file + }, { 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 index 490850d..add3c59 100644 --- a/src/bun/api/games/services/launchGameService.ts +++ b/src/bun/api/games/services/launchGameService.ts @@ -1,23 +1,275 @@ import path from 'node:path'; -import { Glob } from 'bun'; +import { Glob, which } from 'bun'; import fs from 'node:fs/promises'; -import { existsSync } from 'node:fs'; -import { config, taskQueue } from '../../app'; +import { existsSync, readFileSync } from 'node:fs'; +import * as schema from '@schema/emulators'; +import { eq } from 'drizzle-orm'; +import { config, customEmulators, emulatorsDb, taskQueue } from '../../app'; +import os, { platform } from 'node:os'; +import { cores } from '../../emulatorjs/emulatorjs'; import { LaunchGameJob } from '../../jobs/launch-game-job'; -import { getStoreEmulatorPackage } from '../../store/services/gamesService'; -import { getOrCachedScoopPackage } from '../../store/services/emulatorsService'; -import { CommandEntry, EmulatorSourceEntryType, FrontEndId } from '@simeonradivoev/gameflow-sdk/shared'; +import { EmulatorPackageType } from '@/shared/constants'; +import { getStoreEmulatorPackage, getStoreFolder } from '../../store/services/gamesService'; -export async function launchCommand (validCommand: CommandEntry, id: FrontEndId, source?: string, sourceId?: string) +export const varRegex = /%([^%]+)%/g; +export const assignRegex = /(%\w+%)=(\S+) /g; + +export async function launchCommand (validCommand: CommandEntry, source: string, sourceId: string, id: number) { if (taskQueue.hasActiveOfType(LaunchGameJob)) { - throw new Error(`Game currently running`); + throw new Error(`${id} currently running`); } taskQueue.enqueue(LaunchGameJob.id, new LaunchGameJob(id, validCommand, source, sourceId)); } +/** + * Get the emulators related to the given system + * @param systemSlug the ES-DE slug for the system + */ +export async function getEmulatorsForSystem (systemSlug: string) +{ + const system = await emulatorsDb.query.systems.findFirst({ + with: { commands: true }, + where: eq(schema.systems.name, systemSlug) + }); + + if (!system) + { + throw new Error(`Could not find system '${systemSlug}'`); + } + + const emulators = new Set(); + await Promise.all(system.commands.map(async (command, index) => + { + let cmd = command.command; + + const matches = Array.from(cmd.matchAll(varRegex)); + matches.forEach(([value]) => + { + if (value.startsWith("%EMULATOR_")) + { + const emulatorName = value.substring("%EMULATOR_".length, value.length - 1); + emulators.add(emulatorName); + return; + } + }); + })); + + + + if (cores[systemSlug]) + { + emulators.add('EMULATORJS'); + } + + return Array.from(emulators); +} + +/** + * + * @param data Uses es-de system slug + * @returns + */ +export async function getValidLaunchCommands (data: { + systemSlug: string; + gamePath: string; +}): 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}'`); + } + } + + function escapeWindowsArg (arg: string): string + { + if (process.platform === 'win32') + { + return `"${arg + .replace(/(\\*)"/g, '$1$1\\"') // escape quotes + .replace(/(\\*)$/, '$1$1') // escape trailing backslashes + }"`; + } else + { + if (arg.includes(' ')) + { + return `"${arg}"`; + } else + { + return arg; + } + } + } + + const formattedCommands = await Promise.all(system.commands + .filter(c => !c.command.includes(`%ENABLESHORTCUTS%`)) + .map(async (command, index) => + { + const label = command.label; + let cmd = command.command; + + let emulator: string | undefined = undefined; + let rom = validFiles[0]; + + if (cmd.includes('%ESCAPESPECIALS%')) + rom = rom.replace(/[&()^=;,]/g, ''); + + + + const staticVars: Record = { + '%ROM%': escapeWindowsArg(rom), + '%ROMRAW%': validFiles[0], + '%ROMRAWWIN%': escapeWindowsArg(validFiles[0].replaceAll('/', '\\')), + '%ESPATH%': escapeWindowsArg(path.dirname(Bun.main)), + '%ROMPATH%': escapeWindowsArg(gamePath), + '%BASENAME%': escapeWindowsArg(path.basename(validFiles[0], path.extname(validFiles[0]))), + '%FILENAME%': escapeWindowsArg(path.basename(validFiles[0])), + '%ESCAPESPECIALS%': "", + '%HIDEWINDOW%': "" + }; + + cmd = cmd.replace(/\%INJECT\%=(?[\w\%.\/\\]+)/g, (_, injectFile: string) => + { + try + { + const resolvedInjectFile = injectFile.replace(varRegex, (a) => + { + return staticVars[a] ?? a; + }); + if (existsSync(resolvedInjectFile)) + { + const rawContents = readFileSync(resolvedInjectFile, { encoding: 'utf-8' }); + return rawContents.split('\n').map(v => v.replace('\r', '')).join(' '); + } + + return ''; + } catch (error) + { + return ''; + } + }); + + const matches = Array.from(cmd.matchAll(varRegex)); + const varList = await Promise.all(matches.map(async ([value]) => + { + if (value.startsWith("%EMULATOR_")) + { + const emulatorName = value.substring("%EMULATOR_".length, value.length - 1); + let execs = await findExecsByName(emulatorName); + let validExec = execs.find(e => e.exists); + + emulator = emulatorName; + return [ + [value, validExec ? validExec.binPath : undefined] as [string, string | undefined], + [`%EMUSOURCE%`, validExec?.type] as [string, string | undefined], + ['%EMUDIR%', validExec?.rootPath ?? (validExec ? escapeWindowsArg(path.dirname(validExec.binPath)) : undefined)] as [string, string | undefined], + ['%EMUDIRRAW%', validExec?.rootPath ?? (validExec ? path.dirname(validExec.binPath) : undefined)] as [string, string | undefined] + ]; + + } + + const key = value[0].substring(1, value.length - 1); + return [[value, process.env[key]] as [string, string | undefined]]; + })); + + const vars = { ...Object.fromEntries(varList.flatMap(l => l)), ...staticVars }; + let startDir: string | undefined = undefined; + + if ('%STARTDIR%' in vars) + { + delete vars['%STARTDIR%']; + + cmd = cmd.replace(assignRegex, (match, p1, p2) => + { + if (p1 === '%STARTDIR%') + { + startDir = varRegex.test(p2) ? staticVars[p2] : p2; + } + return ""; + }); + } + + // missing variable + const invalid = Object.entries(vars).find(c => c[1] === undefined); + + const formattedCommand = cmd.replace(varRegex, (s) => vars[s] ?? '').trim(); + + return { + id: index, + label: label ?? undefined, + command: formattedCommand, + startDir, + valid: !invalid, emulator, + emulatorSource: vars['%EMUSOURCE%'] as any, + metadata: { + romPath: validFiles[0], + emulatorBin: varList.flatMap(l => l).find(v => v[0].includes('%EMULATOR_'))?.[1], + emulatorDir: vars['%EMUDIRRAW%'] + } + } satisfies CommandEntry; + })); + + return formattedCommands.filter(c => !!c); +} + +export async function findExecsByName (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}`); + } + return findExecs(emulatorName, emulator); +} + export async function findStoreEmulatorExec (id: string, emulator?: { systempath: string[]; }): Promise { const storeEmulatorFolder = path.join(config.get('downloadPath'), 'emulators', id); @@ -33,27 +285,11 @@ export async function findStoreEmulatorExec (id: string, emulator?: { systempath const storeExecName = (await Promise.all(storeEmulator.downloads[`${process.platform}:${process.arch}`].map(async dl => { // glob file search causes issues so do manual search + const glob = new Glob(dl.pattern); if (await fs.exists(storeEmulatorFolder)) { - const glob = (dl as any).pattern ? new Glob((dl as any).pattern) : undefined; - let bin: string | undefined = (dl as any).bin; - if (!bin && dl.type === 'scoop') - { - const data = await getOrCachedScoopPackage(id, dl.url); - - if (data) - { - bin = data.bin; - } - } - const files = (await fs.readdir(storeEmulatorFolder)) - .filter(f => - { - if (glob && glob.match(f)) return true; - if (bin && f === bin) return true; - }); - + .filter(f => glob.match(f)); return files.map(f => path.join(storeEmulatorFolder, f)); } return []; @@ -70,3 +306,112 @@ export async function findStoreEmulatorExec (id: string, emulator?: { systempath return undefined; } +export async function findExecs (id: string, emulator?: { winregistrypath: string[], systempath: string[], staticpath: string[]; }) +{ + const execs: EmulatorSourceEntryType[] = []; + + if (customEmulators.has(id)) + { + execs.push({ binPath: customEmulators.get(id), type: 'custom', exists: await fs.exists(customEmulators.get(id)) }); + } + + if (emulator && emulator.systempath.length > 0) + { + const storePath = await findStoreEmulatorExec(id, emulator); + if (storePath) execs.push(storePath); + } + + if (emulator && os.platform() === 'win32') + { + const regValues = emulator.winregistrypath; + if (regValues.length > 0) + { + for (const node of regValues) + { + const registryValue = await readRegistryValue(node); + if (registryValue) + { + execs.push({ binPath: registryValue, type: 'registry', exists: true }); + } + } + + } + } + + if (emulator && emulator.systempath.length > 0) + { + const systemPath = await resolveSystemPath(emulator.systempath); + if (systemPath) + { + execs.push({ binPath: systemPath, type: 'system', exists: true }); + } + } + + if (emulator && emulator.staticpath.length > 0) + { + const staticPath = await resolveStaticPath(emulator.staticpath); + if (staticPath) + { + execs.push({ binPath: staticPath, type: 'static', exists: true }); + } + } + + return execs; +} + +async function readRegistryValue (text: string) +{ + const params = text.split('|'); + const key = path.dirname(params[0]); + const value = path.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) + { + const resolved = entry.replace("~", os.homedir()); + if (await fs.exists(resolved)) + { + return resolved; + } + } + 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 index 1eaed5b..af9e62a 100644 --- a/src/bun/api/games/services/statusService.ts +++ b/src/bun/api/games/services/statusService.ts @@ -1,19 +1,20 @@ -import { config, db, plugins, taskQueue } from "../../app"; -import { eq } from "drizzle-orm"; -import { getErrorMessage } from "@/bun/utils"; -import { checkFiles, getLocalGameMatch, getSourceGameDetailed } from "./utils"; +import { RPC_URL, } from "@shared/constants"; +import { config, customEmulators, db, emulatorsDb, plugins, taskQueue } from "../../app"; +import { getValidLaunchCommands } from "./launchGameService"; +import * as emulatorSchema from '@schema/emulators'; +import { and, eq } from "drizzle-orm"; +import { getErrorMessage, hashFile } from "@/bun/utils"; +import { checkFiles, getLocalGameMatch } from "./utils"; import fs from 'node:fs/promises'; +import { getStoreGameFromId } from "../../store/services/gamesService"; +import { cores } from "../../emulatorjs/emulatorjs"; +import { host } from "@/bun/utils/host"; import Elysia from "elysia"; import z from "zod"; import { InstallJob, InstallJobStates } from "../../jobs/install-job"; import { LaunchGameJob } from "../../jobs/launch-game-job"; -import * as appSchema from "@schema/app"; -import { RPC_URL } from "@/shared/constants"; -import { DownloadSourceSchema } from '@simeonradivoev/gameflow-sdk/shared'; -import { host } from "@/bun/utils/host"; -import { CommandEntry, FrontEndId, GameLookup, GameStatusType, LocalDownloadFileEntry } from "@simeonradivoev/gameflow-sdk/shared"; -export class CommandSearchError extends Error +class CommandSearchError extends Error { constructor(status: GameStatusType, message: string) { @@ -25,15 +26,7 @@ export class CommandSearchError extends Error export async function getLocalGame (source: string, id: string) { const localGame = await db.query.games.findFirst({ - columns: { - id: true, - path_fs: true, - source: true, - source_id: true, - igdb_id: true, - ra_id: true, - main_glob: true - }, + columns: { id: true, path_fs: true }, where: getLocalGameMatch(id, source), with: { platform: { columns: { slug: true } } @@ -43,243 +36,62 @@ export async function getLocalGame (source: string, id: string) return localGame; } -/** Update local game's metadata from custom source, not the actual source of the game. Say from metadata providers like IGDB */ -export async function customUpdate (source: string, id: string, destination: string, destinationId: string) -{ - const localGame = await getLocalGame(source, id); - if (!localGame) throw new Error("Could not find Local Game"); - - const matchesMap = new Map(); - await plugins.hooks.games.gameLookup.promise(matchesMap, { source: destination, id: destinationId }); - const matches = matchesMap.values().next().value; - if (!matches || matches?.length <= 0) throw new Error("Could not find destination"); - const match = matches[0]; - - await db.transaction(async (tx) => - { - await tx.delete(appSchema.screenshots).where(eq(appSchema.screenshots.game_id, localGame.id)); - - // pre-fetch screenshots - const screenshots = await Promise.all(match.screenshotUrls.map(s => fetch(s))); - - if (screenshots.length > 0) - { - await tx.insert(appSchema.screenshots).values(await Promise.all(screenshots.map(async (response) => - { - const screenshot: typeof appSchema.screenshots.$inferInsert = { - game_id: localGame.id, - content: Buffer.from(await response.arrayBuffer()), - type: response.headers.get('content-type') - }; - - return screenshot; - }))); - } - - let cover: Buffer | undefined = undefined; - if (match.coverUrl) - { - const coverResponse = await fetch(match.coverUrl); - if (coverResponse.ok) - { - cover = Buffer.from(await coverResponse.arrayBuffer()); - } - } - - await tx.update(appSchema.games).set({ - cover, - metadata: { - age_ratings: match.age_ratings, - genres: match.genres, - player_count: match.player_count ?? undefined, - companies: match.companies, - game_modes: match.game_modes, - average_rating: match.average_rating ?? undefined, - first_release_date: match.first_release_date, - } - }).where(eq(appSchema.games.id, localGame.id)); - }); -} - -export async function update (source: string, id: string) -{ - const localGame = await getLocalGame(source, id); - if (!localGame) throw new Error("Could not find Local Game"); - if (!localGame.source || !localGame.source_id) throw new Error("Game has not source defined"); - const sourceGame = await getSourceGameDetailed(localGame.source, localGame.source_id, { sourceOnly: true }); - if (!sourceGame) throw new Error("Could not find source game"); - - await db.transaction(async (tx) => - { - await tx.delete(appSchema.screenshots).where(eq(appSchema.screenshots.game_id, localGame.id)); - - const paths_screenshots: string[] = [...sourceGame.paths_screenshots.map(s => `${RPC_URL(host)}${s}`)]; - if (paths_screenshots.length <= 0 && sourceGame.igdb_id) - { - const matches = new Map(); - await plugins.hooks.games.gameLookup.promise(matches, { source: 'igdb', id: String(sourceGame.igdb_id) }); - if (matches.size > 0) - { - const firstMatches = matches.values().next().value; - if (firstMatches && firstMatches.length > 0) - { - paths_screenshots.push(...firstMatches[0].screenshotUrls); - } - } - } - - // pre-fetch screenshots - const screenshots = await Promise.all(paths_screenshots.map(s => fetch(s))); - - if (screenshots.length > 0) - { - await tx.insert(appSchema.screenshots).values(await Promise.all(screenshots.map(async (response) => - { - const screenshot: typeof appSchema.screenshots.$inferInsert = { - game_id: localGame.id, - content: Buffer.from(await response.arrayBuffer()), - type: response.headers.get('content-type') - }; - - return screenshot; - }))); - } - - await tx.update(appSchema.games).set({ - metadata: { - age_ratings: sourceGame.metadata.age_ratings, - genres: sourceGame.metadata.genres, - player_count: sourceGame.metadata.player_count ?? undefined, - companies: sourceGame.metadata.companies, - game_modes: sourceGame.metadata.game_modes, - average_rating: sourceGame.metadata.average_rating ?? undefined, - first_release_date: sourceGame.metadata.first_release_date?.getTime() ?? undefined, - } - }).where(eq(appSchema.games.id, localGame.id)); - }); -} - -export async function fixSource (source: string, id: string) -{ - const valid = await validateGameSource(source, id); - if (!valid.valid) - { - if (!valid.localGame) throw new Error("No Local Game"); - if (!valid.localGame.source) throw new Error("No Valid Source"); - - const foundGame = await plugins.hooks.games.searchGame.promise({ - igdb_id: valid.localGame.igdb_id ?? undefined, - ra_id: valid.localGame.ra_id ?? undefined, - source: valid.localGame.source - }); - - if (foundGame) - { - await db.update(appSchema.games).set({ - source: foundGame.id.source, - source_id: foundGame.id.id, - metadata: { - age_ratings: foundGame.metadata.age_ratings, - genres: foundGame.metadata.genres, - player_count: foundGame.metadata.player_count ?? undefined, - companies: foundGame.metadata.companies, - game_modes: foundGame.metadata.game_modes, - average_rating: foundGame.metadata.average_rating ?? undefined, - first_release_date: foundGame.metadata.first_release_date?.getTime() ?? undefined, - } - }).where(eq(appSchema.games.id, valid.localGame.id)); - return true; - } else - { - throw new Error("Could not find Source Game"); - } - } else - { - throw new Error("Game Source Already Valid"); - } -} - -export async function validateGameSource (source: string, id: string): Promise<{ - valid: boolean, - localGame?: { id: number; igdb_id: number | null; ra_id: number | null; source: string | null; }, - reason?: string; -}> -{ - const localGame = await getLocalGame(source, id); - if (!localGame) return { valid: true }; - if (localGame.source && localGame.source_id) - { - const sourceGame = await plugins.hooks.games.fetchGame.promise({ source: localGame.source, id: localGame.source_id }); - if (!sourceGame) return { valid: false, reason: "Source Missing", localGame }; - // Store should be immutable - if (localGame.source !== 'store' && sourceGame.igdb_id !== (localGame.igdb_id ?? undefined) && sourceGame.ra_id !== (localGame.ra_id ?? undefined)) - { - return { valid: false, reason: "Metadata Missmatch", localGame }; - } - } - - return { valid: true, localGame }; -} - -export async function updateLocalLastPlayed (id: number) -{ - await db.update(appSchema.games).set({ last_played: new Date() }).where(eq(appSchema.games.id, Number(id))); -} - -export async function getValidLaunchCommandsForGame (source: string, id: string): Promise<{ commands: CommandEntry[], gameId: FrontEndId, source?: string, sourceId?: string; } | Error | undefined> +export async function getValidLaunchCommandsForGame (source: string, id: string) { const localGame = await getLocalGame(source, id); if (localGame) { - const commands = await plugins.hooks.games.buildLaunchCommands.promise({ - source: localGame.source, - sourceId: localGame.source_id, - id: { source: 'local', id: String(localGame.id) }, - systemSlug: localGame.platform.slug, - gamePath: localGame.path_fs, - mainGlob: localGame.main_glob, - }); + const rommPlatform = localGame.platform.slug; + const esPlatform = await emulatorsDb.query.systemMappings.findFirst({ where: and(eq(emulatorSchema.systemMappings.sourceSlug, rommPlatform), eq(emulatorSchema.systemMappings.source, 'romm')) }); - if (commands instanceof Error || !commands) return commands; - - const validCommand = commands.find(c => c.valid); - if (validCommand) + if (esPlatform) { - return { - commands: commands.filter(c => c.valid), - gameId: { id: String(localGame.id), source: 'local' }, - source: localGame.source ?? source, - sourceId: localGame.source_id ? String(localGame.source_id) : id, - }; + if (localGame.path_fs) + { + try + { + const commands = await getValidLaunchCommands({ systemSlug: esPlatform.system, gamePath: localGame.path_fs }); + + if (cores[esPlatform.system]) + { + const gameUrl = `${RPC_URL(host)}/api/romm/rom/${source}/${id}`; + commands.push({ + id: 'EMULATORJS', + label: "Emulator JS", + command: `core=${cores[esPlatform.system]}&gameUrl=${encodeURIComponent(gameUrl)}`, + valid: true, + emulator: 'EMULATORJS', + metadata: { + romPath: gameUrl + } + }); + } + + const validCommand = commands.find(c => c.valid); + if (validCommand) + { + return { commands: commands.filter(c => c.valid), gameId: localGame.id, source: source, sourceId: id }; + } + else + { + return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${Array.from(new Set(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('missing-emulator', `Missing One Of Emulators: ${Array.from(new Set(commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator))).join(', ')}`); + return new CommandSearchError('error', `Missing Platform ${localGame.platform.slug}`); } - } else if (source === 'emulator') - { - const commands = await plugins.hooks.games.buildLaunchCommands.promise({ - source, - sourceId: id, - id: { source: source, id: id }, - systemSlug: "", - gamePath: null - }); - if (commands instanceof Error || !commands) return commands; - - const validCommand = commands.find(c => c.valid); - if (validCommand) - { - return { - commands: commands.filter(c => c.valid), - gameId: { id, source } - }; - } - else - { - return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${Array.from(new Set(commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator))).join(', ')}`); - } } return undefined; @@ -294,7 +106,7 @@ export default function buildStatusResponse () z.object({ status: z.literal('refresh'), localId: z.number().optional() }), z.object({ status: z.literal(['queued']) }), z.object({ status: z.literal('playing'), details: z.string() }), - z.object({ status: z.literal('install'), details: z.string(), sources: DownloadSourceSchema.array() }), + z.object({ status: z.literal('install'), details: z.string() }), z.object({ status: z.literal('present'), details: z.string() }), z.object({ status: z.literal(['download', 'extract']), progress: z.number() }), ]), @@ -308,7 +120,7 @@ export default function buildStatusResponse () }, async open (ws) { - sendLatests().catch(e => ws.send({ status: 'error', error: JSON.stringify(e) })); + sendLatests(); const installJobId = InstallJob.query({ source: ws.data.params.source, id: ws.data.params.id }); async function sendLatests () @@ -331,7 +143,6 @@ export default function buildStatusResponse () } else { - const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(ws.data.params.id, ws.data.params.source) }); const validCommand = await getValidLaunchCommandsForGame(ws.data.params.source, ws.data.params.id); if (validCommand) { @@ -348,11 +159,9 @@ export default function buildStatusResponse () }); } - } else if (!localGame && ws.data.params.source === 'store') + } else if (ws.data.params.source === 'store') { - const downloads = await plugins.hooks.games.fetchDownloads.promise({ source: ws.data.params.source, id: ws.data.params.id }); - const sources = downloads?.map(d => ({ id: d.id, name: d.id })) ?? []; - /*const storeGame = await getStoreGame(ws.data.params.id); + const storeGame = await getStoreGameFromId(ws.data.params.id); const fileResponse = await fetch(storeGame.file, { method: 'HEAD' }); const size = Number(fileResponse.headers.get('content-length')); const stats = await fs.statfs(config.get('downloadPath')); @@ -363,22 +172,19 @@ export default function buildStatusResponse () } else { ws.send({ status: 'install', details: 'Install' }); - }*/ - - ws.send({ status: 'install', details: 'Install', sources }); - } else if (!localGame) + } + } else { const files = await plugins.hooks.games.fetchDownloads.promise({ source: ws.data.params.source, id: ws.data.params.id }); - const sources = files?.map(d => ({ id: d.id, name: d.id })) ?? []; let filesChecked: LocalDownloadFileEntry[] | undefined; - if (files && files.length) + if (files) { - filesChecked = await checkFiles(files[0].files, !!files[0].extract_path); + filesChecked = await checkFiles(files.files, !!files.extract_path); } if (filesChecked && !filesChecked.some(f => f.exists === false || f.matches === false)) @@ -393,16 +199,15 @@ export default function buildStatusResponse () ws.send({ status: 'error', error: "Not Enough Free Space" }); } else if (filesChecked?.some(f => f.exists === true && f.matches === false)) { - ws.send({ status: 'install', details: 'Some Files Present, Install', sources }); + ws.send({ status: 'install', details: 'Some Files Present, Install' }); } else { - ws.send({ status: 'install', details: 'Install', sources }); + ws.send({ status: 'install', details: 'Install' }); } } - } else - { - ws.send({ status: 'error', error: "No Way To Launch" }); + + } } } diff --git a/src/bun/api/games/services/utils.ts b/src/bun/api/games/services/utils.ts index 9bef2f4..f40eb8a 100644 --- a/src/bun/api/games/services/utils.ts +++ b/src/bun/api/games/services/utils.ts @@ -2,31 +2,24 @@ import getFolderSize from "get-folder-size"; import fs from "node:fs/promises"; import path from "node:path"; import { config, db, emulatorsDb, plugins } from "../../app"; -import { and, eq, or } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import * as schema from "@schema/app"; -import { RPC_URL } from "@shared/constants"; -import { hashFile } from "@/bun/utils"; -import { host } from "@/bun/utils/host"; +import { StoreGameType } from "@shared/constants"; +import { DetailedRomSchema, getCurrentUserApiUsersMeGet, getRomApiRomsIdGet, SimpleRomSchema } from "@clients/romm"; import * as emulatorSchema from "@schema/emulators"; -import { DownloadFileEntry, FrontEndGameType, FrontEndGameTypeDetailed, GameLookup, LocalDownloadFileEntry, LocalGameMetadata, ProgressStats } from "@simeonradivoev/gameflow-sdk/shared"; +import { extractStoreGameSourceId, getStoreGame } from "../../store/services/gamesService"; +import { hashFile, isSteamDeck, isSteamDeckGameMode } from "@/bun/utils"; export async function calculateSize (installPath: string | null) { if (!installPath) return null; - const finalPath = path.isAbsolute(installPath) ? installPath : path.join(config.get('downloadPath'), installPath); - return (await getFolderSize(finalPath)).size; + return (await getFolderSize(path.join(config.get('downloadPath'), installPath))).size; } export async function checkInstalled (installPath: string | null) { if (!installPath) return false; - const finalPath = path.isAbsolute(installPath) ? installPath : path.join(config.get('downloadPath'), installPath); - return fs.exists(finalPath); -} - -export function getScreenshotLocalGameMatch (id: string, source: string) -{ - return source !== 'local' ? and(eq(schema.games.source_id, id), eq(schema.games.source, source)) : eq(schema.games.id, Number(id)); + return fs.exists(path.join(config.get('downloadPath'), installPath)); } export function getLocalGameMatch (id: string, source: string) @@ -40,10 +33,10 @@ export function convertLocalToFrontend (g: typeof schema.games.$inferSelect & { }) { const game: FrontEndGameType = { - platform_display_name: g.platform?.name ?? null, + platform_display_name: g.platform?.name ?? "Local", id: { id: String(g.id), source: 'local' }, updated_at: g.created_at, - path_covers: [`/api/romm/game/local/${g.id}/cover`], + 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`, @@ -53,29 +46,22 @@ export function convertLocalToFrontend (g: typeof schema.games.$inferSelect & { slug: g.slug, name: g.name, platform_id: g.platform_id, - platform_slug: g.platform?.slug ?? null, - metadata: { - first_release_date: g.metadata?.first_release_date !== undefined ? new Date(g.metadata?.first_release_date) : null - } + platform_slug: g.platform?.slug ?? null }; return game; } -export async function convertLocalToFrontendDetailed (g: typeof schema.games.$inferSelect & { - platform?: { name: string | null, slug: string | null; } | null; +export function convertLocalToFrontendDetailed (g: typeof schema.games.$inferSelect & { + platform?: typeof schema.platforms.$inferSelect | null; screenshotIds?: number[]; }) { - - const exists = await checkInstalled(g.path_fs); - const fileSize = await calculateSize(g.path_fs); - const game: FrontEndGameTypeDetailed = { platform_display_name: g.platform?.name ?? "Local", id: { id: String(g.id), source: 'local' }, updated_at: g.created_at, - path_covers: [`/api/romm/game/local/${g.id}/cover`], + 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`, @@ -87,28 +73,70 @@ export async function convertLocalToFrontendDetailed (g: typeof schema.games.$in platform_id: g.platform_id, platform_slug: g.platform?.slug ?? null, summary: g.summary, - fs_size_bytes: fileSize, - missing: !exists, - local: true, - ra_id: g.ra_id, - version: g.version, - version_source: g.version_source, - version_system: g.version_system, - igdb_id: g.igdb_id, - metadata: { - genres: g.metadata.genres ?? [], - companies: g.metadata.companies ?? [], - game_modes: g.metadata.game_modes ?? [], - age_ratings: g.metadata.age_ratings ?? [], - player_count: g.metadata.player_count ?? null, - average_rating: g.metadata.average_rating ?? null, - first_release_date: g.metadata.first_release_date ? new Date(g.metadata.first_release_date) : null - } + fs_size_bytes: 0, + missing: false, + local: true }; return game; } +export async function convertStoreToFrontend (system: string, id: string, storeGame: StoreGameType): Promise +{ + const rommSystem = await emulatorsDb.query.systemMappings.findFirst({ + where: and(eq(emulatorSchema.systemMappings.system, system), eq(emulatorSchema.systemMappings.source, 'romm')) + }); + + const platformDef = await emulatorsDb.query.systems.findFirst({ + where: eq(emulatorSchema.systems.name, system), + columns: { fullname: true } + }); + + const gameId = `${system}@${id}`; + + const game: FrontEndGameType = { + platform_display_name: platformDef?.fullname ?? system, + path_platform_cover: `/api/romm/image/romm/assets/platforms/${rommSystem?.sourceSlug ?? system}.svg`, + id: { source: 'store', id: gameId }, + source: null, + source_id: null, + path_fs: null, + path_cover: `/api/romm/image?url=${encodeURIComponent(storeGame.pictures.titlescreens?.[0])}`, + last_played: null, + updated_at: new Date(), + slug: null, + name: storeGame.title, + platform_id: null, + platform_slug: rommSystem?.sourceSlug ?? system, + paths_screenshots: storeGame.pictures.screenshots?.map((s: string) => `/api/romm/image?url=${encodeURIComponent(s)}`) ?? [] + }; + + return game; +} + +export async function convertStoreToFrontendDetailed (system: string, id: string, storeGame: StoreGameType): Promise +{ + let size: number | null = null; + try + { + const fileResponse = await fetch(storeGame.file, { method: 'HEAD' }); + size = Number(fileResponse.headers.get('content-length')); + } catch (error) + { + console.error(error); + } + + const detailed: FrontEndGameTypeDetailed = { + ...await convertStoreToFrontend(system, id, storeGame), + summary: storeGame.description, + fs_size_bytes: size, + missing: false, + local: false, + }; + + return detailed; +} + export async function getLocalGameDetailed (match: any) { const localGame = await db.query.games.findFirst({ @@ -121,13 +149,35 @@ export async function getLocalGameDetailed (match: any) if (localGame) { - return convertLocalToFrontendDetailed({ ...localGame, screenshotIds: localGame.screenshots.map(s => s.id) }); + const exists = await checkInstalled(localGame.path_fs); + const fileSize = await calculateSize(localGame.path_fs); + const game: FrontEndGameTypeDetailed = { + path_cover: `/api/romm/game/local/${localGame.id}/cover`, + updated_at: localGame.created_at, + id: { id: String(localGame.id), source: 'local' }, + path_platform_cover: `/api/romm/platform/local/${localGame.platform_id}/cover`, + fs_size_bytes: fileSize ?? null, + paths_screenshots: localGame.screenshots.map(s => `/api/romm/screenshot/${s.id}`), + local: true, + missing: !exists, + platform_display_name: localGame.platform?.name, + summary: localGame.summary, + source: localGame.source, + source_id: localGame.source_id, + path_fs: localGame.path_fs, + last_played: localGame.last_played, + slug: localGame.slug, + name: localGame.name, + platform_id: localGame.platform_id, + platform_slug: localGame.platform.slug + }; + return game; } return undefined; } -export async function getSourceGameDetailed (source: string, id: string, options?: { sourceOnly?: boolean; }) +export async function getSourceGameDetailed (source: string, id: string) { if (source === 'local') { @@ -139,13 +189,30 @@ export async function getSourceGameDetailed (source: string, id: string, options { const localGame = await getLocalGameDetailed(getLocalGameMatch(id, source)); - const remoteGame = await plugins.hooks.games.fetchGame.promise({ source, id, localGame }); - if (localGame && options?.sourceOnly !== true) + if (source === 'store') { - return localGame; + const gameId = extractStoreGameSourceId(id); + const storeGame = await getStoreGame(gameId.system, gameId.id); + if (!storeGame) return undefined; + const storeFrontendGame = await convertStoreToFrontendDetailed(gameId.system, gameId.id, storeGame); + if (localGame) + { + return { ...storeFrontendGame, ...localGame }; + } + return storeFrontendGame; + } else + { + const remoteGame = await plugins.hooks.games.fetchGame.promise({ source, id, localGame }); + if (remoteGame) + { + return remoteGame; + } else if (localGame) + { + return localGame; + } } - return remoteGame; + return undefined; } } @@ -174,333 +241,4 @@ export async function checkFiles (files: DownloadFileEntry[], isArchive: boolean } return { ...f, exists: false, matches: false } satisfies LocalDownloadFileEntry; })); -} - -export async function findPlatform (info: { - system_slug: string; platform: { - igdb_id?: number; - igdb_slug?: string; - ra_id?: number; - moby_id?: number; - source: string; - source_id?: number; - source_slug?: string; - family_name?: string; - name?: string; - } | undefined; -}): - Promise<{ - type: string | null; - slug?: string | null; - name?: string | null; - family_name?: string | null; - es_slug?: string | null; - coverUrl?: string | null; - }> -{ - // Search for existing platform - const platformSearch = [eq(schema.platforms.slug, info.system_slug)]; - const esPlatformSearch = [eq(emulatorSchema.systemMappings.system, info.system_slug)]; - - if (info.platform) - { - if (info.platform.igdb_id) platformSearch.push(eq(schema.platforms.igdb_id, info.platform.igdb_id)); - if (info.platform.igdb_slug) platformSearch.push(eq(schema.platforms.igdb_slug, info.platform.igdb_slug)); - if (info.platform.ra_id) platformSearch.push(eq(schema.platforms.ra_id, info.platform.ra_id)); - if (info.platform.moby_id) platformSearch.push(eq(schema.platforms.moby_id, info.platform.moby_id)); - - esPlatformSearch.push(eq(emulatorSchema.systemMappings.source, info.platform.source)); - if (info.platform.source_slug) - { - esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceSlug, info.platform.source_slug)); - } else if (info.platform.source_id) - { - esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceId, info.platform.source_id)); - } else - { - throw new Error("Must Provide at least one source id or slug"); - } - } - - const esPlatform = await emulatorsDb.query.systemMappings.findFirst({ - with: { system: true }, - where: and(...esPlatformSearch) - }); - - if (esPlatform) - platformSearch.push(eq(schema.platforms.es_slug, esPlatform.system.name)); - - let existingPlatform = await db.query.platforms.findFirst({ where: or(...platformSearch) }); - - if (!existingPlatform) - { - // TODO: use something else than the romm demo as CDN - - const platformLookup = await plugins.hooks.games.platformLookup.promise({ - slug: info.platform?.source_slug ?? info.system_slug - }); - let platformCover = await fetch(`${config.get('rommAddress') ?? 'https://demo.romm.app'}/assets/platforms/${info.platform?.source_slug ?? info.system_slug}.svg`, { method: "HEAD" }); - if (!platformCover.ok && platformLookup?.url_logo) - { - platformCover = await fetch(platformLookup.url_logo, { method: "HEAD" }); - } - - if (!esPlatform && !info.platform) - { - // go to unknown platform - existingPlatform = await db.query.platforms.findFirst({ where: eq(schema.platforms.slug, "unknown") }); - - if (existingPlatform) - { - return { - type: "existing", - slug: existingPlatform.slug, - name: existingPlatform.name, - family_name: existingPlatform.family_name, - es_slug: existingPlatform.es_slug, - coverUrl: `${RPC_URL(host)}/api/romm/platform/local/${existingPlatform.id}/cover` - }; - } else - { - return { type: "unknown" }; - } - } else - { - return { - type: "new", - slug: info.platform?.source_slug ?? esPlatform?.system.name ?? '', - name: info.platform?.name ?? esPlatform?.system.fullname ?? '', - family_name: info.platform?.family_name, - es_slug: esPlatform?.system.name ?? undefined, - coverUrl: platformCover.url - }; - } - - } else - { - return { - type: "existing", - slug: existingPlatform.slug, - name: existingPlatform.name, - family_name: existingPlatform.family_name, - es_slug: existingPlatform.es_slug, - coverUrl: `${RPC_URL(host)}/api/romm/platform/local/${existingPlatform.id}/cover` - }; - } -} - -export async function createLocalGame (info: { - name: string; - system_slug: string | undefined; - source: string | undefined; - source_id: string | undefined; - slug: string | null | undefined; - path_fs: string | null | undefined; - summary: string | null | undefined; - igdb_id: number | undefined; - ra_id: number | undefined; - main_glob: string | undefined; - cover: Buffer | undefined; - coverType: string | null | undefined; - version: string | undefined; - version_source: string | undefined; - screenshotUrls: string[]; - version_system: string | undefined; - last_played?: Date; - metadata: LocalGameMetadata | undefined, - platform: { - igdb_id?: number; - igdb_slug?: string; - ra_id?: number; - moby_id?: number; - source: string; - source_id?: number; - source_slug?: string; - family_name?: string; - name?: string; - } | undefined; -}) -{ - const id = await db.transaction(async (tx) => - { - // Search for existing platform - const platformSearch = []; - const esPlatformSearch = []; - if (info.system_slug) - { - platformSearch.push(eq(schema.platforms.slug, info.system_slug)); - esPlatformSearch.push(eq(emulatorSchema.systemMappings.system, info.system_slug)); - } - - if (info.platform) - { - if (info.platform.igdb_id) platformSearch.push(eq(schema.platforms.igdb_id, info.platform.igdb_id)); - if (info.platform.igdb_slug) platformSearch.push(eq(schema.platforms.igdb_slug, info.platform.igdb_slug)); - if (info.platform.ra_id) platformSearch.push(eq(schema.platforms.ra_id, info.platform.ra_id)); - if (info.platform.moby_id) platformSearch.push(eq(schema.platforms.moby_id, info.platform.moby_id)); - - esPlatformSearch.push(eq(emulatorSchema.systemMappings.source, info.platform.source)); - if (info.platform.source_slug) - { - esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceSlug, info.platform.source_slug)); - } else if (info.platform.source_id) - { - esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceId, info.platform.source_id)); - } else - { - throw new Error("Must Provide at least one source id or slug"); - } - } - - const esPlatform = await emulatorsDb.query.systemMappings.findFirst({ - with: { system: true }, - where: and(...esPlatformSearch) - }); - - if (esPlatform) - platformSearch.push(eq(schema.platforms.es_slug, esPlatform.system.name)); - - let existingPlatform = await tx.query.platforms.findFirst({ where: or(...platformSearch) }); - let platformId: number; - if (!existingPlatform) - { - // TODO: use something else than the romm demo as CDN - - const platformLookup = await plugins.hooks.games.platformLookup.promise({ - slug: info.platform?.source_slug ?? info.system_slug - }); - let platformCover = await fetch(`${config.get('rommAddress') ?? 'https://demo.romm.app'}/assets/platforms/${info.platform?.source_slug ?? info.system_slug}.svg`); - if (!platformCover.ok && platformLookup?.url_logo) - { - platformCover = await fetch(platformLookup.url_logo); - } - - if (!esPlatform && !info.platform) - { - // go to unknown platform - existingPlatform = await tx.query.platforms.findFirst({ where: eq(schema.platforms.slug, "unknown") }); - - if (existingPlatform) - { - platformId = existingPlatform.id; - } else - { - const [{ id }] = await tx.insert(schema.platforms).values({ - slug: 'unknown', - name: "Unknown" - }).returning({ id: schema.platforms.id }); - platformId = id; - } - } else - { - // Create new local platform - const platform: typeof schema.platforms.$inferInsert = { - slug: info.platform?.source_slug ?? esPlatform?.system.name ?? '', - igdb_id: info.platform?.igdb_id, - igdb_slug: info.platform?.igdb_slug, - ra_id: info.platform?.ra_id, - cover: Buffer.from(await platformCover.arrayBuffer()), - cover_type: platformCover.headers.get('content-type'), - name: info.platform?.name ?? esPlatform?.system.fullname ?? '', - family_name: info.platform?.family_name, - es_slug: esPlatform?.system.name ?? 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: info.source_id, - source: info.source, - slug: info.slug, - path_fs: info.path_fs, - last_played: info.last_played, - platform_id: platformId, - igdb_id: info.igdb_id, - ra_id: info.ra_id, - summary: info.summary, - name: info.name, - cover: info.cover, - cover_type: info.coverType, - metadata: info.metadata, - main_glob: info.main_glob, - version: info.version, - version_source: info.version_source, - version_system: info.version_system - }; - - const [{ id }] = await tx.insert(schema.games).values(game).returning({ id: schema.games.id }); - - if (info.screenshotUrls.length <= 0 && info.igdb_id) - { - const matches = new Map(); - await plugins.hooks.games.gameLookup.promise(matches, { source: 'igdb', id: String(info.igdb_id) }); - info.screenshotUrls.push(...(matches.values().next().value?.[0].screenshotUrls ?? [])); - } - - // pre-fetch screenshots - const screenshots = await Promise.all(info.screenshotUrls.map(s => fetch(s))); - - if (screenshots.length > 0) - { - 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; - }))); - } - - return id; - }); - - return id; -} - -export async function downloadGame (ctx: { - downloads: DownloadFileEntry[], - auth?: string, - id: string, - abortSignal?: AbortSignal, - setProgress?: (progress: number, state: "download" | "extract", info: Partial>) => void, - extract_path?: string; - path_fs?: string; - -}): Promise -{ - const downloadedFiles = await plugins.hooks.downloadFiles.promise({ - id: ctx.id, - auth: ctx.auth, - files: ctx.downloads, - downloadPath: config.get('downloadPath'), - abortSignal: ctx.abortSignal, - updateProgress: (stats) => ctx.setProgress?.(stats.progress, 'download', stats) - }); - - if (!downloadedFiles) - { - return; - } - - const finalFiles = await plugins.hooks.postDownloadFiles.promise({ - files: downloadedFiles.files, - source: downloadedFiles.source, - extract_path: ctx.extract_path, - downloadPath: config.get('downloadPath'), - path_fs: ctx.path_fs - }) ?? downloadedFiles.files; - - return finalFiles; } \ No newline at end of file diff --git a/src/bun/api/hooks/app.ts b/src/bun/api/hooks/app.ts new file mode 100644 index 0000000..bf592a0 --- /dev/null +++ b/src/bun/api/hooks/app.ts @@ -0,0 +1,10 @@ +import { AuthHooks } from "./auth"; +import { EmulatorHooks } from "./emulators"; +import { GameHooks } from "./games"; + +export class GameflowHooks +{ + games = new GameHooks(); + emulators = new EmulatorHooks(); + auth = new AuthHooks(); +} \ No newline at end of file diff --git a/src/packages/gameflow-sdk/hooks/auth.ts b/src/bun/api/hooks/auth.ts similarity index 71% rename from src/packages/gameflow-sdk/hooks/auth.ts rename to src/bun/api/hooks/auth.ts index cb8dd1b..7234992 100644 --- a/src/packages/gameflow-sdk/hooks/auth.ts +++ b/src/bun/api/hooks/auth.ts @@ -1,8 +1,6 @@ - import { AsyncSeriesHook } from "tapable"; -import { DownloadFileEntry } from "../shared"; -export default class AuthHooks +export class AuthHooks { loginComplete = new AsyncSeriesHook<[ctx: { service: string; diff --git a/src/bun/api/hooks/emulators.ts b/src/bun/api/hooks/emulators.ts new file mode 100644 index 0000000..f48ea9f --- /dev/null +++ b/src/bun/api/hooks/emulators.ts @@ -0,0 +1,10 @@ +import { AsyncSeriesBailHook } from "tapable"; + +export class EmulatorHooks +{ + fetchBiosDownload = new AsyncSeriesBailHook<[ctx: { + emulator: string; + systems: EmulatorSystem[]; + biosFolder: string; + }], { auth?: string, files: DownloadFileEntry[]; } | undefined>(['ctx']); +} \ No newline at end of file diff --git a/src/bun/api/hooks/games.ts b/src/bun/api/hooks/games.ts new file mode 100644 index 0000000..ff3ec04 --- /dev/null +++ b/src/bun/api/hooks/games.ts @@ -0,0 +1,64 @@ +import { EmulatorPackageType, GameListFilterType } from '@/shared/constants'; +import { SyncBailHook, AsyncSeriesHook, SyncWaterfallHook, AsyncSeriesBailHook, AsyncHook, AsyncParallelHook, SyncHook, AsyncSeriesWaterfallHook } from 'tapable'; + +export class GameHooks +{ + /** override the launch command for an emulator + * @param ctx.autoValidCommands The auto generated command for example based on the ES-DE listing + * @param ctx.emulator The emulator ID if any + * @param ctx.game.source The source of the game + * @param ctx.game.sourceId The ID of the source. This could be for example the ROMM ID the game was + * @returns The argument list to be used when running the emulator. + * If no emulator bin in the command entry is found the actual command will be used as the bin. + */ + emulatorLaunch = new AsyncSeriesBailHook<[ctx: { + autoValidCommand: CommandEntry; + game: { + source: string; + id: number; + }; + }], string[] | undefined>(['ctx']); + /** + * Fetches and returns a list of games converted to frontend. + * @param ctx.localGameIds This is local game ids in the format '@' + */ + fetchGames = new AsyncSeriesHook<[ctx: { + query: GameListFilterType; + games: FrontEndGameType[]; + }]>(['ctx']); + fetchGame = new AsyncSeriesBailHook<[ctx: { + source: string; + localGame?: FrontEndGameTypeDetailed; + id: string; + }], FrontEndGameTypeDetailed | undefined>(['ctx']); + /** Get download file URLs + * @param ctx.checksum Check if file already exists using checksums + */ + fetchDownloads = new AsyncSeriesBailHook<[ctx: { + source: string; + id: string; + }], DownloadInfo | undefined>(['ctx']); + fetchRecommendedGamesForGame = new AsyncSeriesHook<[ctx: { + game: FrontEndGameTypeDetailed, + games: (FrontEndGameType & { metadata?: any; })[]; + }]>(['ctx']); + fetchRecommendedGamesForEmulator = new AsyncSeriesHook<[cts: { + emulator: EmulatorPackageType; + systems: EmulatorSystem[]; + games: FrontEndGameType[]; + }]>(['ctx']); + fetchPlatform = new AsyncSeriesBailHook<[ctx: { + source: string; + id: string; + }], FrontEndPlatformType | undefined>(['ctx']); + platformLookup = new AsyncSeriesBailHook<[ctx: { + source: string; + id: string; + }], { slug: string; } | undefined>(['ctx']); + fetchPlatforms = new AsyncSeriesHook<[ctx: { + platforms: FrontEndPlatformType[]; + }]>(['ctx']); + updatePlayed = new AsyncSeriesWaterfallHook<[ctx: { source: string, id: string; }], boolean>(["ctx"]); + fetchCollections = new AsyncSeriesHook<[ctx: { collections: FrontEndCollection[]; }]>(['ctx']); + fetchCollection = new AsyncSeriesBailHook<[ctx: { source: string, id: string; }], FrontEndCollection | undefined>(['ctx']); +} \ No newline at end of file diff --git a/src/bun/api/jobs/bios-download-job.ts b/src/bun/api/jobs/bios-download-job.ts index 7a4edba..be46c5f 100644 --- a/src/bun/api/jobs/bios-download-job.ts +++ b/src/bun/api/jobs/bios-download-job.ts @@ -1,44 +1,35 @@ -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; +import z from "zod"; +import { IJob, JobContext } from "../task-queue"; import { config, plugins } from "../app"; import { simulateProgress } from "@/bun/utils"; import { Downloader } from "@/bun/utils/downloader"; import path from 'node:path'; import { ensureDir } from "fs-extra"; import { buildStoreFrontendEmulatorSystems, getStoreEmulatorPackage } from "../store/services/gamesService"; -import { DownloadJobData } from "@simeonradivoev/gameflow-sdk/shared"; -interface BiosDownloadJobData extends DownloadJobData -{ - emulator: string; -} - -export class BiosDownloadJob implements IJob +export class BiosDownloadJob implements IJob, "download"> { static id = "bios-download-job" as const; + static dataSchema = z.object({ emulator: z.string() }); static query = (q: { id: string; }) => `${BiosDownloadJob.id}-${q.id}`; group: string = "bios-download"; - data: BiosDownloadJobData; + emulator: string; dryRun: boolean; constructor(emulator: string, init?: { dryRun?: boolean; }) { - this.data = { - emulator, - name: "Download Emulator Bios" - }; + this.emulator = emulator; this.dryRun = init?.dryRun ?? false; } - async start (context: JobContext, BiosDownloadJobData, "download">) + async start (context: JobContext, "download">, z.infer, "download">) { - const emulator = await getStoreEmulatorPackage(this.data.emulator); + const emulator = await getStoreEmulatorPackage(this.emulator); if (!emulator) throw new Error("Could Not Find Emulator"); - this.data.name = `${emulator.name} Bios`; - this.data.preview_url = emulator.logo; const systems = await buildStoreFrontendEmulatorSystems(emulator); - const biosFolder = path.join(config.get('downloadPath'), "bios", this.data.emulator); + const biosFolder = path.join(config.get('downloadPath'), "bios", this.emulator); await ensureDir(biosFolder); - const files = await plugins.hooks.emulators.fetchBiosDownload.promise({ emulator: this.data.emulator, systems, biosFolder }); + const files = await plugins.hooks.emulators.fetchBiosDownload.promise({ emulator: this.emulator, systems, biosFolder }); if (!files) throw new Error("Could not find source to download from"); @@ -54,12 +45,9 @@ export class BiosDownloadJob implements IJob const downloader = new Downloader('bios-download', files.files, biosFolder, { signal: context.abortSignal, headers, - onProgress: (stats) => + onProgress (stats) { context.setProgress(stats.progress, "download"); - this.data.downloaded = stats.downloaded; - this.data.speed = stats.speed; - this.data.total = stats.total; }, }); @@ -69,6 +57,6 @@ export class BiosDownloadJob implements IJob exposeData () { - return this.data; + return { emulator: this.emulator }; } } \ No newline at end of file diff --git a/src/bun/api/jobs/emulator-download-job.ts b/src/bun/api/jobs/emulator-download-job.ts index de9f538..f79db72 100644 --- a/src/bun/api/jobs/emulator-download-job.ts +++ b/src/bun/api/jobs/emulator-download-job.ts @@ -1,54 +1,66 @@ -import { DownloadJobData, EmulatorPackageType } from '@simeonradivoev/gameflow-sdk/shared'; +import { EmulatorPackageType } from "@/shared/constants"; import { getStoreEmulatorPackage } from "../store/services/gamesService"; -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; -import { config, plugins } from "../app"; +import { IJob, JobContext } from "../task-queue"; +import z from "zod"; +import { Glob } from "bun"; +import { config } from "../app"; import path from 'node:path'; +import { getOrCachedGithubRelease } from "../cache"; import Seven from 'node-7z'; import fs from "node:fs/promises"; import { Downloader } from "@/bun/utils/downloader"; import { ensureDir, move } from "fs-extra"; -import { isArchive, simulateProgress } from "@/bun/utils"; +import { simulateProgress } from "@/bun/utils"; import { path7za } from "7zip-bin"; -import { getEmulatorDownload, getEmulatorPath } from "../store/services/emulatorsService"; -import { $ } from "bun"; -import { EmulatorSourceEntryType } from "@simeonradivoev/gameflow-sdk/shared"; type EmulatorDownloadStates = "download" | "extract"; -interface EmulatorDownloadJobData extends DownloadJobData -{ - emulator: string; -} - -export class EmulatorDownloadJob implements IJob +export class EmulatorDownloadJob implements IJob, EmulatorDownloadStates> { static id = "download-emulator" as const; + static dataSchema = z.object({ emulator: z.string() }); + emulator: string; downloadSource: string; emulatorPackage?: EmulatorPackageType; - dryRun: boolean; - isUpdate: boolean; - data: EmulatorDownloadJobData = { - name: "Download Emulator", - emulator: "" - }; + dryRun?: boolean; - constructor(emulator: string, downloadSource: string, init?: { dryRun?: boolean; isUpdate?: boolean; }) + constructor(emulator: string, downloadSource: string, init?: { dryRun?: boolean; }) { - this.data.emulator = emulator; + this.emulator = emulator; this.downloadSource = downloadSource; this.dryRun = init?.dryRun ?? false; - this.isUpdate = init?.isUpdate ?? false; } - async start (context: JobContext) + async start (context: JobContext, EmulatorDownloadStates>) { - this.emulatorPackage = await getStoreEmulatorPackage(this.data.emulator); + this.emulatorPackage = await getStoreEmulatorPackage(this.emulator); if (!this.emulatorPackage) throw new Error("Emulator not found"); - this.data.name = this.emulatorPackage.name; - this.data.preview_url = this.emulatorPackage.logo; - const { url, info } = await getEmulatorDownload(this.emulatorPackage, this.downloadSource); + if (!this.emulatorPackage.downloads) throw new Error("Emulator has no downloads"); - const emulatorsFolder = getEmulatorPath(this.data.emulator); + const validDownloads = this.emulatorPackage.downloads[`${process.platform}:${process.arch}`]; + if (!validDownloads) throw new Error(`Now downloads in ${this.emulatorPackage.name} for platform ${process.platform}:${process.arch}`); + + const validDownload = validDownloads.find(d => d.type === this.downloadSource); + if (!validDownload) throw new Error(`Download type ${this.downloadSource} not found`); + + let downloadUrl: URL; + if (validDownload.type === 'github') + { + console.log("Trying To Download from ", `https://api.github.com/repos/${validDownload.path}/releases/latest`); + const latestRelease = await getOrCachedGithubRelease(validDownload.path); + const glob = new Glob(validDownload.pattern); + const validAsset = latestRelease.assets.find(a => glob.match(a.name)); + if (!validAsset) throw new Error("Could Not Find Valid Asset"); + downloadUrl = new URL(validAsset.browser_download_url); + } else if (validDownload.type === 'direct') + { + downloadUrl = new URL(validDownload.url); + } else + { + throw new Error("Download Type Unsupported"); + } + + const emulatorsFolder = path.join(config.get('downloadPath'), "emulators", this.emulator); if (this.dryRun) { @@ -57,54 +69,41 @@ export class EmulatorDownloadJob implements IJob + onProgress (stats) { context.setProgress(stats.progress, 'download'); - this.data.total = stats.total; - this.data.downloaded = stats.downloaded; - this.data.speed = stats.speed; }, }); const destinationPaths = await downloader.start(); - context.abortSignal.throwIfAborted(); if (destinationPaths) { - const archive = isArchive(destinationPaths[0]); + const isArchive = destinationPaths[0].endsWith('.7z') || destinationPaths[0].endsWith('.zip'); const isAppImage = destinationPaths[0].endsWith(".AppImage"); - if (!archive && !isAppImage) + if (!isArchive && !isAppImage) { throw new Error("Invalid Download Type"); } - if (archive) + if (isArchive) { if (destinationPaths[0]) { let destinationPath = destinationPaths[0]; - if (destinationPath.endsWith('.tar')) + await new Promise((resolve, reject) => { - context.setProgress(0, "extract"); - await ensureDir(emulatorsFolder); - await $`tar -xf ${destinationPath} -C ${emulatorsFolder}`; - await fs.rm(destinationPath, { recursive: true }); - } else - { - await new Promise((resolve, reject) => - { - const seven = Seven.extractFull(destinationPath, emulatorsFolder, { $bin: process.env.ZIP7_PATH ?? path7za, $progress: true, noRootDuplication: true }); - seven.on('progress', p => context.setProgress(p.percent, "extract")); - seven.on('error', e => reject(e)); - seven.on('end', () => resolve(true)); - }); - await fs.rm(destinationPath, { recursive: true }); - } + const seven = Seven.extractFull(destinationPath, emulatorsFolder, { $bin: process.env.ZIP7_PATH ?? path7za, $progress: true, noRootDuplication: true }); + seven.on('progress', p => context.setProgress(p.percent, "extract")); + seven.on('error', e => reject(e)); + seven.on('end', () => resolve(true)); + }); + await fs.rm(destinationPath, { recursive: true }); // check if 1 root folder we need to get rid of const contents = await fs.readdir(emulatorsFolder); @@ -128,19 +127,6 @@ export class EmulatorDownloadJob implements IJob e.type === 'store')?.binPath ?? emulatorsFolder, - info, - update: this.isUpdate - }); } } @@ -148,7 +134,7 @@ export class EmulatorDownloadJob implements IJob -{ - static id = "update-store" as const; - static dataSchema = z.never(); - packageName: string; - storeVersion: string; - - constructor() - { - this.packageName = process.env.STORE_PACKAGE_NAME ?? "@simeonradivoev/gameflow-store"; - this.storeVersion = process.env.STORE_VERSION ?? "^0.1.0"; - } - - async start (context: JobContext) - { - const storeFolder = getStoreRootFolder(); - await ensureDir(storeFolder); - const storePackageFile = Bun.file(path.join(storeFolder, "package.json")); - if (!await storePackageFile.exists()) - { - await storePackageFile.write(JSON.stringify({ dependencies: {} }, null, 3)); - } - - const storePackage = await Bun.file(path.join(storeFolder, "package.json")).json(); - - if (IsPluginAllowed(sdkPkg.name)) - { - if (!storePackage.dependencies?.[sdkPkg.name] || storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version) - { - let response = await runBunPackageCommand(["add", `${sdkPkg.name}@${sdkPkg.version}`, "--registry", PluginRegistry, '--omit', 'peer']); - console.log(response); - } - - // probably just means we couldn't find a version of the sdk, just install latest - if (storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version) - { - let response = await runBunPackageCommand(["add", '--exact', `${sdkPkg.name}@latest`, "--registry", PluginRegistry, '--omit', 'peer']); - console.log(response); - } - } else - { - console.log("Ignoring SDK package"); - } - - if (process.env.CUSTOM_STORE_PATH) return; - - if (!storePackage.dependencies?.['@simeonradivoev/gameflow-store']) - { - context.setProgress(0.5, "Adding Store"); - let response = await runBunPackageCommand(["add", `${this.packageName}@${this.storeVersion}`, "--registry", PluginRegistry, '--omit', 'peer']); - console.log(response); - } - } -} \ No newline at end of file diff --git a/src/bun/api/jobs/import-job.ts b/src/bun/api/jobs/import-job.ts deleted file mode 100644 index 7f080c7..0000000 --- a/src/bun/api/jobs/import-job.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { eq, inArray, or } from "drizzle-orm"; -import { db, plugins } from "../app"; -import { createLocalGame, downloadGame } from "../games/services/utils"; -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; -import * as schema from "@schema/app"; -import { DownloadJobData, GameLookup } from "@simeonradivoev/gameflow-sdk/shared"; -import { isUrl } from "@/shared/utils"; -import { basename } from "node:path"; -import path from 'node:path'; -import { isArchive } from "@/bun/utils"; - -interface ImportJobData extends DownloadJobData -{ - localId: number | null; -} - -export class ImportJob implements IJob -{ - static id = "import-job" as const; - static query = (q: { source: string; id: string; }) => `${ImportJob.id}-${q.source}-${q.id}`; - data: ImportJobData = { - localId: null, - name: "Import Game" - }; - group?: 'import-job'; - gamePath: string; - source: string; - id: string; - platformId: number; - - constructor(source: string, id: string, gamePath: string, platformId: number) - { - this.gamePath = gamePath; - this.source = source; - this.id = id; - this.platformId = platformId; - } - - exposeData () - { - return this.data; - } - - async start (context: JobContext, ImportJobData, string>): Promise - { - const matchesMap = new Map(); - await plugins.hooks.games.gameLookup.promise(matchesMap, { source: this.source, id: this.id }); - const matches = matchesMap.values().next().value; - if (!matches || matches.length <= 0) throw Error("Could not Find Game"); - const match = matches[0]; - this.data.name = match.name; - this.data.preview_url = match.coverUrl; - - let cover: Buffer | undefined = undefined; - let coverType: string | undefined = undefined; - if (match.coverUrl) - { - const coverResponse = await fetch(match.coverUrl); - if (coverResponse.ok) - { - cover = Buffer.from(await coverResponse.arrayBuffer()); - coverType = coverResponse.headers.get('content-type') ?? undefined; - } - } - - const platformMatch = match.platforms.find(p => p.id === this.platformId); - - const finalFiles: string[] = []; - - if (isUrl(this.gamePath)) - { - const archive = isArchive(this.gamePath); - const downloadedFiles = await downloadGame({ - downloads: [{ - file_path: this.id, - file_name: basename(this.gamePath), - url: new URL(this.gamePath) - }], - extract_path: archive ? '.tmp' : undefined, - path_fs: path.join('roms', platformMatch?.slug ?? this.source, this.id), - abortSignal: context.abortSignal, - id: `game-${this.source}-${this.id}`, - setProgress: (progress, state, info) => - { - context.setProgress(progress, state); - this.data.speed = info.speed; - this.data.total = info.total; - this.data.downloaded = info.downloaded; - }, - }); - - if (downloadedFiles) - finalFiles.push(...downloadedFiles); - } else - { - finalFiles.push(this.gamePath); - } - - const localSearchFilters: any[] = []; - if (match.igdb_id) localSearchFilters.push(eq(schema.games.igdb_id, match.igdb_id)); - if (match.slug) localSearchFilters.push(eq(schema.games.slug, match.slug)); - localSearchFilters.push(eq(schema.games.name, match.name)); - localSearchFilters.push(inArray(schema.games.path_fs, finalFiles)); - const existingLocalGame = await db.query.games.findFirst({ where: or(...localSearchFilters) }); - context.abortSignal.throwIfAborted(); - - if (existingLocalGame) throw new Error("Game Already Exists"); - - this.data.localId = await createLocalGame({ - name: match.name, - system_slug: platformMatch?.slug, - source: undefined, - source_id: undefined, - slug: match.slug, - path_fs: finalFiles[0], - summary: match.summary, - igdb_id: match.igdb_id, - ra_id: undefined, - main_glob: undefined, - cover, - coverType, - version: undefined, - version_source: undefined, - screenshotUrls: match.screenshotUrls, - version_system: undefined, - platform: platformMatch ? { - source_slug: platformMatch.slug, - source_id: platformMatch.id, - source: this.source, - name: platformMatch.displayName - } : undefined, - metadata: { - game_modes: match.game_modes, - companies: match.companies, - first_release_date: match.first_release_date ?? undefined, - player_count: match.player_count, - age_ratings: match.age_ratings, - average_rating: match.average_rating, - genres: match.genres, - } - }); - } -} \ No newline at end of file diff --git a/src/bun/api/jobs/install-job.ts b/src/bun/api/jobs/install-job.ts index 3d3c867..a45fe7b 100644 --- a/src/bun/api/jobs/install-job.ts +++ b/src/bun/api/jobs/install-job.ts @@ -1,23 +1,30 @@ -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; +import { IJob, JobContext } from "../task-queue"; +import { and, eq, or } from 'drizzle-orm'; import fs from 'node:fs/promises'; +import * as schema from "@schema/app"; +import * as emulatorSchema from "@schema/emulators"; import path from 'node:path'; -import { config, events, plugins } from "../app"; +import { config, db, emulatorsDb, events, plugins } from "../app"; +import { extractStoreGameSourceId, getStoreGameFromId } from "../store/services/gamesService"; +import * as igdb from 'ts-igdb-client'; +import secrets from "../secrets"; import { simulateProgress } from "@/bun/utils"; +import { Downloader } from "@/bun/utils/downloader"; +import Seven from 'node-7z'; import z from "zod"; -import { checkFiles, createLocalGame, downloadGame } from "../games/services/utils"; +import { checkFiles } from "../games/services/utils"; import { ensureDir } from "fs-extra"; -import { DownloadInfo, DownloadJobData } from "@simeonradivoev/gameflow-sdk/shared"; +import { path7za } from "7zip-bin"; interface JobConfig { dryRun?: boolean; dryDownload?: boolean; - downloadId?: string; } export type InstallJobStates = 'download' | 'extract'; -export class InstallJob implements IJob +export class InstallJob implements IJob { static id = "install-job" as const; static query = (q: { source: string; id: string; }) => `${InstallJob.id}-${q.source}-${q.id}`; @@ -28,10 +35,6 @@ export class InstallJob implements IJob // The local game ID of newly created entry, if successful public localGameId?: number; public group = InstallJob.id; - public localPath?: string; - data: DownloadJobData = { - name: "Install Game" - }; constructor(id: string, source: string, config?: JobConfig) { @@ -40,47 +43,91 @@ export class InstallJob implements IJob this.source = source; } - public async start (cx: JobContext) + public async start (cx: JobContext) { cx.setProgress(0, 'download'); await fs.mkdir(config.get('downloadPath'), { recursive: true }); const downloadPath = config.get('downloadPath'); - const finalFiles: string[] = []; let info: DownloadInfo | undefined; + switch (this.source) + { + case 'store': + const game = await getStoreGameFromId(this.gameId); + const gameId = extractStoreGameSourceId(this.gameId); + info = { + coverUrl: game.pictures.titlescreens[0], + screenshotUrls: game.pictures.screenshots, + files: [{ + url: new URL(game.file), + file_path: `roms/${game.system}`, + file_name: path.basename(decodeURI(game.file)), + size: 0 + }], + slug: this.gameId, + source_id: this.gameId, + name: game.title, + summary: game.description, + system_slug: gameId.system, + extract_path: path.join('roms', gameId.system), + }; + + break; + default: + info = await plugins.hooks.games.fetchDownloads.promise({ source: this.source, id: this.gameId }); + break; + } + + if (!info) throw new Error(`Could not find downloader for source ${this.source}`); + + const files = await checkFiles(info.files, !!info.extract_path); + if (this.config?.dryRun !== true) { - const allDownloads = await plugins.hooks.games.fetchDownloads.promise({ source: this.source, id: this.gameId, downloadId: this.config?.downloadId }); - info = allDownloads?.[0]; - - if (!info) throw new Error(`Could not find downloader for source ${this.source}`); - - this.data.name = info.name; - this.data.preview_url = info.coverUrl; - - const files = await checkFiles(info.files, !!info.extract_path); - if (this.config?.dryDownload !== true && files.some(f => !f.exists || !f.matches)) { - const downloadedFiles = await downloadGame({ - downloads: files.filter(f => !f.exists || !f.matches), - extract_path: info.extract_path, - path_fs: info.path_fs, - abortSignal: cx.abortSignal, - auth: info.auth, - id: `game-${this.source}-${this.gameId}`, - setProgress: (process, state, info) => + const headers: Record = {}; + if (info.auth) + headers['Authorization'] = info.auth; + const downloader = new Downloader(`game-${this.source}-${this.gameId}`, + files.filter(f => !f.exists || !f.matches), + config.get('downloadPath'), { - cx.setProgress(process, state); - this.data.downloaded = info.downloaded; - this.data.speed = info.speed; - this.data.total = info.total; - }, - }); + signal: cx.abortSignal, + headers, + onProgress (stats) + { + cx.setProgress(stats.progress, 'download'); + }, + }); - if (downloadedFiles) - finalFiles.push(...downloadedFiles); + const downloadedFiles = await downloader.start(); + if (info.extract_path && downloadedFiles) + { + let progress = 0; + const progressDelta = 1 / downloadedFiles.length; + for (const filePath of downloadedFiles) + { + const extractPath = path.join(config.get('downloadPath'), info.extract_path); + await new Promise((resolve, reject) => + { + const seven = Seven.extractFull(filePath, extractPath, { $bin: process.env.ZIP7_PATH ?? path7za, $progress: true }); + seven.on('progress', p => + { + cx.setProgress(progress + p.percent * progressDelta, "extract"); + }); + + seven.on('error', e => reject(e)); + seven.on('end', async () => + { + await fs.rm(filePath); + resolve(true); + }); + }); + progress += progressDelta * 100; + } + } } if (this.config?.dryDownload === true && info.extract_path) @@ -91,34 +138,138 @@ export class InstallJob implements IJob const coverResponse = await fetch(info.coverUrl); const cover = Buffer.from(await coverResponse.arrayBuffer()); - cx.abortSignal.throwIfAborted(); + if (cx.abortSignal.aborted) return; - this.localGameId = await createLocalGame({ - cover, - coverType: coverResponse.headers.get('content-type'), - system_slug: info.system_slug, - source_id: info.source_id, - source: this.source, - slug: info.slug, - path_fs: info.path_fs ?? (info.extract_path ? path.join(downloadPath, info.extract_path) : undefined), - summary: info.summary, - igdb_id: info.igdb_id, - ra_id: info.ra_id, - name: info.name, - main_glob: info.main_glob, - version: info.version, - version_source: info.version_source, - screenshotUrls: info.screenshotUrls, - version_system: info.version_system, - metadata: info.metadata, - platform: info.platform + await db.transaction(async (tx) => + { + // Search for existing platform + const platformSearch = [eq(schema.platforms.slug, info.system_slug)]; + const esPlatformSearch = [eq(emulatorSchema.systemMappings.system, info.system_slug)]; + + if (info.platform) + { + if (info.platform.igdb_id) platformSearch.push(eq(schema.platforms.igdb_id, info.platform.igdb_id)); + if (info.platform.igdb_slug) platformSearch.push(eq(schema.platforms.igdb_slug, info.platform.igdb_slug)); + if (info.platform.ra_id) platformSearch.push(eq(schema.platforms.ra_id, info.platform.ra_id)); + if (info.platform.moby_id) platformSearch.push(eq(schema.platforms.moby_id, info.platform.moby_id)); + + esPlatformSearch.push(eq(emulatorSchema.systemMappings.source, 'romm')); + esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceSlug, info.platform.slug)); + } + + const esPlatform = await emulatorsDb.query.systemMappings.findFirst({ + with: { system: true }, + where: and(...esPlatformSearch) + }); + + if (esPlatform) + platformSearch.push(eq(schema.platforms.es_slug, esPlatform.system.name)); + + let existingPlatform = await tx.query.platforms.findFirst({ where: or(...platformSearch) }); + let platformId: number; + if (!existingPlatform) + { + // TODO: use something else than the romm demo as CDN + const platformCover = await fetch(`https://demo.romm.app/assets/platforms/${info.system_slug}.svg`); + + if (!esPlatform && !info.platform) + { + // go to unknown platform + existingPlatform = await tx.query.platforms.findFirst({ where: eq(schema.platforms.slug, "unknown") }); + + if (existingPlatform) + { + platformId = existingPlatform.id; + } else + { + const [{ id }] = await tx.insert(schema.platforms).values({ + slug: 'unknown', + name: "Unknown" + }).returning({ id: schema.platforms.id }); + platformId = id; + } + } else + { + // Create new local platform + const platform: typeof schema.platforms.$inferInsert = { + slug: info.platform?.slug ?? esPlatform?.system.name ?? '', + igdb_id: info.platform?.igdb_id, + igdb_slug: info.platform?.igdb_slug, + ra_id: info.platform?.ra_id, + cover: Buffer.from(await platformCover.arrayBuffer()), + cover_type: platformCover.headers.get('content-type'), + name: info.platform?.name ?? esPlatform?.system.fullname ?? '', + family_name: info.platform?.family_name, + es_slug: esPlatform?.system.name ?? 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: info.source_id, + source: this.source, + slug: info.slug, + path_fs: info.path_fs, + last_played: info.last_played, + platform_id: platformId, + igdb_id: info.igdb_id, + ra_id: info.ra_id, + summary: info.summary, + name: info.name, + cover, + cover_type: coverResponse.headers.get('content-type'), + metadata: info.metadata + }; + + const [{ id }] = await tx.insert(schema.games).values(game).returning({ id: schema.games.id }); + + if (info.screenshotUrls.length <= 0 && process.env.TWITCH_CLIENT_ID) + { + const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); + if (access_token) + { + const client = igdb.igdb(process.env.TWITCH_CLIENT_ID, access_token); + + const { data } = await client.request('artworks').pipe(igdb.fields(['game', 'url']), igdb.where('game', '=', info.igdb_id)).execute(); + + info.screenshotUrls.push(...data.filter(s => s.url).map(s => s.url!)); + } + } + + // pre-fetch screenshots + const screenshots = await Promise.all(info.screenshotUrls.map(s => fetch(s))); + + if (screenshots.length > 0) + { + 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; + }))); + } + + this.localGameId = id; }); - - if (this.source && this.gameId) await plugins.hooks.games.postInstall.promise({ source: this.source, id: this.gameId, files: finalFiles, info }); - events.emit('notification', { message: `${info.name}: Installed`, type: 'success', duration: 8000 }); } else { await simulateProgress(p => cx.setProgress(p, "download"), cx.abortSignal); } + + + events.emit('notification', { message: `${info.name}: Installed`, type: 'success', duration: 8000 }); } } \ No newline at end of file diff --git a/src/bun/api/jobs/jobs.ts b/src/bun/api/jobs/jobs.ts index 5471e56..8f836a5 100644 --- a/src/bun/api/jobs/jobs.ts +++ b/src/bun/api/jobs/jobs.ts @@ -3,24 +3,21 @@ import z, { _ZodType } from "zod"; import { taskQueue } from "../app"; import { LoginJob } from "./login-job"; import TwitchLoginJob from "./twitch-login-job"; -import EnsureStore from "./ensure-store"; +import UpdateStoreJob from "./update-store"; import { EmulatorDownloadJob } from "./emulator-download-job"; import { getErrorMessage } from "@/bun/utils"; -import { BaseEvent, IJob } from "@simeonradivoev/gameflow-sdk/task-queue"; +import { IJob } from "../task-queue"; import { LaunchGameJob } from "./launch-game-job"; import { BiosDownloadJob } from "./bios-download-job"; import { InstallJob } from "./install-job"; -import ReloadPluginsJob from "./reload-plugins-job"; -import { FrontEndJob } from "@simeonradivoev/gameflow-sdk/shared"; function registerJob< const Path extends string, - Schema, + const Schema extends z.ZodTypeAny, + const Query extends z.ZodTypeAny, const States extends string, -> (_job: { - id: Path; - query?: (q: any) => string; -} & (new (...args: any[]) => IJob)) + T extends IJob, States> +> (_job: { id: Path; dataSchema: Schema; query?: (q: any) => string; } & (new (...args: any[]) => T)) { return new Elysia().ws(_job.id, { body: z.discriminatedUnion('type', [ @@ -32,10 +29,9 @@ function registerJob< type: z.literal(['data', 'started', 'progress']), state: z.string().optional(), progress: z.number(), - data: z.custom() + data: _job.dataSchema }), - z.object({ type: z.literal(['completed', 'ended']), data: z.custom() }), - z.object({ type: z.literal('waiting') }), + z.object({ type: z.literal(['completed', 'ended']), data: _job.dataSchema }), z.object({ type: z.literal('error'), error: z.string() }) ]), open (ws) @@ -44,10 +40,7 @@ function registerJob< const job = taskQueue.findJob(jobId, _job); if (job) { - ws.send({ type: 'data', state: job.state, progress: job.progress, data: job.job.exposeData?.() as Schema }); - } else - { - ws.send({ type: 'waiting' }); + ws.send({ type: 'data', state: job.state, progress: job.progress, data: job.job.exposeData?.() }); } (ws.data as any).cleanup = [ @@ -104,88 +97,10 @@ function registerJob< } export const jobs = new Elysia({ prefix: '/api/jobs' }) - .ws('/list', { - response: z.discriminatedUnion('type', [ - z.object({ type: z.literal("allJobs"), active: z.custom().array(), queued: z.custom().array() }), - z.object({ type: z.literal("started"), job: z.custom() }), - z.object({ type: z.literal("progress"), job: z.custom() }), - z.object({ type: z.literal("queued"), job: z.custom() }), - z.object({ type: z.literal("aborted"), id: z.string() }), - z.object({ type: z.literal("ended"), id: z.string() }), - ]), - body: z.discriminatedUnion('type', [ - z.object({ type: z.literal("cancel"), id: z.string() }) - ]), - message (ws, message) - { - switch (message.type) - { - case "cancel": - taskQueue.cancelJob(message.id); - break; - } - }, - open (ws) - { - ws.send({ - type: 'allJobs', - active: taskQueue.getActiveJobs().map(j => - { - const job: FrontEndJob = { - id: j.id, - data: j.job.exposeData?.(), - progress: j.progress, - state: j.state, - status: j.status - }; - - return job; - }), - queued: taskQueue.getQueuedJobs()?.map(j => - { - const job: FrontEndJob = { - id: j.id, - data: j.job.exposeData?.(), - progress: j.progress, - state: j.state, - status: j.status - }; - - return job; - }) ?? [] - }); - - (ws.data as any).dispose = [taskQueue.on('started', (e: BaseEvent) => - { - ws.send({ type: "started", job: { id: e.id, data: e.job.job.exposeData?.(), progress: e.job.progress, state: e.job.state, status: e.job.status } }); - }), - taskQueue.on('progress', (e: BaseEvent) => - { - ws.send({ type: "progress", job: { id: e.id, data: e.job.job.exposeData?.(), progress: e.job.progress, state: e.job.state, status: e.job.status } }); - }), - taskQueue.on('queued', (e: BaseEvent) => - { - ws.send({ type: "queued", job: { id: e.id, data: e.job.job.exposeData?.(), progress: e.job.progress, state: e.job.state, status: e.job.status } }); - }), - taskQueue.on('abort', (e: BaseEvent) => - { - ws.send({ type: "aborted", id: e.id }); - }), - taskQueue.on('ended', (e: BaseEvent) => - { - ws.send({ type: "ended", id: e.id }); - })]; - }, - close (ws, code, reason) - { - (ws.data as any).dispose.forEach((d: any) => d()); - }, - }) - .use(registerJob(LaunchGameJob)) .use(registerJob(LoginJob)) .use(registerJob(TwitchLoginJob)) - .use(registerJob(EnsureStore)) + .use(registerJob(UpdateStoreJob)) + .use(registerJob(LaunchGameJob)) .use(registerJob(BiosDownloadJob)) .use(registerJob(InstallJob)) - .use(registerJob(ReloadPluginsJob)) .use(registerJob(EmulatorDownloadJob)); diff --git a/src/bun/api/jobs/launch-game-job.ts b/src/bun/api/jobs/launch-game-job.ts index 3ce0e83..91004bb 100644 --- a/src/bun/api/jobs/launch-game-job.ts +++ b/src/bun/api/jobs/launch-game-job.ts @@ -1,272 +1,143 @@ import z from "zod"; -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; -import { ActiveGameSchema, ActiveGameType } from "@simeonradivoev/gameflow-sdk"; -import { config, db, events, plugins } from "../app"; +import { IJob, JobContext } from "../task-queue"; +import { ActiveGameSchema, ActiveGameType } from "@/bun/types/typesc.schema"; +import { db, events, plugins } from "../app"; import * as appSchema from "@schema/app"; -import { eq } from "drizzle-orm"; -import { spawn } from 'node:child_process'; -import { updateLocalLastPlayed } from "../games/services/statusService"; -import { getErrorMessage } from "@/bun/utils"; -import { CommandEntry, FrontEndId, SaveSlots } from "@simeonradivoev/gameflow-sdk/shared"; +import { eq, sql } from "drizzle-orm"; +import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; -export class LaunchGameJob implements IJob, string> +export class LaunchGameJob implements IJob, "playing"> { static id = "launch-game" as const; - static dataSchema = z.nullable(ActiveGameSchema); + static dataSchema = z.optional(ActiveGameSchema); group = "launch-game"; - activeGame: ActiveGameType | null; - gameId: FrontEndId; + activeGame?: ActiveGameType; + gameId: number; validCommand: CommandEntry; - gameSource?: string; - gameSourceId?: string; - changedSaveFiles: Map; - saveSlots: SaveSlots = {}; + gameSource: string; + gameSourceId: string; - constructor(gameId: FrontEndId, validCommand: CommandEntry, source?: string, sourceId?: string) + constructor(gameId: number, validCommand: CommandEntry, source: string, sourceId: string) { this.gameId = gameId; this.validCommand = validCommand; this.gameSource = source; this.gameSourceId = sourceId; - this.activeGame = null; - this.changedSaveFiles = new Map(); } - async postPlay (gameInfo: { platformSlug?: string; }) + async start (context: JobContext, "playing">, z.infer, "playing">) { - if (this.gameId.source === 'local') - { - await updateLocalLastPlayed(Number(this.gameId.id)); - } - - const source = this.gameSource ?? this.gameId.source; - const id = this.gameSourceId ?? this.gameId.id; - - await new Promise(async (resolve) => - { - await plugins.hooks.games.postPlay.promise( - { - source, - id, - command: this.validCommand, - changedSaveFiles: Array.from(this.changedSaveFiles.values()), - validChangedSaveFiles: {}, - saveFolderSlots: this.saveSlots, - gameInfo - }).catch(e => - { - console.error(e); - events.emit('notification', { message: getErrorMessage(e), type: 'error' }); - }).then(() => resolve(false)); - const timeoutHandler = () => resolve(false); - setTimeout(timeoutHandler, 5000); - }); - } - - prePlay (setProgress: (progress: number, state: string) => void, gameInfo: { platformSlug?: string; }) - { - return plugins.hooks.games.prePlay.promise({ - source: this.gameSource ?? this.gameId.source, - id: this.gameSourceId ?? this.gameId.id, - saveFolderSlots: this.saveSlots, - command: this.validCommand, - setProgress: setProgress, - gameInfo - }); - } - - async start (context: JobContext, string>, z.infer, string>) - { - let gameInfo: { name?: string, source_id?: string, source?: string; platformSlug?: string; } | undefined = undefined; - if (this.gameId.source === 'emulator') - { - gameInfo = { name: this.gameId.id }; - } else - { - const localGame = await db.query.games.findFirst({ - where: eq(appSchema.games.id, Number(this.gameId.id)), columns: { - name: true, - source_id: true, - source: true, - }, - with: { - platform: { - columns: { - es_slug: true, - slug: true - } - } - } - }); - - if (localGame) - gameInfo = { - name: localGame.name ?? undefined, - source_id: localGame.source_id ?? undefined, - source: localGame.source ?? undefined, - platformSlug: localGame.platform.es_slug ?? localGame.platform.slug - }; - } - - const commandArgs = await plugins.hooks.games.emulatorLaunch.promise({ - autoValidCommand: this.validCommand, - game: { - source: this.gameSource, - sourceId: this.gameSourceId, - id: this.gameId, - platformSlug: gameInfo?.platformSlug - }, - dryRun: false - }); - - await new Promise(async (resolve, reject) => - { - try - { - let game: any; - if (!commandArgs) - { - await this.prePlay(context.setProgress.bind(context), { platformSlug: gameInfo?.platformSlug }).catch(e => reject(e)); - - if (Array.isArray(this.validCommand.command)) - { - let command = this.validCommand.command; - if (process.env.FLATPAK_BUILD) command = ['flatpak-spawn', '--host', `--directory=${config.get('downloadPath')}`, ...command]; - - const bunGame = Bun.spawn(command, { - cwd: this.validCommand.startDir, - signal: context.abortSignal, - env: { - ...process.env, - ...this.validCommand.env - }, - onExit (subprocess, exitCode, signalCode, error) - { - if (error) - { - console.error(error); - reject(error); - } else - { - resolve(true); - } - }, - }); - - context.setProgress(0, "playing"); - - game = bunGame; - } else - { - - let command = this.validCommand.command; - - if (process.env.FLATPAK_BUILD) command = `flatpak-spawn --host --directory=${config.get('downloadPath')} ${command}`; - - // ES-DE commands require shell execution. Some emulators fail otherwise. - const spawnGame = spawn(command, { - shell: this.validCommand.shell ?? true, - cwd: this.validCommand.startDir, - signal: context.abortSignal, - env: { - ...process.env, - ...this.validCommand.env - }, - }); - - context.setProgress(0, "playing"); - - spawnGame.stdout.on('data', data => console.log(data)); - spawnGame.on('close', (code) => - { - resolve(code); - }); - spawnGame.on('error', e => - { - console.error(e); - resolve(1); - }); - - game = spawnGame; - } - } - else if (this.validCommand.metadata.emulatorBin) - { - this.saveSlots = commandArgs.savesPath ?? {}; - - await this.prePlay(context.setProgress.bind(context), { platformSlug: gameInfo?.platformSlug }); - - let command = [this.validCommand.metadata.emulatorBin, ...commandArgs.args]; - if (process.env.FLATPAK_BUILD) command = ['flatpak-spawn', '--host', `--directory=${config.get('downloadPath')}`, ...command]; - - // We have full control over launching integrated emulators better to use bun spawn - const bunGame = Bun.spawn(command, { - cwd: this.validCommand.startDir, - signal: context.abortSignal, - env: { - ...process.env, - ...commandArgs.env - }, - onExit (subprocess, exitCode, signalCode, error) - { - if (error) - { - console.error(error); - reject(error); - } else - { - resolve(true); - } - }, - }); - - context.setProgress(0, "playing"); - - // TODO: this isn't really useful, maybe add it later if needed - /*if (commandArgs.savesPath && await fs.exists(commandArgs.savesPath)) - { - const savesWatcher = watch(commandArgs.savesPath, { recursive: true, signal: context.abortSignal }); - console.log("Starting To Watch", commandArgs.savesPath, "for save file changes"); - savesWatcher.on('change', (type, filename) => - { - if (typeof filename === 'string') - { - console.log("Save File Changed", filename); - this.changedSaveFiles.set(filename, { subPath: filename, cwd: commandArgs.savesPath! }); - } - }); - - bunGame.exited.then(() => - { - savesWatcher.close(); - console.log("Closing Save File Watching for", commandArgs.savesPath); - }); - }*/ - - game = bunGame; - - } else - { - reject(new Error("No Emulator Bin")); - return; - } - - this.activeGame = { - process: game, - name: gameInfo?.name ?? "Unknown", - gameId: this.gameId, - source: this.gameSource, - sourceId: this.gameSourceId, - command: this.validCommand - }; - } catch (e) - { - context.abort(e); - resolve(e); + const localGame = await db.query.games.findFirst({ + where: eq(appSchema.games.id, this.gameId), columns: { + name: true, + source_id: true, + source: true } }); - await this.postPlay({ platformSlug: gameInfo?.platformSlug }); + const commandArgs = await plugins.hooks.games.emulatorLaunch.promise({ + autoValidCommand: this.validCommand, + game: { source: this.gameSource, id: this.gameId } + }); + + await new Promise((resolve, reject) => + { + let game: any; + if (!commandArgs) + { + // ES-DE commands require shell execution. Some emulators fail otherwise. + const spawnGame = spawn(this.validCommand.command, { + shell: true, + cwd: this.validCommand.startDir, + signal: context.abortSignal + }); + + spawnGame.stdout.on('data', data => console.log(data)); + spawnGame.on('close', (code) => + { + resolve(code); + }); + spawnGame.on('error', e => + { + console.error(e); + reject(e); + }); + + game = spawnGame; + } + else if (this.validCommand.metadata.emulatorBin) + { + // We have full control over launching integrated emulators better to use bun spawn + const bunGame = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs], { + cwd: this.validCommand.startDir, + signal: context.abortSignal + }); + + bunGame.exited.then(resolve).catch(e => + { + console.error(e); + reject(e); + }); + game = bunGame; + } else + { + reject(new Error("No Emulator Bin")); + return; + } + + this.activeGame = { + process: game, + name: localGame?.name ?? "Unknown", + gameId: this.gameId, + command: this.validCommand + }; + + const updatePlayed = async (source: string, id: string) => + { + await db.update(appSchema.games).set({ last_played: new Date() }).where(eq(appSchema.games.id, this.gameId)); + await plugins.hooks.games.updatePlayed.promise({ source, id }).then(v => + { + if (v) events.emit('notification', { message: "Updated Last Played", type: 'success' }); + }); + }; + + if (this.gameSource !== 'local') + { + updatePlayed(this.gameSource, this.gameSourceId); + } + else if (localGame?.source && localGame?.source !== 'local' && localGame.source_id) + { + updatePlayed(localGame.source, localGame.source_id); + } + }); + + /* Old spawn lanching, cases issues, needs to be ran as shell + + const cmd = Array.from(validCommand.command.command.matchAll(/(".*?"|[^\s"]+)/g)).map(m => m[0]); + const game = setActiveGame({ + process: Bun.spawn({ + cmd, + env: { + ...process.env + }, + onExit (subprocess, exitCode, signalCode, error) + { + events.emit('activegameexit', { subprocess, exitCode, signalCode, error }); + }, + stdin: "ignore", + stdout: "inherit", + stderr: "inherit", + }), + 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'); + }*/ } exposeData () diff --git a/src/bun/api/jobs/login-job.ts b/src/bun/api/jobs/login-job.ts index fb5d69a..f0726bd 100644 --- a/src/bun/api/jobs/login-job.ts +++ b/src/bun/api/jobs/login-job.ts @@ -1,5 +1,5 @@ import Elysia, { status } from "elysia"; -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; +import { IJob, JobContext } from "../task-queue"; import { LOGIN_PORT, SERVER_URL } from "@/shared/constants"; import { host, localIp } from "@/bun/utils/host"; import cors from "@elysiajs/cors"; diff --git a/src/bun/api/jobs/plugin-operation-job.ts b/src/bun/api/jobs/plugin-operation-job.ts deleted file mode 100644 index db39819..0000000 --- a/src/bun/api/jobs/plugin-operation-job.ts +++ /dev/null @@ -1,62 +0,0 @@ -import z from "zod"; -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk"; -import { plugins } from "../app"; -import { canUninstall, runBunPackageCommand } from "../plugins/services"; -import { getPlugin, registerPlugin, unregisterPlugin } from "../plugins/register-plugins"; -import { PluginRegistry } from "@/shared/constants"; - -export default class PluginOperationJob implements IJob -{ - static id = "plugin-operation-job" as const; - static dataSchema = z.never(); - group = "plugin-operations"; - operation: "add" | "update" | "remove"; - plugin: string; - - constructor(operation: "add" | "update" | "remove", plugin: string) - { - this.plugin = plugin; - this.operation = operation; - } - - async start (context: JobContext, never, string>) - { - switch (this.operation) - { - case "add": - //TODO: find the latest compatible version with the current sdk version - const addResponse = await runBunPackageCommand(["add", this.plugin, '--omit', 'peer', "--registry", PluginRegistry]); - console.log(addResponse); - const addPlugin = await getPlugin(this.plugin, plugins); - if (!addPlugin) throw new Error(`${this.plugin} Not Found`); - await registerPlugin(addPlugin, 'store', plugins); - break; - case "update": - const existingPlugin = plugins.plugins[this.plugin]; - if (!existingPlugin) throw new Error(`${this.plugin} Not Found`); - if (!existingPlugin.update?.new) throw new Error(`No Update Found`); - let updatePlugin = await getPlugin(this.plugin, plugins); - if (!updatePlugin) throw new Error(`${this.plugin} Not Found`); - await unregisterPlugin(this.plugin, plugins); - const updateResponse = await runBunPackageCommand(["update", `${this.plugin}@${existingPlugin.update?.new}`, '--omit', 'peer', "--registry", PluginRegistry, '--latest']); - console.log(updateResponse); - updatePlugin = await getPlugin(this.plugin, plugins); - if (!updatePlugin) throw new Error(`Something Went Wrong during update. Missing Plugin: ${this.plugin}`); - await registerPlugin(updatePlugin, existingPlugin.source, plugins); - break; - case "remove": - const removePlugin = plugins.plugins[this.plugin]; - if (!removePlugin) throw new Error(`${this.plugin} Not Found`); - if (!canUninstall(removePlugin.description, removePlugin.source)) - { - throw new Error("Uninstall Not Allowed"); - } - const response = await runBunPackageCommand(['remove', this.plugin, "--registry", PluginRegistry, '--omit', 'peer']); - console.log(response); - await unregisterPlugin(this.plugin, plugins); - break; - } - - - } -} \ No newline at end of file diff --git a/src/bun/api/jobs/reload-plugins-job.ts b/src/bun/api/jobs/reload-plugins-job.ts deleted file mode 100644 index 5e404d3..0000000 --- a/src/bun/api/jobs/reload-plugins-job.ts +++ /dev/null @@ -1,15 +0,0 @@ -import z from "zod"; -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk"; -import { plugins } from "../app"; - -export default class ReloadPluginsJob implements IJob -{ - static id = "reload-plugins-job" as const; - static dataSchema = z.never(); - group = "reload-plugins"; - - async start (context: JobContext, never, string>) - { - await plugins.reloadAll(context); - } -} \ No newline at end of file diff --git a/src/bun/api/jobs/self-update-job.ts b/src/bun/api/jobs/self-update-job.ts deleted file mode 100644 index ca2684e..0000000 --- a/src/bun/api/jobs/self-update-job.ts +++ /dev/null @@ -1,121 +0,0 @@ -import z from "zod"; -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk"; -import { events } from "../app"; -import { Downloader } from "@/bun/utils/downloader"; -import path from 'node:path'; -import os from "node:os"; -import winUpdateScript from '@/bun/utils/update-gameflow-windows.bat' with { type: "text" }; -import linuxUpdateScript from '@/bun/utils/update-gameflow-linux.sh' with { type: "text" }; -import mustache from "mustache"; -import pkg from '~/package.json'; -import { sleep } from "bun"; - -export default class SelfUpdateJob implements IJob -{ - static id = "self-update-job" as const; - static dataSchema = z.never(); - group = "self-update"; - - async downloadUpdate (url: URL, dest: string | undefined, filename: string, ctx: JobContext, never, string>) - { - const downloader = new Downloader('update', - [{ - url: url, - file_path: "", - file_name: filename - }], - dest, - { - onProgress (stats) - { - ctx.setProgress(stats.progress, "Downloading Update"); - }, - }); - return downloader.start(); - } - - async start (context: JobContext, never, string>) - { - context.setProgress(0, "Downloading Update"); - await sleep(1000); - const latest = await fetch('https://api.github.com/repos/simeonradivoev/gameflow-deck/releases/latest'); - if (latest.ok) - { - const data = await latest.json(); - let validAsset: any | undefined; - switch (process.platform) - { - case "win32": - validAsset = data.assets.find((e: any) => new Bun.Glob(`Gameflow-${process.platform}-${process.arch}.zip`).match(e.name)); - if (!validAsset) - { - validAsset = data.assets.find((e: any) => new Bun.Glob(`Gameflow-*.zip`).match(e.name)); - } - break; - case "linux": - validAsset = data.assets.find((e: any) => new Bun.Glob(`Gameflow-${process.platform}-${process.arch}.AppImage`).match(e.name)); - if (!validAsset) - { - validAsset = data.assets.find((e: any) => new Bun.Glob(`*.AppImage`).match(e.name)); - } - break; - default: - events.emit('notification', { message: "Unsupported Platfrom", title: 'Failed Update', type: "error" }); - return; - } - - if (!validAsset) - { - events.emit('notification', { message: "Could not find download", title: 'Failed Update', type: "error" }); - return; - } - - console.log("Found Download", validAsset.browser_download_url); - console.log("Starting Download"); - - switch (process.platform) - { - case "linux": - const appimage = process.env.APPIMAGE; - if (!appimage) - { - events.emit('notification', { - message: "Only AppImage supported", - title: 'Failed Update', - type: 'error' - }); - return; - } - const linuxDownloads = await this.downloadUpdate(new URL(validAsset.browser_download_url), undefined, path.basename(appimage), context); - if (!linuxDownloads) return; - const shPath = path.join(os.tmpdir(), "update-gameflow.sh"); - await Bun.write(shPath, mustache.render(linuxUpdateScript, { - tempFile: linuxDownloads[0], - appImagePath: appimage - })); - context.setProgress(0, "Restarting App To Update"); - events.emit('exitapp'); - Bun.spawn(["bash", shPath], { detached: true }); - process.exit(0); - case "win32": - const winDownloads = await this.downloadUpdate(new URL(validAsset.browser_download_url), undefined, "Gameflow-update.zip", context); - if (!winDownloads) return; - const batPath = path.join(os.tmpdir(), "update-gameflow.bat"); - await Bun.write(batPath, mustache.render(winUpdateScript, { - tempFile: winDownloads[0], - installDir: path.dirname(process.execPath), - extractDir: path.join(os.tmpdir(), 'gameflow-update-extract'), - exePath: `${pkg.bin}.exe` - })); - context.setProgress(0, "Restarting App To Update"); - events.emit('exitapp'); - Bun.spawn(["cmd", "/c", "start", "cmd", "/c", batPath], { detached: true }); - process.exit(0); - } - - } else - { - events.emit('notification', { message: latest.statusText, title: 'Failed Update', type: "error" }); - } - } -} \ No newline at end of file diff --git a/src/bun/api/jobs/test-download-job.ts b/src/bun/api/jobs/test-download-job.ts deleted file mode 100644 index 313b00b..0000000 --- a/src/bun/api/jobs/test-download-job.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DownloadJobData } from "@simeonradivoev/gameflow-sdk/shared"; -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue"; -import { sleep } from "bun"; - -export class TestDownloadJob implements IJob -{ - data: DownloadJobData = { - speed: 1686, - downloaded: 0, - total: 6615841, - name: "Test Download Job" - }; - - group = "test-download"; - - async start (context: JobContext, DownloadJobData, string>): Promise - { - for (let i = 0; i < 10; i++) - { - await sleep(1000); - context.setProgress(i / 10 * 100, 'download'); - if (context.abortSignal.aborted) return; - } - } - exposeData (): DownloadJobData - { - return this.data; - } - -} \ No newline at end of file diff --git a/src/bun/api/jobs/twitch-login-job.ts b/src/bun/api/jobs/twitch-login-job.ts index 42d98a9..1023e83 100644 --- a/src/bun/api/jobs/twitch-login-job.ts +++ b/src/bun/api/jobs/twitch-login-job.ts @@ -1,4 +1,4 @@ -import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk"; +import { IJob, JobContext } from "../task-queue"; import secrets from "../secrets"; import open from "open"; import z from "zod"; diff --git a/src/bun/api/jobs/update-store.ts b/src/bun/api/jobs/update-store.ts new file mode 100644 index 0000000..cd99584 --- /dev/null +++ b/src/bun/api/jobs/update-store.ts @@ -0,0 +1,50 @@ +import { ensureDir } from "fs-extra"; +import { IJob, JobContext } from "../task-queue"; +import { getStoreRootFolder } from "../store/services/gamesService"; +import { STORE_VERSION } from "@/shared/constants"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import z from "zod"; + +export default class UpdateStoreJob implements IJob +{ + static id = "update-store" as const; + static dataSchema = z.never(); + packageName: string; + registry: URL; + storeVersion: string; + + constructor() + { + this.packageName = process.env.STORE_PACKAGE_NAME ?? "@simeonradivoev/gameflow-store"; + this.registry = new URL(process.env.STORE_REGISTRY ?? "https://registry.npmjs.org"); + this.storeVersion = process.env.STORE_VERSION ?? STORE_VERSION; + } + + async start (context: JobContext) + { + if (process.env.CUSTOM_STORE_PATH) return; + + const tempCache = path.join(tmpdir(), "gameflow-bun-cache"); + const storeFolder = getStoreRootFolder(); + await ensureDir(storeFolder); + + console.log("Updating Store"); + const proc = Bun.spawn([process.execPath, "add", `${this.packageName}@${this.storeVersion}`, "--production", "--registry", this.registry.href], { + cwd: storeFolder, + stdout: 'pipe', + stderr: 'pipe', + env: { + BUN_BE_BUN: "1", + BUN_INSTALL_CACHE_DIR: tempCache + } + }); + + const stdout = await new Response(proc.stdout).text(); + console.log(stdout); + const stderr = await new Response(proc.stderr).text(); + if (stderr) + console.error(stderr); + await proc.exited; + } +} \ No newline at end of file diff --git a/src/bun/api/notifications.ts b/src/bun/api/notifications.ts index e1c135c..1a49080 100644 --- a/src/bun/api/notifications.ts +++ b/src/bun/api/notifications.ts @@ -1,5 +1,4 @@ -import { FrontendNotification } from '@simeonradivoev/gameflow-sdk/shared'; import { events } from './app'; export default function buildNotificationsStream () diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/cemu.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/cemu.ts deleted file mode 100644 index a9e6865..0000000 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/cemu.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; -import desc from './package.json'; -import path from 'node:path'; -import { config } from "@/bun/api/app"; - -export default class CEMUIntegration implements PluginType -{ - emulator = 'CEMU'; - - async load (ctx: PluginLoadingContextType) - { - ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) => - { - return { id: desc.name, supportLevel: "full", capabilities: ["batch", "fullscreen"] }; - }); - - ctx.hooks.games.postPlay.tapPromise({ name: desc.name }, async ({ saveFolderSlots, validChangedSaveFiles, command }) => - { - if (command.emulator !== this.emulator || !(saveFolderSlots?.[this.emulator]) || !command.metadata.romPath) return; - validChangedSaveFiles[this.emulator] = { - cwd: saveFolderSlots[this.emulator].cwd, - shared: true, - subPath: '*.{tga,xml,dat}', - isGlob: true - }; - }); - - ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => - { - const args: string[] = []; - - args.push(`--fullscreen=${config.get('launchInFullscreen') ? "True" : "False"}`); - - const savesPath = path.join(config.get('downloadPath'), "saves", this.emulator); - - args.push(`--mlc=${savesPath}`); - - if (ctx.autoValidCommand.metadata.romPath) - { - args.push(`--game=${ctx.autoValidCommand.metadata.romPath}`); - } - - return { args, savesPath: { [this.emulator]: { cwd: savesPath } } }; - }); - } -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json deleted file mode 100644 index 9a0c5c6..0000000 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "com.simeonradivoev.gameflow.cemu", - "displayName": "CEMU Integration", - "version": "0.0.1", - "description": "CEMU Emulator Integration", - "main": "./cemu.ts", - "icon": "https://upload.wikimedia.org/wikipedia/commons/9/9e/Cemu_Emulator_Official_Logo.png", - "category": "emulators", - "keywords": [ - "integration", - "emulator", - "wiiu", - "cemu" - ] -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts deleted file mode 100644 index 6a44901..0000000 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin.ts +++ /dev/null @@ -1,87 +0,0 @@ - -import { config } from "@/bun/api/app"; -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; -import path from 'node:path'; -import desc from './package.json'; -import { ensureDir } from "fs-extra"; -import { getSavePaths, getType } from "./utils"; - -export default class DOLPHINIntegration implements PluginType -{ - emulator = 'DOLPHIN'; - - async load (ctx: PluginLoadingContextType) - { - ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) => - { - return { id: desc.name, supportLevel: "full", capabilities: ["batch", "config", "resolution", "fullscreen", "saves"] }; - }); - - ctx.hooks.emulators.emulatorPostInstall.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => - { - await Bun.write(path.join(ctx.path, "portable.txt"), ""); - }); - - ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => - { - const args: string[] = []; - - const storageFolder = path.join(config.get('downloadPath'), "storage", this.emulator); - args.push(`--user=${storageFolder}`); - - args.push(`--config=Dolphin.Display.Fullscreen=${config.get('launchInFullscreen') ? "True" : "False"}`); - args.push(`--config=Dolphin.General.ISOPath0=${path.join(config.get('downloadPath'), 'roms', 'gc')}`); - args.push(`--config=Dolphin.General.ISOPath1=${path.join(config.get('downloadPath'), 'roms', 'wii')}`); - args.push(`--config=Dolphin.Interface.ConfirmStop=False`); - args.push(`--config=Dolphin.Interface.SkipNKitWarning=True`); - args.push(`--config=Dolphin.Analytics.PermissionAsked=True`); - - const resolution = config.get('emulatorResolution'); - const resolutionMapping = { - "720p": 2, - "1080p": 3, - "1440p": 4, - "4k": 6 - }; - args.push(`--config=GFX.Settings.InternalResolution=${resolutionMapping[resolution] ?? 1}`); - args.push(`--config=GFX.Settings.wideScreenHack=${config.get('emulatorWidescreen') ? "True" : "False"}`); - args.push(`--config=GFX.Settings.AspectRatio=${config.get('emulatorWidescreen') ? "1" : "0"}`); - - const savesPath = path.join(config.get('downloadPath'), "saves", this.emulator); - - args.push(`--config=Dolphin.General.WiiSDCardPath=${path.join(savesPath, 'WiiSD.raw')}`); - args.push(`--config=Dolphin.General.WiiSDCardSyncFolder=${path.join(savesPath, 'WiiSDSync')}`); - args.push(`--config=Dolphin.GBA.SavesPath=${path.join(savesPath, 'GBA')}`); - args.push(`--config=Dolphin.Core.GCIFolderAPath=${path.join(savesPath, 'GC')}`); - - if (!ctx.dryRun) - { - await ensureDir(path.join(savesPath, 'GC', "JAP")); - await ensureDir(path.join(savesPath, 'GC', "EUR")); - await ensureDir(path.join(savesPath, 'GC', "USA")); - } - - let finalSavesPath: string | undefined = undefined; - if (ctx.autoValidCommand.metadata.romPath) - { - args.push("--batch"); - args.push(`--exec=${ctx.autoValidCommand.metadata.romPath}`); - - finalSavesPath = await getType(ctx.autoValidCommand.metadata.romPath, ctx.autoValidCommand.metadata.emulatorDir) === 'gamecube' ? savesPath : storageFolder; - return { args, savesPath: { [this.emulator]: { cwd: finalSavesPath } } }; - } - - return { args }; - }); - - ctx.hooks.games.postPlay.tap({ name: desc.name }, async ({ validChangedSaveFiles, saveFolderSlots, command }) => - { - if (command.emulator !== this.emulator || !(saveFolderSlots?.[this.emulator]) || !command.metadata.romPath) return; - validChangedSaveFiles[this.emulator] = { - cwd: saveFolderSlots[this.emulator].cwd, - subPath: await getSavePaths(command.metadata.romPath, saveFolderSlots.dolphin.cwd, command.metadata.emulatorDir), - shared: false - }; - }); - } -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json deleted file mode 100644 index e413d06..0000000 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "com.simeonradivoev.gameflow.dolphin", - "displayName": "DOLPHIN Integration", - "version": "0.0.1", - "description": "DOLPHIN Emulator Integration", - "main": "./dolphin.ts", - "icon": "https://upload.wikimedia.org/wikipedia/commons/5/53/Dolphin_Emulator_Logo_Refresh.svg", - "category": "emulators", - "keywords": [ - "integration", - "emulator", - "wii", - "gc", - "dolphin" - ] -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/utils.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/utils.ts deleted file mode 100644 index 8794e80..0000000 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.dolphin/utils.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { join } from "path"; -import { platform } from "os"; -import fs from "node:fs/promises"; -import path from "node:path"; - -type DolphinLocation = - | { type: "path"; toolPath: string; } - | { type: "appimage"; appImagePath: string; }; - -async function findDolphinTool (bundledDir?: string): Promise -{ - const os = platform(); - const toolName = os === "win32" ? "DolphinTool.exe" : "dolphin-tool"; - - if (bundledDir) - { - if (os === "linux") - { - const glob = new Bun.Glob("*.AppImage"); - for await (const file of glob.scan(bundledDir)) - { - return { type: "appimage", appImagePath: join(bundledDir, file) }; - } - throw new Error(`No AppImage found in ${bundledDir}`); - } else - { - return { type: "path", toolPath: join(bundledDir, toolName) }; - } - } - - // Fallback 1: check PATH - const inPath = Bun.which(toolName); - if (inPath) return { type: "path", toolPath: inPath }; - - // Fallback 2: platform default install locations - if (os === "win32") - { - const candidates = [ - "C:/Program Files/Dolphin/DolphinTool.exe", - "C:/Program Files (x86)/Dolphin/DolphinTool.exe", - ]; - for (const candidate of candidates) - { - if (await Bun.file(candidate).exists()) - { - return { type: "path", toolPath: candidate }; - } - } - } else if (os === "darwin") - { - const candidate = "/Applications/Dolphin.app/Contents/MacOS/dolphin-tool"; - if (await Bun.file(candidate).exists()) - { - return { type: "path", toolPath: candidate }; - } - } else if (os === "linux") - { - const home = process.env.HOME ?? ""; - const candidates = [ - join(home, "Applications/Dolphin-x86_64.AppImage"), - join(home, "Applications/Dolphin.AppImage"), - "/opt/Dolphin-x86_64.AppImage", - ]; - for (const candidate of candidates) - { - if (await Bun.file(candidate).exists()) - { - return { type: "appimage", appImagePath: candidate }; - } - } - } - - throw new Error(`Could not find ${toolName}. Install Dolphin or pass its folder path explicitly.`); -} - -async function runDolphinTool (args: string[], location: DolphinLocation): Promise -{ - if (location.type === "path") - { - const proc = Bun.spawnSync([location.toolPath, ...args]); - if (!proc.success) throw new Error(`dolphin-tool failed: ${proc.stderr.toString()}`); - return proc.stdout.toString(); - } else - { - const mount = Bun.spawn([location.appImagePath, "--appimage-mount"], { - stdout: "pipe", - stderr: "pipe", - }); - const mountPoint = (await new Response(mount.stdout).text()).trim(); - try - { - const proc = Bun.spawnSync([`${mountPoint}/usr/bin/dolphin-tool`, ...args]); - if (!proc.success) throw new Error(`dolphin-tool failed: ${proc.stderr.toString()}`); - return proc.stdout.toString(); - } finally - { - mount.kill(); - } - } -} - -async function readGameId (romPath: string, location: DolphinLocation): Promise -{ - const output = await runDolphinTool(["header", "-i", romPath], location); - const match = output.match(/Game ID:\s*(\w{6})/); - if (!match) throw new Error("Could not read game ID"); - return match[1]; -} - -function getRegion (regionCode: string) -{ - switch (regionCode) - { - case "E": return "USA"; - case "P": return "EUR"; - case "J": return "JAP"; - default: return "USA"; - } -} - -async function getGCSavePaths (romPath: string, savesPath: string, location: DolphinLocation) -{ - const gameId = await readGameId(romPath, location); - const region = getRegion(gameId[3]); - - const makerCode = gameId.slice(4, 6); // e.g. "01" or "7D" — already the right format - const gameCode = gameId.slice(0, 4); // e.g. "GZLE" or "GM5E" - const cardPath = join(savesPath, "GC", region); - - const glob = new Bun.Glob(`${makerCode}-${gameCode}-*.gci`); - const saves: string[] = []; - for await (const file of glob.scan(cardPath)) - { - saves.push(path.join("GC", region, file)); - } - - return saves; -} - -export async function getType (romPath: string, bundledEmulatorDir?: string): Promise<"gamecube" | "wii"> -{ - const location = await findDolphinTool(bundledEmulatorDir); - const gameId = await readGameId(romPath, location); - const isGameCube = gameId[0] === "G" || gameId[0] === "D"; - return isGameCube ? "gamecube" : "wii"; -} - -export async function getSavePaths (romPath: string, savesPath: string, bundledEmulatorDir?: string): Promise -{ - const location = await findDolphinTool(bundledEmulatorDir); - const gameId = await readGameId(romPath, location); - const isGameCube = gameId[0] === "G" || gameId[0] === "D"; - - if (isGameCube) - { - return getGCSavePaths(romPath, savesPath, location); - } else - { - const folder = Buffer.from(gameId.slice(0, 4), "ascii").toString("hex").toUpperCase(); - const rootFolder = join(savesPath, "Wii", "title", "00010000", folder); - const files = await fs.readdir(rootFolder, { recursive: true }); - return files.map(f => path.join("Wii", "title", "00010000", f)); - } -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/PCSX2.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/PCSX2.ini index e1403c5..cbafaf8 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/PCSX2.ini +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/PCSX2.ini @@ -21,6 +21,7 @@ CdvdShareWrite = false EnablePatches = true EnableCheats = false EnablePINE = false +EnableWideScreenPatches = false EnableNoInterlacingPatches = false EnableRecordingTools = true EnableGameFixes = true @@ -91,7 +92,7 @@ VsyncEnable = 0 FramerateNTSC = 59.94 FrameratePAL = 50 SyncToHostRefreshRate = false -AspectRatio = {{ASPECT_RATIO}} +AspectRatio = Auto 4:3/3:2 FMVAspectRatioSwitch = Off ScreenshotSize = 0 ScreenshotFormat = 0 @@ -167,6 +168,7 @@ linear_present_mode = 1 deinterlace_mode = 0 OsdScale = 100 Renderer = 14 +upscale_multiplier = 1 mipmap_hw = -1 accurate_blending_unit = 1 crc_hack_level = -1 @@ -369,6 +371,18 @@ Multitap2_Slot4_Enable = false Multitap2_Slot4_Filename = Mcd-Multitap2-Slot04.ps2 +[Folders] +Bios = {{{BIOS_PATH}}} +Snapshots = {{{SNAPSHOTS_PATH}}} +SaveStates = {{{SAVE_STATES_PATH}}} +MemoryCards = {{{MEMORY_CARDS_PATH}}} +Cache = {{{CACHE_PATH}}} +Covers = {{{COVERS_PATH}}} +Logs = logs +Textures = {{{TEXTURES_PATH}}} +Videos = videos + + [InputSources] Keyboard = true Mouse = true @@ -474,3 +488,6 @@ RDown = SDL-1/+RightY RLeft = SDL-1/-RightX LargeMotor = SDL-1/LargeMotor SmallMotor = SDL-1/SmallMotor + +[GameList] +RecursivePaths = {{{RECURSIVE_PATHS}}} diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json index 6b8c725..bab4f08 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json @@ -5,7 +5,6 @@ "description": "PCSX2 Emulator Integration", "main": "./pcsx2.ts", "icon": "https://upload.wikimedia.org/wikipedia/commons/2/2b/PCSX2_logo4.png", - "category": "emulators", "keywords": [ "integration", "emulator", diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts index 58d61aa..072752b 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2.ts @@ -1,87 +1,34 @@ -import { config } from "@/bun/api/app"; -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; -import defaultConfig from './PCSX2.ini' with { type: 'file' }; +import { config, db } from "@/bun/api/app"; +import { PluginContextType, PluginType } from "@/bun/types/typesc.schema"; +import configFile from './PCSX2.ini' with { type: 'file' }; +import Mustache from 'mustache'; import path from 'node:path'; import { ensureDir } from "fs-extra"; import desc from './package.json'; -import ini from 'ini'; -import { EmulatorCapabilities } from "@simeonradivoev/gameflow-sdk/shared"; export default class PCSX2Integration implements PluginType { - emulator = "PCSX2"; - - async load (ctx: PluginLoadingContextType) + load (ctx: PluginContextType) { - ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) => + ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) => { - const baseCapabilities: EmulatorCapabilities[] = ["batch", "fullscreen"]; - - if (ctx.source?.type === 'store') + if (ctx.autoValidCommand.emulator === 'PCSX2' && ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir) { - return { - id: desc.name, - supportLevel: "full", - capabilities: [...baseCapabilities, "config", "resolution"] - }; - } - else - { - return { id: desc.name, supportLevel: "partial", capabilities: [...baseCapabilities] }; - } - }); + const args = ["-batch"]; + if (config.get('launchInFullscreen')) + { + args.push("-fullscreen"); + } + args.push(...["-bigpicture", "-portable", "--", ctx.autoValidCommand.metadata.romPath]); - ctx.hooks.games.postPlay.tapPromise({ name: desc.name }, async ({ saveFolderSlots, validChangedSaveFiles, command }) => - { - if (command.emulator !== this.emulator || !(saveFolderSlots?.[this.emulator]) || !command.metadata.romPath) return; - validChangedSaveFiles[this.emulator] = { - cwd: saveFolderSlots[this.emulator].cwd, - shared: true, - subPath: '*.ps2', - isGlob: true, - fixedSize: true - }; - }); + const configFileContents = await Bun.file(configFile).text(); - ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => - { - const args: string[] = []; - if (ctx.autoValidCommand.metadata.romPath) - { - args.push(ctx.autoValidCommand.metadata.romPath); - args.push("-batch"); - } - if (config.get('launchInFullscreen')) - { - args.push("-fullscreen"); - } - args.push(...["-bigpicture", "-portable", "--"]); + const biosFolder = path.join(config.get('downloadPath'), "bios", 'PCSX2'); + const storageFolder = path.join(config.get('downloadPath'), "storage", 'PCSX2'); + const savesFolder = path.join(config.get('downloadPath'), "saves", 'PCSX2'); - if (ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir && !ctx.dryRun) - { - let pscx2Path = ''; - if (process.platform === 'win32') - pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, 'inis'); - else - pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, this.emulator, 'inis'); - - const configPath = path.join(pscx2Path, 'PCSX2.ini'); - const existingConfigFile = Bun.file(configPath); - - const configFile = await existingConfigFile.exists() ? ini.parse(await existingConfigFile.text()) : ini.parse(await Bun.file(defaultConfig).text()); - - const biosFolder = path.join(config.get('downloadPath'), "bios", this.emulator); - const storageFolder = path.join(config.get('downloadPath'), "storage", this.emulator); - const savesFolder = path.join(config.get('downloadPath'), "saves", this.emulator); - const resolutionMapping = { - "720p": 2, - "1080p": 3, - "1440p": 4, - "4k": 6, - }; - - const paths = { + const view = { BIOS_PATH: biosFolder, SNAPSHOTS_PATH: path.join(storageFolder, 'snaps'), SAVE_STATES_PATH: path.join(savesFolder, 'states'), @@ -89,37 +36,21 @@ export default class PCSX2Integration implements PluginType CACHE_PATH: path.join(storageFolder, 'cache'), COVERS_PATH: path.join(storageFolder, 'covers'), TEXTURES_PATH: path.join(storageFolder, 'textures'), - VIDEOS_PATH: path.join(storageFolder, 'videos'), - LOGS_PATH: path.join(storageFolder, 'logs'), RECURSIVE_PATHS: path.join(config.get('downloadPath'), 'roms', 'PS2'), }; - await Promise.all(Object.values(paths).map(p => ensureDir(p))); + await Promise.all(Object.values(view).map(p => ensureDir(p))); - configFile.EmuCore ??= {}; - configFile.EmuCore.EnableWideScreenPatches = config.get('emulatorWidescreen'); - configFile['EmuCore/GS'] ??= {}; - configFile['EmuCore/GS'].AspectRatio = config.get('emulatorWidescreen') ? "16:9" : "Auto 4:3/3:2"; - configFile['EmuCore/GS'].upscale_multiplier = resolutionMapping[config.get('emulatorResolution')] ?? 1; - configFile.Folders ??= {}; - configFile.Folders.Bios = paths.BIOS_PATH; - configFile.Folders.Snapshots = paths.SNAPSHOTS_PATH; - configFile.Folders.SaveStates = paths.SAVE_STATES_PATH; - configFile.Folders.MemoryCards = paths.MEMORY_CARDS_PATH; - configFile.Folders.Cache = paths.CACHE_PATH; - configFile.Folders.Covers = paths.COVERS_PATH; - configFile.Folders.Textures = paths.TEXTURES_PATH; - configFile.Folders.Videos = paths.VIDEOS_PATH; - configFile.Folders.Logs = paths.LOGS_PATH; - configFile.GameList ??= {}; - configFile.GameList.RecursivePaths = paths.RECURSIVE_PATHS; + let pscx2Path = ''; + if (process.platform === 'win32') + pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, 'inis'); + else + pscx2Path = path.join(ctx.autoValidCommand.metadata.emulatorDir, "PCSX2", 'inis'); - await Bun.write(configPath, ini.stringify(configFile)); + await Bun.write(path.join(pscx2Path, 'PCSX2.ini'), Mustache.render(configFileContents, view)); - return { args, savesPath: { [this.emulator]: { cwd: paths.MEMORY_CARDS_PATH } } }; + return args; } - - return { args }; }); } } \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/ppsspp.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/ppsspp.ini index c138918..edd196b 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/ppsspp.ini +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/linux/ppsspp.ini @@ -96,6 +96,7 @@ HardwareTransform = True SoftwareSkinning = True TextureFiltering = 1 BufferFiltering = 1 +InternalResolution = 3 AndroidHwScale = 1 HighQualityDepth = 1 FrameSkip = 0 @@ -108,6 +109,7 @@ AnisotropyLevel = 4 VertexDecCache = False TextureBackoffCache = False TextureSecondaryCache = False +FullScreen = True FullScreenMulti = False SmallDisplayZoomType = 2 SmallDisplayOffsetX = 0.500000 diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json index 3801e34..f8e00f5 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json @@ -5,7 +5,6 @@ "description": "PPSSPP Emulator Integration", "main": "./ppsspp.ts", "icon": "https://www.ppsspp.org/static/img/platform/ppsspp-icon.png", - "category": "emulators", "keywords": [ "integration", "emulator", diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts index f69fdaf..8384213 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp.ts @@ -1,4 +1,4 @@ -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import { PluginContextType, PluginType } from "@/bun/types/typesc.schema"; import desc from './package.json'; import { config } from "@/bun/api/app"; import configFilePathWin32 from './win32/ppsspp.ini' with { type: 'file' }; @@ -9,93 +9,40 @@ import path from "node:path"; import Mustache from "mustache"; import { ensureDir } from "fs-extra"; import { homedir } from "node:os"; -import ini from 'ini'; -import fs from 'node:fs/promises'; -import { EmulatorCapabilities } from "@simeonradivoev/gameflow-sdk/shared"; -export default class PPSSPPIntegration implements PluginType +export default class PCSX2Integration implements PluginType { - emulator = "PPSSPP"; - - async load (ctx: PluginLoadingContextType) + load (ctx: PluginContextType) { - ctx.hooks.emulators.emulatorPostInstall.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => + ctx.hooks.games.emulatorLaunch.tapPromise(desc.name, async (ctx) => { - const stat = await fs.stat(ctx.path); - if (stat.isDirectory()) + if (ctx.autoValidCommand.emulator === 'PPSSPP' && ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir) { - await Bun.write(path.join(ctx.path, "portable.txt"), ""); - if (process.platform === 'win32') + const args = [ctx.autoValidCommand.metadata.romPath, "--escape-exit", "--pause-menu-exit"]; + if (config.get('launchInFullscreen')) { - await Bun.write(path.join(ctx.path, "installed.txt"), path.join(config.get('downloadPath'), 'saves', this.emulator)); + args.push("--fullscreen"); } - } - }); - ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) => - { - const baseCapabilities: EmulatorCapabilities[] = ["batch", "fullscreen"]; - - if (ctx.source?.type === 'store') - { - return { - id: desc.name, - supportLevel: "full", - capabilities: [...baseCapabilities, "config", "resolution"] - }; - } - else - { - return { id: desc.name, supportLevel: "partial", capabilities: [...baseCapabilities] }; - } - }); - - ctx.hooks.games.postPlay.tapPromise({ name: desc.name }, async ({ saveFolderSlots, validChangedSaveFiles, command }) => - { - if (command.emulator !== this.emulator || !(saveFolderSlots?.[this.emulator]) || !command.metadata.romPath) return; - validChangedSaveFiles[this.emulator] = { - cwd: saveFolderSlots[this.emulator].cwd, - shared: true, - subPath: '*.{SFO,sfo,PNG,png}', - isGlob: true - }; - }); - - ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => - { - const args: string[] = []; - if (ctx.autoValidCommand.metadata.romPath) - { - args.push(ctx.autoValidCommand.metadata.romPath); - } - - args.push("--escape-exit", "--pause-menu-exit"); - if (config.get('launchInFullscreen')) - { - args.push("--fullscreen"); - } - - if (ctx.autoValidCommand.emulatorSource === 'store' && ctx.autoValidCommand.metadata.emulatorDir && !ctx.dryRun) - { - let defaultConfigPath: string | undefined = undefined; - let defaultControlsPath: string | undefined = undefined; + let confPath: string | undefined = undefined; + let controlsPath: string | undefined = undefined; switch (process.platform) { case "win32": - defaultConfigPath = configFilePathWin32; - defaultControlsPath = configControlsFilePathWin32; + confPath = configFilePathWin32; + controlsPath = configControlsFilePathWin32; break; case 'linux': - defaultConfigPath = configFilePathLinux; - defaultControlsPath = configControlsFilePathLinux; + confPath = configFilePathLinux; + controlsPath = configControlsFilePathLinux; break; } let ppssppPath = ''; if (process.platform === 'win32') { - ppssppPath = path.join(config.get('downloadPath'), 'saves', this.emulator, 'PSP', 'SYSTEM'); + ppssppPath = path.join(ctx.autoValidCommand.metadata.emulatorDir, 'memstick', 'PSP', 'SYSTEM'); } else { //TODO: Use way to set custom memstick path when they support it @@ -105,43 +52,20 @@ export default class PPSSPPIntegration implements PluginType ensureDir(ppssppPath); - if (defaultConfigPath) + if (confPath) { - const resolutionMapping: Record = { - "720p": 2, - "1080p": 4, - "1440p": 6, - "4k": 8 - }; - const configPath = path.join(ppssppPath, 'ppsspp.ini'); - const configFile = Bun.file(configPath); - - const ppssppConfig = await configFile.exists() ? ini.parse(await configFile.text()) : ini.parse(await Bun.file(defaultConfigPath).text()); - - ppssppConfig.Graphics ??= {}; - ppssppConfig.Graphics.InternalResolution = resolutionMapping[config.get('emulatorResolution')] ?? 0; - ppssppConfig.Graphics.FullScreen = config.get('launchInFullscreen'); - - await Bun.write(configPath, ini.stringify(ppssppConfig)); + const configFileContents = await Bun.file(confPath).text(); + await Bun.write(path.join(ppssppPath, 'ppsspp.ini'), Mustache.render(configFileContents, {})); } - if (defaultControlsPath) + if (controlsPath) { - const controlsFileContents = await Bun.file(defaultControlsPath).text(); + const controlsFileContents = await Bun.file(controlsPath).text(); await Bun.write(path.join(ppssppPath, 'controls.ini'), Mustache.render(controlsFileContents, {})); } - return { - args, - savesPath: { - [this.emulator]: { - cwd: path.join(config.get('downloadPath'), 'saves', this.emulator, "PSP", "SAVEDATA") - } - } - }; + return args; } - - return { args }; }); } } \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/ppsspp.ini b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/ppsspp.ini index f448165..f24ea4b 100644 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/ppsspp.ini +++ b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.ppsspp/win32/ppsspp.ini @@ -96,6 +96,7 @@ HardwareTransform = True SoftwareSkinning = True TextureFiltering = 1 BufferFiltering = 1 +InternalResolution = 3 AndroidHwScale = 1 HighQualityDepth = 1 FrameSkip = 0 @@ -108,6 +109,7 @@ AnisotropyLevel = 4 VertexDecCache = False TextureBackoffCache = False TextureSecondaryCache = False +FullScreen = True FullScreenMulti = False SmallDisplayZoomType = 2 SmallDisplayOffsetX = 0.500000 diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/eeprom.bin b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/eeprom.bin deleted file mode 100644 index 55874b0..0000000 Binary files a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/eeprom.bin and /dev/null differ diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/package.json deleted file mode 100644 index 937ebc3..0000000 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "com.simeonradivoev.gameflow.xemu", - "displayName": "XEMU Integration", - "version": "0.0.1", - "description": "XEMU Emulator Integration", - "main": "./xemu.ts", - "icon": "https://upload.wikimedia.org/wikipedia/commons/8/8e/Xemu_logo_green.svg", - "category": "emulators", - "keywords": [ - "integration", - "emulator", - "xbox", - "xemu" - ] -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/xemu.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/xemu.ts deleted file mode 100644 index 57506de..0000000 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xemu/xemu.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; -import desc from './package.json'; -import { config } from "@/bun/api/app"; -import path from "node:path"; -import toml, { TomlTable } from 'smol-toml'; -import fs from 'node:fs/promises'; -import bin from './eeprom.bin' with { type: 'file' }; - -export default class XEMUIntegration implements PluginType -{ - emulator = 'XEMU'; - - async load (ctx: PluginLoadingContextType) - { - ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, (ctx) => - { - return { id: desc.name, supportLevel: "full", capabilities: ["batch", "fullscreen"] }; - }); - - ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, async (ctx) => - { - const args: string[] = []; - - if (config.get('launchInFullscreen')) - { - args.push("-full-screen"); - } - - if (ctx.autoValidCommand.metadata.romPath) - { - args.push("-dvd_path"); - args.push(ctx.autoValidCommand.metadata.romPath); - } - - const configPath = path.join(config.get('downloadPath'), 'storage', this.emulator, 'xemu.toml'); - let configFile: { general: TomlTable & { misc: TomlTable; }, sys: TomlTable & { files: TomlTable; }; } = { general: { misc: {} }, sys: { files: {} } }; - if (await Bun.file(configPath).exists()) - { - configFile = toml.parse(await Bun.file(configPath).text()) as any; - } - - configFile.general.misc ??= {}; - configFile.general.misc.skip_boot_anim = true; - configFile.general.show_welcome = false; - configFile.general.games_dir = path.join(config.get('downloadPath'), 'roms', 'xbox'); - configFile.sys.mem_limit = '128'; - const biosFolder = path.join(config.get('downloadPath'), "bios", this.emulator); - if (await fs.exists(biosFolder)) - { - const biosPaths = (await fs.readdir(biosFolder)); - const flash = biosPaths.find(f => f.endsWith('.bin') && !f.includes('mcpx')); - const bootrom = biosPaths.find(f => f.endsWith('.bin') && f.includes('mcpx')); - const hardDrive = biosPaths.find(f => f.endsWith('qcow2')); - if (flash) configFile.sys.files.flashrom_path = path.join(biosFolder, flash); - if (bootrom) configFile.sys.files.bootrom_path = path.join(biosFolder, bootrom); - if (hardDrive) configFile.sys.files.hdd_path = path.join(biosFolder, hardDrive); - } - - if (!ctx.dryRun) - { - const eepromPath = path.join(config.get('downloadPath'), "storage", this.emulator, 'eeprom.bin'); - await Bun.write(eepromPath, await Bun.file(bin).arrayBuffer()); - configFile.sys.files.eeprom_path = eepromPath; - - await Bun.write(configPath, toml.stringify(configFile)); - args.push("-config_path"); - args.push(configPath); - } - - - return { args }; - }); - } -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/package.json b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/package.json deleted file mode 100644 index 280f14f..0000000 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "com.simeonradivoev.gameflow.xenia", - "displayName": "XENIA Integration", - "version": "0.0.1", - "description": "XENIA Emulator Integration", - "main": "./xenia.ts", - "icon": "https://xenia.jp/images/logo-256x256.png", - "category": "emulators", - "keywords": [ - "integration", - "emulator", - "xbox360", - "xenia", - "xenia-edge" - ] -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/utils.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/utils.ts deleted file mode 100644 index b3c26f9..0000000 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/utils.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { join } from "path"; - -const SECTOR_SIZE = 0x800; -const MAGIC = "MICROSOFT*XBOX*MEDIA"; - -const PARTITION_OFFSETS: Record = { - XSF: 0x0, - GDF: 0xFD90000, - XGD3: 0x2080000, -}; - -async function readBytes (file: ReturnType, offset: number, length: number): Promise -{ - return Buffer.from(await file.slice(offset, offset + length).arrayBuffer()); -} - -async function parseTitleIdFromXexReader ( - read: (offset: number, length: number) => Promise -): Promise -{ - // Read just the fixed header (magic + flags + offsets + header count) - const header = await read(0, 0x18); - if (header.toString("ascii", 0, 4) !== "XEX2") - { - throw new Error("Not a valid XEX2 file"); - } - - const headerCount = header.readUInt32BE(0x14); - const EXEC_INFO_KEY = 0x40006; - - // Read the optional header table - const table = await read(0x18, headerCount * 8); - - for (let i = 0; i < headerCount; i++) - { - const key = table.readUInt32BE(i * 8); - const valueOrOffset = table.readUInt32BE(i * 8 + 4); - - if (key === EXEC_INFO_KEY) - { - // valueOrOffset is a file offset — read the exec info struct there - // TitleID is at +0x0C within it - const execInfo = await read(valueOrOffset, 0x18); - return execInfo.readUInt32BE(0x0C) - .toString(16).toUpperCase().padStart(8, "0"); - } - } - - throw new Error("Execution info header not found in XEX"); -} - -async function titleIdFromXexFile (xexPath: string): Promise -{ - const file = Bun.file(xexPath); - return parseTitleIdFromXexReader((offset, length) => - readBytes(file, offset, length) - ); -} - -async function titleIdFromIso (isoPath: string): Promise -{ - const file = Bun.file(isoPath); - const fileSize = file.size; - - for (const partitionOffset of Object.values(PARTITION_OFFSETS)) - { - const vdOffset = partitionOffset + 0x20 * SECTOR_SIZE; - if (vdOffset + 28 > fileSize) continue; - - const vd = await readBytes(file, vdOffset, 28); - if (vd.toString("ascii", 0, 20) !== MAGIC) continue; - - const rootSector = vd.readUInt32LE(20); - const rootSize = vd.readUInt32LE(24); - const rootOffset = partitionOffset + rootSector * SECTOR_SIZE; - const dir = await readBytes(file, rootOffset, rootSize); - - let pos = 0; - while (pos < dir.length) - { - if (dir[pos] === 0xFF) break; - if (pos + 14 > dir.length) break; - - const nameLen = dir[pos + 13]; - if (nameLen === 0 || nameLen === 0xFF) break; - if (pos + 14 + nameLen > dir.length) break; - - const name = dir.toString("ascii", pos + 14, pos + 14 + nameLen); - const fileSector = dir.readUInt32LE(pos + 4); - - if (name.toLowerCase() === "default.xex") - { - const xexBase = partitionOffset + fileSector * SECTOR_SIZE; - // Reader that translates relative XEX offsets to absolute ISO offsets - return parseTitleIdFromXexReader((offset, length) => - readBytes(file, xexBase + offset, length) - ); - } - - const entryLen = 14 + nameLen; - pos += (entryLen + 3) & ~3; - } - } - - throw new Error("Not a valid Xbox 360 ISO or default.xex not found"); -} - -async function titleIdFromFolder (folderPath: string): Promise -{ - return titleIdFromXexFile(join(folderPath, "default.xex")); -} - -type XeniaRomType = "iso" | "xex" | "folder"; - -function detectRomType (romPath: string): XeniaRomType -{ - const lower = romPath.toLowerCase(); - if (lower.endsWith(".iso")) return "iso"; - if (lower.endsWith(".xex")) return "xex"; - return "folder"; // extracted game folder containing default.xex -} - -async function getTitleId (romPath: string): Promise -{ - switch (detectRomType(romPath)) - { - case "iso": return titleIdFromIso(romPath); - case "xex": return titleIdFromXexFile(romPath); - case "folder": return titleIdFromFolder(romPath); - } -} - -export async function getXeniaSavePaths ( - romPath: string, - xeniaDir: string -): Promise -{ - const titleId = await getTitleId(romPath); - return join(xeniaDir, titleId); -}; \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/xenia.ts b/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/xenia.ts deleted file mode 100644 index 9d37d99..0000000 --- a/src/bun/api/plugins/builtin/emulators/com.simeonradivoev.gameflow.xenia/xenia.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; -import desc from './package.json'; -import { GameflowHooks } from "@simeonradivoev/gameflow-sdk"; -import { config } from "@/bun/api/app"; -import path from "node:path"; -import { ensureDir } from "fs-extra"; -import toml, { TomlTable } from 'smol-toml'; -import fs from 'node:fs/promises'; -import { getXeniaSavePaths } from "./utils"; - -export default class XENIAIntegration implements PluginType -{ - emulator = 'XENIA'; - emulatorEdge = 'XENIA-EDGE'; - - async handlePostInstall (ctx: Parameters['0']) - { - await Bun.write(path.join(ctx.path, "portable.txt"), ""); - } - - async handleLaunch (ctx: Parameters['0']): - ReturnType - { - const args: string[] = []; - - if (ctx.autoValidCommand.metadata.romPath) - { - args.push(ctx.autoValidCommand.metadata.romPath); - } - - const configPath = path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!, `${ctx.autoValidCommand.emulator}.toml`); - - args.push(`--config`, configPath); - - if (config.get('launchInFullscreen')) - { - args.push(`--fullscreen`); - } - - if (!ctx.dryRun) - { - await ensureDir(path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!)); - let configFile: TomlTable & { Storage: TomlTable, GPU: TomlTable, Display: TomlTable; } = { Storage: {}, GPU: {}, Display: {} }; - if (await fs.exists(configPath)) - { - configFile = toml.parse(await Bun.file(configPath).text()) as any; - } - - const resolutionMapping = { - "720p": 1, - "1080p": 2, - "1440p": 3, - "4k": 3 - }; - - configFile.Display.fullscreen = config.get('launchInFullscreen'); - configFile.GPU.draw_resolution_scale_x = resolutionMapping[config.get('emulatorResolution')] ?? 1; - configFile.GPU.draw_resolution_scale_y = resolutionMapping[config.get('emulatorResolution')] ?? 1; - const savesPath = path.join(config.get('downloadPath'), 'saves', ctx.autoValidCommand.emulator!); - await ensureDir(savesPath); - configFile.Storage.content_root = path.join(config.get('downloadPath'), 'saves', ctx.autoValidCommand.emulator!); - configFile.Storage.storage_root = path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!, 'config'); - configFile.Storage.cache_root = path.join(config.get('downloadPath'), 'storage', ctx.autoValidCommand.emulator!, 'cache'); - - await Bun.write(configPath, toml.stringify(configFile)); - - let finalSavesPath: string | undefined = undefined; - if (ctx.autoValidCommand.metadata.romPath) - { - finalSavesPath = await getXeniaSavePaths(ctx.autoValidCommand.metadata.romPath, savesPath); - return { args, savesPath: { [this.emulator]: { cwd: finalSavesPath } } }; - } - - return { args }; - }; - - return { args }; - } - - handleEmulatorLaunchSupport (ctx: Parameters['0']): - ReturnType - { - return { id: desc.name, supportLevel: "full", capabilities: ["batch", "fullscreen", "saves"] }; - } - - async load (ctx: PluginLoadingContextType) - { - ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulator }, this.handleEmulatorLaunchSupport); - ctx.hooks.games.emulatorLaunchSupport.tap({ name: desc.name, emulator: this.emulatorEdge }, this.handleEmulatorLaunchSupport); - - ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulator }, this.handleLaunch); - ctx.hooks.games.emulatorLaunch.tapPromise({ name: desc.name, emulator: this.emulatorEdge }, this.handleLaunch); - - ctx.hooks.games.postPlay.tap({ name: desc.name }, async ({ validChangedSaveFiles, saveFolderSlots, command }) => - { - if (command.emulator !== this.emulator || !(saveFolderSlots?.[this.emulator]) || !command.metadata.romPath) return; - const files = await fs.readdir(saveFolderSlots[this.emulator].cwd, { recursive: true }); - validChangedSaveFiles.xenia = { cwd: saveFolderSlots[this.emulator].cwd, subPath: files, shared: false }; - - }); - } -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/es-de.ts b/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/es-de.ts deleted file mode 100644 index 77cd201..0000000 --- a/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/es-de.ts +++ /dev/null @@ -1,521 +0,0 @@ -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; -import desc from './package.json'; -import { config, customEmulators, db, emulatorsDb } from "@/bun/api/app"; -import * as emulatorSchema from '@schema/emulators'; -import { and, eq } from "drizzle-orm"; -import { cores } from "@/bun/api/emulatorjs/emulatorjs"; -import { RPC_URL } from "@/shared/constants"; -import { host } from "@/bun/utils/host"; -import path from 'node:path'; -import { existsSync, readFileSync } from "node:fs"; -import fs from "node:fs/promises"; -import { findStoreEmulatorExec } from "@/bun/api/games/services/launchGameService"; -import { which } from "bun"; -import os from 'node:os'; -import { getLocalGameMatch } from "@/bun/api/games/services/utils"; -import { CommandEntry, EmulatorSourceEntryType } from "@simeonradivoev/gameflow-sdk/shared"; - -export default class IgdbIntegration implements PluginType -{ - varRegex = /%([^%]+)%/g; - assignRegex = /(%\w+%)=(\S+) /g; - - /** - * Get the emulators related to the given system - * @param systemSlug the ES-DE slug for the system - */ - async getEmulatorsForSystem (systemSlug: string) - { - const system = await emulatorsDb.query.systems.findFirst({ - with: { commands: true }, - where: eq(emulatorSchema.systems.name, systemSlug) - }); - - if (!system) - { - throw new Error(`Could not find system '${systemSlug}'`); - } - - const emulators = new Set(); - await Promise.all(system.commands.map(async (command, index) => - { - let cmd = command.command; - - const matches = Array.from(cmd.matchAll(this.varRegex)); - matches.forEach(([value]) => - { - if (value.startsWith("%EMULATOR_")) - { - const emulatorName = value.substring("%EMULATOR_".length, value.length - 1); - emulators.add(emulatorName); - return; - } - }); - })); - - if (cores[systemSlug]) - { - emulators.add('EMULATORJS'); - } - - return Array.from(emulators); - } - - async findExecs (id: string, emulator?: { winregistrypath: string[], systempath: string[], staticpath: string[]; }) - { - const execs: EmulatorSourceEntryType[] = []; - - if (customEmulators.has(id)) - { - execs.push({ binPath: customEmulators.get(id), type: 'custom', exists: await fs.exists(customEmulators.get(id)) }); - } - - if (emulator && emulator.systempath.length > 0) - { - const storePath = await findStoreEmulatorExec(id, emulator); - if (storePath) execs.push(storePath); - } - - if (emulator && process.platform === 'win32') - { - const regValues = emulator.winregistrypath; - if (regValues.length > 0) - { - for (const node of regValues) - { - const registryValue = await this.readRegistryValue(node); - if (registryValue) - { - execs.push({ binPath: registryValue, type: 'registry', exists: true }); - } - } - - } - } - - if (emulator && emulator.systempath.length > 0) - { - const systemPath = await this.resolveSystemPath(emulator.systempath); - if (systemPath) - { - execs.push({ binPath: systemPath, type: 'system', exists: true }); - } - } - - if (emulator && emulator.staticpath.length > 0) - { - const staticPath = await this.resolveStaticPath(emulator.staticpath); - if (staticPath) - { - execs.push({ binPath: staticPath, type: 'static', exists: true }); - } - } - - return execs; - } - - async readRegistryValue (text: string) - { - const params = text.split('|'); - const key = path.dirname(params[0]); - const value = path.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 resolveStaticPath (entries: string[]) - { - for (const entry of entries) - { - const resolved = entry.replace("~", os.homedir()); - if (await fs.exists(resolved)) - { - return resolved; - } - } - return null; - } - - async resolveSystemPath (entries: string[]) - { - for (const entry of entries) - { - try - { - const found = which(entry); - return found; - } catch { } - } - return null; - } - - async findExecsByName (emulatorName: string) - { - const emulator = await emulatorsDb.query.emulators.findFirst({ where: eq(emulatorSchema.emulators.name, emulatorName) }); - if (!emulator) - { - return []; - } - return this.findExecs(emulatorName, emulator); - } - - async getRomFilePaths (gamePath: string, config: { systemSlug?: string; mainGlob?: string | null; }) - { - if (!existsSync(gamePath)) - { - throw new Error(`Provided rom path is missing: '${gamePath}'`); - } - - const gamePathStat = await fs.stat(gamePath); - const validFiles: string[] = []; - - if (gamePathStat.isDirectory()) - { - if (config.mainGlob) - { - const files = await Array.fromAsync(fs.glob(config.mainGlob, { cwd: gamePath })); - if (files.length > 1) - { - throw new Error("Found multiple rom files"); - } else if (files.length === 0) - { - throw new Error("Found no valid roms"); - } - - validFiles.push(path.join(gamePath, files[0])); - } else - { - if (!config.systemSlug) throw new Error("Needs system to find valid file"); - - const system = await emulatorsDb.query.systems.findFirst({ - with: { commands: true }, - where: eq(emulatorSchema.systems.name, config.systemSlug) - }); - - if (!system) - { - throw new Error(`Could not find system '${config.systemSlug}'`); - } - - const extensionList = system.extension.join(','); - - 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 (config.systemSlug) - { - const system = await emulatorsDb.query.systems.findFirst({ - with: { commands: true }, - where: eq(emulatorSchema.systems.name, config.systemSlug) - }); - - if (!system) - { - throw new Error(`Could not find system '${config.systemSlug}'`); - } - - if (system.extension.some(e => gamePath.toLocaleLowerCase().endsWith(e.toLocaleLowerCase()))) - { - validFiles.push(gamePath); - } - else - { - const extensionList = system.extension.join(','); - throw new Error(`Invalid Rom File. Must be a file that ends in '${extensionList}'`); - } - } else - { - validFiles.push(gamePath); - } - - return validFiles; - } - - /** - * - * @param data Uses es-de system slug - * @param mainGlob The main file glob supported pattern to search for if game path is a directory - * @returns - */ - async getValidLaunchCommands (data: { - systemSlug: string; - gamePath: string; - mainGlob?: string | null; - }): Promise - { - - const system = await emulatorsDb.query.systems.findFirst({ - with: { commands: true }, - where: eq(emulatorSchema.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.isAbsolute(data.gamePath) ? data.gamePath : path.join(downloadPath, data.gamePath); - - const validFiles: string[] = await this.getRomFilePaths(gamePath, { systemSlug: data.systemSlug, mainGlob: data.mainGlob }); - - function escapeWindowsArg (arg: string): string - { - if (process.platform === 'win32') - { - return `"${arg - .replace(/(\\*)"/g, '$1$1\\"') // escape quotes - .replace(/(\\*)$/, '$1$1') // escape trailing backslashes - }"`; - } else - { - if (arg.includes(' ')) - { - return `"${arg}"`; - } else - { - return arg; - } - } - } - - const formattedCommands = await Promise.all(system.commands - .filter(c => !c.command.includes(`%ENABLESHORTCUTS%`)) - .map(async (command, index) => - { - const label = command.label; - let cmd = command.command; - - let emulator: string | undefined = undefined; - let rom = validFiles[0]; - - if (cmd.includes('%ESCAPESPECIALS%')) - rom = rom.replace(/[&()^=;,]/g, ''); - - - - const staticVars: Record = { - '%ROM%': escapeWindowsArg(rom), - '%ROMRAW%': validFiles[0], - '%ROMRAWWIN%': escapeWindowsArg(validFiles[0].replaceAll('/', '\\')), - '%ESPATH%': escapeWindowsArg(path.dirname(Bun.main)), - '%ROMPATH%': escapeWindowsArg(gamePath), - '%BASENAME%': escapeWindowsArg(path.basename(validFiles[0], path.extname(validFiles[0]))), - '%FILENAME%': escapeWindowsArg(path.basename(validFiles[0])), - '%ESCAPESPECIALS%': "", - '%HIDEWINDOW%': "" - }; - - cmd = cmd.replace(/\%INJECT\%=(?[\w\%.\/\\]+)/g, (_, injectFile: string) => - { - try - { - const resolvedInjectFile = injectFile.replace(this.varRegex, (a) => - { - return staticVars[a] ?? a; - }); - if (existsSync(resolvedInjectFile)) - { - const rawContents = readFileSync(resolvedInjectFile, { encoding: 'utf-8' }); - return rawContents.split('\n').map(v => v.replace('\r', '')).join(' '); - } - - return ''; - } catch (error) - { - return ''; - } - }); - - const matches = Array.from(cmd.matchAll(this.varRegex)); - const varList = await Promise.all(matches.map(async ([value]) => - { - if (value.startsWith("%EMULATOR_")) - { - const emulatorName = value.substring("%EMULATOR_".length, value.length - 1); - let execs = await this.findExecsByName(emulatorName); - let validExec = execs.find(e => e.exists); - - emulator = emulatorName; - return [ - [value, validExec ? validExec.binPath : undefined] as [string, string | undefined], - [`%EMUSOURCE%`, validExec?.type] as [string, string | undefined], - ['%EMUDIR%', validExec?.rootPath ?? (validExec ? escapeWindowsArg(path.dirname(validExec.binPath)) : undefined)] as [string, string | undefined], - ['%EMUDIRRAW%', validExec?.rootPath ?? (validExec ? path.dirname(validExec.binPath) : undefined)] as [string, string | undefined] - ]; - - } - - const key = value[0].substring(1, value.length - 1); - return [[value, process.env[key]] as [string, string | undefined]]; - })); - - const vars = { ...Object.fromEntries(varList.flatMap(l => l)), ...staticVars }; - let startDir: string | undefined = undefined; - - if ('%STARTDIR%' in vars) - { - delete vars['%STARTDIR%']; - - cmd = cmd.replace(this.assignRegex, (match, p1, p2) => - { - if (p1 === '%STARTDIR%') - { - startDir = this.varRegex.test(p2) ? staticVars[p2] : p2; - } - return ""; - }); - } - - // missing variable - const invalid = Object.entries(vars).find(c => c[1] === undefined); - - const formattedCommand = cmd.replace(this.varRegex, (s) => vars[s] ?? '').trim(); - - return { - id: index, - label: label ?? undefined, - command: formattedCommand, - startDir, - valid: !invalid, emulator, - emulatorSource: vars['%EMUSOURCE%'] as any, - metadata: { - romPath: validFiles[0], - emulatorBin: varList.flatMap(l => l).find(v => v[0].includes('%EMULATOR_'))?.[1], - emulatorDir: vars['%EMUDIRRAW%'] - } - } satisfies CommandEntry; - })); - - return formattedCommands.filter(c => !!c); - } - - async load (ctx: PluginLoadingContextType) - { - ctx.hooks.emulators.findEmulatorSource.tapPromise(desc.name, async ({ sources, emulator }) => - { - sources.push(...await this.findExecsByName(emulator)); - }); - - ctx.hooks.emulators.findEmulatorForSystem.tapPromise(desc.name, async ({ system, emulators }) => - { - emulators.push(...await this.getEmulatorsForSystem(system)); - }); - - ctx.hooks.games.fetchRomFiles.tapPromise(desc.name, async ({ source, id }) => - { - const localGame = await db.query.games.findFirst({ - where: getLocalGameMatch(id, source), - columns: { path_fs: true, main_glob: true }, - with: { platform: { columns: { es_slug: true } } } - }); - - if (!localGame?.path_fs) - { - return; - } - - const downloadPath = config.get('downloadPath'); - const path_fs = path.isAbsolute(localGame.path_fs) ? localGame.path_fs : path.join(downloadPath, localGame.path_fs); - - return this.getRomFilePaths(path_fs, { systemSlug: localGame.platform.es_slug ?? undefined, mainGlob: localGame.main_glob }); - }); - - ctx.hooks.games.buildLaunchCommands.tapPromise(desc.name, async ({ systemSlug, source, id, gamePath, mainGlob }) => - { - if (source === 'emulator') - { - const esEmulator = await emulatorsDb.query.emulators.findFirst({ where: eq(emulatorSchema.emulators.name, id.id) }); - const allExecs = await this.findExecs(id.id, esEmulator); - return allExecs.map(exec => ({ - command: exec.binPath, - id: exec.type, - emulator: id.id, - emulatorSource: exec.type, - metadata: { - emulatorBin: exec.binPath, - emulatorDir: exec.rootPath - }, - valid: true - } satisfies CommandEntry)); - } - - const rommPlatform = systemSlug; - let esSystem: string | undefined = undefined; - const systemMapping = await emulatorsDb.query.systemMappings.findFirst({ - where: and(eq(emulatorSchema.systemMappings.sourceSlug, rommPlatform), eq(emulatorSchema.systemMappings.source, 'romm')) - }); - - if (systemMapping) esSystem = systemMapping.system; - - if (!esSystem) - { - const system = await emulatorsDb.query.systems.findFirst({ where: eq(emulatorSchema.systems.name, systemSlug), columns: { name: true } }); - if (system) esSystem = system.name; - } - - if (esSystem && gamePath) - { - try - { - const commands = await this.getValidLaunchCommands({ systemSlug: esSystem, gamePath, mainGlob }); - - if (cores[esSystem]) - { - const gameUrl = `${RPC_URL(host)}/api/romm/rom/${id.source}/${id.id}`; - commands.push({ - id: 'EMULATORJS', - label: "Emulator JS", - command: `core=${cores[esSystem]}&gameUrl=${encodeURIComponent(gameUrl)}`, - valid: true, - emulator: 'EMULATORJS', - metadata: { - romPath: gameUrl - } - }); - } - - return commands; - } catch (error) - { - console.error(error); - if (error instanceof Error) return error; - } - } - }); - } -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/package.json b/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/package.json deleted file mode 100644 index 90815b7..0000000 --- a/src/bun/api/plugins/builtin/launchers/com.simeonradivoev.gameflow.es/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "com.simeonradivoev.gameflow.es", - "displayName": "ES-DE Launcher", - "version": "0.0.1", - "description": "ES-DE Launch Configurations. Used as fallback", - "main": "./es-de.ts", - "icon": "https://impro.usercontent.one/appid/oneComWsb/domain/es-de.org/media/es-de.org/onewebmedia/ES-DE_logo.png", - "canDisable": false, - "category": "launchers", - "keywords": [ - "integration", - "es-de" - ] -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/package.json b/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/package.json deleted file mode 100644 index 2c42339..0000000 --- a/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "com.simeonradivoev.gameflow.rclone", - "displayName": "Rclone Integration", - "version": "0.0.1", - "description": "Rclone integration for syncing saves", - "main": "./rclone.ts", - "icon": "data:image/svg+xml,%3Csvg%20role%3D%22img%22%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22currentColor%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3ERclone%3C%2Ftitle%3E%3Cpath%20d%3D%22M11.842.6258C9.3647.6813%206.9754%201.9906%205.646%204.2933c-.7593%201.3144-1.0647%202.7662-.966%204.1745a7.99%207.99%200%200%201%202.6568-.4541l1.4705-.0013c-.0093-.5594.1245-1.1284.4245-1.6482.8827-1.5284%202.837-2.0522%204.3654-1.1695%201.5284.8824%202.0519%202.8366%201.1695%204.365l-1.4782%202.5647%201.1955%202.0714%202.3914-.0004%201.4775-2.5655c2.0262-3.5088.8239-7.9959-2.6853-10.0217C14.4614.9118%2013.1396.5967%2011.842.6258m-1.5451%208.073-2.9605.0029C3.2844%208.7017%200%2011.9867%200%2016.0383c0%204.052%203.2844%207.3367%207.3364%207.3367%201.5174%200%202.9267-.4609%204.0967-1.2497a8%208%200%200%201-1.72-2.0748l-.7368-1.273c-.4799.288-1.0392.4565-1.6395.4565-1.765%200-3.1958-1.4307-3.1958-3.1958%200-1.7647%201.4307-3.1954%203.1958-3.1954l2.96-.0022%201.1962-2.0708zm9.587.7475a7.99%207.99%200%200%201-.935%202.5278l-.7344%201.2745c.4892.2717.915.6719%201.2153%201.192.8823%201.528.3585%203.4826-1.1699%204.365-1.528.8823-3.4828.3588-4.3651-1.1696l-1.482-2.5628h-2.3915L8.8256%2017.144l1.483%202.5626c2.0262%203.5091%206.513%204.7112%2010.022%202.685%203.5089-2.0257%204.7112-6.5125%202.6853-10.0216-.7588-1.3144-1.863-2.3052-3.132-2.9237%22%20%2F%3E%3C%2Fsvg%3E", - "category": "saves", - "keywords": [ - "integration", - "rclone" - ] -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/rclone.ts b/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/rclone.ts deleted file mode 100644 index 8ab31a0..0000000 --- a/src/bun/api/plugins/builtin/other/com.simeonradivoev.gameflow.rclone/rclone.ts +++ /dev/null @@ -1,473 +0,0 @@ -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; -import desc from './package.json'; -import { config, db, events } from "@/bun/api/app"; -import path from 'node:path'; -import unzip from 'unzip-stream'; -import { ensureDir } from "fs-extra"; -import { Readable } from "node:stream"; -import { pipeline } from "node:stream/promises"; -import fs from 'node:fs/promises'; -import { randomUUIDv7 } from "bun"; -import z from "zod"; -import { createInterface } from "node:readline"; -import { getLocalGameMatch } from "@/bun/api/games/services/utils"; -import { getErrorMessage } from "@/bun/utils"; - -const DefaultLocalName = "Default_Local"; - -const SettingsSchema = z.object({ - runWebGui: z.boolean() - .default(false) - .describe("Run the Web GUI that can be accessed at http://localhost:5572") - .meta({ title: "Run Web GUI" }), - globalConfig: z.boolean().default(false).describe("Use the Global Config file if already setup"), - webGuiPassword: z.string().optional().readonly().describe("Randomly Generated. Read Only. Username is gameflow"), - remoteName: z.string().default(DefaultLocalName), - verboseLog: z.boolean() - .default(false) - .describe("Show detailed log of operation for debugging") - .meta({ $comment: JSON.stringify({ category: "debug" }) }), - importSaves: z.boolean().default(true).describe("Import Saves From the Destination. This will override local saves"), - exportSaves: z.boolean().default(true).describe("Export saves to remove. This will sync current saves with remote") -}); - -type SettingsType = z.infer; -const loginTokenUrlRegex = /http:\/\/[\w\d:\-@\[\]\.\/?=]+/gm; - -export default class RcloneIntegration implements PluginType -{ - settingsSchema = SettingsSchema; - rclonePath: string | undefined; - server: Bun.Subprocess | undefined; - password: string; - user = "gameflow"; - loginUrl: string | undefined = undefined; - eventsNames = [{ - id: "open-web-gui", - title: "Open Web GUI", - description: "Open Web GUI", - action: "Open" - }, { - id: "refresh", - title: "Refresh Sources", - action: "Refresh" - }]; - - constructor() - { - this.password = randomUUIDv7(); - } - - async onEvent (id: string) - { - switch (id) - { - case "open-web-gui": - return { openTab: this.loginUrl }; - break; - case "refresh": - await this.refresh(); - return { reload: true }; - break; - } - } - - async setup (ctx: PluginLoadingContextType) - { - ctx.zodRegistry.add(SettingsSchema.shape.runWebGui, { requiresRestart: true }); - ctx.zodRegistry.add(SettingsSchema.shape.globalConfig, { requiresRestart: true }); - - const toolsPath = path.join(config.get('downloadPath'), "tools"); - await ensureDir(toolsPath); - const binaryMap: Record = { - win32: '**/rclone.exe', - linux: 'rclone-*/rclone', - darwin: 'rclone-*/rclone' - }; - const existingRclones = await Array.fromAsync(fs.glob(binaryMap[process.platform], { cwd: toolsPath })); - if (existingRclones[0]) - { - this.rclonePath = path.join(toolsPath, existingRclones[0]); - await this.startServer(ctx); - return; - } - - ctx.setProgress(0.5, "Downloading RClone"); - const platformMap: Record = { - linux: "linux", - win32: "windows", - darwin: "osx" - }; - const archMap: Record = { - x64: "amd64", - arm64: "arm64" - }; - const downloadUrl = `https://downloads.rclone.org/rclone-current-${platformMap[process.platform]}-${archMap[process.arch]}.zip`; - console.log("Starting Download", downloadUrl); - const rcCloseZip = await fetch(downloadUrl); - - await ensureDir(toolsPath); - await pipeline(Readable.fromWeb(rcCloseZip.body as any), unzip.Extract({ path: toolsPath })); - const dests = await Array.fromAsync(fs.glob(binaryMap[process.platform], { cwd: toolsPath })); - if (dests[0]) - { - this.rclonePath = path.join(toolsPath, dests[0]); - await fs.chmod(this.rclonePath, 0o755); - await this.startServer(ctx); - return; - } - } - - async refresh () - { - try - { - const data = await this.request('/config/listremotes', {}); - z.globalRegistry.add(SettingsSchema.shape.remoteName, { - examples: [''].concat(...data.remotes), - description: "The name of the remote to sync with" - }); - } catch (error) - { - events.emit('notification', { message: getErrorMessage(error), type: 'error' }); - z.globalRegistry.add(SettingsSchema.shape.remoteName, { - examples: [''], - description: "The name of the remote to sync with" - }); - } - } - - async startServer (ctx: PluginLoadingContextType) - { - const args: string[] = []; - if (ctx.config.get('runWebGui')) - { - args.push("--rc-web-gui"); - args.push("--rc-web-gui-no-open-browser"); - } - if (ctx.config.get('')) - { - args.push('-vv'); - } - let env: Record | undefined = undefined; - if (!ctx.config.get('globalConfig')) - { - env = { RCLONE_CONFIG: path.join(config.get('downloadPath'), 'tools', 'config', 'rclone', 'rclone.conf') }; - } - ctx.config.set('webGuiPassword', this.password); - this.server = Bun.spawn([this.rclonePath!, "rcd", '--use-json-log', `--rc-user=${this.user}`, ...args, `--rc-pass=${this.password}`, "--rc-addr", "localhost:5572"], { - stdout: "pipe", - stderr: "pipe", - env - }); - const rl = createInterface({ input: Readable.fromWeb(this.server.stderr as any) }); - rl.on('line', e => - { - try - { - const data = JSON.parse(e); - - if (data.level === 'error') - { - console.error(data.msg); - } else if (data.level === 'critical') - { - console.error(data.msg); - } - - else - { - console.log(e); - if (loginTokenUrlRegex.test(data.msg)) - { - this.loginUrl = (data.msg as string).match(loginTokenUrlRegex)?.find(e => e); - } - } - } catch (error) - { - console.log(e); - } - - }); - - await new Promise((resolve, reject) => - { - const handleResolve = (line: string) => - { - try - { - const data = JSON.parse(line); - if (!loginTokenUrlRegex.test(data.msg)) return; - rl.off('line', handleResolve); - resolve(data); - } catch (error) - { - - } - }; - rl.on('line', handleResolve); - setTimeout(() => { reject("Timeout"); }, 5000); - }); - - await this.refresh(); - } - - async request (path: string, body: any) - { - const response = await fetch(`http://localhost:5572${path}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Basic ${Buffer.from(`${this.user}:${this.password}`).toString('base64')}` - }, - body: JSON.stringify(body) - }); - - const data = await response.json(); - if (response.ok) - { - return data; - } else - { - throw new Error(response.statusText, { cause: data }); - } - } - - async cleanup () - { - await new Promise((resolve) => - { - this.request('/core/quit', {}).catch(e => - { - this.server?.kill("SIGKILL"); - this.server = undefined; - }); - - setTimeout(() => - { - this.request('/core/quit', { exitCode: 9 }).then(e => - { - resolve(false); - this.server = undefined; - }).catch(e => - { - resolve(false); - this.server?.kill("SIGKILL"); - this.server = undefined; - }); - - - }, 5000); - - this.server?.exited.then(() => resolve(true)); - }); - - } - - async load (ctx: PluginLoadingContextType) - { - await this.setup(ctx); - - ctx.hooks.games.prePlay.tapPromise({ - name: desc.name, - stage: 10, - }, async ({ source, id, setProgress, saveFolderSlots, command }) => - { - if (!this.rclonePath || !saveFolderSlots || !ctx.config.get('importSaves')) return; - - const destination = source === 'store' ? [source, id] : command.emulator ? [command.emulator] : undefined; - if (!destination) return; - - const remoteName = ctx.config.get('remoteName'); - - for await (const [slot, { cwd }] of Object.entries(saveFolderSlots)) - { - let supportsMetadata = true; - let src: string; - - if (remoteName && remoteName !== DefaultLocalName) - { - src = `${remoteName}:gameflow/saves/${destination.join('/')}/${slot}`; - - const exists = await this.request('/operations/stat', { - fs: `${remoteName}:`, - remote: `gameflow/saves/${destination.join('/')}/${slot}` - }).catch(e => undefined); - if (!exists || !exists.item) return; - const remote = await this.request('/operations/fsinfo', { - fs: `${remoteName}:` - }); - supportsMetadata = !remote.ReadMetadata; - if (supportsMetadata) - { - console.warn("Remote", remoteName, "does not support metadata"); - } - } else - { - src = path.join(config.get('downloadPath'), 'saves', ...destination, slot); - if (!await fs.exists(path.join(config.get('downloadPath'), 'saves', ...destination, slot))) return; - } - - const job = await this.request('/sync/copy', { - srcFs: src, - dstFs: cwd, - createEmptySrcDirs: true, - _async: true, - _config: { - CheckFirst: true, - Metadata: true, - NoCheckDest: supportsMetadata - } - }).catch(e => - { - events.emit('notification', { message: `RClone: ${e.cause?.error ?? e.message ?? e}`, type: 'error' }); - return undefined; - });; - - await new Promise(async (resolve, reject) => - { - setProgress(0, "RClone: Syncing Saves"); - - const checkInterval = setInterval(async () => - { - const stat = await this.request('/job/status', { jobid: job.jobid }); - if (stat.finished) - { - clearInterval(checkInterval); - console.log(stat.output); - resolve(true); - - } else if (stat.error) - { - reject(stat.error); - } else - { - setProgress(stat.progress, "RClone: Syncing Saves"); - } - }, 500); - }); - } - - }); - - ctx.hooks.games.postPlay.tapPromise({ name: desc.name, stage: 10 }, async ({ source, id, validChangedSaveFiles, command }) => - { - if (!this.rclonePath || !ctx.config.get('exportSaves')) return; - const local = await db.query.games.findFirst({ where: getLocalGameMatch(id, source) }); - console.log("Save Files", Object.values(validChangedSaveFiles).flatMap(c => Array.isArray(c.subPath) ? c.subPath : [c.subPath]).join(",")); - - const destination = source === 'store' ? [source, id] : command.emulator ? [command.emulator] : undefined; - if (!destination) return; - - const remoteName = ctx.config.get('remoteName'); - - await Promise.all(Object.entries(validChangedSaveFiles).map(async ([slot, change]) => - { - let suportsMetadata = false; - let dest: string; - if (remoteName && remoteName !== DefaultLocalName) - { - dest = `${remoteName}:gameflow/saves/${destination.join('/')}/${slot}`; - const remote = await this.request('/operations/fsinfo', { - fs: `${remoteName}:` - }); - suportsMetadata = !remote.ReadMetadata; - if (suportsMetadata) - { - console.warn("Remote", remoteName, "does not support metadata"); - } - } else - { - dest = path.join(config.get('downloadPath'), 'saves', ...destination, slot); - } - - const filter = { - IncludeRule: Array.isArray(change.subPath) ? - change.subPath.map(s => - { - if (change.isGlob) return s; - else s.replaceAll('\\', '/'); - }) : - [change.isGlob ? change.subPath : change.subPath.replaceAll('\\', '/')] - }; - - let jobid: number | undefined = undefined; - - if (change.fixedSize) - { - await this.request('/sync/copy', { - srcFs: change.cwd, - dstFs: dest, - createEmptySrcDirs: true, - _async: true, - _config: { - NoCheckDest: true - }, - _filter: filter - }) - .then(job => jobid = job.jobid) - .catch(e => - { - events.emit('notification', { message: `RClone: ${e.cause?.error ?? e.message ?? e}`, type: 'error' }); - return undefined; - }); - } else - { - await this.request('/sync/sync', { - srcFs: change.cwd, - dstFs: dest, - createEmptySrcDirs: true, - _async: true, - _config: { - CheckSum: true, - CheckFirst: true, - Metadata: true, - MetadataSet: { - igdb_id: local?.igdb_id ? String(local?.igdb_id) : undefined, - ra_id: local?.ra_id ? String(local?.ra_id) : undefined - } - }, - _filter: filter - }) - .then(job => jobid = job.jobid) - .catch(e => - { - events.emit('notification', { message: `RClone: ${e.cause?.error ?? e.message ?? e}`, type: 'error' }); - return undefined; - }); - } - - if (!jobid) return; - await new Promise(async (resolve, reject) => - { - const checkInterval = setInterval(async () => - { - const stat = await this.request('/job/status', { jobid }); - if (stat.finished) - { - clearInterval(checkInterval); - console.log(stat.output); - resolve(true); - - } else if (stat.error) - { - reject(stat.error); - } else - { - - } - }, 500); - }); - - const stats = await this.request('/core/stats', { - group: `job/${jobid}` - }); - - if (stats.transfers > 0) - { - events.emit('notification', { message: "RClone: Save Synced", type: 'success', icon: 'save' }); - } - })); - }); - } -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/igdb.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/igdb.ts deleted file mode 100644 index c2be6d3..0000000 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/igdb.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; -import desc from './package.json'; -import secrets from "@/bun/api/secrets"; -import PQueue from 'p-queue'; -import * as igdb from '@phalcode/ts-igdb-client'; -import { checkLoginAndRefreshTwitch } from "@/bun/api/auth"; -import { GameLookup } from "@simeonradivoev/gameflow-sdk/shared"; - -export default class IgdbIntegration implements PluginType -{ - queue: PQueue; - - constructor() - { - this.queue = new PQueue({ concurrency: 8, interval: 1000, intervalCap: 4, strict: true }); - } - - async apiCall (subPath: string, query: string) - { - const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); - const headers = new Headers({ - "Client-ID": process.env.TWITCH_CLIENT_ID ?? '', - Authorization: `Bearer ${access_token}`, - Accept: "application/json" - }); - const response = await this.queue.add(() => fetch(`https://api.igdb.com/v4${subPath}`, { - headers: headers, - method: "POST", - body: query - })); - if (response.ok) - { - return response.json() as T; - } - } - - async cleanup () - { - this.queue.clear(); - } - - async load (ctx: PluginLoadingContextType) - { - await checkLoginAndRefreshTwitch(); - - ctx.hooks.games.gameLookup.tapPromise(desc.name, async (matches, { source, id, search }) => - { - if (!process.env.TWITCH_CLIENT_ID) return matches; - const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' }); - if (!access_token) - { - return matches; - } - - if ((source === 'igdb' && id) || search) - { - const client = igdb.igdb(process.env.TWITCH_CLIENT_ID, access_token); - - const { data: games } = await this.queue.add(() => client.request('games') - .pipe(...(search ? [igdb.search(search)] : []), - igdb.fields(['id', 'name', 'summary', 'screenshots.image_id', 'slug', 'first_release_date', 'rating', 'genres.name', 'involved_companies.company.name', 'keywords.name', 'game_modes.name', 'cover.image_id', 'age_ratings.rating_category.rating', 'platforms.name', 'platforms.abbreviation', 'platforms.slug']), - ...(source === 'igdb' && id ? [igdb.where('id', '=', Number(id))] : []), - igdb.limit(10)).execute()); - - matches.set(desc.name, games.filter(g => !!g.name) - .map(g => - { - const lookup: GameLookup = { - source: 'igdb', - id: String(g.id), - coverUrl: g.cover ? `https://images.igdb.com/igdb/image/upload/t_720p/${g.cover.image_id}.webp` : undefined, - screenshotUrls: g.screenshots?.map(s => `https://images.igdb.com/igdb/image/upload/t_720p/${s.image_id}.webp`) ?? [], - name: g.name!, - summary: g.summary, - genres: g.genres?.map(g => g.name!) ?? [], - companies: g.involved_companies?.filter(c => c.company?.name).map(c => c.company?.name!) ?? [], - game_modes: g.game_modes?.map(m => m.name!) ?? [], - age_ratings: g.age_ratings?.map(r => r.rating_category?.rating!) ?? [], - player_count: undefined, - // UNIX date, needs to be converted - first_release_date: g.first_release_date ? g.first_release_date * 1000 : undefined, - average_rating: g.rating ?? undefined, - keywords: g.keywords?.map(k => k.name!) ?? [], - igdb_id: g.id ?? undefined, - platforms: g.platforms?.map(p => ({ id: p.id!, name: p.abbreviation, displayName: p.name!, slug: p.slug! })) ?? [], - slug: g.slug - }; - - return lookup; - })); - - return matches; - } - - return matches.set(desc.name, []); - }); - - ctx.hooks.games.platformLookup.tapPromise(desc.name, async ({ source, id, slug }) => - { - let query: string | undefined = undefined; - if (source && id) - { - if (source !== 'igdb') return; - query = `fields name, slug, platform_logo.image_id, platform_logo.url, platform_family.name; where id = ${id};`; - - } - else if (slug) - { - query = `fields name, slug, platform_logo.image_id, platform_logo.url, platform_family.name; where slug = "${slug}";`; - } - - if (query) - { - const data = await this.apiCall<[any]>('/platforms', query); - if (!data || data.length <= 0) return; - return { - slug: data[0].slug, - url_logo: `https://images.igdb.com/igdb/image/upload/t_logo_med/${data[0].platform_logo.image_id}.png`, - name: data[0].name, - family_name: data[0].platform_family?.name - }; - } - }); - } -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/package.json b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/package.json deleted file mode 100644 index 55939bb..0000000 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.igdb/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "com.simeonradivoev.gameflow.igdb", - "displayName": "IGDB Integration", - "version": "0.0.1", - "description": "IGDB Metadata Integration", - "main": "./igdb.ts", - "icon": "data:image/svg+xml,%3Csvg%20role%3D%22img%22%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22currentColor%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctitle%3EIGDB%3C%2Ftitle%3E%3Cpath%20d%3D%22M24%206.228c-8%20.002-16%200-24%200v11.543a88.875%2088.875%200%200%201%202.271-.333%2074.051%2074.051%200%200%201%2017.038-.28c1.57.153%203.134.363%204.69.614V6.228zm-.706.707v10.013a74.747%2074.747%200%200%200-22.588%200V6.934h22.588ZM7.729%208.84a2.624%202.624%200%200%200-1.857.72%202.55%202.55%200%200%200-.73%201.33c-.098.5-.063%201.03.112%201.51.177.488.515.917.954%201.196.547.354%201.224.472%201.865.401a3.242%203.242%200%200%200%201.786-.777c-.003-.724.002-1.449-.002-2.173-.725.004-1.45-.002-2.174.003.003.317%200%20.634.001.951h1.105c.002.236%200%20.473.002.71-.268.196-.603.286-.932.298-.32.02-.65-.05-.922-.225a1.464%201.464%200%200%201-.59-.744c-.18-.499-.134-1.085.163-1.53.23-.355.619-.61%201.043-.647a1.8%201.8%200%200%201%201.012.206c.152.082.286.192.424.295.228-.281.461-.559.692-.838a3.033%203.033%200%200%200-.595-.403c-.418-.212-.892-.285-1.357-.283Zm11.66.086c-.093%200-.187.002-.28%200-.68.002-1.359-.004-2.038.003.003%201.666%200%203.332.002%204.998h2.497c.239-.002.478-.034.709-.097.276-.076.546-.208.742-.422.194-.208.297-.492.304-.776.016-.278-.032-.572-.195-.804-.175-.252-.453-.408-.734-.514.211-.122.407-.285.521-.505.134-.246.149-.535.117-.807a1.156%201.156%200%200%200-.436-.73c-.264-.207-.599-.304-.93-.334a2.757%202.757%200%200%200-.279-.012Zm-16.715%200v5.002h1.102V8.927c-.368-.002-.735%200-1.102%200zm8.524%200v5.002h2.016a2.87%202.87%200%200%200%201.07-.211%202.445%202.445%200%200%200%201.174-.993c.34-.555.429-1.244.292-1.876a2.367%202.367%200%200%200-.828-1.338c-.478-.387-1.096-.577-1.707-.584h-2.017zm6.949.967c.392.002.784-.001%201.176.002.183.011.38.054.51.19.11.112.136.28.112.43a.436.436%200%200%201-.22.316%201.082%201.082%200%200%201-.483.116c-.365.002-.73-.001-1.094.001-.002-.351%200-.703-.001-1.054zm-5.031.026c.28%200%20.567.053.815.19.274.149.491.396.607.685.113.272.138.574.107.865a1.456%201.456%200%200%201-.335.786%201.425%201.425%200%200%201-.865.466c-.168.031-.34.022-.51.023h-.632V9.92h.813zm5.03%201.948h1.36c.174.006.354.035.505.127.11.066.191.18.212.308.025.15.004.32-.099.44-.102.12-.258.176-.409.2-.172.032-.348.02-.522.022-.35-.001-.698.002-1.047-.001v-1.096z%22%20%2F%3E%3C%2Fsvg%3E", - "category": "sources", - "keywords": [ - "integration", - "igdb" - ] -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/package.json b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/package.json index 52c2376..815ddb0 100644 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/package.json +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/package.json @@ -5,7 +5,6 @@ "description": "ROMM Server Integration", "main": "./romm.ts", "icon": "https://romm.app/_ipx/q_80/images/blocks/logos/romm.svg", - "category": "sources", "keywords": [ "integration", "romm" diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts index 2be6e68..654fb2a 100644 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts +++ b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.romm/romm.ts @@ -1,74 +1,41 @@ -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; +import { PluginContextType, PluginType } from "@/bun/types/typesc.schema"; import desc from './package.json'; -import { DetailedRomSchema, getCollectionApiCollectionsIdGet, getCollectionsApiCollectionsGet, getCurrentUserApiUsersMeGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformsApiPlatformsGet, getRomApiRomsIdGet, getRomByMetadataProviderApiRomsByMetadataProviderGet, getRomContentApiRomsIdContentFileNameGet, getRomFiltersApiRomsFiltersGet, getRomsApiRomsGet, getSavesSummaryApiSavesSummaryGet, PlatformSchema, SimpleRomSchema, updateRomUserApiRomsIdPropsPut } from "@/clients/romm"; -import { config, events } from "@/bun/api/app"; +import { DetailedRomSchema, getCollectionApiCollectionsIdGet, getCollectionsApiCollectionsGet, getCurrentUserApiUsersMeGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformsApiPlatformsGet, getRomApiRomsIdGet, getRomsApiRomsGet, SimpleRomSchema, updateRomUserApiRomsIdPropsPut } from "@/clients/romm"; +import { config } from "@/bun/api/app"; import path from 'node:path'; import fs from 'node:fs/promises'; -import { hashFile, isArchive, isSteamDeckGameMode } from "@/bun/utils"; +import { hashFile, isSteamDeckGameMode } from "@/bun/utils"; import { CACHE_KEYS, getOrCached } from "@/bun/api/cache"; import secrets from "@/bun/api/secrets"; import { getAuthToken } from "@/clients/romm/core/auth.gen"; import { client } from "@/clients/romm/client.gen"; -import { validateGameSource } from "@/bun/api/games/services/statusService"; -import z from "zod"; -import { checkLoginAndRefreshRomm } from "@/bun/api/auth"; -import { DownloadFileEntry, DownloadInfo, FrontEndCollection, FrontEndGameType, FrontEndGameTypeDetailed, FrontEndGameTypeDetailedAchievement, FrontEndGameTypeWithIds, FrontEndPlatformType } from "@simeonradivoev/gameflow-sdk/shared"; -import Conf from "conf"; -const SettingsSchema = z.object({ - savesSync: z.boolean().default(false).describe("Experimental save sync support"), - clientApiToken: z.string().optional().describe("Generate a long lived token from the ROMM server") -}); - -type SettingsType = z.infer; - -export default class RommIntegration implements PluginType +export default class RommIntegration implements PluginType { - settingsSchema = SettingsSchema; isSteamDeck = false; - orderByMap: Record = { - added: "created_at", - activity: "created_at", - name: "name", - release: "metadatum.first_release_date" - }; - async checkRemote () - { - if (!config.has('rommAddress')) return false; - return true; - } - - async getAccessToken (config: Conf) - { - if (process.env.ROMM_CLIENT_TOKEN) return process.env.ROMM_CLIENT_TOKEN; - const client_token = config.get('clientApiToken'); - if (client_token) return client_token; - return (await secrets.get({ service: 'gameflow', name: 'romm_access_token' })) ?? undefined; - } - - async updateClient (pluginConfig: Conf) + async updateClient () { client.setConfig({ baseUrl: config.get('rommAddress'), - auth: (auth) => + async auth (auth) { if (auth.scheme === 'bearer') { - return this.getAccessToken(pluginConfig); + return (await secrets.get({ service: 'gameflow', name: 'romm_access_token' })) ?? undefined; } } }); } - async getAuthToken (config: Conf) + async getAuthToken () { return getAuthToken({ scheme: 'bearer', type: "http" - }, async (a) => this.getAccessToken(config)); + }, async (a) => (await secrets.get({ service: "gameflow", name: 'romm_access_token' })) ?? undefined); } async getAllRommPlatforms () @@ -80,12 +47,9 @@ export default class RommIntegration implements PluginType { const game: FrontEndGameType = { id: { id: String(rom.id), source: 'romm' }, - path_covers: [`/api/romm/image/romm${this.isSteamDeck ? rom.path_cover_small : rom.path_cover_large}`], - last_played: rom.rom_user.last_played !== null ? new Date(rom.rom_user.last_played) : null, + path_cover: `/api/romm/image/romm${this.isSteamDeck ? rom.path_cover_small : rom.path_cover_large}`, + last_played: rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null, updated_at: new Date(rom.created_at), - metadata: { - first_release_date: rom.metadatum.first_release_date !== null ? new Date(rom.metadatum.first_release_date) : null, - }, slug: rom.slug, platform_id: rom.platform_id, platform_display_name: rom.platform_display_name, @@ -109,17 +73,9 @@ export default class RommIntegration implements PluginType fs_size_bytes: rom.fs_size_bytes, local: false, missing: rom.missing_from_fs, - igdb_id: rom.igdb_id, - ra_id: rom.ra_id, - metadata: { - age_ratings: rom.metadatum.age_ratings, - genres: rom.metadatum.genres, - companies: rom.metadatum.companies, - game_modes: rom.metadatum.game_modes, - player_count: rom.metadatum.player_count, - average_rating: rom.metadatum.average_rating, - first_release_date: rom.metadatum.first_release_date ? new Date(rom.metadatum.first_release_date) : null - } + genres: rom.metadatum.genres, + companies: rom.metadatum.companies, + release_date: rom.metadatum.first_release_date ? new Date(rom.metadatum.first_release_date) : undefined }; const userData = await getCurrentUserApiUsersMeGet(); @@ -152,75 +108,62 @@ export default class RommIntegration implements PluginType return detailed; } - async load (ctx: PluginLoadingContextType) + async setup () { this.isSteamDeck = isSteamDeckGameMode(); - ctx.setProgress(0, "Logging Into Romm"); - await this.updateClient(ctx.config); - await checkLoginAndRefreshRomm(); - await this.updateClient(ctx.config); + await this.updateClient(); + } + load (ctx: PluginContextType) + { ctx.hooks.games.fetchGames.tapPromise(desc.name, async ({ query, games }) => { - if (!await this.checkRemote()) return; if (((!query.platform_source || query.platform_source === 'romm') || !!query.collection_id) && (!query.source || query.source === 'romm')) { + + const orderByMap: Record = { + added: "created_at", + activity: "created_at", + name: "name" + }; + const rommGames = await getRomsApiRomsGet({ query: { platform_ids: query.platform_id ? [query.platform_id] : undefined, collection_id: query.collection_id, limit: query.limit, offset: query.offset, - order_by: this.orderByMap[query.orderBy ?? ''], - with_filter_values: false, - genres: query.genres, - genres_logic: "all", - age_ratings: query.age_ratings, - search_term: query.search, + order_by: orderByMap[query.orderBy ?? ''] }, throwOnError: true }); - games.push(...rommGames.data.items.map(g => { - const game: FrontEndGameTypeWithIds = { - ...this.convertRomToFrontend(g), - igdb_id: g.igdb_id, - ra_id: g.ra_id - }; - return game; + return this.convertRomToFrontend(g); })); } }); - ctx.hooks.games.fetchFilters.tapPromise(desc.name, async ({ filters, source }) => - { - if (!await this.checkRemote()) return; - if (source && source !== 'romm') return; - - const rommFilters = await getRomFiltersApiRomsFiltersGet({ throwOnError: true }); - rommFilters.data.age_ratings.forEach(r => filters.age_ratings.add(r)); - rommFilters.data.companies.forEach(r => filters.companies.add(r)); - rommFilters.data.languages.forEach(r => filters.languages.add(r)); - rommFilters.data.player_counts.forEach(r => filters.player_counts.add(r)); - rommFilters.data.genres.forEach(r => filters.genres.add(r)); - }); - ctx.hooks.auth.loginComplete.tapPromise(desc.name, async ({ service }) => { - if (!await this.checkRemote()) return; if (service !== 'romm') return; - await this.updateClient(ctx.config); + await this.updateClient(); }); - ctx.hooks.games.fetchGame.tapPromise(desc.name, async ({ source, id }) => + ctx.hooks.games.fetchGame.tapPromise(desc.name, async ({ source, id, localGame }) => { - if (!await this.checkRemote()) return; if (source !== 'romm') return; const rom = await getRomApiRomsIdGet({ path: { id: Number(id) } }); if (rom.data) { const romGame = await this.convertRomToFrontendDetailed(rom.data); + if (localGame) + { + return { + ...romGame, + ...localGame, + }; + } return romGame; } @@ -229,7 +172,6 @@ export default class RommIntegration implements PluginType ctx.hooks.games.fetchDownloads.tapPromise(desc.name, async ({ source, id }) => { - if (!await this.checkRemote()) return; if (source !== 'romm') return; const rom = (await getRomApiRomsIdGet({ path: { id: Number(id) }, throwOnError: true })).data; @@ -239,9 +181,8 @@ export default class RommIntegration implements PluginType const files = await Promise.all(rom.files.map(async f => { - getRomContentApiRomsIdContentFileNameGet; const file: DownloadFileEntry = { - url: new URL(`${config.get('rommAddress')}/api/roms/${f.id}/files/content/${f.file_name}`), + url: new URL(`${config.get('rommAddress')}/api/romsfiles/${f.id}/content/${f.file_name}`), file_name: f.file_name, file_path: f.file_path, size: f.file_size_bytes, @@ -250,21 +191,8 @@ export default class RommIntegration implements PluginType return file; })); - let extract_path: string | undefined = undefined; - let path_fs = path.join(rom.fs_path, rom.fs_name); - if (files.length === 1) - { - if (isArchive(files[0].file_name)) - { - extract_path = '.'; - path_fs = path.join(rom.fs_path, rom.slug ?? rom.fs_name_no_ext); - } - } - const info: DownloadInfo = { platform: { - source: 'romm', - id: String(rommPlatform.id), slug: rommPlatform.slug, name: rommPlatform.name, family_name: rommPlatform.family_name ?? undefined @@ -276,24 +204,21 @@ export default class RommIntegration implements PluginType ra_id: rom.ra_id ?? undefined, summary: rom.summary ?? undefined, name: rom.name ?? "Unknown", - path_fs, + path_fs: path.join(rom.fs_path, rom.fs_name), source_id: String(rom.id), slug: rom.slug ?? undefined, system_slug: rommPlatform.slug, metadata: rom.metadatum, files, - auth: await this.getAuthToken(ctx.config), - extract_path, - id: "romm" + auth: await this.getAuthToken() }; - return [info]; + return info; }); ctx.hooks.emulators.fetchBiosDownload.tapPromise(desc.name, async ({ systems, biosFolder }) => { - if (!await this.checkRemote()) return; const files: DownloadFileEntry[] = []; const allRommPlatforms = await this.getAllRommPlatforms(); @@ -319,22 +244,21 @@ export default class RommIntegration implements PluginType } } - if (files.length > 0) return { files, auth: await this.getAuthToken(ctx.config) }; + if (files.length > 0) return { files, auth: await this.getAuthToken() }; }); ctx.hooks.games.fetchRecommendedGamesForGame.tapPromise(desc.name, async ({ game, games }) => { - if (!await this.checkRemote()) return; const rommPlatforms = await this.getAllRommPlatforms(); if (rommPlatforms) { const rommPlatform = rommPlatforms.find(p => p.slug === game.platform_slug); if (rommPlatform) { - const rommGames = await getRomsApiRomsGet({ query: { genres: game.metadata.genres, genres_logic: 'any' } }); + const rommGames = await getRomsApiRomsGet({ query: { genres: game.genres, genres_logic: 'any' } }); if (rommGames.data) { - games.push(...rommGames.data.items.map(g => ({ ...this.convertRomToFrontend(g) }))); + games.push(...rommGames.data.items.map(g => ({ ...this.convertRomToFrontend(g), metadata: g.metadatum }))); } } } @@ -342,7 +266,7 @@ export default class RommIntegration implements PluginType ctx.hooks.games.fetchRecommendedGamesForEmulator.tapPromise(desc.name, async ({ emulator, games, systems }) => { - if (!await this.checkRemote()) return; + const rommPlatforms = await this.getAllRommPlatforms(); const systemsRommSlugSet = new Set(systems.filter(s => s.romm_slug).map(s => s.romm_slug!)); if (rommPlatforms) @@ -372,7 +296,6 @@ export default class RommIntegration implements PluginType ctx.hooks.games.fetchPlatform.tapPromise(desc.name, async ({ source, id }) => { - if (!await this.checkRemote()) return; if (source !== 'romm') return; const { data: rommPlatform } = await getPlatformApiPlatformsIdGet({ path: { id: Number(id) } }); if (rommPlatform) @@ -395,13 +318,7 @@ export default class RommIntegration implements PluginType ctx.hooks.games.fetchPlatforms.tapPromise(desc.name, async ({ platforms }) => { - if (!await this.checkRemote()) return; - const rommPlatforms = await this.getAllRommPlatforms().catch(e => - { - console.error(e); - return undefined; - }); - + const rommPlatforms = await this.getAllRommPlatforms(); if (rommPlatforms) { const frontEndPlatforms = await Promise.all(rommPlatforms.map(async p => @@ -435,139 +352,16 @@ export default class RommIntegration implements PluginType } }); - ctx.hooks.games.prePlay.tapPromise(desc.name, async ({ source, id, saveFolderSlots, setProgress }) => + ctx.hooks.games.updatePlayed.tapPromise(desc.name, async ({ source, id }) => { - if (!await this.checkRemote()) return; - if (source !== 'romm' || !ctx.config.get('savesSync')) return; - if (!saveFolderSlots) return; - - for await (const [slot, { cwd }] of Object.entries(saveFolderSlots)) - { - setProgress(0, "saves"); - - const saveFiles = await getSavesSummaryApiSavesSummaryGet({ query: { rom_id: Number(id) } }); - if (saveFiles.error) - { - console.error(saveFiles.error); - } else - { - const rommSlot = saveFiles.data.slots.find(s => s.slot === 'gameflow' && s.latest.file_name_no_tags === slot); - if (rommSlot) - { - const auth = await this.getAuthToken(ctx.config); - const headers: Record = {}; - if (auth) - headers['Authorization'] = auth; - - const saveResponse = await fetch(`${config.get('rommAddress')}${rommSlot.latest.download_path}`, { headers }); - if (!saveResponse.ok) - { - console.error("Error downloading save", saveResponse.statusText); - return; - } - - const saveArchive = new Bun.Archive(await saveResponse.blob()); - setProgress(50, "saves"); - const count = await saveArchive.extract(cwd); - setProgress(100, "saves"); - console.log("Loaded", count, "save files"); - } - } - - setProgress(100, "saves"); - await Bun.sleep(1000); - } - }); - - // Should run after emulators decide on saves - ctx.hooks.games.postPlay.tapPromise({ name: desc.name, stage: 10 }, async ({ source, id, validChangedSaveFiles, command }) => - { - if (!await this.checkRemote()) return; - if (source !== 'romm' || !ctx.config.get('savesSync')) return; - - const sourceValidation = await validateGameSource(source, id); - if (!sourceValidation.valid) - { - console.warn("Invalid Source", sourceValidation.reason, "Skipping updates"); - return; - } - - /*const finalSavePaths = validChangedSaveFiles.filter(f => !f.shared && !f.isGlob).flatMap(s => Array.isArray(s.subPath) ? s.subPath.map(p => ({ cwd: s.cwd, subPath: p })) : [{ cwd: s.cwd, subPath: s.subPath }]); - - const saveFiles = await getSavesSummaryApiSavesSummaryGet({ query: { rom_id: Number(id) } }); - if (saveFiles.error) - { - console.error(saveFiles.error); - } else if (saveFolderPath) - { - for (let i = 0; i < saveFiles.data.slots.length; i++) - { - const slot = saveFiles.data.slots[i]; - const savePath = path.join(saveFolderPath, slot.slot ?? '', `${slot.latest.file_name_no_tags}.${slot.latest.file_extension}`); - if (await fs.exists(savePath)) - { - const stat = await fs.stat(savePath); - if (stat.mtimeMs > new Date(slot.latest.updated_at).getTime()) - { - const subPath = path.join(slot.slot ?? '', `${slot.latest.file_name_no_tags}.${slot.latest.file_extension}`); - if (!finalSavePaths.some(f => f.subPath === subPath)) - { - // Add newer files to the list, maybe they were changed offscreen. - finalSavePaths.push({ subPath, cwd: saveFolderPath }); - } - } - } - } - }*/ - - const finalSavePaths = Object.entries(validChangedSaveFiles).filter(([slot, change]) => !change.isGlob && !change.shared); - - if (finalSavePaths.length > 0) - { - console.log("Files Changed:", finalSavePaths.map(([slot, change]) => Array.isArray(change.subPath) ? change.subPath.join(',') : change.subPath)?.join(", ")); - - await Promise.all(finalSavePaths.map(async ([slot, change]) => - { - const savesArray = Array.isArray(change.subPath) ? change.subPath : [change.subPath]; - - // TODO: handle directories - const archive = new Bun.Archive(Object.fromEntries(savesArray.map(s => [s, Bun.file(path.join(change.cwd, s))]))); - const data: FormData = new FormData(); - data.append('saveFile', await archive.blob(), slot); - - const url = new URL(`${config.get('rommAddress')}/api/saves`); - url.searchParams.set('rom_id', id); - url.searchParams.set('slot', slot); - url.searchParams.set('autocleanup', "true"); - url.searchParams.set('autocleanup_limit', "2"); - if (command.emulator) - url.searchParams.set('emulator', command.emulator); - url.searchParams.set('overwrite', "true"); - - const auth = await this.getAuthToken(ctx.config); - const headers: Record = {}; - if (auth) - headers['Authorization'] = auth; - - const response = await fetch(url, { - body: data, - method: "POST", - headers - }); - if (!response.ok) console.error(response.statusText); - })); - - events.emit('notification', { message: "Saves Uploaded", icon: 'upload', type: "success" }); - } - + if (source !== 'romm') return false; const resp = await updateRomUserApiRomsIdPropsPut({ path: { id: Number(id) }, body: { update_last_played: true } }); if (resp.error) console.error(resp.error); - events.emit('notification', { message: "Updated Played", type: "success", icon: "clock" }); + return resp.response.ok; }); ctx.hooks.games.fetchCollections.tapPromise(desc.name, async ({ collections }) => { - if (!await this.checkRemote()) return; const rommCollections = await getCollectionsApiCollectionsGet(); if (rommCollections.response.ok && rommCollections.data) { @@ -588,7 +382,6 @@ export default class RommIntegration implements PluginType ctx.hooks.games.fetchCollection.tapPromise(desc.name, async ({ source, id }) => { - if (!await this.checkRemote()) return; if (source !== 'romm') return; const collection = await getCollectionApiCollectionsIdGet({ path: { id: Number(id) } }); if (collection.data) @@ -605,35 +398,11 @@ export default class RommIntegration implements PluginType }); - ctx.hooks.games.platformLookup.tapPromise(desc.name, async ({ source, id, slug }) => + ctx.hooks.games.platformLookup.tapPromise(desc.name, async ({ source, id }) => { - if (!await this.checkRemote()) return; - let platform: PlatformSchema | undefined = undefined; - - if (id && source) - { - if (source !== 'romm') return; - const platforms = await this.getAllRommPlatforms(); - platform = platforms.find(p => p.id === Number(id)); - - } else if (slug) - { - const platforms = await this.getAllRommPlatforms(); - platform = platforms.find(p => p.slug === slug); - } - - if (!platform) return; - return { slug: platform?.slug, url_logo: platform.url_logo, name: platform.display_name, family_name: platform.family_name ?? undefined }; - }); - - ctx.hooks.games.searchGame.tapPromise(desc.name, async ({ source, igdb_id, ra_id }) => - { - if (!await this.checkRemote()) return; if (source !== 'romm') return; - const roms = await getRomByMetadataProviderApiRomsByMetadataProviderGet({ query: { igdb_id, ra_id } }); - if (roms.error) throw roms.error; - if (!roms.data) return; - return this.convertRomToFrontendDetailed(roms.data); + const platforms = await this.getAllRommPlatforms(); + return platforms.find(p => p.id === Number(id)); }); } } \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/package.json b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/package.json deleted file mode 100644 index 644c332..0000000 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "com.simeonradivoev.gameflow.store", - "displayName": "Gameflow Store Integration", - "version": "0.0.1", - "description": "The internal gameflow store integration. This is the logic of the store that uses the data only store package", - "main": "./store.ts", - "category": "sources", - "canDisable": false, - "keywords": [ - "internal", - "store" - ] -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/services.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/services.ts deleted file mode 100644 index 4935dd7..0000000 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/services.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { getStoreFolder } from "@/bun/api/store/services/gamesService"; -import os from 'node:os'; -import path from "node:path"; -import * as appSchema from '@schema/app'; -import * as emulatorSchema from '@schema/emulators'; -import { config, db, emulatorsDb, plugins } from "@/bun/api/app"; -import { and, eq } from "drizzle-orm"; -import { getOrCached } from "@/bun/api/cache"; -import { Glob } from "bun"; -import { shuffleInPlace } from "@/bun/utils"; -import mustache from "mustache"; -import { getEmulatorDownload, getEmulatorPath } from "@/bun/api/store/services/emulatorsService"; -import fs from "node:fs/promises"; -import { CommandEntry, EmulatorSourceEntryType, EmulatorSystem, FrontEndEmulator, FrontEndFilterSets, FrontEndGameType, FrontEndGameTypeDetailed, SaveFileChange, EmulatorDownloadInfoType, StoreDownloadType, StoreGameType, EmulatorPackageType, EmulatorDownloadInfoSchema, StoreGameSchema } from "@simeonradivoev/gameflow-sdk/shared"; -import { isUrl } from "@/shared/utils"; - -export async function getStoreGames (gamesManifest: any[], filter?: { limit?: number; offset?: number; }) -{ - const offset = filter?.offset ?? 0; - const limit = Math.min(50, filter?.limit ?? 10); - - const games = await Promise.all(gamesManifest.slice(offset, Math.min(offset + limit, gamesManifest.length)).map((e: any) => - { - return fetch(e.url).then(e => e.json()).then(game => StoreGameSchema.parseAsync(JSON.parse(atob(game.content.replace(/\n/g, ""))))); - })); - - return games; -} - -export async function getStoreGame (id: string) -{ - const file = Bun.file(path.join(getStoreFolder(), 'buckets', 'games', `${id}.json`)); - if (!(await file.exists())) return undefined; - const game = file - .json() - .then(g => StoreGameSchema.parseAsync(g)) - .then(g => ({ ...g, id })); - return game; -} - -function convertStoreMediaToPath (c: string) -{ - if (isUrl(c)) - { - return `/api/romm/image?url=${encodeURIComponent(c)}`; - } else - { - return `/api/store/media/${c}`; - } -} - -export async function convertStoreToFrontend (id: string, storeGame: StoreGameType): Promise -{ - const validDownloads = getValidDownloads(storeGame); - - let platform_slug: string | null = null; - let platform_id: number | null = null; - let platform_display_name: string | null = null; - let path_platform_cover: string | null = null; - - if (validDownloads.length > 0 && validDownloads[0].system) - { - let system = validDownloads[0].system.split(':')[0]; - if (system === 'win32') system = 'win'; - - const localPlatform = await db.query.platforms.findFirst({ where: eq(appSchema.platforms.slug, system), columns: { id: true, slug: true, name: true } }); - if (localPlatform) - { - platform_id = localPlatform.id; - platform_slug = localPlatform.slug; - path_platform_cover = `/api/romm/platform/local/${localPlatform.id}/cover`; - platform_display_name = localPlatform.name; - } - - if (platform_slug === null) - { - const rommSystem = await emulatorsDb.query.systemMappings.findFirst({ - where: and(eq(emulatorSchema.systemMappings.sourceSlug, system), eq(emulatorSchema.systemMappings.source, 'romm')) - }); - - if (rommSystem?.system) - { - const platformDef = await emulatorsDb.query.systems.findFirst({ - where: eq(emulatorSchema.systems.name, rommSystem?.system), - columns: { fullname: true } - }); - - platform_slug = rommSystem.system; - platform_display_name = platformDef?.fullname ?? null; - path_platform_cover = `/api/romm/image/romm/assets/platforms/${rommSystem.sourceSlug}.svg`; - - } else - { - const platformDef = await emulatorsDb.query.systems.findFirst({ - where: eq(emulatorSchema.systems.name, system), - columns: { fullname: true } - }); - - platform_slug = system; - platform_display_name = platformDef?.fullname ?? null; - } - - platform_slug ??= system; - } - } - - - const game: FrontEndGameType = { - platform_display_name, - path_platform_cover, - id: { source: 'store', id: id }, - source: null, - source_id: null, - path_fs: null, - path_covers: storeGame.covers?.map(convertStoreMediaToPath) ?? [], - last_played: null, - updated_at: new Date(), - slug: id, - name: storeGame.name, - platform_id, - platform_slug, - paths_screenshots: storeGame.screenshots?.map((s: string) => `/api/romm/image?url=${encodeURIComponent(s)}`) ?? [], - metadata: { - first_release_date: typeof storeGame.first_release_date === 'number' ? new Date(storeGame.first_release_date) : storeGame.first_release_date ?? null - } - }; - - return game; -} - - -export async function convertStoreToFrontendDetailed (id: string, storeGame: StoreGameType): Promise -{ - const validDownloads = getValidDownloads(storeGame); - let size: number | null = null; - if (validDownloads.length > 0 && validDownloads[0].url) - { - try - { - const fileResponse = await fetch(validDownloads[0]?.url, { method: 'HEAD' }); - size = Number(fileResponse.headers.get('content-length')); - } catch (error) - { - console.error(error); - } - } - - const detailed: FrontEndGameTypeDetailed = { - ...await convertStoreToFrontend(id, storeGame), - summary: storeGame.description, - fs_size_bytes: size, - missing: false, - local: false, - version: storeGame.version, - igdb_id: storeGame.igdb_id ?? null, - ra_id: storeGame.ra_id ?? null, - metadata: { - genres: storeGame.genres ?? [], - companies: storeGame.companies ?? [], - game_modes: [], - age_ratings: [], - player_count: storeGame.player_count ?? null, - average_rating: null, - first_release_date: typeof storeGame.first_release_date === 'number' ? new Date(storeGame.first_release_date) : storeGame.first_release_date ?? null - } - }; - - return detailed; -} - -export function getValidDownloads (game: StoreGameType, downloadId?: string) -{ - const downloads = Object.entries(game.downloads).map(([k, d]) => ({ id: k, ...d })); - const supportedDownloads = downloads.filter(d => d.type === 'direct'); - - if (downloadId) - { - return supportedDownloads.filter(d => d.id === downloadId); - } else - { - return supportedDownloads.filter(d => - { - if (d.system === `${process.platform}:${process.arch}`) return true; - - // TODO: Add linux proton support - //if (process.platform === 'linux' && d.system === `win32:${process.arch}`) return true; - - // emulator fallback - return !d.system.includes(':'); - }).toSorted((a, b) => - { - const bScore = b.system.includes(':') ? 0 : 1; - const aScore = a.system.includes(':') ? 0 : 1; - - return bScore - aScore; - }); - } -} - -export async function getShuffledStoreGames () -{ - return getOrCached('shuffled-store-games', async () => - { - const files = new Glob(path.join(getStoreFolder(), 'buckets', 'games', '*.json')).scan(); - const allGamePaths = await Array.fromAsync(files); - const allStoreGames = await Promise.all(allGamePaths.map(p => Bun.file(p).json().then(g => StoreGameSchema.parseAsync(g)).then(g => ({ ...g, id: path.basename(p, '.json') })))); - shuffleInPlace(allStoreGames, Math.round(new Date().getTime() / 1000 / 60 / 60)); - return allStoreGames; - }, { expireMs: 1000 / 60 / 60 }); -} - -export async function buildFilters (filters: FrontEndFilterSets) -{ - const filtersFile = Bun.file(path.join(getStoreFolder(), 'manifests', 'filters.json')); - if (!await filtersFile.exists()) return; - const storeFilters = await filtersFile.json(); - - storeFilters.genres?.forEach((g: string) => filters.genres.add(g)); - storeFilters.age_ratings?.forEach((g: string) => filters.age_ratings.add(g)); - if (storeFilters.player_count) - filters.player_counts.add(storeFilters.player_count); - storeFilters.companies?.forEach((g: string) => filters.companies.add(g)); -} - -function getAppData () -{ - if (process.platform === "win32") return process.env.APPDATA!; - if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support"); - // linux - return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"); -} - -function getLocalAppData () -{ - if (process.platform === "win32") return process.env.LOCALAPPDATA!; - if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Caches"); - // Linux / Unix - return process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"); -} - -export function buildSaves (command: CommandEntry, storeGame: StoreGameType, download?: StoreDownloadType) -{ - let saveFileGlobs: Record | undefined = undefined; - if (download && download.saves) - { - saveFileGlobs = download.saves; - - } else if (storeGame.saves) - { - const platformSaves = storeGame.saves[`${process.platform}:${process.arch}`]; - if (platformSaves) - { - saveFileGlobs = platformSaves; - } - } - - const view = { - GAMEDIR: command.startDir, - HOMEDIR: os.homedir(), - TMPDIR: os.tmpdir(), - APPDATA: getAppData(), - LOCALAPPDATA: getLocalAppData(), - }; - - if (!saveFileGlobs) return; - - return Object.entries(saveFileGlobs).map(([slot, save]) => - { - const cwd = mustache.render(save.cwd, view); - const change: SaveFileChange = { - cwd, - shared: false, - isGlob: true, - subPath: save.globs - }; - return [slot, change] as [string, SaveFileChange]; - }); -} - -export async function convertStoreEmulatorToFrontend (emulator: EmulatorPackageType, systems: EmulatorSystem[]) -{ - const execPaths: EmulatorSourceEntryType[] = []; - await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: emulator.name, sources: execPaths }); - - const em: FrontEndEmulator = { - name: emulator.name, - logo: emulator.logo, - systems, - gameCount: 0, - validSources: execPaths, - integrations: [], - source: "store" - }; - - return em; -} - -export async function getExistingStoreEmulatorDownload (emulator: EmulatorPackageType): Promise<(EmulatorDownloadInfoType & { hasUpdate: boolean; }) | undefined> -{ - const existingPackagePath = `${getEmulatorPath(emulator.name)}.json`; - if (await fs.exists(existingPackagePath)) - { - const existingPackage = await EmulatorDownloadInfoSchema.parseAsync(await Bun.file(existingPackagePath).json()); - const download = await getEmulatorDownload(emulator, existingPackage.type).catch(d => undefined); - if (!download) return { ...existingPackage, hasUpdate: false }; - if (download.info.version) - { - if (existingPackage.version !== download.info.version) return { ...existingPackage, hasUpdate: true }; - } else if (existingPackage.id !== download.info.id) - { - return { ...existingPackage, hasUpdate: true }; - } - - return { ...existingPackage, hasUpdate: false }; - } - - // this should only happen if download info is missing maybe manually deleted or wasn't saved. - return undefined; -} - -export async function buildLaunchCommand (ctx: { gamePath: string; systemSlug: string; mainGlob?: string | null; }): Promise -{ - if (ctx.systemSlug !== 'win' && ctx.systemSlug !== 'linux' && ctx.systemSlug !== 'mac') return; - const downloadPath = config.get('downloadPath'); - const gamePathAbsolute = path.join(downloadPath, ctx.gamePath); - if (!(await fs.exists(gamePathAbsolute))) return; - const gamePathStat = await fs.stat(gamePathAbsolute); - - if (gamePathStat.isDirectory()) - { - let mainGlob = ctx.mainGlob; - if (!mainGlob && ctx.systemSlug === 'win') mainGlob = '**/*.exe'; - if (!mainGlob) return; - const fileGlob = new Glob(mainGlob); - for await (const file of fileGlob.scan({ cwd: path.join(downloadPath, ctx.gamePath) })) - { - return { - startDir: path.join(downloadPath, ctx.gamePath, path.dirname(file)), - command: [`./${path.basename(file)}`], - id: `store-${process.platform}`, - shell: false, - valid: true, - metadata: { - romPath: path.join(downloadPath, ctx.gamePath, file) - } - }; - } - - } else - { - return { - startDir: path.join(downloadPath, path.dirname(ctx.gamePath)), - command: [`./${path.basename(ctx.gamePath)}`], - id: `store-${process.platform}`, - valid: true, - shell: false, - metadata: { - romPath: path.join(downloadPath, ctx.gamePath), - } - }; - } -} \ No newline at end of file diff --git a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts b/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts deleted file mode 100644 index 92ce1f9..0000000 --- a/src/bun/api/plugins/builtin/sources/com.simeonradivoev.gameflow.store/store.ts +++ /dev/null @@ -1,459 +0,0 @@ -import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; -import desc from './package.json'; -import path, { } from 'node:path'; -import { buildStoreFrontendEmulatorSystems, getAllStoreEmulatorPackages, getStoreEmulatorPackage, getStoreFolder } from "@/bun/api/store/services/gamesService"; -import { Glob, pathToFileURL, which } from "bun"; -import { and, eq } from "drizzle-orm"; -import * as emulatorSchema from '@schema/emulators'; - -import { config, emulatorsDb, taskQueue } from "@/bun/api/app"; -import fs from "node:fs/promises"; -import { getSourceGameDetailed } from "@/bun/api/games/services/utils"; -import EnsureStore from "@/bun/api/jobs/ensure-store"; -import { getEmulatorDownload, getEmulatorPath } from "@/bun/api/store/services/emulatorsService"; -import { buildFilters, buildLaunchCommand, buildSaves, convertStoreEmulatorToFrontend, convertStoreToFrontend, convertStoreToFrontendDetailed, getExistingStoreEmulatorDownload, getShuffledStoreGames, getStoreGame, getValidDownloads } from "./services"; -import { DownloadInfo, FrontEndEmulatorDetailed, FrontEndGameTypeWithIds } from "@simeonradivoev/gameflow-sdk/shared"; -import { isUrl } from "@/shared/utils"; -import { Downloader } from "@/bun/utils/downloader"; -import { ensureDir, move } from "fs-extra"; -import StreamZip from "node-stream-zip"; -import { path7za } from "7zip-bin"; -import Seven from 'node-7z'; - -export default class StoreIntegration implements PluginType -{ - eventsNames = [{ id: 'updateStore', title: "Update Store", description: "Update the Store Manifest", action: "Update" }]; - - async onEvent (e: string) - { - switch (e) - { - case 'updateStore': - await taskQueue.enqueue(EnsureStore.id, new EnsureStore()); - return { reload: true }; - } - } - - async setup (ctx: PluginLoadingContextType) - { - console.log("Store Directory is ", getStoreFolder()); - ctx.setProgress(0, "Updating Store"); - await taskQueue.enqueue(EnsureStore.id, new EnsureStore()); - } - - async load (ctx: PluginLoadingContextType) - { - await this.setup(ctx); - - ctx.hooks.store.fetchDownload.tapPromise(desc.name, async ({ id }) => - { - const emulatorPackage = await getStoreEmulatorPackage(id); - if (!emulatorPackage) return; - const downloadInfo = await getExistingStoreEmulatorDownload(emulatorPackage); - return downloadInfo; - }); - - ctx.hooks.store.fetchEmulator.tapPromise(desc.name, async ({ id }) => - { - const emulatorPackage = await getStoreEmulatorPackage(id); - if (!emulatorPackage) return undefined; - - const systems = await buildStoreFrontendEmulatorSystems(emulatorPackage); - - const emulatorScreenshotsPath = path.join(getStoreFolder(), "media", "screenshots", id); - const screenshots = await fs.exists(emulatorScreenshotsPath) ? await fs.readdir(emulatorScreenshotsPath) : []; - const biosDirPath = path.join(config.get('downloadPath'), 'bios', id); - const biosFiles = await fs.exists(biosDirPath) ? await fs.readdir(biosDirPath) : []; - const storeDownloadInfo = await getExistingStoreEmulatorDownload(emulatorPackage); - - const emulator: FrontEndEmulatorDetailed = { - name: emulatorPackage.name, - description: emulatorPackage.description, - source: "store", - systems, - validSources: [], - screenshots: screenshots.map(s => `/api/store/screenshot/emulator/${id}/${s}`), - gameCount: 0, - homepage: emulatorPackage.homepage, - downloads: (await Promise.all(emulatorPackage.downloads?.[`${process.platform}:${process.arch}`].map(async d => - { - const download = await getEmulatorDownload(emulatorPackage, d.type).catch(e => undefined); - return download?.info; - }) ?? [])).filter(d => !!d).map(d => ({ name: d.type, type: d.type, version: d.version })), - logo: emulatorPackage.logo, - biosRequirement: emulatorPackage.bios, - bios: biosFiles, - integrations: [], - storeDownloadInfo: storeDownloadInfo - }; - - return emulator; - }); - - ctx.hooks.store.fetchEmulators.tapPromise(desc.name, async ({ emulators, search }) => - { - const emulatesParsed = await getAllStoreEmulatorPackages(); - emulators.push(...await Promise.all(emulatesParsed - .filter(e => - { - if (!e.os.includes(process.platform as any)) return false; - if (search) - { - if (e.name.toLocaleLowerCase().includes(search) || e.systems.some(s => s.toLocaleLowerCase().includes(search)) || e.keywords?.some(k => k.toLocaleLowerCase().includes(search))) - { - return true; - } - - return false; - } - return true; - }) - .map(async (emulator) => - { - const systems = await buildStoreFrontendEmulatorSystems(emulator); - return convertStoreEmulatorToFrontend(emulator, systems); - }))); - }); - - ctx.hooks.games.prePlay.tapPromise(desc.name, async ({ source, id, saveFolderSlots, command }) => - { - if (source !== 'store') return; - const storeGame = await getStoreGame(id); - const localGame = await getSourceGameDetailed(source, id); - - if (!localGame || !storeGame) return; - if (!localGame.version_source) return; - - const download = storeGame.downloads[localGame.version_source]; - const saves = buildSaves(command, storeGame, download); - - saves?.forEach(([slot, save]) => saveFolderSlots[slot] = { cwd: save.cwd }); - }); - - ctx.hooks.games.postPlay.tapPromise(desc.name, async ({ validChangedSaveFiles, source, id, command }) => - { - if (source !== 'store') return; - const storeGame = await getStoreGame(id); - const localGame = await getSourceGameDetailed(source, id); - - if (!localGame || !storeGame) return; - if (!localGame.version_source) return; - - const download = storeGame.downloads[localGame.version_source]; - - const saves = buildSaves(command, storeGame, download); - saves?.forEach(([key, val]) => validChangedSaveFiles[key] = val); - }); - - ctx.hooks.emulators.findEmulatorSource.tapPromise(desc.name, async ({ emulator, sources }) => - { - const emulatorPackage = await getStoreEmulatorPackage(emulator); - if (!emulatorPackage) return undefined; - const storeDownloadInfo = await getExistingStoreEmulatorDownload(emulatorPackage); - if (!storeDownloadInfo) return; - const emulatorPath = getEmulatorPath(emulator); - if (!await fs.exists(emulatorPath)) return; - const validDownload = emulatorPackage.downloads?.[`${process.platform}:${process.arch}`].find(d => d.type === storeDownloadInfo?.type); - if (!validDownload || !validDownload.bin) return; - const glob = new Glob(validDownload.bin); - const files = await Array.fromAsync(glob.scan({ cwd: emulatorPath })); - // es-de also searches for store executables so there might be duplicates, check first. - if (files.length > 0 && !sources.find(s => s.type === 'store')) - { - sources.push({ binPath: path.join(emulatorPath, files[0]), exists: true, rootPath: emulatorPath, type: 'store' }); - } - }); - - ctx.hooks.emulators.emulatorPostInstall.tapPromise({ name: desc.name, emulator: 'UMU' }, async ({ path: emulatorPath }) => - { - const pathStat = await fs.stat(emulatorPath); - if (pathStat.isFile()) - { - await fs.chmod(emulatorPath, 0o755); - } - }); - - ctx.hooks.games.postInstall.tapPromise(desc.name, async ({ source, id, files, info }) => - { - if (source !== 'store') return; - if (files.length === 1) - { - const command = await buildLaunchCommand({ gamePath: files[0], systemSlug: info.system_slug, mainGlob: info.main_glob }); - if (command && command.metadata.romPath) - { - await fs.chmod(command.metadata.romPath, 0o755); - } - } - }); - - ctx.hooks.games.buildLaunchCommands.tapPromise({ name: desc.name, before: 'com.simeonradivoev.gameflow.es' }, async ({ gamePath, source, sourceId, systemSlug, mainGlob }) => - { - if (source !== 'store' || !gamePath) return; - const command = await buildLaunchCommand({ gamePath, systemSlug, mainGlob }); - if (!command) return; - return [command]; - }); - - ctx.hooks.games.fetchFilters.tapPromise(desc.name, async ({ filters, source }) => - { - if (!source || source !== 'store') return; - await buildFilters(filters); - }); - - ctx.hooks.store.fetchFeaturedGames.tapPromise(desc.name, async ({ games }) => - { - const allGames = await getShuffledStoreGames(); - const convertedGames = await Promise.all(allGames.slice(0, 3).map(async g => - { - return convertStoreToFrontendDetailed(g.id, g); - })); - games.push(...convertedGames); - }); - - ctx.hooks.games.fetchGames.tapPromise(desc.name, async ({ query, games }) => - { - if (!query.source || query.source !== 'store') return; - if (query.collection_source || query.collection_id) return; - - const shuffledGames = await getShuffledStoreGames(); - const storeGames = await Promise.all(shuffledGames.filter(g => - { - if (query.search) - return path.basename(g.name).toLocaleLowerCase().includes(query.search.toLocaleLowerCase()); - return true; - }) - .slice(query.offset ?? 0, Math.min((query.offset ?? 0) + (query.limit ?? 50), shuffledGames.length)) - .map(async (e) => - { - const game: FrontEndGameTypeWithIds = { - ...await convertStoreToFrontend(e.id, e), - igdb_id: e.igdb_id ?? null, - ra_id: e.ra_id ?? null - }; - return game; - })); - games.push(...storeGames.filter(g => g !== undefined)); - }); - - ctx.hooks.games.fetchRecommendedGamesForGame.tapPromise(desc.name, async ({ game, games }) => - { - const esSystem = game.platform_slug ? await emulatorsDb.query.systemMappings.findFirst({ where: and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.sourceSlug, game.platform_slug)), columns: { system: true } }) : undefined; - - const shuffledGames = await getShuffledStoreGames(); - const storeGames = await Promise.all(shuffledGames - .filter(g => - { - if (esSystem) - { - if (Object.values(g.downloads).some(d => d.system === esSystem.system)) return true; - } - - return false; - }) - .map(async (e) => - { - return convertStoreToFrontend(e.id, e); - })); - - if (storeGames) - { - games.push(...storeGames.slice(0, 3)); - } - }); - - ctx.hooks.games.fetchRecommendedGamesForEmulator.tapPromise(desc.name, async ({ emulator, games, systems }) => - { - const systemsIdSet = new Set(systems.map(s => s.id)); - const gamesManifest = await getShuffledStoreGames(); - const storeGames = await Promise.all(gamesManifest - .filter(g => Object.values(g.downloads).some(d => systemsIdSet.has(d.system))) - .map(async (e) => - { - - return convertStoreToFrontend(e.id, e); - })); - - games.push(...storeGames.filter(g => g !== undefined).slice(0, 3)); - }); - - ctx.hooks.games.fetchGame.tapPromise(desc.name, async ({ source, id }) => - { - if (source !== 'store') return; - const storeGame = await getStoreGame(id); - if (storeGame) - { - return convertStoreToFrontendDetailed(id, storeGame); - } - }); - - ctx.hooks.games.fetchDownloads.tapPromise(desc.name, async ({ source, id, downloadId }) => - { - if (source !== 'store') return; - const game = await getStoreGame(id); - if (!game) throw new Error("Missing Store Game"); - - const validDownloads = getValidDownloads(game, downloadId); - - return validDownloads.map(validDownload => - { - let system = validDownload.system.split(":")[0]; - if (system === 'win32') system = 'win'; - - const info: DownloadInfo = { - id: validDownload.id, - coverUrl: game.covers?.[0] ? isUrl(game.covers[0]) ? game.covers[0] : pathToFileURL(path.join(getStoreFolder(), game.covers[0])).href : "", - screenshotUrls: game.screenshots ?? [], - files: [{ - url: new URL(validDownload.url), - file_path: `roms/${system}`, - file_name: path.basename(decodeURI(validDownload.url)), - size: 0 - }], - slug: id, - source_id: id, - name: game.name, - summary: game.description, - system_slug: system, - path_fs: path.join('roms', system, game.id), - extract_path: '.', - main_glob: validDownload.main, - version: game.version, - version_system: validDownload.system, - version_source: validDownload.id, - platform: { - source: 'store', - id: system, - slug: system, - name: system - } - }; - - return info; - }); - }); - - ctx.hooks.downloadFiles.tapPromise(desc.name, async ({ id, files, downloadPath, abortSignal, auth, updateProgress }) => - { - const headers: Record = {}; - if (auth) - headers['Authorization'] = auth; - const downloader = new Downloader(id, - files, - downloadPath, - { - signal: abortSignal, - headers, - onProgress: updateProgress, - }); - - const downloadedFiles = await downloader.start(); - if (downloadedFiles) - { - return { source: desc.name, files: downloadedFiles }; - } - }); - - ctx.hooks.postDownloadFiles.tapPromise(desc.name, async ({ files, extract_path, source, downloadPath, path_fs }) => - { - if (extract_path && files && source === desc.name) - { - let progress = 0; - const progressDelta = 1 / files.length; - const extractPath = path.join(downloadPath, path_fs ?? '', extract_path); - - for (const filePath of files) - { - await new Promise(async (resolve, reject) => - { - let sevenZipPath = process.env.ZIP7_PATH ?? path7za; - - if (filePath.endsWith('.rar')) - { - let newPath: string | undefined; - if (process.platform === 'win32' && await fs.exists("C:\\Program Files\\7-Zip\\7z.exe")) - { - newPath = "C:\\Program Files\\7-Zip\\7z.exe"; - } else - { - newPath = which('7z') ?? undefined; - } - - if (!newPath) - { - await fs.rm(filePath); - reject(new Error("No RAR Support")); - return; - } - - sevenZipPath = newPath; - } - - let rejected = false; - const seven = Seven.extractFull(filePath, extractPath, { $bin: sevenZipPath, $progress: true }); - seven.on('progress', p => - { - ctx.setProgress?.(progress + p.percent * progressDelta, "extract", { - speed: 0, - total: 0, - downloaded: 0 - }); - }); - seven.on('error', e => - { - reject(e); - rejected = true; - }); - seven.on('end', async () => - { - if (rejected) return; - await fs.rm(filePath); - resolve(true); - }); - }).catch(async e => - { - if (filePath.endsWith('.zip')) - { - ctx.setProgress?.(0, "extract", {}); - console.error(e); - console.warn("Could not extract", filePath, "with 7zip trying zip extractor"); - await ensureDir(extractPath); - const zip = new StreamZip.async({ file: filePath }); - let entryCount = await zip.entriesCount; - let entryCounter = entryCount; - zip.on('extract', (entry, outPath) => - { - entryCounter--; - ctx.setProgress?.(progress + (1 - (entryCounter / entryCount)) * 100 * progressDelta, "extract", {}); - }); - const count = await zip.extract(null, extractPath); - console.log(`Extracted ${count} entries`); - await zip.close(); - await fs.rm(filePath); - } else - { - throw e; - } - }); - - progress += progressDelta * 100; - } - - // check if 1 root folder we need to get rid of - const contents = await fs.readdir(extractPath); - if (contents.length === 1) - { - const stat = await fs.stat(path.join(extractPath, contents[0])); - if (stat.isDirectory()) - { - console.log("Found 1 root folder, using that instead"); - const tmpGameFolder = `${extractPath} (1)`; - await move(path.join(extractPath, contents[0]), tmpGameFolder, { overwrite: true }); - await move(tmpGameFolder, extractPath, { overwrite: true }); - } - } - - return [extractPath]; - } - }); - } -} \ No newline at end of file diff --git a/src/bun/api/plugins/plugin-manager.ts b/src/bun/api/plugins/plugin-manager.ts index 8b2a1b5..90392f1 100644 --- a/src/bun/api/plugins/plugin-manager.ts +++ b/src/bun/api/plugins/plugin-manager.ts @@ -1,18 +1,6 @@ -import { GameflowHooks } from "@simeonradivoev/gameflow-sdk"; -import { PluginDescriptionType, PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk"; -import { config, events, taskQueue } from "../app"; -import Conf from "conf"; -import projectPackage from '~/package.json'; -import z from "zod"; -import { PluginSourceType, PluginUpdateCheck } from "@simeonradivoev/gameflow-sdk/shared"; -import { getUpdates } from "./services"; -import sdkPkg from '@simeonradivoev/gameflow-sdk/package.json'; -import { semver } from "bun"; - -export const pluginZodRegistry = z.registry<{ - requiresRestart?: boolean; - readOnly?: boolean; -}>(); +import { GameflowHooks } from "../hooks/app"; +import { PluginContextType, PluginDescriptionType, PluginType } from "../../types/typesc.schema"; +import { config } from "../app"; export class PluginManager { @@ -23,21 +11,10 @@ export class PluginManager plugin: PluginType; description: PluginDescriptionType, source: PluginSourceType; - config?: Conf; - update?: PluginUpdateCheck; - incompatible?: boolean; }> = {}; - unregister (id: string) - { - if (!this.plugins[id]) return false; - delete this.plugins[id]; - console.log("Plugin", id, "unregistered"); - return true; - } - - register (plugin: PluginType, description: PluginDescriptionType, source: PluginSourceType) + async register (plugin: PluginType, description: PluginDescriptionType, source: PluginSourceType) { try { @@ -47,29 +24,15 @@ export class PluginManager } else { - let pluginConfig: Conf | undefined = undefined; - if (plugin.settingsSchema) - { - pluginConfig = new Conf({ - projectName: projectPackage.name, - configName: description.name, - projectSuffix: 'bun', - cwd: process.env.CONFIG_CWD, - schema: Object.fromEntries(Object.entries(plugin.settingsSchema.shape).map(([key, schema]) => [key, (schema as z.ZodObject).toJSONSchema() as any])) as any, - defaults: plugin.settingsSchema.parse({}), - migrations: plugin.settingsMigrations as any, - projectVersion: description.version - }); - } - + if (plugin.setup) await plugin.setup(); this.plugins[description.name] = { enabled: !config.get('disabledPlugins').includes(description.name), loaded: false, plugin: plugin, source: source, - description: description, - config: pluginConfig + description: description }; + this.reload(description.name); console.log("Plugin", description.name, "registered"); } @@ -81,55 +44,24 @@ export class PluginManager }; } - checkValidity (plugin: PluginDescriptionType) - { - const sdkDep = plugin.peerDependencies?.[sdkPkg.name]; - if (sdkDep) - { - return semver.satisfies(sdkPkg.version, sdkDep); - } - return true; - } - - private async reload (name: string, reloadCtx: { setProgress: (progress: number, state: string) => void; }, update: string | undefined | null) + private reload (name: string) { const plugin = this.plugins[name]; if (plugin) { - plugin.update = update && !semver.satisfies(plugin.description.version, update) ? { current: plugin.description.version, new: update } : undefined; - - const ctx: PluginLoadingContextType = { - hooks: this.hooks, - setProgress: reloadCtx.setProgress.bind(reloadCtx), - config: plugin.config as any, - zodRegistry: pluginZodRegistry, - app: { - config, - events, - taskQueue - } - }; + const ctx: PluginContextType = { hooks: this.hooks }; if (plugin.loaded) { - await plugin.plugin.cleanup?.(); + plugin.plugin.onBeforeReload?.(ctx); plugin.loaded = false; } try { - plugin.incompatible = !this.checkValidity(plugin.description); - if (plugin.incompatible) + if (plugin.enabled) { - console.error(plugin.description.name, "Incompatible sdk verison"); - return; - } - - if (plugin.enabled || plugin.description.canDisable === false || plugin.description.name === '@simeonradivoev/gameflow-store') - { - console.log("Loading Plugin", plugin.description.name); - await plugin.plugin.load(ctx); - console.log("Loaded Plugin", plugin.description.name); + plugin.plugin.load(ctx); plugin.loaded = true; } } catch (error) @@ -140,17 +72,10 @@ export class PluginManager } } - async reloadAll (ctx: { setProgress: (progress: number, state: string) => void; }) + reloadAll () { this.hooks = new GameflowHooks(); - - const outdated = await getUpdates(); - - for await (const id of Object.keys(this.plugins)) - { - ctx.setProgress(0, `Loading ${id}`); - await this.reload(id, ctx, outdated.find(i => i.package === id)?.update); - } + Object.keys(this.plugins).forEach(id => this.reload(id)); } async cleanup () @@ -159,15 +84,10 @@ export class PluginManager { try { - if (p.loaded) - { - console.log("Starting", p.description.name, "plugin cleanup"); - await p.plugin.cleanup!(); - console.log(p.description.name, "cleanup complete"); - } + await p.plugin.cleanup!(); } catch (error) { - console.error("Error for plugin", p.description.name, "while cleaning up"); + console.log("Error for plugin", p.description.name, "while cleaning up"); console.error(error); } })); diff --git a/src/bun/api/plugins/plugins.ts b/src/bun/api/plugins/plugins.ts index ddfad06..e276f92 100644 --- a/src/bun/api/plugins/plugins.ts +++ b/src/bun/api/plugins/plugins.ts @@ -1,11 +1,7 @@ import Elysia, { status } from "elysia"; -import { plugins, taskQueue } from "../app"; +import { plugins } from "../app"; import z from "zod"; import { toggleElementInConfig } from "@/bun/utils"; -import ReloadPluginsJob from "../jobs/reload-plugins-job"; -import { FrontendPlugin } from "@simeonradivoev/gameflow-sdk/shared"; -import { canDisable, canUninstall } from "./services"; -import PluginOperationJob from "../jobs/plugin-operation-job"; export default new Elysia({ prefix: '/plugins' }) .get('/', async () => @@ -19,59 +15,23 @@ export default new Elysia({ prefix: '/plugins' }) description: p.description.description, source: p.source, version: p.description.version, - canDisable: canDisable(p.description), - icon: p.description.icon, - category: p.description.category, - hasSettings: !!p.config || !!p.plugin.eventsNames, - canUninstall: canUninstall(p.description, p.source), - update: p.update + icon: p.description.icon }; return plugin; }); }) - .get('/:id', async ({ params: { id } }) => - { - const plugin = plugins.plugins[decodeURIComponent(id)]; - return { ...plugin.description, update: plugin.update }; - }) .post('/:id', async ({ params: { id }, body: { enabled } }) => { - const plugin = plugins.plugins[decodeURIComponent(id)]; + const plugin = plugins.plugins[id]; if (plugin) { - if (!canDisable(plugin.description)) - { - return status("Forbidden"); - } plugin.enabled = enabled; toggleElementInConfig('disabledPlugins', plugin.description.name, enabled); - await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); + plugins.reloadAll(); } else { return status("Not Found"); } }, { body: z.object({ enabled: z.boolean() }) - }).post('/install', async ({ body: { id } }) => - { - if (taskQueue.hasActiveOfType(PluginOperationJob) || taskQueue.hasActiveOfType(ReloadPluginsJob)) return; - await taskQueue.enqueue(PluginOperationJob.id, new PluginOperationJob("add", id)); - await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); - }, { - body: z.object({ id: z.string() }) - }).post('/update', async ({ body: { id } }) => - { - if (taskQueue.hasActiveOfType(PluginOperationJob) || taskQueue.hasActiveOfType(ReloadPluginsJob)) return; - await taskQueue.enqueue(PluginOperationJob.id, new PluginOperationJob("update", id)); - await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); - }, { - body: z.object({ id: z.string() }) - }) - .post('/uninstall', async ({ body: { id } }) => - { - if (taskQueue.hasActiveOfType(PluginOperationJob) || taskQueue.hasActiveOfType(ReloadPluginsJob)) return; - await taskQueue.enqueue(PluginOperationJob.id, new PluginOperationJob("remove", id)); - await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); - }, { - body: z.object({ id: z.string() }) }); \ No newline at end of file diff --git a/src/bun/api/plugins/register-plugins.ts b/src/bun/api/plugins/register-plugins.ts index 5746947..9f223b2 100644 --- a/src/bun/api/plugins/register-plugins.ts +++ b/src/bun/api/plugins/register-plugins.ts @@ -2,152 +2,27 @@ import { PluginManager } from "./plugin-manager"; import pcsx2 from './builtin/emulators/com.simeonradivoev.gameflow.pcsx2/package.json'; import ppsspp from './builtin/emulators/com.simeonradivoev.gameflow.ppsspp/package.json'; -import dolphin from './builtin/emulators/com.simeonradivoev.gameflow.dolphin/package.json'; -import cemu from './builtin/emulators/com.simeonradivoev.gameflow.cemu/package.json'; -import xenia from './builtin/emulators/com.simeonradivoev.gameflow.xenia/package.json'; -import xemu from './builtin/emulators/com.simeonradivoev.gameflow.xemu/package.json'; import romm from './builtin/sources/com.simeonradivoev.gameflow.romm/package.json'; -import igdb from './builtin/sources/com.simeonradivoev.gameflow.igdb/package.json'; -import store from './builtin/sources/com.simeonradivoev.gameflow.store/package.json'; -import es from './builtin/launchers/com.simeonradivoev.gameflow.es/package.json'; -import rclone from './builtin/other/com.simeonradivoev.gameflow.rclone/package.json'; -import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@simeonradivoev/gameflow-sdk"; -import path from 'node:path'; -import { getStoreRootFolder } from "../store/services/gamesService"; -import { getUpdates, runBunPackageCommand } from "./services"; -import { PluginSourceType } from "@simeonradivoev/gameflow-sdk/shared"; -import { taskQueue } from "../app"; -import EnsureStore from "../jobs/ensure-store"; -import { PluginRegistry } from "@/shared/constants"; -import { IsPluginAllowed } from "@/bun/utils"; - -type PluginEntry = PluginDescriptionType & { load: () => Promise; }; - -const blacklist = new Set(['@simeonradivoev/gameflow-sdk']); - -export async function getPlugin (id: string, pluginManager: PluginManager) -{ - const pluginPath = path.join(getStoreRootFolder(), 'node_modules', id); - const pluginPackageFile = Bun.file(path.join(pluginPath, 'package.json')); - if (await pluginPackageFile.exists()) - { - const pluginPackage = await PluginDescriptionSchema.safeParseAsync(await pluginPackageFile.json()); - if (pluginPackage.success) - { - const mainPath = path.join(pluginPath, pluginPackage.data.main); - if (await Bun.file(mainPath).exists()) - { - const entry: PluginEntry = { ...pluginPackage.data, load: () => import(mainPath) }; - return entry; - } else - { - console.error("Main file for", id, "does not exist"); - } - } else - { - console.error("Invalid Package for", id, pluginPackage.error.message); - } - } else - { - console.error("Package for", id, "does not exist"); - } -} - -export async function unregisterPlugin (id: string, pluginManager: PluginManager) -{ - return pluginManager.unregister(id); -} - -export async function registerPlugin (plugin: PluginEntry, source: PluginSourceType, pluginManager: PluginManager) -{ - if (!IsPluginAllowed(plugin.name)) - { - console.log("Skipping", plugin.name, "plugin not allowed"); - return; - } - - const file = await plugin.load(); - if (file.default && typeof file.default === 'function') - { - const pluginInstance = new file.default(); - await PluginSchema.parseAsync(pluginInstance); - const description = await PluginDescriptionSchema.parseAsync(plugin); - pluginManager.register(pluginInstance, description, source); - } else - { - console.log("Skipping", plugin.name, "invalid main. Has to be class with load method"); - } -} +import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@/bun/types/typesc.schema"; export default async function register (pluginManager: PluginManager) { - const plugins: PluginEntry[] = [ + + const plugins: (PluginDescriptionType & { main: string; load: () => Promise; })[] = [ { ...pcsx2, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.pcsx2/pcsx2') }, { ...ppsspp, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.ppsspp/ppsspp') }, - { ...dolphin, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.dolphin/dolphin') }, - { ...cemu, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.cemu/cemu') }, - { ...xenia, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.xenia/xenia') }, - { ...xemu, load: () => import('./builtin/emulators/com.simeonradivoev.gameflow.xemu/xemu') }, { ...romm, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.romm/romm') }, - { ...igdb, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.igdb/igdb') }, - { ...es, load: () => import('./builtin/launchers/com.simeonradivoev.gameflow.es/es-de') }, - { ...store, load: () => import('./builtin/sources/com.simeonradivoev.gameflow.store/store') }, - { ...rclone, load: () => import('./builtin/other/com.simeonradivoev.gameflow.rclone/rclone') }, ]; - await Promise.all(plugins.map(p => registerPlugin(p, 'builtin', pluginManager))); - - if (IsPluginAllowed('@simeonradivoev/gameflow-store')) + await Promise.all(plugins.map(async (pluginPackage) => { - const storePackageFilePath = path.join(getStoreRootFolder(), 'package.json'); - if (!await Bun.file(storePackageFilePath).exists()) + const file = await pluginPackage.load(); + if (file.default && typeof file.default === 'function') { - console.log("Store is missing. Updating it."); - await taskQueue.enqueue(EnsureStore.id, new EnsureStore()); - console.log("Store Updated"); + const pluginInstance = new file.default(); + await PluginSchema.parseAsync(pluginInstance); + const description = await PluginDescriptionSchema.parseAsync(pluginPackage); + pluginManager.register(pluginInstance, description, 'builtin'); } - const storePackage = await Bun.file(storePackageFilePath).json(); - - if (storePackage?.dependencies) - { - const storePlugins = await Promise.all(Object.keys(storePackage.dependencies).filter(p => !blacklist.has(p)).map(async p => - { - return getPlugin(p, pluginManager); - })); - - console.log("Checking for outdated packages"); - const outdated = await getUpdates(); - - const validPlugins = storePlugins.filter(p => !!p); - - if (outdated) - { - for (let i = 0; i < validPlugins.length; i++) - { - const plugin = validPlugins[i]; - const newVersion = outdated.find(i => i.package === plugin.name); - if (newVersion) - { - console.log("Plugin", plugin.name, "has update", plugin.version, "=>", newVersion.update); - - if (plugin.autoUpdate || plugin.name === '@simeonradivoev/gameflow-store') - { - console.log("Auto Updating Plugin", plugin.name); - let response = await runBunPackageCommand(["add", `${plugin.name}@${newVersion?.update}`, "--registry", PluginRegistry, '--omit', 'peer']); - console.log(response); - // Update plugin package - const newPlugin = await getPlugin(plugin.name, pluginManager); - if (newPlugin) - validPlugins[i] = newPlugin; - } - } - } - } - - await Promise.all(validPlugins.map(p => registerPlugin(p, 'store', pluginManager))); - } - } else - { - console.log('Skipping Store Packages'); - } + })); } \ No newline at end of file diff --git a/src/bun/api/plugins/services.ts b/src/bun/api/plugins/services.ts deleted file mode 100644 index 8878809..0000000 --- a/src/bun/api/plugins/services.ts +++ /dev/null @@ -1,64 +0,0 @@ -import path from 'node:path'; -import os from 'node:os'; -import { getStoreRootFolder } from '../store/services/gamesService'; -import { PluginDescriptionType } from '@simeonradivoev/gameflow-sdk'; -import { existsSync } from 'node:fs'; -import { checkOutdated } from './update-check'; - -export function canDisable (description: PluginDescriptionType) -{ - if (description.name === '@simeonradivoev/gameflow-store') - { - return false; - } - return description.canDisable ?? true; -} - -export async function getUpdates () -{ - if (!existsSync(getStoreRootFolder())) return []; - const results = await checkOutdated(getStoreRootFolder()); - return results; -} - -export function canUninstall (description: PluginDescriptionType, source: string) -{ - if (description.name === '@simeonradivoev/gameflow-store') - { - return false; - } - return source !== 'builtin'; -} - -export async function runBunPackageCommand (commands: string[]) -{ - const tempCache = path.join(os.tmpdir(), "gameflow-bun-cache"); - const storeFolder = getStoreRootFolder(); - - let proc = Bun.spawn([process.execPath, ...commands, '--json'], { - cwd: storeFolder, - stdout: 'pipe', - stderr: 'pipe', - env: { - BUN_BE_BUN: "1", - BUN_INSTALL_CACHE_DIR: tempCache - } - }); - - let stdout = await new Response(proc.stdout).text(); - let stderr = await new Response(proc.stderr).text(); - if (stderr) - console.error(stderr); - await proc.exited; - return stdout; -} - -export async function hasPackage (id: string) -{ - const storeFolder = getStoreRootFolder(); - const packagePath = path.join(storeFolder, 'package.json'); - const packageFile = Bun.file(packagePath); - if (!await packageFile.exists()) return false; - const pkg = await packageFile.json(); - return !!pkg.dependencies?.[id]; -} \ No newline at end of file diff --git a/src/bun/api/plugins/update-check.ts b/src/bun/api/plugins/update-check.ts deleted file mode 100644 index 66cf381..0000000 --- a/src/bun/api/plugins/update-check.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { semver } from "bun"; -import { readFile } from "fs/promises"; -import { join } from "path"; -import { getOrCached } from "../cache"; -import { PluginRegistry } from "@/shared/constants"; -import sdkPkg from '@/packages/gameflow-sdk/package.json'; - -interface UpdateInfo -{ - package: string, - current: string, - update: string | null, - latest: string, - sdkConstrained: boolean, - sdkRange: string, - note: string | null; -} - -function parseBunOutdated (cwd: string) -{ - const proc = Bun.spawnSync([process.execPath, "outdated"], { - stderr: "inherit", env: { - BUN_BE_BUN: "1", - NO_COLOR: "1", - }, cwd: cwd - }); - const output = proc.stdout.toString(); - const lines = output.split("\n").filter(Boolean); - - const headerIndex = lines.findIndex( - (l) => l.includes("Package") && l.includes("Current") - ); - if (headerIndex === -1) return []; - - return lines - .slice(headerIndex + 1) - .filter((line) => !/^[-─╌| ]+$/.test(line)) - .map((line) => - { - const [, pkg, current, , latest] = line.split("|").map((c) => c.trim()); - return pkg ? { package: pkg, current, latest } : null; - }) - .filter(p => p !== null); -} - -async function getInstalledVersion (cwd: string, pkg: string) -{ - try - { - const raw = await readFile(join(cwd, "node_modules", pkg, "package.json"), "utf8"); - return JSON.parse(raw).version ?? null; - } catch - { - return null; - } -} - -async function fetchAllVersions (pkg: string) -{ - const res = await fetch(`${PluginRegistry}/${pkg}`); - if (!res.ok) return []; - const data = await res.json(); - return Object.keys(data.versions ?? {}); -} - -async function fetchPeerDeps (pkg: string, version: string) -{ - const peerDependencies = await getOrCached(`npm-${pkg}-${version}`, async () => - { - const res = await fetch(`${PluginRegistry}/${pkg}/${version}`); - if (!res.ok) - { - throw new Error(`Error while fetching peer deps for ${pkg} ${version} ${res.status} ${res.statusText}`); - } - const data = await res.json(); - return data.peerDependencies ?? {}; - }, { - //5 days - expireMs: 1000 * 60 * 60 * 24 * 5 - }); - - - return peerDependencies; -} - -async function findBestVersion (pkg: string, allVersions: string[], sdkVersion: string) -{ - // Sort descending so we find the highest compatible version first - const sorted = [...allVersions].sort((a, b) => semver.order(b, a)); - - for (const version of sorted) - { - const peers = await fetchPeerDeps(pkg, version); - const sdkRange = peers[sdkPkg.name]; - - if (!sdkRange) - { - // No peer dep on SDK — compatible by default - return { version, sdkRange: null }; - } - - if (semver.satisfies(sdkVersion, sdkRange)) - { - return { version, sdkRange }; - } - } - - return null; -} - -export async function checkOutdated (cwd: string) -{ - const outdated = parseBunOutdated(cwd); - - if (outdated.length === 0) - { - return []; - } - - const sdkVersion = await getInstalledVersion(cwd, sdkPkg.name); - if (!sdkVersion) - { - console.error(`Could not find installed version of ${sdkPkg.name} in node_modules.`); - process.exit(1); - } - - const results = await Promise.all( - outdated.map(async ({ package: pkg, current, latest }) => - { - const allVersions = await fetchAllVersions(pkg); - - // Check if the outright latest is already SDK compatible - const latestPeers = await fetchPeerDeps(pkg, latest); - const latestSdkRange = latestPeers[sdkPkg.name]; - - const latestCompatible = - !latestSdkRange || semver.satisfies(sdkVersion, latestSdkRange); - - if (latestCompatible) - { - return { - package: pkg, - current, - update: latest, - latest, - sdkConstrained: false, - sdkRange: latestSdkRange ?? null, - note: null - } satisfies UpdateInfo as UpdateInfo; - } - - const best = await findBestVersion(pkg, allVersions, sdkVersion); - - return { - package: pkg, - current, - update: best?.version ?? null, - latest, - sdkConstrained: true, - sdkRange: best?.sdkRange ?? null, - note: best - ? `Latest (${latest}) requires incompatible SDK range; best compatible: ${best.version}` - : `No version of ${pkg} is compatible with ${sdkPkg.name}@${sdkVersion}`, - } satisfies UpdateInfo as UpdateInfo; - }) - ); - - return results; -} \ No newline at end of file diff --git a/src/bun/api/schema/app.ts b/src/bun/api/schema/app.ts index 7db68ba..fafa32a 100644 --- a/src/bun/api/schema/app.ts +++ b/src/bun/api/schema/app.ts @@ -1,5 +1,3 @@ - -import { LocalGameMetadata } from "@simeonradivoev/gameflow-sdk/shared"; import { sql, relations } from "drizzle-orm"; import { integer, text, sqliteTable, blob } from "drizzle-orm/sqlite-core"; @@ -11,18 +9,14 @@ export const games = sqliteTable('games', { name: text("name"), ra_id: integer('ra_id').unique(), path_fs: text("path_fs"), - main_glob: text("main_glob"), 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`'{}'`).$type().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"), - version: text('version'), - version_source: text("version_source"), - version_system: text("version_system"), + summary: text("summary") }); export const gamesRelations = relations(games, ({ many, one }) => ({ diff --git a/src/bun/api/settings/services.ts b/src/bun/api/settings/services.ts index e0897ea..afaa5fe 100644 --- a/src/bun/api/settings/services.ts +++ b/src/bun/api/settings/services.ts @@ -2,12 +2,11 @@ import * as appSchema from '@schema/app'; import * as emulatorSchema from "@schema/emulators"; import { eq, inArray } from 'drizzle-orm'; -import { db, emulatorsDb, plugins } from '../app'; +import { db, emulatorsDb } from '../app'; import { cores } from '../emulatorjs/emulatorjs'; import { SERVER_URL } from '@/shared/constants'; +import { findExecsByName } from '../games/services/launchGameService'; import { host } from '@/bun/utils/host'; -import { findEmulatorPluginIntegration } from '../store/services/emulatorsService'; -import { EmulatorSourceEntryType, FrontEndEmulator } from '@simeonradivoev/gameflow-sdk/shared'; /** * Get emulators based on local games. Only the ones we probably need. @@ -54,9 +53,7 @@ export async function getRelevantEmulators () const groupedEmulators = Map.groupBy(emulators, ({ emulator }) => emulator); const finalEmulators = await Promise.all(Array.from(groupedEmulators.entries()).map(async ([emulator, system_slug]) => { - const execPaths: EmulatorSourceEntryType[] = []; - await plugins.hooks.emulators.findEmulatorSource.promise({ emulator, sources: execPaths }); - const integrations = findEmulatorPluginIntegration(emulator, execPaths); + const execPaths = await findExecsByName(emulator); let platform: number | null | undefined = null; const validSystemSlug = system_slug.find(s => s.system); @@ -70,39 +67,26 @@ export async function getRelevantEmulators () systems.forEach(s => platformViability.set(s, true)); } - const storeEmulator = await plugins.hooks.store.fetchEmulator.promise({ id: emulator }); - - if (storeEmulator) - { - storeEmulator.validSources = execPaths; - storeEmulator.integrations = integrations; - return { ...storeEmulator, isCritical: false }; - } - const em: FrontEndEmulator & { isCritical: boolean; } = { - source: 'local', name: emulator, logo: platform ? `/api/romm/platform/local/${platform}/cover` : '', systems: systems.map(s => platformLookup.get(s)).filter(s => !!s).map(e => ({ iconUrl: `/api/romm/image/romm/assets/platforms/${e.es_slug}.svg`, name: e.platform_name ?? 'Unknown', id: e.es_slug ?? '' })), gameCount: 0, isCritical: false, - validSources: execPaths, - integrations + validSources: execPaths }; return em; })); finalEmulators.push({ - source: 'local', name: 'EMULATORJS', validSources: [{ binPath: `${SERVER_URL(host)}`, type: 'embedded', exists: true }], logo: `/api/romm/image?url=${encodeURIComponent('https://emulatorjs.org/logo/EmulatorJS.png')}`, systems: [], gameCount: 0, isCritical: false, - description: "Embedded Emulator. Uses Retroarch Cores", - integrations: [] + description: "Embedded Emulator. Uses Retroarch Cores" }); return finalEmulators.map(e => diff --git a/src/bun/api/settings/settings.ts b/src/bun/api/settings/settings.ts index ebd5b91..dda53b9 100644 --- a/src/bun/api/settings/settings.ts +++ b/src/bun/api/settings/settings.ts @@ -1,17 +1,12 @@ import z from "zod"; -import { SettingsSchema } from '@simeonradivoev/gameflow-sdk/shared'; +import { SettingsSchema } from "@shared/constants"; import Elysia, { status } from "elysia"; -import { config, customEmulators, plugins, taskQueue } from "../app"; +import { config, customEmulators, taskQueue } from "../app"; import fs from 'node:fs/promises'; import { existsSync } from "node:fs"; import { InstallJob } from "../jobs/install-job"; import { move } from "fs-extra"; import { getRelevantEmulators } from "./services"; -import type { JSONSchema7 } from "json-schema"; -import ReloadPluginsJob from "../jobs/reload-plugins-job"; -import { pluginZodRegistry } from "../plugins/plugin-manager"; -import { TestDownloadJob } from "../jobs/test-download-job"; -import { randomUUIDv7 } from "bun"; export const settings = new Elysia({ prefix: '/api/settings' }) .get('/emulators/automatic', async () => @@ -82,63 +77,18 @@ export const settings = new Elysia({ prefix: '/api/settings' }) drive: z.string().optional() }) }) - .get("local/:id", async ({ params: { id } }) => + .get("/:id", async ({ params: { id } }) => { const value = config.get(id); return { value: value }; }, { params: z.object({ id: z.keyof(SettingsSchema) }), - }).post('local/:id', + }).post('/:id', async ({ params: { id }, body: { value }, }) => { config.set(id, value); }, { params: z.object({ id: z.keyof(SettingsSchema) }), body: z.object({ value: z.any() }), - }) - .get('/definitions/:source', async ({ params: { source } }) => - { - return plugins.plugins[decodeURIComponent(source)].plugin.settingsSchema?.toJSONSchema() as JSONSchema7; - }) - .get('/actions/:source', async ({ params: { source } }) => - { - const plugin = plugins.plugins[decodeURIComponent(source)]?.plugin; - if (!plugin.eventsNames) return []; - return plugin.eventsNames; - }) - .post('/actions/:source/:id', async ({ params: { source, id } }) => - { - return await plugins.plugins[decodeURIComponent(source)]?.plugin.onEvent?.(decodeURIComponent(id)); - }) - .get('/:source/:id', async ({ params: { source, id } }) => - { - return { value: plugins.plugins[decodeURIComponent(source)].config?.get(decodeURIComponent(id)) }; - }) - .post('/test/download', async () => - { - taskQueue.enqueue(randomUUIDv7(), new TestDownloadJob()); - }) - .put('/:source/:id', async ({ params: { source, id }, body: { value } }) => - { - const plugin = plugins.plugins[decodeURIComponent(source)]; - if (!plugin.config) return status("Not Found", "Plugin has no config"); - const settingSchema = plugin.plugin.settingsSchema?.shape[decodeURIComponent(id)] as z.ZodObject; - if (!settingSchema) return status("Not Found", "Could not find setting"); - const meta = pluginZodRegistry.get(settingSchema); - - if (meta?.readOnly) - { - return; - } - - plugin.config?.set(id, value); - - if (meta?.requiresRestart) - { - await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob()); - } - }, - { - body: z.object({ value: z.any() }) - }); + }); diff --git a/src/bun/api/store/services/emulatorsService.ts b/src/bun/api/store/services/emulatorsService.ts index 6dfcde1..1340731 100644 --- a/src/bun/api/store/services/emulatorsService.ts +++ b/src/bun/api/store/services/emulatorsService.ts @@ -1,114 +1,36 @@ -import { config, plugins } from "../../app"; -import { getOrCached, getOrCachedGithubRelease } from "../../cache"; -import path from "node:path"; -import { EmulatorSourceEntryType, EmulatorSupport, ScoopPackageSchema, EmulatorPackageType, EmulatorDownloadInfoType } from "@simeonradivoev/gameflow-sdk/shared"; +import { EmulatorPackageType } from "@/shared/constants"; +import { emulatorsDb, plugins } from "../../app"; +import * as emulatorSchema from '@schema/emulators'; +import { findExecs } from "../../games/services/launchGameService"; +import { eq } from "drizzle-orm"; -export function findEmulatorPluginIntegration (name: string, validSources: (EmulatorSourceEntryType | undefined)[]): EmulatorSupport[] +export async function convertStoreEmulatorToFrontend (emulator: EmulatorPackageType, gameCount: number, systems: EmulatorSystem[]) { - const hasSupport = validSources.concat(undefined).map(s => + const execPaths: EmulatorSourceEntryType[] = []; + const esEmulator = await emulatorsDb.query.emulators.findFirst({ where: eq(emulatorSchema.emulators.name, emulator.name) }); + + if (esEmulator) { - const support = plugins.hooks.games.emulatorLaunchSupport.call({ emulator: name, source: s }); - if (support) - { - return { ...support, source: s }; - } - - return undefined; - }).filter(s => !!s); - - if (hasSupport.length <= 0) return []; - return hasSupport; -} - -export function getEmulatorPath (emulator: string) -{ - return path.join(config.get('downloadPath'), "emulators", emulator); -} - -export async function getEmulatorDownload (emulator: EmulatorPackageType, source: string) -{ - if (!emulator.downloads) throw new Error("Emulator has no downloads"); - - const validDownloads = emulator.downloads[`${process.platform}:${process.arch}`]; - if (!validDownloads) throw new Error(`Now downloads in ${emulator.name} for platform ${process.platform}:${process.arch}`); - - const validDownload = validDownloads.find(d => d.type === source); - if (!validDownload) throw new Error(`Download type ${source} not found`); - - let downloadUrl: URL; - let versionInfo: EmulatorDownloadInfoType = { - id: "", - downloadDate: new Date(), - type: validDownload.type - }; - if (validDownload.type === 'github') - { - const latestRelease = await getOrCachedGithubRelease(validDownload.path); - const glob = new Bun.Glob(validDownload.pattern); - const validAsset = latestRelease.assets.find(a => glob.match(a.name)); - if (!validAsset) throw new Error("Could Not Find Valid Asset"); - downloadUrl = new URL(validAsset.browser_download_url); - versionInfo.version = latestRelease.tag_name; - versionInfo.url = latestRelease.url; - versionInfo.id = String(latestRelease.id); - versionInfo.description = latestRelease.body; - - } else if (validDownload.type === 'direct') - { - downloadUrl = new URL(validDownload.url); - versionInfo.id = validDownload.url; - versionInfo.url = validDownload.url; - } else if (validDownload.type === 'scoop') - { - const data = await getOrCachedScoopPackage(emulator.name, validDownload.url); - let scoopDownload: URL | undefined; - if (data) - { - if (data.url) - { - scoopDownload = new URL(data.url); - } else if (data.architecture) - { - if (process.arch === 'x64' && data.architecture["64bit"]) - { - scoopDownload = new URL(data.architecture["64bit"].url); - } else if (process.arch === "arm64" && data.architecture["arm64"]) - { - scoopDownload = new URL(data.architecture["arm64"].url); - } - } - } - - if (scoopDownload) - { - downloadUrl = scoopDownload; - versionInfo.version = data?.version; - versionInfo.url = data?.url; - versionInfo.description = data?.description; - } else - { - throw new Error("Could not find scoop download"); - } - } else - { - throw new Error("Download Type Unsupported"); + const allExecs = await findExecs(emulator.name, esEmulator); + execPaths.push(...allExecs); } - return { url: downloadUrl, info: versionInfo }; + const em: FrontEndEmulator = { + name: emulator.name, + logo: emulator.logo, + systems, + gameCount, + validSources: execPaths, + integration: findEmulatorPluginIntegration(emulator.name) + }; + + return em; } -export async function getOrCachedScoopPackage (id: string, url: string) +export function findEmulatorPluginIntegration (name: string) { - const data = await getOrCached(`scoop-dl-${id}`, async () => - { - const res = await fetch(url); - if (res.ok) - { - return ScoopPackageSchema.parseAsync(await res.json()); - } - console.error(res.statusText); - return undefined; - }); - - return data; + const lowerCaseName = name.toLowerCase(); + const integration = Object.entries(plugins.plugins).find(p => p[1].description.keywords?.includes(lowerCaseName)); + if (!integration) return undefined; + return { name: integration[0], version: integration[1].description.version }; } \ No newline at end of file diff --git a/src/bun/api/store/services/gamesService.ts b/src/bun/api/store/services/gamesService.ts index b475b89..ae0181f 100644 --- a/src/bun/api/store/services/gamesService.ts +++ b/src/bun/api/store/services/gamesService.ts @@ -1,9 +1,79 @@ -import { and, eq, or } from "drizzle-orm"; +import { EmulatorPackageSchema, EmulatorPackageType, GithubManifestSchema, StoreGameSchema } from "@/shared/constants"; +import { CACHE_KEYS, getOrCached } from "../../cache"; +import { and, eq } from "drizzle-orm"; import { config, emulatorsDb } from '../../app'; import path from "node:path"; import fs from 'node:fs/promises'; import * as emulatorSchema from '@schema/emulators'; -import { EmulatorSystem, EmulatorPackageType, EmulatorPackageSchema } from "@simeonradivoev/gameflow-sdk/shared"; +import { shuffleInPlace } from "@/bun/utils"; + +export async function getShuffledStoreGames () +{ + return getOrCached('shuffled-store-games', async () => + { + const gamesManifest = await getStoreGameManifest(); + const allStoreGames = gamesManifest.filter(g => g.type === 'blob'); + shuffleInPlace(allStoreGames, Math.round(new Date().getTime() / 1000 / 60 / 60)); + return allStoreGames; + }, { expireMs: 1000 / 60 / 60 }); +} + +export async function getStoreGameManifest () +{ + return getOrCached(CACHE_KEYS.STORE_GAME_MANIFEST, async () => + { + const store = await fetch('https://api.github.com/repos/dragoonDorise/EmuDeck/git/trees/50261b66d69c1758efa28c6d7c54e45259a0c9c5?recursive=true').then(r => r.json()).then(data => GithubManifestSchema.parseAsync(data)); + + return store.tree.filter((e: any) => + { + if (e.type === 'blob' && e.path !== "featured.json") + { + return true; + } + return false; + }); + }); +} + +export async function getStoreGames (gamesManifest: any[], filter?: { limit?: number; offset?: number; }) +{ + const offset = filter?.offset ?? 0; + const limit = Math.min(50, filter?.limit ?? 10); + + const games = await Promise.all(gamesManifest.slice(offset, Math.min(offset + limit, gamesManifest.length)).map((e: any) => + { + return fetch(e.url).then(e => e.json()).then(game => StoreGameSchema.parseAsync(JSON.parse(atob(game.content.replace(/\n/g, ""))))); + })); + + return games; +} + +export function extractStoreGameSourceId (id: string) +{ + const gameId = id.split('@'); + if (gameId.length !== 2) + throw new Error("Store ID should include platform and name with @ separator"); + return { system: gameId[0], id: gameId[1] }; +} + +export function getStoreGameFromId (id: string) +{ + const data = extractStoreGameSourceId(id); + return getStoreGame(data.system, data.id); +} + +export async function getStoreGame (system: string, id: string) +{ + return getStoreGameFromPath(`${system}/${encodeURIComponent(id)}.json`); +} + +export async function getStoreGameFromPath (path: string) +{ + const game = await getOrCached(CACHE_KEYS.STORE_GAME(path), () => fetch(`https://cdn.jsdelivr.net/gh/dragoonDorise/EmuDeck/store/${path}`) + .then(e => e.json()) + .then(g => StoreGameSchema.parseAsync(g))); + return game; +} export function getStoreRootFolder () { @@ -31,7 +101,14 @@ export async function getAllStoreEmulatorPackages () const emulators = await fs.readdir(emulatorsBucket); const emulatorsRawData = await Promise.all(emulators.map(e => fs.readFile(path.join(emulatorsBucket, e), 'utf-8'))); - const emulatesParsed = emulatorsRawData.map(d => EmulatorPackageSchema.parse(JSON.parse(d))); + const emulatesParsed = emulatorsRawData.map(d => EmulatorPackageSchema.safeParse(JSON.parse(d))).filter(e => + { + if (e.error) + { + console.error(e.error); + } + return e.data; + }).map(e => e.data!); return emulatesParsed; } @@ -41,10 +118,10 @@ export async function buildStoreFrontendEmulatorSystems (emulator: EmulatorPacka const systems = await Promise.all(emulator.systems.map(async system => { const rommSystem = await emulatorsDb.query.systemMappings.findFirst({ - where: or(and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.system, system)), and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.sourceSlug, system))) + where: and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.system, system)) }); - const esSystem = await emulatorsDb.query.systems.findFirst({ where: or(eq(emulatorSchema.emulators.name, system), eq(emulatorSchema.emulators.name, rommSystem?.system ?? '')), columns: { fullname: true } }); + const esSystem = await emulatorsDb.query.systems.findFirst({ where: eq(emulatorSchema.emulators.name, system), columns: { fullname: true } }); let icon: string = `/api/romm/image/romm/assets/platforms/${rommSystem?.sourceSlug ?? system}.svg`; diff --git a/src/bun/api/store/store.ts b/src/bun/api/store/store.ts index 7706699..d29746d 100644 --- a/src/bun/api/store/store.ts +++ b/src/bun/api/store/store.ts @@ -1,28 +1,20 @@ import Elysia, { status } from "elysia"; -import { config, db, plugins, taskQueue } from "../app"; +import { config, db, taskQueue } from "../app"; import path from "node:path"; import fs from 'node:fs/promises'; +import { StoreGameSchema } from "@/shared/constants"; +import { findExecsByName } from "../games/services/launchGameService"; import * as appSchema from '@schema/app'; import z from "zod"; -import { convertLocalToFrontendDetailed, getLocalGameMatch } from "../games/services/utils"; +import { convertLocalToFrontendDetailed, convertStoreToFrontendDetailed, getLocalGameMatch } from "../games/services/utils"; import { getPlatformsApiPlatformsGet } from "@/clients/romm"; -import { CACHE_KEYS, getOrCached } from "../cache"; -import { getStoreFolder } from "./services/gamesService"; +import { CACHE_KEYS, getOrCached, getOrCachedGithubRelease } from "../cache"; +import { buildStoreFrontendEmulatorSystems, getAllStoreEmulatorPackages, getStoreEmulatorPackage, getStoreFolder } from "./services/gamesService"; import { EmulatorDownloadJob } from "../jobs/emulator-download-job"; +import { Glob } from "bun"; +import { convertStoreEmulatorToFrontend, findEmulatorPluginIntegration } from "./services/emulatorsService"; import { BiosDownloadJob } from "../jobs/bios-download-job"; -import { findEmulatorPluginIntegration, getEmulatorPath } from "./services/emulatorsService"; -import { EmulatorSourceEntryType, FrontEndEmulator, FrontEndGameTypeDetailed, PluginBunDetailsSchema, PluginEntrySchema, EmulatorDownloadInfoSchema } from "@simeonradivoev/gameflow-sdk/shared"; -import PQueue from "p-queue"; -import { hasPackage, runBunPackageCommand } from "../plugins/services"; -import { semver } from "bun"; - -const npmQueue = new PQueue({ intervalCap: 60, interval: 1000 * 60, strict: true }); -const pluginsResponseSchema = z.object({ - objects: z.array(PluginEntrySchema), - total: z.number(), - time: z.coerce.date() -}); export const store = new Elysia({ prefix: '/api/store' }) .get('/emulators', async ({ query }) => @@ -32,32 +24,27 @@ export const store = new Elysia({ prefix: '/api/store' }) console.error(e); return undefined; }); - - - let frontEndEmulators: FrontEndEmulator[] = []; - await plugins.hooks.store.fetchEmulators.promise({ emulators: frontEndEmulators, search: query.search }); - - await Promise.all(frontEndEmulators.map(async e => - { - const gameCounts = e.systems.map((s) => + const emulatesParsed = await getAllStoreEmulatorPackages(); + let frontEndEmulators = await Promise.all(emulatesParsed + .filter(e => e.os.includes(process.platform as any)) + .map(async (emulator) => { - const romPlatform = rommPlatforms?.find(p => p.slug === (s.romm_slug ?? s.id)); - if (romPlatform) + const systems = await buildStoreFrontendEmulatorSystems(emulator); + const gameCounts = await Promise.all(systems.map(async (s) => { - return romPlatform.rom_count; - } + const romPlatform = rommPlatforms?.find(p => p.slug === (s.romm_slug ?? s.id)); + if (romPlatform) + { + return romPlatform.rom_count; + } - return 0; + return 0; - }); + })); - const execPaths: EmulatorSourceEntryType[] = []; - await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: e.name, sources: execPaths }); - const integrations = findEmulatorPluginIntegration(e.name, execPaths); - - e.gameCount = gameCounts.reduce((a, c) => a + c); - e.integrations = integrations; - })); + const gameCount = gameCounts.reduce((a, c) => a + c); + return convertStoreEmulatorToFrontend(emulator, gameCount, systems); + })); if (query.missing) { @@ -91,132 +78,99 @@ export const store = new Elysia({ prefix: '/api/store' }) limit: z.coerce.number().optional(), missing: z.stringbool().optional().describe("Show Only Non Installed emulators"), orderBy: z.enum(['name', 'recently_updated', 'importance']).optional(), - related: z.string().optional(), - search: z.string().optional() + related: z.string().optional() }) }) .get('/games/featured', async () => { - const games: FrontEndGameTypeDetailed[] = []; - await plugins.hooks.store.fetchFeaturedGames.promise({ games }); - - return Promise.all(games.map(async g => + const response = await fetch('https://cdn.jsdelivr.net/gh/dragoonDorise/EmuDeck/store/featured.json'); + const games = await z.object({ featured: z.array(StoreGameSchema) }).parseAsync(await response.json()); + return Promise.all(games.featured.map(async g => { - const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(g.id.id, g.id.source) }); + const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(`${g.system}@${g.title}`, 'store') }); if (localGame) return convertLocalToFrontendDetailed(localGame); - return g; + return convertStoreToFrontendDetailed(g.system, g.title, g); })); }) .get('/stats', async () => { - let frontEndEmulators: FrontEndEmulator[] = []; - await plugins.hooks.store.fetchEmulators.promise({ emulators: frontEndEmulators }); - const storeEmulatorCount = frontEndEmulators.length; + const emulatesParsed = await getAllStoreEmulatorPackages(); + const storeEmulatorCount = emulatesParsed.filter(e => e.os.includes(process.platform as any)).length; const gameCount = await db.$count(appSchema.games); return { storeEmulatorCount, gameCount }; }) - .get('/plugin', async ({ query: { plugin } }) => - { - const pluginsRes = await runBunPackageCommand(['info', plugin]); - const pluginData = await PluginBunDetailsSchema.parseAsync(JSON.parse(pluginsRes)); - const existingVersion = plugins.plugins[plugin]?.description.version; - - return { - ...pluginData, - installed: !!plugins.plugins[plugin] || await hasPackage(plugin), - update: existingVersion && semver.order(pluginData.version, existingVersion) > 0 ? { from: existingVersion } : undefined - }; - }, - { - query: z.object({ plugin: z.string() }) - }) - .get('/plugins', async ({ query: { search } }) => - { - //TODO: Find a better way to search keywords and a search term at the same time - const pluginsRes = await npmQueue.add(() => fetch(`https://registry.npmjs.com/-/v1/search?text=keywords:gameflow-plugin`)); - if (!pluginsRes.ok) return status(pluginsRes.status, pluginsRes.statusText); - const data: z.infer = await pluginsRes.json(); - if (search) - { - data.objects = data.objects.filter(o => - { - if (o.package.description && o.package.description.includes(search)) return true; - if (o.package.name.includes(search)) return true; - if (o.package.keywords.includes(search)) return true; - return false; - }); - data.total = data.objects.length; - } - await Promise.all(data.objects.map(async o => - { - const existingVersion = plugins.plugins[o.package.name]?.description.version; - o.installed = !!plugins.plugins[o.package.name] || await hasPackage(o.package.name); - o.update = existingVersion && semver.order(o.package.version, existingVersion) > 0 ? { from: existingVersion } : undefined; - })); - return data as any; - }, { - query: z.object({ search: z.string().optional() }), - response: pluginsResponseSchema - }) - .get('/media/*', async ({ params }) => - { - return Bun.file(path.join(getStoreFolder(), params["*"])); - }) .get('/screenshot/emulator/:id/:name', async ({ params: { id, name } }) => { return Bun.file(path.join(getStoreFolder(), "media", "screenshots", id, name)); }, { params: z.object({ id: z.string(), name: z.string() }) }) - .get('/emulator/:id/update', async ({ params: { id } }) => - { - return plugins.hooks.store.fetchDownload.promise({ id }); - }, - { - response: z.union([z.intersection(EmulatorDownloadInfoSchema, z.object({ hasUpdate: z.boolean() })), z.undefined()]) - }) .get('/emulator/:id', async ({ params: { id } }) => { - const emulator = await plugins.hooks.store.fetchEmulator.promise({ id }); - if (!emulator) return status("Not Found"); - const sources: EmulatorSourceEntryType[] = []; - await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: emulator.name, sources }); - const integrations = findEmulatorPluginIntegration(emulator.name, sources); - emulator.validSources = sources; - emulator.integrations = integrations; + const emulatorPackage = await getStoreEmulatorPackage(id); + if (!emulatorPackage) return status("Not Found"); + + const systems = await buildStoreFrontendEmulatorSystems(emulatorPackage); + + const execPaths = await findExecsByName(emulatorPackage.name); + + const emulatorScreenshotsPath = path.join(getStoreFolder(), "media", "screenshots", id); + const screenshots = await fs.exists(emulatorScreenshotsPath) ? await fs.readdir(emulatorScreenshotsPath) : []; + const biosDirPath = path.join(config.get('downloadPath'), 'bios', id); + const biosFiles = await fs.exists(biosDirPath) ? await fs.readdir(biosDirPath) : []; + + const emulator: FrontEndEmulatorDetailed = { + name: emulatorPackage.name, + description: emulatorPackage.description, + systems, + validSources: execPaths, + screenshots: screenshots.map(s => `/api/store/screenshot/emulator/${id}/${s}`), + gameCount: 0, + homepage: emulatorPackage.homepage, + downloads: await Promise.all(emulatorPackage.downloads?.[`${process.platform}:${process.arch}`].map(async d => + { + if (d.type === 'github' && d.path) + { + const release = await getOrCachedGithubRelease(d.path); + const glob = new Glob(d.pattern); + const download: FrontEndEmulatorDetailedDownload = { + name: d.type, + type: release.assets.find(a => glob.match(a.name))?.content_type + }; + return download; + }; + + return { name: d.type, type: "Unknown" }; + }) ?? []), + logo: emulatorPackage.logo, + sources: execPaths, + biosRequirement: emulatorPackage.bios, + bios: biosFiles, + integration: findEmulatorPluginIntegration(emulatorPackage.name) + }; + return emulator; }, { params: z.object({ id: z.string() }) }) - .post('/install/emulator/:id/:source', async ({ params: { source, id }, body }) => + .post('/install/emulator/:id/:source', async ({ params: { source, id } }) => { if (taskQueue.hasActiveOfType(EmulatorDownloadJob)) { return status("Conflict", "Installation already running"); } - const job = new EmulatorDownloadJob(id, source, body); + const job = new EmulatorDownloadJob(id, source); return taskQueue.enqueue(EmulatorDownloadJob.id, job); - }, { - body: z.object({ isUpdate: z.boolean().optional() }).optional() }) .delete('/emulator/:id', async ({ params: { id } }) => { - const storeEmulatorFolder = getEmulatorPath(id); - const existingPackagePath = `${storeEmulatorFolder}.json`; - let hadDelete = false; - if (await fs.exists(existingPackagePath)) - { - await fs.rm(existingPackagePath); - hadDelete = true; - } - + const storeEmulatorFolder = path.join(config.get('downloadPath'), 'emulators', id); if (await fs.exists(storeEmulatorFolder)) { fs.rm(storeEmulatorFolder, { recursive: true }); - hadDelete = true; + return status("OK"); } - - return hadDelete ? status("OK") : status("Not Found"); + return status("Not Found"); }) .post('/download/bios/:id', async ({ params: { id } }) => { diff --git a/src/bun/api/system.ts b/src/bun/api/system.ts index 2124144..aa9207e 100644 --- a/src/bun/api/system.ts +++ b/src/bun/api/system.ts @@ -2,31 +2,16 @@ import Elysia from "elysia"; import open from 'open'; import z from "zod"; import os from 'node:os'; -import { cachePath, config, events, taskQueue } from "./app"; -import { getAppVersion, isSteamDeck, openExternal } from "../utils"; +import { cachePath, config, events } from "./app"; +import { isSteamDeck, openExternal } from "../utils"; import fs from 'node:fs/promises'; import buildNotificationsStream from "./notifications"; import path, { dirname } from "node:path"; -import { SystemInfoSchema, DirSchema, DownloadsDrive } from '@simeonradivoev/gameflow-sdk/shared'; +import { DirSchema, SystemInfoSchema } from "@/shared/constants"; import { getDevices, getDevicesCurated } from "./drives"; import getFolderSize from "get-folder-size"; -import si from 'systeminformation'; +import si, { battery } from 'systeminformation'; import { getStoreFolder } from "./store/services/gamesService"; -import ReloadPluginsJob from "./jobs/reload-plugins-job"; -import { semver } from "bun"; -import { getOrCachedGithubRelease } from "./cache"; -import SelfUpdateJob from "./jobs/self-update-job"; - -async function checkUpdate (force?: boolean) -{ - const latest = await getOrCachedGithubRelease('simeonradivoev/gameflow-deck', force); - if (!latest || !latest.tag_name) return { - hasUpdate: 0, - version: getAppVersion() - }; - const hasUpdate = semver.order(latest.tag_name, getAppVersion()); - return { hasUpdate, version: latest.tag_name, info: latest.body }; -} export const system = new Elysia({ prefix: '/api/system' }) .post('/show_keyboard', async ({ body: { XPosition, YPosition, Width, Height } }) => @@ -66,8 +51,7 @@ export const system = new Elysia({ prefix: '/api/system' }) machine: os.machine(), source, cacheSize: (await fs.stat(cachePath)).size, - storeSize: (await getFolderSize(getStoreFolder())).size, - version: getAppVersion() + storeSize: (await getFolderSize(getStoreFolder())).size }; }) .get('/notifications', ({ set }) => @@ -76,80 +60,29 @@ export const system = new Elysia({ prefix: '/api/system' }) set.headers["cache-control"] = 'no-cache'; set.headers['connection'] = 'keep-alive'; return new Response(buildNotificationsStream()); - }) - .get('/notifications/all', ({ }) => - { - }) .ws('/info/system', { response: z.discriminatedUnion('type', [ z.object({ type: z.literal('info'), data: SystemInfoSchema }), - z.object({ type: z.literal('focus') }), - z.object({ type: z.literal('loading'), progress: z.number(), state: z.string().optional() }), - z.object({ type: z.literal('activeTask'), progress: z.number().nullable() }), - z.object({ type: z.literal('loaded') }), + z.object({ type: z.literal('focus') }) ]), async open (ws) { - const existingLoading = taskQueue.findJob(ReloadPluginsJob.id, ReloadPluginsJob); - if (existingLoading) ws.send({ type: 'loading', progress: existingLoading.progress, state: existingLoading.state }); - else ws.send({ type: 'loaded' }); - - ws.send({ type: 'activeTask', progress: taskQueue.getActiveJobs()[0]?.progress }); - - const startInfo = async () => - { - const battery = await si.battery(); - const wifi = await si.wifiConnections(); - const bluetooth = await si.bluetoothDevices(); - ws.send({ - type: 'info', - data: { - battery: battery, - wifiConnections: wifi, - bluetoothDevices: bluetooth - } - }, true); - }; - startInfo(); + const battery = await si.battery(); + const wifi = await si.wifiConnections(); + const bluetooth = await si.bluetoothDevices(); + ws.send({ + type: 'info', + data: { + battery: battery, + wifiConnections: wifi, + bluetoothDevices: bluetooth + } + }, true); const handleFocus = () => ws.send({ type: 'focus' }); events.on('focus', handleFocus); - const dispose: (() => void)[] = []; - - dispose.push(taskQueue.on('progress', e => - { - ws.send({ type: 'activeTask', progress: e.progress }); - - if (e.id === ReloadPluginsJob.id) - { - ws.send({ type: "loading", progress: e.progress, state: e.state }); - } - else if (e.id === SelfUpdateJob.id) - { - ws.send({ type: "loading", progress: e.progress, state: e.state }); - } - })); - dispose.push(taskQueue.on('started', e => - { - ws.send({ type: 'activeTask', progress: 0 }); - - if (e.id === ReloadPluginsJob.id) - ws.send({ type: "loading", progress: e.job.progress, state: e.job.state }); - else if (e.id === SelfUpdateJob.id) - ws.send({ type: "loading", progress: e.job.progress, state: e.job.state }); - })); - dispose.push(taskQueue.on('ended', e => - { - ws.send({ type: 'activeTask', progress: null }); - if (e.id !== ReloadPluginsJob.id && e.id !== SelfUpdateJob.id) return; - ws.send({ type: "loaded" }); - })); - - (ws.data as any).dispose = [...dispose, () => - { - events.removeListener('focus', handleFocus); - }]; + (ws.data as any).dispose = [() => events.removeListener('focus', handleFocus)]; (ws.data as any).observer = setInterval(async () => { const battery = await si.battery(); @@ -247,10 +180,6 @@ export const system = new Elysia({ prefix: '/api/system' }) { currentPath = path.resolve(process.cwd(), currentPath); } - const currentPathExists = await fs.exists(currentPath); - if (!currentPathExists) currentPath = dirname(process.cwd()); - const currentPathStat = await fs.stat(currentPath); - if (!currentPathStat.isDirectory()) currentPath = dirname(currentPath); const paths = await fs.readdir(currentPath, { withFileTypes: true }); return { name: path.basename(currentPath), @@ -280,16 +209,4 @@ export const system = new Elysia({ prefix: '/api/system' }) await openExternal(url); }, { body: z.object({ url: z.string() }) - }) - .get('/update', async () => - { - return checkUpdate(); - }) - .post('/update', async () => - { - return taskQueue.enqueue(SelfUpdateJob.id, new SelfUpdateJob()); - }) - .post('/update/check', async () => - { - return checkUpdate(true); }); \ No newline at end of file diff --git a/src/packages/gameflow-sdk/task-queue.ts b/src/bun/api/task-queue.ts similarity index 60% rename from src/packages/gameflow-sdk/task-queue.ts rename to src/bun/api/task-queue.ts index e86cebc..2ef241c 100644 --- a/src/packages/gameflow-sdk/task-queue.ts +++ b/src/bun/api/task-queue.ts @@ -1,7 +1,7 @@ + import EventEmitter from 'node:events'; import z from 'zod'; -import { JobStatus } from './shared'; export class TaskQueue { @@ -9,33 +9,14 @@ export class TaskQueue private queue?: JobContext, any, string>[] = []; private events?: EventEmitter = new EventEmitter(); - constructor() - { - // we need a default error listener or app crashes - this.events?.addListener('error', e => - { - console.error(e); - }); - } - - public enqueue (id: string, job: T, options?: { throwOnCancel?: boolean; }): T extends IJob + public enqueue (id: string, job: T): T extends IJob ? Promise : never { this.disposeSafeguard(); if (!this.queue || !this.events) throw new Error("Queue disposed"); - if (this.activeQueue.some(j => j.id === id)) throw new Error(`Job with ID ${id} already active`); - if (this.queue.some(j => j.id === id)) throw new Error(`Job with ${id} already queued`); - const context = new JobContext(id, this.events, job, options); + const context = new JobContext(id, this.events, job); this.queue.push(context as any); - context.abortSignal.addEventListener('abort', () => - { - const queueIndex = this.queue?.findIndex(c => c === context); - if (queueIndex !== undefined && queueIndex >= 0) - { - this.queue?.splice(queueIndex, 1); - } - }); this.events?.emit('queued', { id: context.id, job: context }); this.processQueue(); return context.promise.promise as any; @@ -45,24 +26,7 @@ export class TaskQueue { if (!this.queue) return Promise.resolve(); - let activeGroupsSet = new Set(this.activeQueue.filter(j => j.job.group).map(j => j.job.group)); - const next = this.queue.filter(j => - { - if (j.job.group) - { - // Only take one task per group to be active - if (!activeGroupsSet.has(j.job.group)) - { - activeGroupsSet.add(j.job.group); - return true; - } - } else - { - return true; - } - - return false; - }).map((job, i) => ({ i, job })); + const next = this.queue.filter(j => !j.job.group || !this.activeQueue.some(a => a.job.group === j.job.group)).map((job, i) => ({ i, job })); next.reverse().forEach(({ i }) => this.queue!.splice(i, 1)); @@ -70,7 +34,7 @@ export class TaskQueue { job.job.start(); this.activeQueue.push(job.job); - job.job.promise.promise.catch(e => { }).finally(() => + job.job.promise.promise.finally(() => { const index = this.activeQueue.indexOf(job.job); this.activeQueue.splice(index, 1); @@ -91,11 +55,6 @@ export class TaskQueue return this.activeQueue.length > 0; } - public hasQueued () - { - return this.queue && this.queue.length > 0; - } - public hasActiveOfType (type: any) { for (const entry of this.activeQueue) @@ -114,38 +73,6 @@ export class TaskQueue return job?.promise.promise ?? Promise.resolve(); } - public waitForAll () - { - return new Promise((resolve) => - { - if (!this.hasActive()) - { - resolve(true); - return; - } - - const handleEnded = () => - { - if (!this.hasActive() && !this.hasQueued()) - { - resolve(true); - this.events?.removeListener('ended', handleEnded); - this.events?.removeListener('abort', handleEnded); - } - }; - this.events?.on('ended', handleEnded); - this.events?.on('abort', handleEnded); - }); - } - - public cancelJob (id: string) - { - const job = this.queue?.find(j => j.id === id) - ?? this.activeQueue?.find(j => j.id === id); - - job?.abort('cancel'); - } - public findJob ( id: string, type: new (...args: any[]) => T @@ -163,16 +90,6 @@ export class TaskQueue return undefined as any; } - public getActiveJobs () - { - return this.activeQueue; - } - - public getQueuedJobs () - { - return this.queue; - } - 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); @@ -188,18 +105,7 @@ export class TaskQueue { this.queue = []; this.activeQueue.forEach(c => c.abort()); - return Promise.all(this.activeQueue.map(c => - { - return new Promise(resolve => - { - c.promise.promise.then(resolve).catch(e => - { - console.error("Error During Task Queue Closing"); - resolve(false); - }); - setTimeout(resolve, 5000); - }); - })); + return Promise.all(this.activeQueue.map(c => c.promise.promise)); } } @@ -215,36 +121,35 @@ export interface EventsList queued: [e: BaseEvent]; } -export interface BaseEvent +interface BaseEvent { id: string; job: IPublicJob; } -export interface ErrorEvent extends BaseEvent +interface ErrorEvent extends BaseEvent { error: unknown; } -export interface AbortEvent extends BaseEvent +interface AbortEvent extends BaseEvent { reason?: any; } -export interface ProgressEvent extends BaseEvent +interface ProgressEvent extends BaseEvent { progress: number; state?: string; } -export interface CompletedEvent extends BaseEvent +interface CompletedEvent extends BaseEvent { } export interface IJob { - /** What group does the job belong to. Grouped jobs can only have 1 active job per group */ group?: string; start (context: JobContext, TData, TState>): Promise; exposeData?(): TData; @@ -285,14 +190,12 @@ export class JobContext, TData, TState extends str private events: EventEmitter; private abortController: AbortController; private m_promise: PromiseWithResolvers; - private throwOnCancel: boolean; private readonly m_job: T; - constructor(id: string, events: EventEmitter, job: T, options?: { throwOnCancel?: boolean; }) + constructor(id: string, events: EventEmitter, job: T) { this.m_id = id; this.m_job = job; - this.throwOnCancel = options?.throwOnCancel ?? false; this.abortController = new AbortController(); this.abortController.signal.addEventListener('abort', () => { @@ -309,40 +212,20 @@ export class JobContext, TData, TState extends str { this.events.emit('started', { id: this.m_id, job: this }); await this.m_job.start(this); - if (!this.abortSignal.aborted) - { - this.completed = true; - this.events.emit('completed', { id: this.m_id, job: this }); - this.m_promise.resolve(this.m_job.exposeData?.()); - } else - { - this.m_promise.resolve(undefined); - } + this.completed = true; + this.events.emit('completed', { id: this.m_id, job: this }); + this.m_promise.resolve(this.m_job.exposeData?.()); + } catch (error) { - if (error instanceof Event) + if (error !== 'cancel') { - if (error.target instanceof AbortSignal) - { - if (this.throwOnCancel) - { - this.m_promise.reject(this.abortSignal.reason); - } else - { - this.m_promise.resolve(undefined); - } - } else - { - console.error(error); - this.m_promise.reject(error); - } - } else - { - this.events.emit('error', { id: this.m_id, job: this, error }); - this.error = error; - this.m_promise.reject(error); + console.error(error); } + this.events.emit('error', { id: this.m_id, job: this, error }); + this.error = error; + this.m_promise.reject(error); } finally { this.running = false; diff --git a/src/bun/browser.ts b/src/bun/browser.ts index 8d71427..79c01b8 100644 --- a/src/bun/browser.ts +++ b/src/bun/browser.ts @@ -3,45 +3,22 @@ import { BrowserParams, BuildParams } from './utils/browser-params'; import os from 'node:os'; import { EventEmitter } from 'node:stream'; import { dlopen, FFIType, Pointer } from "bun:ffi"; -import { SERVER_URL } from '@/shared/constants'; -import { host } from './utils/host'; -import fs from 'node:fs/promises'; -import { ensureDir } from 'fs-extra'; -import path from 'node:path'; -export default async function init (events: EventEmitter, params: BrowserParams) +export default async function init (events: EventEmitter, forceBrowser: boolean, params: BrowserParams) { - if (params.forceNWJS) - { - await runNW(events, params); - return; - } - - if (params.forceBrowser) + if (forceBrowser) { await runBrowser(events, params); - return; + } else + { + try + { + await runWebview(events, params); + } catch (error) + { + await runBrowser(events, params); + } } - - try - { - await runWebview(events, params); - return; - } catch (error) - { - console.error(error); - } - - try - { - await runNW(events, params); - return; - } catch (error) - { - console.error(error); - } - - await runBrowser(events, params); } function focusWindow (id: Pointer) @@ -67,61 +44,8 @@ function focusWindow (id: Pointer) } } -async function runNW (events: EventEmitter, params: BrowserParams) -{ - let nwPath = process.platform === 'win32' ? './bin/nw/nw.exe' : './bin/nw/nw'; - if (process.env.FLATPAK_BUILD) - { - nwPath = '/app/bin/nw/nw'; - } else if (process.env.APPIMAGE) - { - nwPath = path.join(process.env.APPDIR ?? '', 'usr', 'bin', 'nw'); - } - - if (!await fs.exists(nwPath)) - { - throw new Error(`Could not find NW.js at ${nwPath}`); - } - const signalHandler = new AbortController(); - const chromeArgs: string[] = ['--in-process-gpu']; - if (params.isSteamDeckGameMode) - { - chromeArgs.push('--kiosk'); - chromeArgs.push(`--window-size=1280,800`); - } else if (params.windowSize) - { - chromeArgs.push(`--window-size=${params.windowSize.width},${params.windowSize.height}`); - } - if (params.windowPosition) chromeArgs.push(`--window-position=${params.windowPosition.x},${params.windowPosition.y}`); - events.on('exitapp', () => signalHandler.abort()); - const configPath = path.join(params.configPath, 'nw-user-data'); - await ensureDir(configPath); - console.log("NW config path at:", configPath); - const args = [nwPath, `--url=${SERVER_URL(host)}`, `--user-data-dir=${configPath}`]; - - if (process.env.NODE_ENV !== 'development') - { - console.log("Disabling devtools"); - args.push("--disable-devtools"); - } - console.log("Launching NW.js"); - const nwProcess = Bun.spawn(args, { - signal: signalHandler.signal, - killSignal: "SIGKILL", - env: { - ...process.env, - NW_PRE_ARGS: chromeArgs.join(" ") - } - }); - await nwProcess.exited; -} - async function runWebview (events: EventEmitter, params: BrowserParams) { - if (process.platform !== 'win32') - { - throw new Error("Webview only supported on windows"); - } const webviewPath = process.env.IS_BINARY ? `./webview/${os.platform()}` : new URL(`./webview/${os.platform()}`, import.meta.url).href; console.log("Launching Webview Worker at: ", webviewPath); const config: Record = {}; @@ -182,7 +106,8 @@ async function runBrowser (events: EventEmitter, params: BrowserParams) const browserParams = await BuildParams(params); if (!browserParams) { - throw new Error("Could not find valid browser"); + console.error("Could not find valid browser"); + return Promise.resolve(); } else if (!Bun.env.HEADLESS) { diff --git a/src/bun/index.ts b/src/bun/index.ts index 7ad5803..146c195 100644 --- a/src/bun/index.ts +++ b/src/bun/index.ts @@ -5,27 +5,14 @@ import { dirname } from 'pathe'; import { createInterface } from 'readline'; import { isSteamDeckGameMode } from './utils'; -async function cleanup (code: number) +async function cleanup () { - app.cleanup() - .then(() => - { - process.exit(code); - }) - .catch(e => console.error); + await app.cleanup(); + process.exit(0); } await app.load(); -async function shutdown (code: number) -{ - console.log("Graceful Shutdown"); - await cleanup(code); -} - -process.on("SIGINT", () => shutdown(0)); -process.on("SIGTERM", () => shutdown(0)); - if (process.env.HEADLESS) { const rl = createInterface({ input: process.stdin }); @@ -35,7 +22,7 @@ if (process.env.HEADLESS) if (line.trim() === "shutdown") { console.log("Graceful Shutdown"); - await cleanup(0); + await cleanup(); } }); @@ -43,23 +30,23 @@ if (process.env.HEADLESS) app.events.on('exitapp', () => { process.stdout.write('exitapp\n'); - process.send?.("exitapp"); - cleanup(0); + cleanup(); }); app.events.on('focus', () => { process.stdout.write("focus\n"); - process.send?.("focus"); }); } else { - await init(app.events, { + await init(app.events, process.env.FORCE_BROWSER === "true", { configPath: dirname(app.config.path), windowPosition: app.config.get('windowPosition'), windowSize: app.config.get('windowSize'), - isSteamDeckGameMode: isSteamDeckGameMode(), - forceBrowser: process.env.FORCE_BROWSER === "true", - forceNWJS: process.env.FORCE_NWJS === "true" + isSteamDeckGameMode: isSteamDeckGameMode() }); - await cleanup(0); -} \ No newline at end of file + await cleanup(); +} + + + + diff --git a/src/bun/types/helpers.d.ts b/src/bun/types/helpers.d.ts deleted file mode 100644 index afd8ea5..0000000 --- a/src/bun/types/helpers.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare module '*.bat' { - const content: string; - export default content; -} - -declare module '*.sh' { - const content: string; - export default content; -} - -declare module '*.ini' { - const content: string; - export default content; -} - -declare module '*.bin' { - const content: string; - export default content; -} \ 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..1bc7a22 --- /dev/null +++ b/src/bun/types/types.d.ts @@ -0,0 +1,32 @@ +declare interface ObjectConstructor +{ + /** + * Groups members of an iterable according to the return value of the passed callback. + * @param items An iterable. + * @param keySelector A callback which will be invoked for each item in items. + */ + groupBy ( + items: Iterable, + keySelector: (item: T, index: number) => K, + ): Partial>; +} + +declare interface MapConstructor +{ + /** + * Groups members of an iterable according to the return value of the passed callback. + * @param items An iterable. + * @param keySelector A callback which will be invoked for each item in items. + */ + groupBy ( + items: Iterable, + keySelector: (item: T, index: number) => K, + ): Map; +} + +declare interface AppEventMap +{ + exitapp: []; + notification: [FrontendNotification]; + focus: []; +} \ No newline at end of file diff --git a/src/bun/types/typesc.schema.ts b/src/bun/types/typesc.schema.ts new file mode 100644 index 0000000..fba1435 --- /dev/null +++ b/src/bun/types/typesc.schema.ts @@ -0,0 +1,35 @@ +import z from "zod"; +import { GameflowHooks } from "../api/hooks/app"; +import { ChildProcess } from "node:child_process"; + +export const PluginContextSchema = z.object({ + hooks: z.instanceof(GameflowHooks) +}); + +export const PluginDescriptionSchema = z.object({ + name: z.string(), + displayName: z.string(), + version: z.string(), + description: z.string(), + icon: z.url().optional(), + keywords: z.array(z.string()).optional() +}); + +export const PluginSchema = z.object({ + setup: z.function().output(z.promise(z.void())).optional(), + load: z.function().input([PluginContextSchema]).output(z.void()), + onBeforeReload: z.function().input([PluginContextSchema]).output(z.void()).optional(), + cleanup: z.function().output(z.promise(z.void())).optional() +}); + +export type PluginType = z.infer; +export type PluginContextType = z.infer; +export type PluginDescriptionType = z.infer; + +export const ActiveGameSchema = z.object({ + process: z.any().optional(), + gameId: z.number(), + name: z.string(), + command: z.object({ command: z.string(), startDir: z.string().optional() }) +}); +export type ActiveGameType = z.infer; \ No newline at end of file diff --git a/src/bun/utils.ts b/src/bun/utils.ts index 6fbc630..c21a78b 100644 --- a/src/bun/utils.ts +++ b/src/bun/utils.ts @@ -1,11 +1,8 @@ import { $, sleep } from 'bun'; import path from 'node:path'; -import { SettingsType, KeysWithValueAssignableTo } from '@simeonradivoev/gameflow-sdk/shared'; +import { SettingsType } from '@/shared/constants'; import { config } from './api/app'; import fs from 'node:fs/promises'; -import packageDef from '~/package.json'; - -const archiveRegex = /.(zip|rar|7zip|7z|tar|tar.gz)$/i; export function checkRunning (pid: number) { @@ -175,29 +172,4 @@ export async function moveAllFiles (srcDir: string, destDir: string) }); } } -} - -export function getAppVersion () -{ - return process.env.VERSION_OVERRIDE ?? packageDef.version; -} - -export function isArchive (path: string) -{ - return archiveRegex.test(path); -} - -export function IsPluginAllowed (id: string) -{ - if (process.env.PLUGIN_WHITELIST && !process.env.PLUGIN_WHITELIST.includes(id)) - { - return false; - } - - if (process.env.PLUGIN_BLACKLIST && process.env.PLUGIN_BLACKLIST.includes(id)) - { - return false; - } - - return true; } \ No newline at end of file diff --git a/src/bun/utils/browser-params.ts b/src/bun/utils/browser-params.ts index 5059137..0023219 100644 --- a/src/bun/utils/browser-params.ts +++ b/src/bun/utils/browser-params.ts @@ -11,8 +11,6 @@ export interface BrowserParams windowPosition?: { x: number, y: number; }; windowSize?: { width?: number, height?: number; }; isSteamDeckGameMode: boolean; - forceBrowser?: boolean; - forceNWJS?: boolean; } export async function BuildParams (data: BrowserParams) @@ -56,13 +54,6 @@ export async function BuildParams (data: BrowserParams) args.push('--allow-insecure-localhost'); args.push('--auto-accept-camera-and-microphone-capture'); - if (process.env.FLATPAK_BUILD) - { - args.push('--no-sandbox'); - args.push('--disable-gpu-sandbox'); - args.push('--test-type'); - } - if (data.isSteamDeckGameMode) { args.push('--kiosk'); diff --git a/src/bun/utils/downloader.ts b/src/bun/utils/downloader.ts index 920e7c8..2a014b9 100644 --- a/src/bun/utils/downloader.ts +++ b/src/bun/utils/downloader.ts @@ -1,11 +1,15 @@ -import { ensureDir } from "fs-extra"; +import { ensureDir, move } from "fs-extra"; import path from 'node:path'; import fs from 'node:fs/promises'; import { createWriteStream } from "node:fs"; import { config, jar } from "../api/app"; import { moveAllFiles } from "../utils"; -import { DownloadFileEntry, ProgressStats } from "@simeonradivoev/gameflow-sdk/shared"; + +export interface ProgressStats +{ + progress: number; +} interface TmpDownloadMetadata { @@ -23,23 +27,15 @@ export class Downloader onProgress?: (stats: ProgressStats) => void; signal?: AbortSignal; activeFile?: DownloadFileEntry; - downloadPath: string | undefined; + downloadPath: string; id: string; tmpPath: string; tmpPathMeta: string; - downloadSpeed: number = 0; - /** - * - * @param id Id of the download. Should be unique - * @param files All the files to download - * @param downloadPath The destination path when all downloads are complete they will bemoved here. If undefined they will remain in the tmp path. - */ constructor( id: string, files: DownloadFileEntry[], - downloadPath: string | undefined, - init?: { + downloadPath: string, init?: { headers?: Record, onProgress?: (stats: ProgressStats) => void; signal?: AbortSignal; @@ -159,7 +155,10 @@ export class Downloader }); const totalBytes = totalSize || Number(res.headers.get("content-length")) || 0; - bytesReceived += start; + if (totalSize <= 0) + bytesReceived = 0; + else + bytesReceived += start; const reader = res.body!.getReader(); @@ -174,11 +173,10 @@ export class Downloader if (totalBytes > 0 && this.onProgress) { const percent = (bytesReceived / totalBytes) * 100; - const timeDelta = Date.now() - lastUpdate; - if (timeDelta > 100) + + if (Date.now() - lastUpdate > 100) { - this.downloadSpeed = this.downloadSpeed * 0.8 + Math.round(value.length / (timeDelta / 1000)) * 0.2; - this.onProgress({ progress: percent, downloaded: bytesReceived, total: totalBytes, speed: this.downloadSpeed }); + this.onProgress({ progress: percent }); lastUpdate = Date.now(); } } @@ -188,7 +186,7 @@ export class Downloader if (this.signal.reason === 'cancel') { console.log("Canceling Download and cleaning up files"); - await fs.rm(this.tmpPath, { recursive: true, maxRetries: 3, retryDelay: 3 }); + await fs.rm(this.tmpPath, { recursive: true }); await fs.rm(this.tmpPathMeta); return; } @@ -212,19 +210,11 @@ export class Downloader }); } - if (this.downloadPath === undefined) - { - await fs.rm(this.tmpPathMeta); - return this.files.map(f => path.join(this.tmpPath, f.file_path, f.file_name)); - } else - { - await moveAllFiles(this.tmpPath, this.downloadPath); - if (await fs.exists(this.tmpPath)) - await fs.rm(this.tmpPath, { recursive: true }); - await fs.rm(this.tmpPathMeta); - - return this.files.map(f => path.join(this.downloadPath!, f.file_path, f.file_name)); - } + await moveAllFiles(this.tmpPath, this.downloadPath); + if (await fs.exists(this.tmpPath)) + await fs.rm(this.tmpPath, { recursive: true }); + await fs.rm(this.tmpPathMeta); + return this.files.map(f => path.join(this.downloadPath, f.file_path, f.file_name)); } } \ No newline at end of file diff --git a/src/bun/utils/get-browser.ts b/src/bun/utils/get-browser.ts index 7ba788e..ffcf07c 100644 --- a/src/bun/utils/get-browser.ts +++ b/src/bun/utils/get-browser.ts @@ -1,4 +1,4 @@ -import { Glob, spawnSync } from "bun"; +import { spawnSync } from "bun"; import { platform } from "node:os"; import { RunBrowserType } from "./browser-spawner"; import path from 'node:path'; @@ -35,18 +35,25 @@ interface BrowserResult source: GetBrowserSource; } -/** The expected binary path per platform after extraction */ -async function getBundledBinaryPath (outDir: string, version: string, platform: string, arch: string): Promise -{ - let glob: Glob | undefined = undefined; - if (platform === "linux") glob = new Glob(`**/chrome`); - else if (platform === "darwin") glob = new Glob(`**/Chromium.app`); - else glob = new Glob(`**/chrome.exe`); +const PLATFORM_MAP: Record = { + linux: "linux", + win32: "windows", + darwin: 'macos' +}; - for await (const bin of glob.scan({ cwd: outDir })) - { - return path.join(outDir, bin); - } +const ARCH_MAP: Record> = { + linux: { x64: "x86_64", arm64: "arm64" }, + darwin: { x64: "x86_64", arm64: "arm64" }, + win32: { x64: "x64", arm64: "arm64" }, +}; + +/** The expected binary path per platform after extraction */ +function getBundledBinaryPath (outDir: string, version: string, platform: string, arch: string): string +{ + const subFolder = `ungoogled-chromium_${version}_${PLATFORM_MAP[platform]}_${ARCH_MAP[platform][arch]}`; + if (platform === "linux") return path.join(outDir, subFolder, "chrome"); + if (platform === "darwin") return path.join(outDir, "Chromium.app"); + return path.join(outDir, subFolder, "chrome.exe"); } /** @@ -94,14 +101,10 @@ export async function getBrowserPath (config?: BrowserPriorityConfig): Promisenul -echo Cleaning up... -rmdir /S /Q "{{{extractDir}}}" -del "{{{tempFile}}}" -echo Done! Restarting... -start "" /D "{{{installDir}}}" "{{{exePath}}}" -del "%~f0" \ No newline at end of file diff --git a/src/bun/webview/linux.ts b/src/bun/webview/linux.ts index 6683249..c53ccca 100644 --- a/src/bun/webview/linux.ts +++ b/src/bun/webview/linux.ts @@ -1,9 +1,36 @@ import { Size, SizeHint, Webview } from 'webview-bun'; import webviewWorkerBase from "./base"; -console.log("Launching Webview"); -let size: Size | undefined = undefined; -if (process.env.WINDOW_WIDTH && process.env.WINDOW_HEIGHT) - size = { width: Number(process.env.WINDOW_WIDTH), height: Number(process.env.WINDOW_HEIGHT), hint: SizeHint.NONE }; -const webview = new Webview(process.env.NODE_ENV === 'development', size); -webviewWorkerBase(webview); \ No newline at end of file +if (process.env.FLATPAK_BUILD === "true") +{ + let webview: Bun.Subprocess | undefined = undefined; + let hostUrl: string | undefined = undefined; + webviewWorkerBase({ + navigate: (url) => + { + hostUrl = url; + + }, destroy: () => webview?.kill(), run: () => + { + webview = Bun.spawn(["webview", hostUrl ?? ''], { + stdout: "inherit", + stderr: "inherit", + env: { + ...process.env, + }, + onExit () + { + postMessage({ data: 'destroyed' }); + } + }); + } + }); +} else +{ + console.log("Launching Webview"); + let size: Size | undefined = undefined; + if (process.env.WINDOW_WIDTH && process.env.WINDOW_HEIGHT) + size = { width: Number(process.env.WINDOW_WIDTH), height: Number(process.env.WINDOW_HEIGHT), hint: SizeHint.NONE }; + const webview = new Webview(process.env.NODE_ENV === 'development', size); + webviewWorkerBase(webview); +} \ No newline at end of file diff --git a/src/mainview/App.tsx b/src/mainview/App.tsx deleted file mode 100644 index 605f15d..0000000 --- a/src/mainview/App.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation"; -import { Router } from "."; -import { useEffect } from "react"; -import audioCallbacks from "./scripts/feedbackCallbacks"; -import { client as rommClient } from "../clients/romm/client.gen"; -import { RPC_URL } from "@/shared/constants"; - -export const focusQueue: string[] = []; - -export default function App (data: { children: any; }) -{ - useEffect(() => - { - const focusMap = new Map(); - rommClient.setConfig({ - baseUrl: `${RPC_URL(__HOST__)}/api/romm`, - credentials: "include", - mode: "cors", - }); - - const unsub = Router.history.subscribe((op) => - { - if (op.action.type === 'PUSH') - { - focusMap.set(op.location.state.__TSR_index - 1, getCurrentFocusKey()); - } else if (op.action.type === 'BACK') - { - if (focusMap.has(op.location.state.__TSR_index)) - { - focusQueue.pop(); - focusQueue.push(focusMap.get(op.location.state.__TSR_index)!); - focusMap.delete(op.location.state.__TSR_index); - } - } - }); - - const audio = audioCallbacks(); - - return () => - { - unsub(); - audio.cleanup(); - }; - }, []); - - return <>{data.children}; -} \ No newline at end of file diff --git a/src/mainview/assets/intro.ogg b/src/mainview/assets/intro.ogg deleted file mode 100644 index e1505f9..0000000 --- a/src/mainview/assets/intro.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:231ac69f71f4a0a770ae4bbfd42db9ea136dad6813ddae68a211c74a16e21778 -size 74296 diff --git a/src/mainview/assets/sounds.json b/src/mainview/assets/sounds.json deleted file mode 100644 index 309705d..0000000 --- a/src/mainview/assets/sounds.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "sprite": { - "Classic UI SFX - Chords #2": [ - 0, - 4005.215419501134 - ], - "Classic UI SFX - Short - Low #2": [ - 6000, - 2583.333333333334 - ], - "Classic UI SFX - Short - Low #5": [ - 10000, - 3161.473922902495 - ], - "Classic UI SFX - Short - High #9": [ - 15000, - 2250 - ], - "UI_TwoNote Up_Set 11_01": [ - 21000, - 129.16099773242706 - ], - "UI_TwoNote Up_Set 11_02": [ - 23000, - 250 - ], - "Classic UI SFX - Short - High #3": [ - 25000, - 2864.6031746031754 - ], - "Classic UI SFX - Short - High #19": [ - 29000, - 3052.0861678004535 - ], - "Classic UI SFX - Short - High #22": [ - 34000, - 2489.5918367346967 - ], - "Classic UI SFX - Short - High #25": [ - 38000, - 2005.215419501134 - ], - "Classic UI SFX - Chords #16": [ - 42000, - 4005.215419501134 - ], - "Classic UI SFX - Short - High #8": [ - 48000, - 2916.6666666666642 - ], - "UI_Single_Set 16_03": [ - 52000, - 309.5918367346968 - ], - "UI_Single_Set 16_01": [ - 54000, - 309.5918367346968 - ], - "UI_Single_Set 5_02": [ - 56000, - 875.0113378684787 - ], - "UI_Single_Set 5_04": [ - 58000, - 531.247165532882 - ], - "UI_Single_Set 5_03": [ - 60000, - 531.247165532882 - ], - "UI_Single_Set 5_01": [ - 62000, - 875.0113378684787 - ], - "UI_Single_Set 11_02": [ - 64000, - 93.74149659863917 - ], - "Classic UI SFX - Short - Low #6": [ - 66000, - 2333.3333333333285 - ], - "UI SFX_InGameMenu_Open": [ - 70000, - 2614.104308390026 - ] - } -} \ No newline at end of file diff --git a/src/mainview/assets/sounds.ogg b/src/mainview/assets/sounds.ogg deleted file mode 100644 index 835432f..0000000 --- a/src/mainview/assets/sounds.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38721ebc90eb07ef7e00c0a1a64bd363e61dbbd08aa32b12a33da5ead0597948 -size 408079 diff --git a/src/mainview/components/AnimatedBackground.tsx b/src/mainview/components/AnimatedBackground.tsx index 36caedf..31ea52a 100644 --- a/src/mainview/components/AnimatedBackground.tsx +++ b/src/mainview/components/AnimatedBackground.tsx @@ -25,13 +25,17 @@ export function AnimatedBackground (data: { ) : useState(); + const [lastBackgroundUrl, setLastBackgroundUrl] = useState(undefined); const backgroundElementRef = useRef(null); useEffect(() => { + const lastBg = backgroundUrl; + if (data.backgroundUrl != backgroundUrl) { setBackgroundUrl(data.backgroundUrl ? (data.backgroundUrl instanceof URL ? data.backgroundUrl.href : data.backgroundUrl) : undefined); + setLastBackgroundUrl(lastBg); } }, [data.backgroundUrl]); @@ -40,6 +44,13 @@ export function AnimatedBackground (data: { { finalBackgroundUrl = backgroundUrl ? new URL(backgroundUrl) : undefined; } catch { } + + let finalLastBackgroundUrl: URL | undefined; + try + { + finalLastBackgroundUrl = lastBackgroundUrl ? new URL(lastBackgroundUrl) : undefined; + } catch { } + const blur = useLocalSetting('backgroundBlur'); if (blur) { @@ -48,7 +59,13 @@ export function AnimatedBackground (data: { finalBackgroundUrl?.searchParams.set('blur', String(24)); } + if (!finalLastBackgroundUrl?.searchParams.has('blur')) + { + finalLastBackgroundUrl?.searchParams.set('blur', String(24)); + } + finalBackgroundUrl?.searchParams.set('height', String(320)); + finalLastBackgroundUrl?.searchParams.set('height', String(320)); } useEffect(() => @@ -73,6 +90,8 @@ export function AnimatedBackground (data: { function handleSetBackground (url: string) { + + setLastBackgroundUrl(backgroundUrl); setBackgroundUrl(url); } @@ -101,7 +120,7 @@ export function AnimatedBackground (data: { > {!data.scrolling &&
- + {blur && finalLastBackgroundUrl && } {finalBackgroundUrl ? (); - const [appContext, setAppContext] = useState({} as AppInfoContext); - const [loadingInfo, setLoadingInfo] = useState(undefined); - const [loading, setLoading] = useState(true); - const loadingProgressBarRef = useRef(null); + const [systemInfo, setSystemInfo] = useState(); useEffect(() => { const sub = systemApi.api.system.info.system.subscribe(); @@ -26,42 +20,14 @@ export default function AppCommunication (data: { children: any; }) case "focus": window.focus(); break; - case "activeTask": - setAppContext(c => ({ ...c, activeTaskProgress: data.progress })); - break; - case "loading": - setLoadingInfo(data.state); - if (loadingProgressBarRef.current) - loadingProgressBarRef.current.value = data.progress; - setLoading(true); - break; - case "loaded": - setLoading(false); - break; } + }); document.documentElement.dataset.loaded = "true"; - return () => - { - sub.close(); - }; }, []); return - - {loading ? - -
-
- - {loadingInfo} -
- -
-
- : data.children} - -
+ {data.children}
; } \ No newline at end of file diff --git a/src/mainview/components/AutoFocus.tsx b/src/mainview/components/AutoFocus.tsx index 7bed71b..2744e8e 100644 --- a/src/mainview/components/AutoFocus.tsx +++ b/src/mainview/components/AutoFocus.tsx @@ -12,11 +12,7 @@ export function AutoFocus (data: { { let delayTimeout: number | undefined; - const focusDoesntExist = !doesFocusableExist(getCurrentFocusKey()); - const parentFocus = getCurrentFocusKey() === data.parentKey; - const noFocus = !getCurrentFocusKey(); - - if (data.force || noFocus || parentFocus || focusDoesntExist) + if (data.force || !getCurrentFocusKey() || getCurrentFocusKey() === data.parentKey || !doesFocusableExist(getCurrentFocusKey())) { if (data.delay) { @@ -25,8 +21,8 @@ export function AutoFocus (data: { { data.focus({ instant: true }); } - } + } return () => { if (delayTimeout) diff --git a/src/mainview/components/CardElement.tsx b/src/mainview/components/CardElement.tsx index 6e27664..7f5f2dc 100644 --- a/src/mainview/components/CardElement.tsx +++ b/src/mainview/components/CardElement.tsx @@ -1,10 +1,8 @@ -import { useFocusable } from "@noriginmedia/norigin-spatial-navigation"; +import { FocusDetails, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import classNames from "classnames"; import { JSX } from "react"; import { twMerge } from "tailwind-merge"; import useActiveControl from "../scripts/gamepads"; -import { oneShot } from "../scripts/audio/audio"; -import ImageWithFallbacks from "./ImageWithFallbacks"; export function GameCardSkeleton () { @@ -19,17 +17,20 @@ export function GameCardSkeleton () ); } -export interface GameCardParams extends FocusParams +export type GameCardFocusHandler = (id: string, node: HTMLElement, details: FocusDetails) => void; + +export interface GameCardParams { title: string; - subtitle?: string | JSX.Element; - preview?: string | JSX.Element | URL[] | ((p: { focused: boolean; }) => JSX.Element); + subtitle: string | JSX.Element; + preview?: string | JSX.Element | ((p: { focused: boolean; }) => JSX.Element); srcset?: string; focusKey: string; index: number; id: string; badges?: JSX.Element[]; className?: string; + onFocus?: GameCardFocusHandler; onBlur?: (id: string) => void; clickFocuses?: boolean; previewClassName?: string; @@ -37,34 +38,14 @@ export interface GameCardParams extends FocusParams export default function CardElement (data: GameCardParams & InteractParams) { - const handleAction = (event?: Event) => - { - data.onAction?.({ event, focusKey }); - oneShot('click'); - }; - const { ref, focused, focusSelf, focusKey } = useFocusable({ + const { ref, focused, focusSelf } = useFocusable({ focusKey: data.focusKey, - onFocus: (l, p, details) => data.onFocus?.(focusKey, ref.current as any, details), - onEnterPress: handleAction, + onFocus: (l, p, details) => data.onFocus?.(data.id, ref.current as any, details), + onEnterPress: () => data.onAction?.(), onBlur: () => data.onBlur?.(data.id), }); const { isPointer } = useActiveControl(); - let preview: any = undefined; - if (typeof data.preview === "string") - { - preview = ; - } else if (Array.isArray(data.preview)) - { - preview = ; - } else if (typeof data.preview === 'function') - { - preview = data.preview({ focused }); - } else - { - preview = data.preview; - } - return (
  • + onDoubleClick={e => data.onAction?.(e.nativeEvent)} + onClick={() => { focusSelf(); - handleAction(e.nativeEvent); + data.onAction?.(); }} className={twMerge( "relative game-card light:bg-base-100 dark:bg-base-300 flex flex-col z-5 overflow-hidden transition-all duration-200 not-mobile:drop-shadow-lg cursor-pointer focusable focusable-primary focusable-hover select-none focused focused:not-control-mouse:animate-wiggle focused:not-control-mouse:bg-base-content focused:not-control-mouse:text-base-300 focused:not-control-mouse:drop-shadow-lg focused:not-control-mouse:drop-shadow-black/30 focused:not-control-mouse:scale-102 focused:not-control-mouse:z-10 group control-mouse:hover:bg-base-200 h-full [--tw-border-style:inset] border-2 border-base-content/5 backdrop-opacity-0 active:bg-base-content! active:text-base-100 active:transition-none", @@ -92,7 +74,11 @@ export default function CardElement (data: GameCardParams & InteractParams) focused ? "md:mt-2 md:mx-2" : "md:mt-2 md:mx-2", classNames({ "h-full": typeof data.preview === "string" }) )}> - {preview} + {typeof data.preview === "string" ? ( + + ) : ( + typeof data.preview === 'function' ? data.preview({ focused }) : data.preview + )}
  • diff --git a/src/mainview/components/CardList.tsx b/src/mainview/components/CardList.tsx index 8511374..0518d2c 100644 --- a/src/mainview/components/CardList.tsx +++ b/src/mainview/components/CardList.tsx @@ -3,11 +3,11 @@ import FocusContext, useFocusable, } from "@noriginmedia/norigin-spatial-navigation"; -import CardElement, { GameCardParams } from "./CardElement"; +import { GameMeta } from "../../shared/constants"; +import CardElement, { GameCardFocusHandler, GameCardParams } from "./CardElement"; import { JSX } from "react"; import { twMerge } from "tailwind-merge"; -import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts"; -import { oneShot } from "../scripts/audio/audio"; +import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts"; export interface GameMetaExtra extends GameMeta { @@ -16,43 +16,20 @@ export interface GameMetaExtra extends GameMeta focusKey: string; } -function LocalCardElement (data: { game: GameMetaExtra, i: number; onQuickAction?: (ctx: InteractParamsArgs) => void; } & FocusParams & InteractParams) +function LocalCardElement (data: { game: GameMetaExtra, i: number; } & FocusParams & InteractParams) { let preview: GameCardParams['preview'] = data.game.preview; - if (!preview && data.game.previewUrls) + if (!preview && data.game.previewUrl) { - preview = data.game.previewUrls; + preview = data.game.previewUrl; } - const handleAction = (ctx: InteractParamsArgs) => + const handleAction = () => { data.game.onSelect?.(); - data.onAction?.({ event, focusKey: data.game.focusKey }); - oneShot('click'); + data.onAction?.(); }; - - const handleAltAction = (ctx: InteractParamsArgs) => - { - data.game.onQuickAction?.(); - data.onQuickAction?.({ event, focusKey: data.game.focusKey }); - oneShot('click'); - }; - - useShortcuts(data.game.focusKey, () => - { - const options: Shortcut[] = [{ - label: "Details", - button: GamePadButtonCode.A, - action: event => handleAction({ event, focusKey: data.game.focusKey }) - }]; - - if (data.onQuickAction || data.game.onQuickAction) - { - options.push({ label: "Play", button: GamePadButtonCode.X, action: event => handleAltAction({ event, focusKey: data.game.focusKey }) }); - } - - return options; - }, [data.onQuickAction, data.game.onQuickAction, data.game.focusKey]); + useShortcuts(data.game.focusKey, () => [{ label: "Select", button: GamePadButtonCode.A, action: handleAction }]); return ( + onFocus={(id, node, details) => { - data.game.onFocus?.(focusKey, node, details); - data.onFocus?.(focusKey, node, { ...details, id: data.game.id }); + data.game.onFocus?.(details); + data.onFocus?.(id, node, details); }} onAction={handleAction} preview={preview} @@ -81,16 +58,16 @@ export function CardList (data: { games: GameMetaExtra[]; grid?: boolean; onSelectGame?: (id: string) => void; - focus?: string; + onGameFocus?: GameCardFocusHandler; className?: string; - finalElement?: JSX.Element | JSX.Element[]; + finalElement?: JSX.Element; saveChildFocus?: 'session' | 'local'; -} & FocusParams) +}) { const { ref, focusKey } = useFocusable({ focusKey: data.id, - focusable: data.games.length > 0 || (!!data.finalElement && (Array.isArray(data.finalElement) ? data.finalElement.length > 0 : !!data.finalElement)), - preferredChildFocusKey: data.focus + forceFocus: true, + autoRestoreFocus: true }); return ( @@ -112,12 +89,7 @@ export function CardList (data: { > {data.games.map((g, i) => data.onSelectGame?.(g.id)} - i={i} - />)} + key={g.id} onFocus={data.onGameFocus} game={g} onAction={() => data.onSelectGame?.(g.id)} i={i} />)} {data.finalElement} diff --git a/src/mainview/components/CollectionList.tsx b/src/mainview/components/CollectionList.tsx index ac30c57..8bb02a4 100644 --- a/src/mainview/components/CollectionList.tsx +++ b/src/mainview/components/CollectionList.tsx @@ -1,24 +1,25 @@ import { RPC_URL } from "@/shared/constants"; import { useSuspenseQuery } from "@tanstack/react-query"; import { CardList, GameMetaExtra } from "./CardList"; +import { GameCardFocusHandler } from "./CardElement"; import { getCollectionsQuery } from "@queries/romm"; -import { useRouter } from "@tanstack/react-router"; +import { Router } from ".."; export default function CollectionList (data: { id: string, setBackground: (url: string) => void; className?: string; + onFocus?: GameCardFocusHandler; onSelect?: (id: string) => void; saveChildFocus?: 'session' | 'local'; -} & FocusParams) +}) { - const router = useRouter(); const { data: collections } = useSuspenseQuery(getCollectionsQuery); const handleDefaultSelect = (gameId: string) => { const [source, id] = gameId.split('@'); - router.navigate({ + Router.navigate({ to: `/collection/$source/$id`, params: { source, id }, search: { countHint: collections.find(c => c.id.id === id && c.id.source === source)?.game_count } @@ -36,7 +37,8 @@ export default function CollectionList (data: { id: `${g.id.source}@${g.id.id}`, title: g.name, focusKey: `collection-${g.id}`, - previewUrls: `${RPC_URL(__HOST__)}${g.path_platform_cover}`, + subtitle: "", + previewUrl: `${RPC_URL(__HOST__)}${g.path_platform_cover}`, badges: [ {g.game_count} @@ -44,7 +46,7 @@ export default function CollectionList (data: { ], } satisfies GameMetaExtra))} onSelectGame={data.onSelect ? data.onSelect : handleDefaultSelect} - onFocus={(id, node, details) => + onGameFocus={(id, node, details) => { data.setBackground( `https://picsum.photos/id/${10 + (id ?? 0)}/100/100.webp?blur=10`, diff --git a/src/mainview/components/CollectionsDetail.tsx b/src/mainview/components/CollectionsDetail.tsx index 72c391a..35aae6f 100644 --- a/src/mainview/components/CollectionsDetail.tsx +++ b/src/mainview/components/CollectionsDetail.tsx @@ -1,53 +1,53 @@ -import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; -import { HeaderButton, StickyHeaderUI } from './Header'; +import { AnimatedBackground } from './AnimatedBackground'; +import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; +import { HeaderUI, StickyHeaderUI } from './Header'; import { GameList } from './GameList'; -import { JSX, Suspense } from 'react'; -import { FloatingShortcuts } from './Shortcuts'; +import { Search, Settings2 } from 'lucide-react'; +import { JSX, Suspense, useEffect } from 'react'; +import Shortcuts from './Shortcuts'; import { AutoFocus } from './AutoFocus'; -import { GamePadButtonCode, useShortcuts } from '../scripts/shortcuts'; -import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared'; -import { HandleGoBack } from '../scripts/utils'; +import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts'; +import { GameListFilterType } from '@/shared/constants'; +import { GameCardFocusHandler } from './CardElement'; +import { HandleGoBack, useStickyDataAttr } from '../scripts/utils'; import LoadingCardList from './LoadingCardList'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { gameFiltersQuery, gameQuery } from '../scripts/queries/romm'; -import { useRouter } from '@tanstack/react-router'; -import SelectMenu from './SelectMenu'; -import SideFilters from './SideFilters'; +import { gameQuery } from '../scripts/queries/romm'; export interface CollectionsDetailParams { id?: string; setBackground?: (url: string) => void; filters?: GameListFilterType; - setLocalFilter: (filter: GameListFilterType) => void, - localFilter: GameListFilterType, + builder?: () => Promise<{ filter?: GameListFilterType, title?: JSX.Element; }>; headerTitle?: JSX.Element; - headerChildren?: any; title?: JSX.Element; footer?: JSX.Element; focus?: string; - countHint?: number; - headerButtons?: HeaderButton[]; - headerButtonElements?: JSX.Element | JSX.Element[]; + countHit?: number; } export function CollectionsDetail (data: CollectionsDetailParams) { - const router = useRouter(); + const builtData = useQuery({ + queryKey: ['filter', data.id], queryFn: async () => + { + return data.builder?.() ?? { filter: data.filters, title: data.title }; + } + }); const queryClient = useQueryClient(); - const finalFilter = { ...data.localFilter, ...data.filters }; - const focusKey = `game-list-${data.id}`; + const focusKey = `game-list-${data.id}-${data?.filters ? Object.values(data?.filters).map(f => String(f)).join(",") : ''}`; const { ref, focusSelf } = useFocusable({ focusKey, preferredChildFocusKey: `${focusKey}-list` }); - const { data: filterValues } = useQuery(gameFiltersQuery({ source: data.filters?.source })); + useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]); + const { shortcuts } = useShortcutContext(); - useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: (e) => HandleGoBack(router, e) }], [router]); - - const handleScroll: FocusParams['onFocus'] = (cardId, node, details) => + const handleScroll: GameCardFocusHandler = (cardId, node, details) => { + const [source, id] = cardId.split('@'); queryClient.prefetchQuery(gameQuery(source, id)); @@ -60,39 +60,31 @@ export function CollectionsDetail (data: CollectionsDetailParams) return (
    - - {data.headerChildren} - -
    -
    -
    -
    -
    - {!!finalFilter && data.title} - {}> + }, { id: "filter", icon: }]} ref={ref} /> +
    +
    + {builtData.data?.filter && data.title} + {(builtData.data?.filter || (!data.filters && !data.builder)) && }> - + } +
    +
    +
    {data.footer}
    - +
    -
    - -
    - ); } \ No newline at end of file diff --git a/src/mainview/components/Constants.tsx b/src/mainview/components/Constants.tsx deleted file mode 100644 index 5286dfb..0000000 --- a/src/mainview/components/Constants.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { CloudSync, Gamepad2, HardDrive, MonitorPlay, Store, Terminal } from "lucide-react"; - -export const sourceIconMap: Record = { - store: , - local: , - romm: -}; - -export const pluginCategoryIcons: Record = { - saves: , - sources: , - launchers: , - emulators: -}; - -export const pluginCategoryPriorities: Record = { - saves: 100, - sources: 90, - launchers: 80, - emulators: 60 -}; \ No newline at end of file diff --git a/src/mainview/components/ContextDialog.tsx b/src/mainview/components/ContextDialog.tsx index 54babea..bf9b757 100644 --- a/src/mainview/components/ContextDialog.tsx +++ b/src/mainview/components/ContextDialog.tsx @@ -6,8 +6,6 @@ import { X } from "lucide-react"; import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts"; import { ContextDialogContext } from "../scripts/contexts"; import { FOCUS_KEYS } from "../scripts/types"; -import { oneShot } from "../scripts/audio/audio"; -import { oneShotRumble } from "../scripts/gamepads"; export function ContextList (data: { options?: DialogEntry[]; @@ -18,8 +16,8 @@ export function ContextList (data: { { const context = useContext(ContextDialogContext); return
      - {data.options?.map((o, i) => )} - {data.showCloseButton !== false &&
      } + {data.options?.map(o => )} +
      {data.showCloseButton !== false && } action={() => context.close()} id="close-context-dialog" content="Close" />}
    ; } @@ -35,12 +33,11 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class const handleAction = () => { if (data.disabled === true) return; - data.action?.({ close: context.close, focus: focusSelf, selected: data.selected }); - oneShot('click'); + data.action?.({ close: context.close, focus: focusSelf }); }; const { ref, focusSelf, focusKey } = useFocusable({ focusKey: FOCUS_KEYS.CONTEXT_DIALOG_OPTION(context.id, data.id), - onEnterPress: handleAction, + onEnterPress: data.shortcuts ? undefined : handleAction, onFocus: handleFocus, trackChildren: typeof data.content !== 'string' }); @@ -60,11 +57,10 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class onClick={handleAction} data-selected={data.selected} aria-disabled={data.disabled} - data-sound-category={"menu"} className={ twMerge("flex cursor-pointer sm:text-sm md:text-base group-focusable scroll-m-4")}> -
    @@ -82,13 +78,13 @@ export interface DialogEntry icon?: string | JSX.Element; type: 'primary' | 'secondary' | 'accent' | 'info' | 'warning' | 'error'; selected?: boolean; - action?: (ctx: { close: () => void, focus: (focusDetails?: FocusDetails | undefined) => void; selected?: boolean; }) => void; + action?: (ctx: { close: () => void, focus: (focusDetails?: FocusDetails | undefined) => void; }) => void; shortcuts?: Shortcut[]; } -export function useContextDialog (id: string, data: { content?: JSX.Element; className?: string; preferredChildFocusKey?: string; onClose?: () => void; canClose?: boolean; defaultOpen?: boolean; backdropClassName?: string; }) +export function useContextDialog (id: string, data: { content?: JSX.Element; className?: string; preferredChildFocusKey?: string; onClose?: () => void; canClose?: boolean; }) { - const [open, setOpen] = useState(data.defaultOpen ?? false); + const [open, setOpen] = useState(false); const [sourceFocusKey, setSourceFocusKey] = useState(undefined); const handleClose = (value: boolean, newSourceFocusKey?: string) => { @@ -102,29 +98,23 @@ export function useContextDialog (id: string, data: { content?: JSX.Element; cla { setOpen(false); data.onClose?.(); - oneShot('closeContext'); if (newSourceFocusKey) { - setFocus(newSourceFocusKey, { instant: true }); + setFocus(newSourceFocusKey); } else if (sourceFocusKey) { - setFocus(sourceFocusKey, { instant: true }); + setFocus(sourceFocusKey); } } }; - const dialog = + const dialog = {data.content} ; return { dialog, open, - setOpen: handleClose, - setToggle: (focNewSourceFocusKey?: string | undefined) => - { - if (open) handleClose(false, focNewSourceFocusKey); - else handleClose(true, focNewSourceFocusKey); - } + setOpen: handleClose }; } @@ -134,13 +124,12 @@ export function ContextDialog (data: { open: boolean, close: (open: boolean) => void; className?: string; - backdropClassName?: string; preferredChildFocusKey?: string; }) { const { ref, focusKey, focusSelf } = useFocusable({ focusable: data.open, - focusKey: FOCUS_KEYS.CONTEXT_DIALOG(data.id), + focusKey: `${data.id}-context-dialog`, isFocusBoundary: true, saveLastFocusedChild: !data.preferredChildFocusKey, preferredChildFocusKey: data.preferredChildFocusKey @@ -153,9 +142,7 @@ export function ContextDialog (data: { { if (data.open) { - focusSelf({ instant: true }); - oneShot('openContext'); - oneShotRumble('openContext', { all: true }); + focusSelf(); } }, [data.open]); @@ -166,15 +153,15 @@ export function ContextDialog (data: { }] : [], [data.open]); return
    router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } }); + const handleReturn = () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } }); useShortcuts(focusKey, () => [{ label: "Return Home", button: GamePadButtonCode.B, action: handleReturn }]); + const { shortcuts } = useShortcutContext(); - useEffect(() => { focusSelf({ instant: true }); }, []); + useEffect(() => { focusSelf(); }, []); return
    @@ -29,7 +30,7 @@ export default function Error (data: ErrorComponentProps)
    - +
    ; } \ No newline at end of file diff --git a/src/mainview/components/FilePicker.tsx b/src/mainview/components/FilePicker.tsx index aefa842..689900d 100644 --- a/src/mainview/components/FilePicker.tsx +++ b/src/mainview/components/FilePicker.tsx @@ -1,10 +1,11 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { ContextList, DialogEntry } from "./ContextDialog"; -import { FocusEventHandler, useContext, useRef, useState } from "react"; +import { systemApi } from "../scripts/clientApi"; +import { useContext, useRef, useState } from "react"; import path from "pathe"; -import { Check, File, FileInput, Folder, FolderInput, FolderOutput, FolderPlus, HardDrive, Usb, X } from "lucide-react"; +import { Check, File, Folder, FolderInput, FolderOutput, FolderPlus, HardDrive, Usb, X } from "lucide-react"; import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; -import { DirType } from '@simeonradivoev/gameflow-sdk/shared'; +import { DirType } from "@/shared/constants"; import classNames from "classnames"; import { twMerge } from "tailwind-merge"; import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts"; @@ -46,7 +47,7 @@ function List (data: { let icon = ; if (isDefaultPath) { - icon = f.isDirectory ? : ; + icon = ; } else if (!f.isDirectory) { icon = ; @@ -91,9 +92,10 @@ function NewFolderInput (data: { id: string, name: string | undefined, setName: onEnterPress: () => inputRef.current?.focus(), onBlur: () => inputRef.current?.blur(), }); - const handleFocus: FocusEventHandler = (e) => + const handleFocus = () => { focusSelf(); + systemApi.api.system.show_keyboard.post(); }; return
    - { - data.onFocus?.(data.id, ref.current, details); - }, + onFocus: (l, p, details) => data.onFocus?.(data.id, ref.current, details), onEnterPress: data.onAction }); @@ -31,8 +27,7 @@ function FilterCat (
  • focusSelf({ event: e.nativeEvent })} - data-sound-category={data.active ? undefined : "filter"} + onClick={focusSelf} className={"sm:text-sm sm:px-2 flex md:px-4 items-center justify-center rounded-full transition-all md:text-lg focusable focusable-primary hover:not-focused:not-aria-selected:bg-base-content/40 not-focused:cursor-pointer aria-selected:bg-base-content aria-selected:text-base-300 aria-selected:drop-shadow aria-selected:cursor-default active:bg-accent! active:text-accent-content! active:ring-offset-7 active:ring-offset-base-content select-none gap-1"} > {data.icon ? <>
    {data.icon}
    {data.children ?? data.label}
    :
    {data.children ?? data.label}
    } @@ -73,10 +68,6 @@ export function FilterUI (data: { if (!data.options[newFilter].selected) { data.setSelected(newFilter); - oneShot('selectFilter'); - } else - { - oneShot('invalidNavigation'); } }, button: GamePadButtonCode.R1 @@ -89,13 +80,7 @@ export function FilterUI (data: { const selectedFilterIndex = Math.max(0, filterIndex - 1,); const newFilter = filterKeys[selectedFilterIndex]; if (!data.options[newFilter].selected) - { data.setSelected(newFilter); - oneShot('selectFilter'); - } else - { - oneShot('invalidNavigation'); - } }, button: GamePadButtonCode.L1 }], [data.options]); @@ -105,7 +90,7 @@ export function FilterUI (data: { { if (hasFocusedChild) { - setFocus(`${data.id}-${defaultFocus}`, { instant: true }); + setFocus(`${data.id}-${defaultFocus}`); } }, [hasFocusedChild, defaultFocus, data.id]); @@ -116,7 +101,7 @@ export function FilterUI (data: { style={{ viewTransitionName: `filter-${data.id}` }} > -
      +
        {!!data.rootFocusKey && (data.showShortcuts ?? true) &&
      • } diff --git a/src/mainview/components/FocusDots.tsx b/src/mainview/components/FocusDots.tsx index 192e388..0fc2af1 100644 --- a/src/mainview/components/FocusDots.tsx +++ b/src/mainview/components/FocusDots.tsx @@ -51,7 +51,7 @@ export default function FocusDots (data: { { const focused = em === focusedKey; return ; }); @@ -69,7 +69,7 @@ export default function FocusDots (data: { } }, [data.elements, data.scrollElement?.current]); - return
        + return
        {elements}
        ; } \ No newline at end of file diff --git a/src/mainview/components/FocusTooltip.tsx b/src/mainview/components/FocusTooltip.tsx index 28f4cf1..5a165b1 100644 --- a/src/mainview/components/FocusTooltip.tsx +++ b/src/mainview/components/FocusTooltip.tsx @@ -1,4 +1,4 @@ -import { RefObject, useState } from "react"; +import { Ref, RefObject, useEffect, useState } from "react"; import { useFocusEventListener } from "../scripts/spatialNavigation"; import useActiveControl from "../scripts/gamepads"; import { twMerge } from "tailwind-merge"; @@ -12,7 +12,7 @@ export default function FocusTooltip (data: { parentRef: RefObject; visible { const dataTooltip = e.getAttribute('data-tooltip'); setHoverText(dataTooltip ?? undefined); - setHoverTextType(e.getAttribute('data-tooltip-type') ?? 'accent'); + setHoverTextType(e.getAttribute('data-tooltip_type') ?? 'accent'); }; const { isPointer } = useActiveControl(); @@ -29,10 +29,7 @@ export default function FocusTooltip (data: { parentRef: RefObject; visible const tooltipStyles = { base: 'bg-base-100 text-base-content', accent: 'bg-accent text-accent-content', - error: 'bg-error text-error-content', - warning: 'bg-warning text-warning-content', - info: 'bg-info text-info-content', - success: 'bg-success text-success-content' + error: 'bg-error text-error-content' }; return !!hoverText && (data.visible ?? true) && !isPointer &&

        {hoverText}

        ; diff --git a/src/mainview/components/FrontEndGameCard.tsx b/src/mainview/components/FrontEndGameCard.tsx index 093be25..533eb29 100644 --- a/src/mainview/components/FrontEndGameCard.tsx +++ b/src/mainview/components/FrontEndGameCard.tsx @@ -1,37 +1,27 @@ import { RPC_URL } from "@/shared/constants"; import CardElement from "./CardElement"; +import { Router } from ".."; import { FileQuestion, HardDrive, Store } from "lucide-react"; import { JSX } from "react"; import { FOCUS_KEYS } from "../scripts/types"; -import { useRouter } from "@tanstack/react-router"; -import { FrontEndGameType, FrontEndId } from "@simeonradivoev/gameflow-sdk/shared"; export default function FrontEndGameCard (data: { index: number, game: FrontEndGameType; showSource?: boolean; } & FocusParams & InteractParams) { - const router = useRouter(); function handleDefaultSelect (id: FrontEndId, source: string | null, sourceId: string | null) { - router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } }); + Router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } }); }; - let subtitle: any = undefined; - if (data.game.path_platform_cover) - { - const platformUrl = new URL(`${RPC_URL(__HOST__)}${data.game.path_platform_cover}`); - platformUrl.searchParams.set('width', "64"); - subtitle =
        - {!!data.game.path_platform_cover && } -

        {data.game.platform_display_name}

        -
        ; - } + const platformUrl = new URL(`${RPC_URL(__HOST__)}${data.game.path_platform_cover}`); + platformUrl.searchParams.set('width', "64"); + const subtitle =
        + {!!data.game.path_platform_cover && } +

        {data.game.platform_display_name}

        +
        ; - const previewUrls = data.game.path_covers.map(c => - { - const url = new URL(`${RPC_URL(__HOST__)}${c}`); - url.searchParams.delete('ts'); - url.searchParams.set('width', "640"); - return url; - }); + const previewUrl = new URL(`${RPC_URL(__HOST__)}${data.game.path_cover}`); + previewUrl.searchParams.delete('ts'); + previewUrl.searchParams.set('width', "640"); const badges: JSX.Element[] = []; @@ -62,7 +52,7 @@ export default function FrontEndGameCard (data: { index: number, game: FrontEndG badges={badges} onFocus={data.onFocus} onAction={(e) => data.onAction ? data.onAction(e) : handleDefaultSelect(data.game.id, data.game.source, data.game.source_id)} - preview={previewUrls} + preview={previewUrl.href} title={data.game.name ?? ""} subtitle={subtitle} focusKey={FOCUS_KEYS.GAME_CARD(data.game.id)} diff --git a/src/mainview/components/GameList.tsx b/src/mainview/components/GameList.tsx index 1075a9f..d998f80 100644 --- a/src/mainview/components/GameList.tsx +++ b/src/mainview/components/GameList.tsx @@ -1,35 +1,30 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { GameMetaExtra, CardList } from "./CardList"; -import { DefaultRommStaleTime, RPC_URL } from "@shared/constants"; -import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared'; -import { useNavigate, useRouter } from "@tanstack/react-router"; +import { DefaultRommStaleTime, GameListFilterType, RPC_URL } from "@shared/constants"; +import { useNavigate } from "@tanstack/react-router"; import { HardDrive } from "lucide-react"; import { JSX, useContext } from "react"; +import { GameCardFocusHandler } from "./CardElement"; import { useLocalSetting } from "../scripts/utils"; import { AnimatedBackgroundContext } from "../scripts/contexts"; import { allGamesQuery } from "@queries/romm"; -import { FrontEndGameType, FrontEndId } from "@simeonradivoev/gameflow-sdk/shared"; -import { isUrl } from "@/shared/utils"; -import { FOCUS_KEYS } from "../scripts/types"; -export interface GameListParams extends FocusParams +export interface GameListParams { id: string, filters?: GameListFilterType, grid?: boolean, setBackground?: (url: string) => void; onGameSelect?: (id: FrontEndId, source: string | null, sourceId: string | null) => void; - onQuickAction?: (id: FrontEndId, source: string | null, sourceId: string | null) => void; - focus?: string; + onFocus?: GameCardFocusHandler; className?: string; - finalElement?: JSX.Element | JSX.Element[]; - emptyElement?: JSX.Element | JSX.Element[]; + finalElement?: JSX.Element; saveChildFocus?: "session" | "local"; } export function GameList (data: GameListParams) { - const games = useSuspenseQuery({ ...allGamesQuery(data.filters), queryKey: ['games', data.filters ?? 'all'], staleTime: DefaultRommStaleTime }); + const games = useSuspenseQuery({ ...allGamesQuery(data.filters), staleTime: DefaultRommStaleTime }); const navigator = useNavigate(); const blur = useLocalSetting('backgroundBlur'); const backgroundContext = useContext(AnimatedBackgroundContext); @@ -42,7 +37,7 @@ export function GameList (data: GameListParams) try { const screenshotUrl = game.paths_screenshots && game.paths_screenshots.length > 0 ? new URL(`${RPC_URL(__HOST__)}${game.paths_screenshots[new Date().getMinutes() % game.paths_screenshots.length]}`) : undefined; - const coverUrl = new URL(`${RPC_URL(__HOST__)}${game.path_covers[0]}`); + const coverUrl = new URL(`${RPC_URL(__HOST__)}${game.path_cover}`); const previewUrl = blur ? coverUrl : (screenshotUrl ?? coverUrl); previewUrl.searchParams.delete('ts'); data.setBackground?.(previewUrl.href) ?? backgroundContext.setBackground(previewUrl.href); @@ -58,25 +53,6 @@ export function GameList (data: GameListParams) navigator({ to: '/game/$source/$id', params: { id: String(g.source_id ?? g.id.id), source: g.source ?? g.id.source } }); }; - const finalElement: JSX.Element[] = []; - if (!games.isFetching && !!games.data && games.data.games.length <= 0) - { - if (Array.isArray(data.emptyElement)) - { - finalElement.push(...data.emptyElement); - } else if (data.emptyElement) - { - finalElement.push(data.emptyElement); - } - } - if (Array.isArray(data.finalElement)) - { - finalElement.push(...data.finalElement); - } else if (data.finalElement) - { - finalElement.push(data.finalElement); - } - return ( <> @@ -98,34 +73,25 @@ export function GameList (data: GameListParams) badges.push(); } - const previewUrls = g.path_covers.map(c => - { - const url = isUrl(c) ? new URL(c) : new URL(`${RPC_URL(__HOST__)}${c}`); - url.searchParams.delete('ts'); - return url; - }); + const previewUrl = new URL(`${RPC_URL(__HOST__)}${g.path_cover}`); + previewUrl.searchParams.delete('ts'); - let platformUrl: URL | undefined = undefined; - if (g.path_platform_cover) - { - platformUrl = isUrl(g.path_platform_cover) ? new URL(g.path_platform_cover) : new URL(`${RPC_URL(__HOST__)}${g.path_platform_cover}`); - platformUrl.searchParams.set('width', "64"); - } + const platformUrl = new URL(`${RPC_URL(__HOST__)}${g.path_platform_cover}`); + platformUrl.searchParams.set('width', "64"); return { id: `${g.id.source}@${g.id.id}`, - focusKey: FOCUS_KEYS.GAME_LIST_CARD(data.id, g.id), + focusKey: g.slug ?? `game-${g.id}`, title: g.name ?? "", subtitle: (
        - + {!!g.path_platform_cover && }

        {g.platform_display_name}

        ), - previewUrls: previewUrls, + previewUrl: previewUrl.href, badges: badges, onSelect: () => data.onGameSelect ? data.onGameSelect(g.id, g.source, g.source_id) : handleDefaultSelect(g), - onQuickAction: data.onQuickAction ? () => data.onQuickAction?.(g.id, g.source, g.source_id) : undefined, onFocus: () => handleFocus(g.id, g.source, g.source_id) } satisfies GameMetaExtra; }, diff --git a/src/mainview/components/GamepadKeyboard.tsx b/src/mainview/components/GamepadKeyboard.tsx deleted file mode 100644 index 75005a1..0000000 --- a/src/mainview/components/GamepadKeyboard.tsx +++ /dev/null @@ -1,520 +0,0 @@ -import { createRef, JSX, RefObject, useEffect, useRef, useState } from "react"; -import useActiveControl, { GamepadButtonEvent } from "../scripts/gamepads"; -import { oneShot } from "../scripts/audio/audio"; -import { ArrowLeft, ArrowRight, CornerDownLeft, Delete, Space } from "lucide-react"; -import { GamePadButtonCode } from "../scripts/shortcuts"; -import { GamepadIconMap } from "./Shortcuts"; -import ShortcutPrompt from "./ShortcutPrompt"; -import { getLocalSetting, showKeyboardHandler } from "../scripts/utils"; - -const Keys = [ - ['E', 'R', 'T', 'F', 'D', 'G', 'V', 'C', 'S', 'X', 'Z', 'B', 'A', 'Q', 'W'], - ['I', '⌫', 'O', '⏎', 'P', 'L', 'N', '␣', 'M', 'J', 'K', 'H', 'Y', 'U'] -]; -const Characters = [ - ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "%", "$", "#", "@", "+"], - [",", '⌫', ".", '⏎', "/", "[", "]", '␣', "(", ")", ":", "!", "?", "&"] -]; -function GetKeys (characters: boolean) -{ - return characters ? Characters : Keys; -} -const KeyColors: Record = { - '⌫': { bg: "var(--color-accent)", color: "var(--color-accent-content)" }, - '⏎': { bg: "var(--color-secondary)", color: "var(--color-secondary-content)" }, - '␣': { bg: "var(--color-info)", color: "var(--color-info-content)" }, -}; -const Shortcuts: Record = { - '⌫': GamePadButtonCode.X, - '␣': GamePadButtonCode.Y, - '⏎': GamePadButtonCode.A, - '←': GamePadButtonCode.Left, - '→': GamePadButtonCode.Right, - '⇧': GamePadButtonCode.RJoy, - '⌥': GamePadButtonCode.LJoy -}; -const KeyElements: Record = { - '⌫': , - '␣': , - '⏎': , - '←': , - '→': , -}; -const DZ = 0.22; - -function ang (x: number, y: number) -{ - if (Math.sqrt(x * x + y * y) < DZ) return null; - let a = Math.atan2(x, -y); - if (a < 0) a += Math.PI * 2; - return a; -} - -function gidx (a: number | null, n: number) -{ - return a === null ? -1 : Math.floor(a / (Math.PI * 2) * n) % n; -} - -function buildWheel (side: 0 | 1, shift: boolean, characters: boolean) -{ - const elements: JSX.Element[] = []; - const refs: RefObject[] = []; - const positions: { left: string; top: string; }[] = []; - const n = GetKeys(characters)[side].length, GAP = 0.028; - - for (let i = 0; i < n; i++) - { - const a0 = i / n * Math.PI * 2 - Math.PI / 2 + GAP; - const a1 = (i + 1) / n * Math.PI * 2 - Math.PI / 2 - GAP; - const am = (a0 + a1) / 2; - const ref = createRef(); - const x = Math.cos(am); - const y = Math.sin(am); - refs.push(ref); - - const tr = 66; - positions.push({ left: `50% + ${tr * x}% - 16px`, top: `50% + ${tr * y}% - 16px` }); - - elements.push(<> - - {KeyElements[GetKeys(characters)[side][i]] ?? shift ? GetKeys(characters)[side][i].toUpperCase() : GetKeys(characters)[side][i].toLocaleLowerCase()} - - ); - } - - return { elements, refs, positions }; -} - -export type EditableInput = HTMLInputElement | HTMLTextAreaElement; - -export function typeKey (el: EditableInput, key: string): void -{ - const start = el.selectionStart ?? 0; - const end = el.selectionEnd ?? 0; - - el.value = - el.value.slice(0, start) + - key + - el.value.slice(end); - - const pos = start + key.length; - el.setSelectionRange(pos, pos); -} - -export function backspace (el: EditableInput): void -{ - const start = el.selectionStart ?? 0; - const end = el.selectionEnd ?? 0; - - // selection delete - if (start !== end) - { - el.value = - el.value.slice(0, start) + - el.value.slice(end); - - el.setSelectionRange(start, start); - return; - } - - // nothing to delete - if (start === 0) return; - - el.value = - el.value.slice(0, start - 1) + - el.value.slice(end); - - el.setSelectionRange(start - 1, start - 1); -} - -export function deleteForward (el: EditableInput): void -{ - const start = el.selectionStart ?? 0; - const end = el.selectionEnd ?? 0; - - if (start !== end) - { - el.value = - el.value.slice(0, start) + - el.value.slice(end); - - el.setSelectionRange(start, start); - return; - } - - if (start >= el.value.length) return; - - el.value = - el.value.slice(0, start) + - el.value.slice(start + 1); - - el.setSelectionRange(start, start); -} - -export function enter (el: EditableInput): void -{ - if (el instanceof HTMLTextAreaElement) - { - - const start = el.selectionStart ?? 0; - const end = el.selectionEnd ?? 0; - - const insert = "\n"; - - el.value = - el.value.slice(0, start) + - insert + - el.value.slice(end); - - const pos = start + 1; - el.setSelectionRange(pos, pos); - - } else - { - el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', charCode: 13, keyCode: 13, view: window, bubbles: true })); - el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', charCode: 13, keyCode: 13, view: window, bubbles: true })); - } - -} - -export function arrowLeft (el: EditableInput): void -{ - const pos = el.selectionStart ?? 0; - const newPos = Math.max(0, pos - 1); - - el.setSelectionRange(newPos, newPos); -} - -export function arrowRight (el: EditableInput): void -{ - const pos = el.selectionStart ?? 0; - const newPos = Math.min(el.value.length, pos + 1); - - el.setSelectionRange(newPos, newPos); -} - -export function GamepadKeyboard () -{ - const triggerThreshold = 0.85; - const [focusedInput, setFocusedInput] = useState(null); - const circleRefs = [useRef(null), useRef(null)]; - const sideRefs = [useRef(null), useRef(null)]; - const keyIndicatorRefs = [useRef(null), useRef(null)]; - const activeControl = useActiveControl(); - const hidden = !focusedInput || activeControl.control !== 'gamepad'; - const keyboardRef = useRef(null); - const [shift, setShift] = useState(false); - const [characters, setCharacters] = useState(false); - - useEffect(() => - { - if (!hidden) - { - oneShot('openKeyboard'); - } - }, [hidden]); - - const elements = [buildWheel(0, shift, characters), buildWheel(1, shift, characters)]; - - useEffect(() => - { - let disposed = false; - const lockedIds: [number | undefined, number | undefined] = [undefined, undefined]; - const actionRepeatTimeout: [NodeJS.Timeout | undefined, NodeJS.Timeout | undefined] = [undefined, undefined]; - const actionRepeatCount = [0, 0]; - const prevTriggerValues = [0, 0]; - const buttonValues: Record = {}; - const buttonRepeatTimeout: Record = {}; - const buttonRepeatCounts: Record = {}; - const lastIndexes = [-1, -1]; - - function update () - { - const gps = navigator.getGamepads ? navigator.getGamepads() : []; - const gp = [...gps].find(g => g); - - if (keyboardRef.current && focusedInput && !hidden) - { - const targetRect = focusedInput.getBoundingClientRect(); - const el = keyboardRef.current; - - // First, measure the element itself - const elRect = el.getBoundingClientRect(); - - const margin = 64; // keep some space from edges - - let left = targetRect.left; - let top = targetRect.bottom + 128; - - // Clamp horizontally - if (left + elRect.width > window.innerWidth - margin) - { - left = window.innerWidth - elRect.width - margin; - } - - if (left < margin) - { - left = margin; - } - - // Clamp vertically - if (top + elRect.height > window.innerHeight - margin) - { - // flip above the input if it doesn't fit below - top = targetRect.top - elRect.height - 128; - } - - if (top < margin) - { - top = margin; - } - - el.style.position = "fixed"; - el.style.left = `${left}px`; - el.style.top = `${top}px`; - } - - if (gp && !hidden) - { - function pressKey (el: EditableInput, key: string, repeatCount: number): void - { - const hapticIntensity = 1 / Math.max(repeatCount, 1); - const soundIntensity = 1 / Math.min(2, Math.max(repeatCount * 0.2, 1)); - gp?.vibrationActuator.playEffect('dual-rumble', { duration: 60, strongMagnitude: hapticIntensity, weakMagnitude: hapticIntensity }); - - switch (key) - { - case "⌫": - oneShot('keyPressBackspace', { volume: soundIntensity }); - return backspace(el); - case "Delete": - oneShot('keyPressBackspace', { volume: soundIntensity }); - return deleteForward(el); - case "←": - oneShot('keyPress', { volume: soundIntensity }); - return arrowLeft(el); - case "→": - oneShot('keyPress', { volume: soundIntensity }); - return arrowRight(el); - case "⏎": - oneShot('keyPress', { volume: soundIntensity }); - return enter(el); - case "␣": - oneShot('keyPressSpace', { volume: soundIntensity }); - return typeKey(el, ' '); - case "⇧": - setShift(v => !v); - return; - case "⌥": - setCharacters(v => !v); - return; - default: - oneShot('keyPress', { volume: soundIntensity }); - return typeKey(el, shift ? key.toUpperCase() : key.toLocaleLowerCase()); - } - } - - for (let side = 0; side < 2; side++) - { - const x = gp.axes[side * 2] ?? 0; - const y = gp.axes[side * 2 + 1] ?? 0; - const triggerValue = Math.max(gp.buttons[6 + side]?.value ?? 0, gp.buttons[4 + side]?.value ?? 0); - const angle = ang(x, y); - const keyIndex = lockedIds[side] !== undefined ? lockedIds[side]! : gidx(angle, GetKeys(characters)[side].length); - - elements[side].refs.filter(e => e.current).forEach((e, i) => - { - const active = keyIndex === i; - const key = GetKeys(characters)[side][i]; - const elem = e.current!; - elem.style.backgroundColor = active ? 'var(--color-primary)' : KeyColors[key]?.bg ?? ''; - elem.style.color = active ? 'var(--color-primary-content)' : KeyColors[key]?.color ?? ''; - elem.style.scale = `${active ? 150 : 100}%`; - elem.style.fontStyle = active ? 'bold' : 'normal'; - }); - - const circle = circleRefs[side].current!; - - // Update actions - if (keyIndex >= 0) - { - if (focusedInput) - { - if (triggerValue >= triggerThreshold && prevTriggerValues[side] < triggerThreshold) - { - const timeoutCalc = () => 400 / Math.min(4, Math.max(1, 1 + (actionRepeatCount[side] ?? 0))); - const handleRepeat = () => - { - elements[side].refs[keyIndex].current!.animate([ - { boxShadow: "0 0 0 0 var(--color-base-content)" }, - { boxShadow: "0 0 0 10px transparent" } - ], - { duration: 300, easing: 'ease-out', fill: 'none' } - ); - pressKey(focusedInput, GetKeys(characters)[side][keyIndex], actionRepeatCount[side]); - actionRepeatCount[side]++; - actionRepeatTimeout[side] = setTimeout(handleRepeat, timeoutCalc()); - }; - handleRepeat(); - } - else if (triggerValue < triggerThreshold && prevTriggerValues[side] >= triggerThreshold) - { - clearTimeout(actionRepeatTimeout[side]); - actionRepeatCount[side] = -1; - } - - if (lockedIds[side] === undefined && triggerValue > 0.1) - { - lockedIds[side] = keyIndex; - } else if (lockedIds[side] !== undefined && triggerValue <= 0.1) - { - lockedIds[side] = undefined; - } - } - - keyIndicatorRefs[side].current!.textContent = shift ? GetKeys(characters)[side][keyIndex].toUpperCase() : GetKeys(characters)[side][keyIndex].toLowerCase(); - } else - { - keyIndicatorRefs[side].current!.textContent = ""; - } - - // Update cirlce - const magnitudeSqr = (x * x) + (y * y); - const magnitude = Math.sqrt(magnitudeSqr); - - circle.style.left = `calc(50% + ${50 * x}% - 16px)`; - circle.style.top = `calc(50% + ${50 * y}% - 16px)`; - circle.style.opacity = `${1 - Math.pow(magnitude, 2)}`; - circle.style.backgroundColor = `color-mix(in srgb, var(--color-base-content), 'var(--color-primary)'} ${magnitude * 100}%)`; - - if (sideRefs[side].current) - { - sideRefs[side].current!.style.background = `radial-gradient( - circle at calc(50% + ${100 * x}px) calc(50% + ${100 * y}px), - color-mix(in srgb, var(--color-primary) 20%, transparent), - transparent - )`; - } - - - if (lastIndexes[side] !== keyIndex) - { - gp.vibrationActuator.playEffect('dual-rumble', { duration: 30, strongMagnitude: 0, weakMagnitude: 0.2 }); - oneShot('keyHover'); - } - - prevTriggerValues[side] = triggerValue; - lastIndexes[side] = keyIndex; - } - - const shortcutKeys = Object.entries(Shortcuts); - function handleButton (key: number, repeatCount: number) - { - if (!focusedInput) return; - const entry = shortcutKeys.find(([n, value]) => value === key); - if (key === GamePadButtonCode.A) return; - if (entry) - { - pressKey(focusedInput, entry[0], repeatCount); - } - } - - for (let i = 0; i < gp.buttons.length; i++) - { - const btn = gp.buttons[i]; - if (btn.value >= 0.85 && buttonValues[i] < 0.85) - { - const timeoutCalc = () => 400 / Math.min(8, Math.max(1, 1 + (buttonRepeatCounts[i] ?? 0))); - const handleRepeat = () => - { - handleButton(i, buttonRepeatCounts[i]); - buttonRepeatCounts[i] = (buttonRepeatCounts[i] ?? -1) + 1; - buttonRepeatTimeout[i] = setTimeout(handleRepeat, timeoutCalc()); - }; - handleRepeat(); - } - else if (btn.value < 0.85 && buttonValues[i] >= 0.85) - { - clearTimeout(buttonRepeatTimeout[i]); - buttonRepeatCounts[i] = -1; - } - - buttonValues[i] = btn.value; - } - } - - if (!disposed && !hidden) requestAnimationFrame(update); - } - - if (!disposed && !hidden) requestAnimationFrame(update); - - const gamepadButtonHandler = (e: Event) => - { - if (!(e instanceof GamepadButtonEvent) || disposed || hidden) return; - if (e.button === GamePadButtonCode.L1 || e.button === GamePadButtonCode.R1 || e.button === GamePadButtonCode.L2 || e.button === GamePadButtonCode.R2) - { - e.preventDefault(); - e.stopImmediatePropagation(); - } - - }; - window.addEventListener('gamepadbuttondown', gamepadButtonHandler); - window.addEventListener('gamepadbuttonup', gamepadButtonHandler); - - return () => - { - disposed = true; - Object.values(buttonRepeatTimeout).forEach(v => clearTimeout(v)); - Object.values(actionRepeatTimeout).forEach(v => clearTimeout(v)); - window.removeEventListener('gamepadbuttondown', gamepadButtonHandler); - window.removeEventListener('gamepadbuttonup', gamepadButtonHandler); - }; - }, [focusedInput, elements, shift, characters, hidden]); - - useEffect(() => - { - - const handleFocus = (e: FocusEvent) => - { - if (e.target instanceof HTMLInputElement && (e.target.type === 'text' || e.target.type === 'search')) - { - if (!getLocalSetting('autoKeybaord')) return; - if (getLocalSetting('useGameflowKeyboard')) - { - setFocusedInput(e.target); - } else - { - showKeyboardHandler(activeControl.control, e.target); - } - } - }; - - const handleBlur = (e: FocusEvent) => - { - setFocusedInput(null); - }; - - document.addEventListener('focusin', handleFocus); - document.addEventListener('focusout', handleBlur); - - return () => - { - document.removeEventListener('focusin', handleFocus); - document.removeEventListener('focusout', handleBlur); - }; - }, []); - - return ; -} \ No newline at end of file diff --git a/src/mainview/components/GlobalContextDialog.tsx b/src/mainview/components/GlobalContextDialog.tsx deleted file mode 100644 index 0fcc23d..0000000 --- a/src/mainview/components/GlobalContextDialog.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useState } from "react"; -import { GlobalDialogContext } from "../scripts/contexts"; -import { useContextDialog } from "./ContextDialog"; - -export default function GlobalContextDialog (data: { children: any; }) -{ - const [currentContext, setCurrentContext] = useState(undefined); - const [preferredChildFocusKey, setPreferredChildFocusKey] = useState(undefined); - const [onCloseCallback, setOnCloseCallback] = useState<(() => void) | undefined>(undefined); - - const { dialog, setOpen } = useContextDialog('global-context-dialog', { - content: currentContext, - onClose: onCloseCallback, - preferredChildFocusKey: preferredChildFocusKey - }); - return - {data.children} - {dialog} - ; -} \ No newline at end of file diff --git a/src/mainview/components/Header.tsx b/src/mainview/components/Header.tsx index d38ef5b..1dad439 100644 --- a/src/mainview/components/Header.tsx +++ b/src/mainview/components/Header.tsx @@ -13,8 +13,8 @@ import BatteryWarning, Bell, Bluetooth, - CircleFadingArrowUp, Clock, + Plug, Settings, Wifi, WifiHigh, @@ -23,17 +23,17 @@ import } from "lucide-react"; import { RoundButton } from "./RoundButton"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { RPC_URL, SystemInfoType } from "../../shared/constants"; import { JSX, RefObject, useContext, useEffect, useRef, useState } from "react"; +import { systemApi } from "../scripts/clientApi"; +import { Router } from ".."; import { useStickyDataAttr } from "../scripts/utils"; import { twMerge } from "tailwind-merge"; import { TwitchIcon } from "../scripts/brandIcons"; -import { rommLoggedInQuery } from "../scripts/queries/romm"; +import { rommLoggedInQuery, rommUserQuery } from "../scripts/queries/romm"; import { twitchLoginVerificationQuery } from "../scripts/queries/settings"; -import { AppContext, SystemInfoContext } from "../scripts/contexts"; -import { useNavigate, useRouter } from "@tanstack/react-router"; -import { oneShot } from "../scripts/audio/audio"; -import { hasUpdateQuery } from "../scripts/queries/system"; -import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts"; +import { da } from "zod/v4/locales"; +import { SystemInfoContext } from "../scripts/contexts"; function HeaderAvatar (data: { id: string; @@ -73,8 +73,6 @@ export interface HeaderButton icon: JSX.Element; external?: boolean; action?: () => void; - className?: string; - shortcutLabel?: string; } export interface HeaderAccount @@ -87,48 +85,24 @@ export interface HeaderAccount action?: () => void; } -function UpdateStatus () -{ - const handleSelect = () => - { - navigate({ to: '/settings/update' }); - }; - const hasUnread = false; - const navigate = useNavigate(); - const { ref } = useFocusable({ - focusKey: 'update-bt', onEnterPress: handleSelect - }); - return
        - -
        ; -} - function NotificationStatus () { const hasUnread = false; - return
        + return
        ; } function ClockStatus () { - const navigate = useNavigate(); - const app = useContext(AppContext); - const refClock = useRef(null); - const activeTaskProgress = app.activeTaskProgress; - const handleTaskClick = () => - { - navigate({ to: '/settings/tasks' }); - }; - const { ref, focusKey } = useFocusable({ focusKey: 'tasks-indicator', focusable: !!activeTaskProgress, onEnterPress: handleTaskClick }); + const ref = useRef(null); useEffect(() => { function update () { - if (refClock.current) + if (ref.current) { - refClock.current.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + ref.current.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } } @@ -152,16 +126,7 @@ function ClockStatus () return () => clearTimeout(timeout); }, []); - useShortcuts(focusKey, () => [{ - label: "Downloads", button: GamePadButtonCode.A, action (e) - { - handleTaskClick(); - }, - }]); - - return
        - - {activeTaskProgress ?
        : }
        ; + return
        ; } function BluetoothStatus () @@ -180,7 +145,7 @@ function WiFiStatus () return systemContext && systemContext.wifiConnections.length > 0 ?
        {systemContext.wifiConnections.map(w => { - const className = "w-10 h-10"; + const className = "w-6 h-6"; let icon = ; if (w.signalLevel >= -60) icon = ; @@ -191,7 +156,7 @@ function WiFiStatus () else if (w.signalLevel >= -90) icon = ; - return
        + return
        {icon}
        ; })} @@ -241,18 +206,19 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; }) placeholderData: keepPreviousData }); - const handleSelect = () => - { - router.navigate({ to: '/settings/accounts' }); - oneShot('click'); - }; + const { ref } = useFocusable({ focusKey: 'accounts' }); const accounts: HeaderAccount[] = []; if (data.accounts) accounts.push(...data.accounts); + if (rommUser.data?.hasLogin || rommUser.isError) { accounts.push({ id: 'romm', preview: `https://romm.app/_ipx/q_80/images/blocks/logos/romm.svg`, + action: () => + { + Router.navigate({ to: '/settings/accounts', search: { focus: 'rommAddress' } }); + }, className: rommUser.data?.hasLogin && !rommUser.isError ? undefined : "border-error", type: 'secondary' }); @@ -262,21 +228,15 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; }) { accounts.push({ id: 'twitch', preview: TwitchIcon, + action: () => + { + Router.navigate({ to: '/settings/accounts', search: { focus: 'rommAddress' } }); + }, type: 'secondary' }); } - const hasAccounts = accounts.length > 0; - const router = useRouter(); - const { ref } = useFocusable({ - focusKey: 'accounts', - onEnterPress: handleSelect, - focusable: hasAccounts - }); - - - - return
        + return
        {accounts?.map(a => - -
        - - - - - {!!update && update.hasUpdate >= 1 && } - -
        - {!!data.buttons &&
        } -
        - {data.buttonElements} - {data.buttons?.map(b => {b.icon})} -
        -
        + return
        +
        + + + + + +
        + {!!data.buttons &&
        } +
        + {data.buttonElements ?? data.buttons?.map(b => {b.icon})} +
        ; } @@ -332,41 +285,26 @@ interface HeaderUIParams export function HeaderUI (data: HeaderUIParams) { const { ref, focusKey } = useFocusable({ focusKey: "header-elements", focusable: data.focusable, preferredChildFocusKey: data.preferredChildFocusKey }); - const router = useRouter(); const goToSettings = () => { - router.navigate({ to: '/settings/accounts' }); + Router.navigate({ to: '/settings/accounts' }); }; return ( - -
        - - + +
        + {data.title} - , - id: "header-settings-btn", - action: goToSettings, - external: true, - shortcutLabel: "Settings" - } - ]} /> - - -
        + , id: "settings", action: goToSettings, external: true }]} /> +
        + ); } -export function StickyHeaderUI (data: { ref: RefObject; className?: string; children?: any; } & HeaderUIParams) +export function StickyHeaderUI (data: { ref: RefObject; } & HeaderUIParams) { const [isStuck, setIsStuck] = useState(false); const headerRef = useRef(null); @@ -375,9 +313,8 @@ export function StickyHeaderUI (data: { ref: RefObject; className?: string; return <>
        -
        +
        - {data.children}
        ; } \ No newline at end of file diff --git a/src/mainview/components/HeaderSearchField.tsx b/src/mainview/components/HeaderSearchField.tsx deleted file mode 100644 index 198c552..0000000 --- a/src/mainview/components/HeaderSearchField.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; -import { useEffect, useRef, useState } from "react"; -import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts"; -import { oneShot } from "../scripts/audio/audio"; -import { Search } from "lucide-react"; -import { RoundButton } from "./RoundButton"; -import { useEventListener } from "usehooks-ts"; -import { twMerge } from "tailwind-merge"; - -function SearchInput (data: { - id: string; - autoSearch?: boolean; - search: string | undefined; - compact: boolean | undefined; - onInputFocus: () => void; - setShowInput: (show: boolean) => void; - className?: string; - onSubmit: (search: string | undefined) => void; -} & FocusParams) -{ - const { ref, focusKey } = useFocusable({ - onBlur: () => inputRef.current?.blur(), - onFocus: (l, p, d) => - { - data.onFocus?.(focusKey, ref.current, { ...d, inputRef }); - if (data.autoSearch) inputRef.current?.focus(); - }, - focusKey: data.id, - onEnterPress: () => - { - if (document.activeElement === inputRef.current) - { - if (inputRef.current) - data.onSubmit?.(inputRef.current.value); - } else - { - inputRef.current?.focus(); - } - } - }); - - const inputRef = useRef(null); - const [localSearch, setLocalSearch] = useState(data.search); - - useEffect(() => - { - setLocalSearch(data.search ?? ""); - }, [data.search]); - - useShortcuts(focusKey, () => document.activeElement === inputRef.current ? [{ - label: "Cancel", - button: GamePadButtonCode.B, action (e) - { - inputRef.current?.blur(); - oneShot('returnGeneric'); - }, - }] : [], [inputRef.current, document.activeElement]); - - useEventListener('search' as any, e => - { - data.onSubmit?.(undefined); - }, inputRef as any); - - return ; -} - -export default function HeaderSearchField (data: { - id: string; - autoSearch?: boolean; - search: string | undefined, - onSubmit: (search: string | undefined) => void; - className?: string; - compact?: boolean; -} & FocusParams) -{ - const [showInput, setShowInput] = useState(false); - - const { ref, focusKey, focusSelf } = useFocusable({ - focusKey: data.id, - focusBoundaryDirections: ['left', "right"], - isFocusBoundary: data.compact && showInput - }); - - return
        - - {(!data.compact || showInput) && } - {data.compact && !showInput && setShowInput(true)} className="header-icon sm:size-10 md:size-14" id={`${data.id}-field`} >} - -
        ; -} \ No newline at end of file diff --git a/src/mainview/components/ImageWithFallbacks.tsx b/src/mainview/components/ImageWithFallbacks.tsx deleted file mode 100644 index 6779b26..0000000 --- a/src/mainview/components/ImageWithFallbacks.tsx +++ /dev/null @@ -1,32 +0,0 @@ -export default function ImageWithFallbacks (data: { - src: URL[]; - draggable?: boolean; - className?: string; -}) -{ - const handleError = (e: React.SyntheticEvent) => - { - const img = e.currentTarget; - const nextIndex = Number(img.dataset.index) + 1; - - if (nextIndex < data.src.length) - { - img.dataset.index = String(nextIndex); - img.src = data.src[nextIndex].href; - - } - }; - return - { - e.currentTarget.dataset.loaded = "true"; - }} - > - - ; -} \ No newline at end of file diff --git a/src/mainview/components/LoadMoreButton.tsx b/src/mainview/components/LoadMoreButton.tsx index d52e4e0..5d2cb03 100644 --- a/src/mainview/components/LoadMoreButton.tsx +++ b/src/mainview/components/LoadMoreButton.tsx @@ -1,24 +1,25 @@ import { setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { FOCUS_KEYS } from "../scripts/types"; import { useIntersectionObserver } from "usehooks-ts"; -import { FrontEndId } from "@simeonradivoev/gameflow-sdk/shared"; +import { useEffect } from "react"; -export default function LoadMoreButton (data: { isFetching: boolean; hidden?: boolean, lastId?: FrontEndId; } & FocusParams & InteractParams) +export default function LoadMoreButton (data: { isFetching: boolean; lastId?: FrontEndId; } & FocusParams & InteractParams) { - const handleAction = (event?: Event) => + const handleAction = (e?: Event) => { - data.onAction?.({ event, focusKey }); + data.onAction?.(e); if (data.lastId && focused) setFocus(FOCUS_KEYS.GAME_CARD(data.lastId)); }; const { ref, focusKey, focused } = useFocusable({ - focusable: !data.isFetching && data.hidden !== true, focusKey: 'load-more-btn', onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details), onEnterPress: handleAction }); + + const { ref: intersct } = useIntersectionObserver({ initialIsIntersecting: true, rootMargin: "20%", @@ -35,5 +36,5 @@ export default function LoadMoreButton (data: { isFetching: boolean; hidden?: bo { ref.current = r; intersct(r); - }} className='flex data-[hidden=true]:invisible bg-base-100 game-card focusable focusable-accent focusable-hover text-2xl justify-center items-center cursor-pointer' data-hidden={data.hidden} onClick={e => handleAction(e.nativeEvent)} id='load-more-btn'>{data.isFetching ? : "Load More"}
        ; + }} className='flex bg-base-100 game-card focusable focusable-accent focusable-hover text-2xl justify-center items-center cursor-pointer' onClick={e => handleAction(e.nativeEvent)} id='load-more-btn'>{data.isFetching ? : "Load More"}
        ; } \ No newline at end of file diff --git a/src/mainview/components/LoadingCardList.tsx b/src/mainview/components/LoadingCardList.tsx index d811d55..e25dc26 100644 --- a/src/mainview/components/LoadingCardList.tsx +++ b/src/mainview/components/LoadingCardList.tsx @@ -17,6 +17,7 @@ export default function LoadingCardList (data: { id: string, placeholderCount: n ref={ref} title="Games" id={`card-list-placeholder`} + save-child-focus="session" className={twMerge("items-center justify-center-safe h-full", data.grid ? "grid h-fit sm:gap-2 md:gap-5 auto-rows-min grid-cols-[repeat(auto-fill,var(--game-card-width))]" : 'landscape:grid landscape:grid-flow-col landscape:auto-cols-min auto-rows-[1fr] sm:gap-2 md:gap-4 portrait:grid portrait:auto-rows-min portrait:grid-cols-[repeat(auto-fill,var(--game-card-width))] *:portrait:aspect-8/10 *:landscape:aspect-8/12 sm:landscape:max-h-84 md:max-h-128!', diff --git a/src/mainview/components/LoadingScreen.tsx b/src/mainview/components/LoadingScreen.tsx deleted file mode 100644 index f92cc98..0000000 --- a/src/mainview/components/LoadingScreen.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function LoadingScreen (data: { children?: any; }) -{ - return
        -
        -
        -
        - {data.children} -
        ; -} \ No newline at end of file diff --git a/src/mainview/components/NotFound.tsx b/src/mainview/components/NotFound.tsx index 2774240..ea70534 100644 --- a/src/mainview/components/NotFound.tsx +++ b/src/mainview/components/NotFound.tsx @@ -1,19 +1,19 @@ import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { Home, TriangleAlert } from "lucide-react"; -import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts"; +import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts"; +import { Router } from ".."; +import Shortcuts from "./Shortcuts"; import { Button } from "./options/Button"; import { useEffect } from "react"; -import { useRouter } from "@tanstack/react-router"; -import { FloatingShortcuts } from "./Shortcuts"; export default function NotFound () { const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "not-found" }); - const router = useRouter(); - const handleReturn = () => router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } }); + const handleReturn = () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } }); useShortcuts(focusKey, () => [{ label: "Return Home", button: GamePadButtonCode.B, action: handleReturn }]); + const { shortcuts } = useShortcutContext(); - useEffect(() => { focusSelf({ instant: true }); }, []); + useEffect(() => { focusSelf(); }, []); return
        @@ -26,7 +26,7 @@ export default function NotFound ()
        - +
        ; } \ No newline at end of file diff --git a/src/mainview/components/Notifications.tsx b/src/mainview/components/Notifications.tsx index c13c4b6..37edb26 100644 --- a/src/mainview/components/Notifications.tsx +++ b/src/mainview/components/Notifications.tsx @@ -1,16 +1,7 @@ import { RPC_URL } from "@/shared/constants"; -import { FrontendNotification } from "@simeonradivoev/gameflow-sdk/shared"; -import { Clock, CloudUpload, Save } from "lucide-react"; import { useEffect } from "react"; import toast, { ToastOptions } from "react-hot-toast"; - -const customIconMap = { - save: , - upload: , - clock: -}; - export default function Notifications (data: {}) { useEffect(() => @@ -19,13 +10,7 @@ export default function Notifications (data: {}) es.addEventListener('notification', (e) => { const notification = JSON.parse(e.data) as FrontendNotification; - const options: ToastOptions = { - removeDelay: notification.duration, - style: { - borderRadius: "64px" - } - }; - if (notification.icon) options.icon = customIconMap[notification.icon]; + const options: ToastOptions = { removeDelay: notification.duration }; if (notification.type === 'error') { toast.error(notification.message, options); diff --git a/src/mainview/components/PlatformsList.tsx b/src/mainview/components/PlatformsList.tsx index 790a33d..48af4a3 100644 --- a/src/mainview/components/PlatformsList.tsx +++ b/src/mainview/components/PlatformsList.tsx @@ -5,45 +5,21 @@ import { CardList, GameMetaExtra } from "./CardList"; import { rommApi } from "../scripts/clientApi"; import { JSX, useMemo } from "react"; import { HardDrive } from "lucide-react"; +import { GameCardFocusHandler } from "./CardElement"; import { mobileCheck } from "../scripts/utils"; import { twMerge } from "tailwind-merge"; -import placeholder from '../assets/256x256.png?url'; - -function Preview (data: { index: number, pathCover: string | null; }) -{ - const coverUrl = new URL(`${RPC_URL(__HOST__)}${data.pathCover}`); - coverUrl.searchParams.set('width', "320"); - const isMobile = mobileCheck(); - return
        - e.currentTarget.src = placeholder} - src={coverUrl.href} - > - -
        ; -} export function PlatformsList (data: { id: string, setBackground: (url: string) => void; className?: string; + onFocus?: GameCardFocusHandler; grid?: boolean; onSelect?: (source: string, id: string) => void; saveChildFocus?: "session" | "local"; -} & FocusParams) +}) { - + const isMobile = mobileCheck(); const navigate = useNavigate(); const { data: platforms } = useSuspenseQuery( { @@ -70,19 +46,37 @@ export function PlatformsList (data: { badges.push({g.game_count}); if (g.hasLocal) badges.push(); - + const coverUrl = new URL(`${RPC_URL(__HOST__)}${g.path_cover}`); + coverUrl.searchParams.set('width', "320"); const entry: GameMetaExtra = { id: g.slug, focusKey: g.slug, title: g.name, - subtitle: g.family_name ?? undefined, - previewUrls: "", + subtitle: g.family_name ?? "", + previewUrl: "", badges, onFocus: () => data.setBackground( g.paths_screenshots.length > 0 ? `${RPC_URL(__HOST__)}${g.paths_screenshots[new Date().getMinutes() % g.paths_screenshots.length]}` : `${RPC_URL(__HOST__)}/api/romm/image?url=https://picsum.photos/id/${10 + i}/1280/720.webp`, ), onSelect: () => data.onSelect ? data.onSelect(g.id.source, g.id.id) : handleDefaultSelect(g.id.source, g.id.id), - preview: () => + preview: + () =>
        + +
        + , }; return entry; }), [platforms]); @@ -94,7 +88,7 @@ export function PlatformsList (data: { id={data.id} grid={data.grid} className={twMerge('*:aspect-8/10! md:py-12', data.className)} - onFocus={data.onFocus} + onGameFocus={data.onFocus} games={platformsMapped} onSelectGame={(id) => { diff --git a/src/mainview/components/RoundButton.tsx b/src/mainview/components/RoundButton.tsx index 01f9017..386723b 100644 --- a/src/mainview/components/RoundButton.tsx +++ b/src/mainview/components/RoundButton.tsx @@ -9,11 +9,10 @@ export function RoundButton (data: { external?: boolean; style?: ButtonStyle; cssStyle?: CSSProperties; - shortcutLabel?: string; } & InteractParams & FocusParams) { return ( - diff --git a/src/mainview/components/Screenshots.tsx b/src/mainview/components/Screenshots.tsx index e65a965..3689b61 100644 --- a/src/mainview/components/Screenshots.tsx +++ b/src/mainview/components/Screenshots.tsx @@ -8,24 +8,22 @@ import Carousel from "./Carousel"; import { ContextDialog } from "./ContextDialog"; import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts"; import { twMerge } from "tailwind-merge"; -import { isUrl } from "@/shared/utils"; function Screenshot (data: { path: string; index: number; setFocused?: (index: number) => void; } & InteractParams) { const imageRef = useRef(null); - const { ref, focusSelf, focusKey } = useFocusable({ + const { ref, focusSelf } = useFocusable({ focusKey: `screenshot-${data.index}`, - onEnterPress: () => data.onAction?.({ focusKey }), + onEnterPress: () => data.onAction?.(), onFocus: (e, p, details) => { data.setFocused?.(data.index); scrollIntoNearestParent(ref.current, { behavior: details.instant ? 'instant' : 'smooth' }); } }); 4096; - const url = isUrl(data.path) ? data.path : `${RPC_URL(__HOST__)}${data.path}`; return
        - focusSelf({ nativeEvent: e.nativeEvent })} src={url} loading="lazy" decoding="async" /> -
        data.onAction?.({ event: e.nativeEvent, focusKey })}>
        + focusSelf({ nativeEvent: e.nativeEvent })} src={`${RPC_URL(__HOST__)}${data.path}`} loading="lazy" decoding="async" /> +
        data.onAction?.(e.nativeEvent)}>
        ; } @@ -61,9 +59,8 @@ function Preview (data: { id: string; screenshots?: string[]; preview: number; s } } ], [data.preview, focusKey, data.screenshots?.length ?? 0]); - const url = isUrl(data.screenshots?.[data.preview]) ? data.screenshots?.[data.preview] : `${RPC_URL(__HOST__)}${data.screenshots?.[data.preview]}`; - return ; + return ; } export default function Screenshots (data: { screenshots?: string[]; className?: string; } & FocusParams) @@ -86,7 +83,7 @@ export default function Screenshots (data: { screenshots?: string[]; className?: const closest = findClosestElementToCenter(scrollRef.current); if (!closest) return; const closestIndex = Array.from(scrollRef.current.children).indexOf(closest); - setFocus(`screenshot-${closestIndex}`, { instant: true }); + setFocus(`screenshot-${closestIndex}`); } }, [focused, hasFocusedChild, scrollRef.current]); diff --git a/src/mainview/components/SelectMenu.tsx b/src/mainview/components/SelectMenu.tsx deleted file mode 100644 index 42d21c8..0000000 --- a/src/mainview/components/SelectMenu.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { ContextList, DialogEntry, useContextDialog } from "./ContextDialog"; -import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts"; -import { useMatchRoute, useNavigate, useRouter } from "@tanstack/react-router"; -import { getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation"; -import { DoorOpen, Gamepad2, Home, Puzzle, RefreshCcw, Settings, Store } from "lucide-react"; -import { systemApi } from "../scripts/clientApi"; -import { FOCUS_KEYS } from "../scripts/types"; - -export default function SelectMenu (data: { rootFocusKey: string; }) -{ - const navigate = useNavigate(); - const matchRoute = useMatchRoute(); - const router = useRouter(); - - const options: DialogEntry[] = [ - { - content: "Home", - icon: , - action (ctx) - { - setOpen(false); - navigate({ to: "/" }); - }, - selected: !!matchRoute({ to: '/' }), - type: "primary", - id: "home-m" - }, - { - content: "Library", - icon: , - action (ctx) - { - setOpen(false); - navigate({ to: "/games" }); - }, - selected: !!matchRoute({ to: '/games' }), - type: "secondary", - id: "library-m" - }, - { - content: "Store", - icon: , - action (ctx) - { - setOpen(false); - navigate({ to: "/store/tab" }); - }, - selected: !!matchRoute({ to: '/store/tab' }), - type: "info", - id: "store-m" - }, - { - content: "Settings", - icon: , - action (ctx) - { - setOpen(false); - navigate({ to: "/settings/interface" }); - }, - selected: !!matchRoute({ to: '/settings' }) && !matchRoute({ to: '/settings/plugins' }) && !matchRoute({ to: '/settings/plugin/$source' }), - type: "accent", - id: "settings-m" - }, - { - content: "Plugins", - icon: , - action (ctx) - { - setOpen(false); - navigate({ to: "/settings/plugins" }); - }, - selected: !!matchRoute({ to: '/settings/plugins' }) && !matchRoute({ to: '/settings/plugin/$source' }), - type: "accent", - id: "plugins-m" - }, - { - content: "Reload", - icon: , - action (ctx) - { - setOpen(false); - navigation.reload(); - }, - type: "accent", - id: "reload-m" - }, - { - content: "Quit", - icon: , - action (ctx) - { - systemApi.api.system.exit.post(); - }, - type: 'error', - id: "quit-m" - } - ]; - const { dialog, setOpen, open } = useContextDialog('select-menu', { - content: <>
        {router.history.location.pathname}
        , - className: 'absolute flex flex-col justify-center left-0 top-0 bottom-0 rounded-none max-h-screen', - preferredChildFocusKey: FOCUS_KEYS.CONTEXT_DIALOG_OPTION('select-menu', options.find(o => o.selected)?.id ?? '') - }); - useShortcuts(data.rootFocusKey, () => [{ - label: "Menu", side: 'left', button: GamePadButtonCode.Select, action (e) - { - if (open) - { - setOpen(false); - } else - { - setOpen(true, getCurrentFocusKey()); - } - - }, - }], [open]); - - return <>{dialog}; -} \ No newline at end of file diff --git a/src/mainview/components/ShortcutPrompt.tsx b/src/mainview/components/ShortcutPrompt.tsx index 4cb8353..63a4ffd 100644 --- a/src/mainview/components/ShortcutPrompt.tsx +++ b/src/mainview/components/ShortcutPrompt.tsx @@ -1,4 +1,4 @@ -import { JSX, MouseEventHandler } from "react"; +import { MouseEventHandler } from "react"; import SvgIcon, { IconType } from "./SvgIcon"; import classNames from "classnames"; import { twMerge } from "tailwind-merge"; @@ -6,9 +6,8 @@ import { twMerge } from "tailwind-merge"; export default function ShortcutPrompt (data: { id: string; icon?: IconType; - label?: string | JSX.Element; + label?: string; className?: string; - iconClassName?: string; onClick?: MouseEventHandler; }) { @@ -24,7 +23,7 @@ export default function ShortcutPrompt (data: { }) )} > - {data.icon && } + {data.icon && } {data.label}
        ); diff --git a/src/mainview/components/Shortcuts.tsx b/src/mainview/components/Shortcuts.tsx index a71eeca..d8fc94c 100644 --- a/src/mainview/components/Shortcuts.tsx +++ b/src/mainview/components/Shortcuts.tsx @@ -1,36 +1,29 @@ import useActiveControl, { GamepadButtonEvent } from '../scripts/gamepads'; -import { GamePadButtonCode, useShortcutContext } from '../scripts/shortcuts'; +import { GamePadButtonCode, Shortcut } from '../scripts/shortcuts'; import ShortcutPrompt from './ShortcutPrompt'; import { IconType } from './SvgIcon'; -export function FloatingShortcuts () +export default function Shortcuts (data: { shortcuts?: Shortcut[]; }) { - return
        ; -} - -export const GamepadIconMap: Record = { - [GamePadButtonCode.A]: 'steamdeck_button_a', - [GamePadButtonCode.B]: 'steamdeck_button_b', - [GamePadButtonCode.X]: 'steamdeck_button_x', - [GamePadButtonCode.Y]: 'steamdeck_button_y', - [GamePadButtonCode.L1]: 'steamdeck_button_l1', - [GamePadButtonCode.R1]: 'steamdeck_button_r1', - [GamePadButtonCode.L2]: 'steamdeck_button_l2', - [GamePadButtonCode.R2]: 'steamdeck_button_r2', - [GamePadButtonCode.Select]: 'steamdeck_button_guide', - [GamePadButtonCode.Start]: 'steamdeck_button_options', - [GamePadButtonCode.LJoy]: 'steamdeck_stick_l_press', - [GamePadButtonCode.RJoy]: 'steamdeck_stick_r_press', - [GamePadButtonCode.Up]: 'steamdeck_dpad_up', - [GamePadButtonCode.Down]: 'steamdeck_dpad_down', - [GamePadButtonCode.Left]: 'steamdeck_dpad_left', - [GamePadButtonCode.Right]: 'steamdeck_dpad_right', - [GamePadButtonCode.Steam]: 'steamdeck_button_quickaccess' -}; - -export default function Shortcuts (data: { centerElement?: any; }) -{ - + const iconMap: Record = { + [GamePadButtonCode.A]: 'steamdeck_button_a', + [GamePadButtonCode.B]: 'steamdeck_button_b', + [GamePadButtonCode.X]: 'steamdeck_button_x', + [GamePadButtonCode.Y]: 'steamdeck_button_y', + [GamePadButtonCode.L1]: 'steamdeck_button_l1', + [GamePadButtonCode.R1]: 'steamdeck_button_r1', + [GamePadButtonCode.L2]: 'steamdeck_button_l2', + [GamePadButtonCode.R2]: 'steamdeck_button_r2', + [GamePadButtonCode.Select]: 'steamdeck_button_guide', + [GamePadButtonCode.Start]: 'steamdeck_button_options', + [GamePadButtonCode.LJoy]: 'steamdeck_stick_l_press', + [GamePadButtonCode.RJoy]: 'steamdeck_stick_r_press', + [GamePadButtonCode.Up]: 'steamdeck_dpad_up', + [GamePadButtonCode.Down]: 'steamdeck_dpad_down', + [GamePadButtonCode.Left]: 'steamdeck_dpad_left', + [GamePadButtonCode.Right]: 'steamdeck_dpad_right', + [GamePadButtonCode.Steam]: 'steamdeck_button_quickaccess' + }; const keyboardMap: Record = { [GamePadButtonCode.A]: 'ENTER', @@ -54,28 +47,15 @@ export default function Shortcuts (data: { centerElement?: any; }) const { control } = useActiveControl(); const showKeyboard = control === 'keyboard' || control === 'mouse'; - const { shortcuts } = useShortcutContext(); return ( - <> -
        - {shortcuts?.filter(s => !!s.label && s.side === 'left').map((s, i) => s.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: s.button, isClick: true }))} - icon={showKeyboard ? undefined : GamepadIconMap[s.button]} - label={showKeyboard ? `${keyboardMap[s.button]} | ${s.label}` : s.label} /> - )} -
        - {data.centerElement} -
        - {shortcuts?.filter(s => !!s.label && s.side !== 'left').map((s, i) => s.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: s.button, isClick: true }))} - icon={showKeyboard ? undefined : GamepadIconMap[s.button]} - label={showKeyboard ? `${keyboardMap[s.button]} | ${s.label}` : s.label} /> - )} -
        - +
        + {data.shortcuts?.filter(s => !!s.label).map((s, i) => s.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: s.button, isClick: true }))} + icon={showKeyboard ? undefined : iconMap[s.button]} + label={showKeyboard ? `${keyboardMap[s.button]} | ${s.label}` : s.label} /> + )} +
        ); } diff --git a/src/mainview/components/SideFilters.tsx b/src/mainview/components/SideFilters.tsx deleted file mode 100644 index 930bf2b..0000000 --- a/src/mainview/components/SideFilters.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { DownloadsLookupFilter, DownloadsLookupFilterValues, GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared'; -import { RoundButton } from "./RoundButton"; -import classNames from "classnames"; -import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts"; -import { useFocusable, FocusContext } from "@noriginmedia/norigin-spatial-navigation"; -import { ArrowDownAz, ClockArrowDown, CalendarArrowDown, Rocket, HardDrive, SortDesc, User, Drama, FunnelX, Store, ArrowUpDown, ArrowDown, ArrowUp } from "lucide-react"; -import { sourceIconMap } from "./Constants"; -import { ContextList, DialogEntry } from "./ContextDialog"; -import { FrontEndFilterLists } from "@simeonradivoev/gameflow-sdk/shared"; -import { useContext } from 'react'; -import { GlobalDialogContext } from '../scripts/contexts'; - -function FilterButton (data: { - id: string, - filters?: GameListFilterType, - tooltip: string, - icon: any; - dialog: (focNewSourceFocusKey: string) => void; - isActive: boolean; -}) -{ - const handleAction = () => data.dialog(data.id); - useShortcuts(data.id, () => [{ label: data.tooltip, action: handleAction, button: GamePadButtonCode.A }]); - return
        - - {data.icon} - -
        ; -} - -export function SideDownloadFilters (data: { - id: string, - filters?: DownloadsLookupFilter; - setLocalFilter: (filter: DownloadsLookupFilter) => void, - localFilter: DownloadsLookupFilter, - filterValues: DownloadsLookupFilterValues | undefined; -}) -{ - - const { ref, focusKey } = useFocusable({ focusKey: data.id }); - const globalDialog = useContext(GlobalDialogContext); - const orderByDialog = (focusKey: string) => globalDialog.openContext({ - content: ({ - content: o, - selected: data.localFilter.orderBy === o, - id: `sort-by-${o}`, - type: 'primary', - action (ctx) - { - data.setLocalFilter({ ...data.localFilter, orderBy: o }); - ctx.close(); - }, - }))} />, - preferredChildFocusKey: `sort-by-${data.localFilter.orderBy}` - }, focusKey); - - const orderDirectionDialog = (focusKey: string) => globalDialog.openContext({ - content: }, { label: 'desc', icon: }] - .map(o => ({ - content: o.label, - selected: data.localFilter.sortDirection === o.label, - icon: o.icon, - id: `sort-direction-${o.label}`, - type: 'primary', - action (ctx) - { - data.setLocalFilter({ ...data.localFilter, sortDirection: o.label as any }); - ctx.close(); - }, - })) - } />, - preferredChildFocusKey: `sort-direction-${data.localFilter.orderBy}` - }, focusKey); - - const sourceFilterDialog = (focusKey: string) => globalDialog.openContext({ - content: (o => ({ - content: o, - icon: sourceIconMap[o], - selected: data.localFilter.source === o, - id: `source-filter-${o}`, - type: 'primary', - action (ctx) - { - if (ctx.selected) data.setLocalFilter({ ...data.localFilter, source: undefined }); - else data.setLocalFilter({ ...data.localFilter, source: o }); - ctx.close(); - }, - }))} />, - preferredChildFocusKey: `source-filter-${data.localFilter.source}` - }, focusKey); - - return
        - - } /> - } /> - - {!data.filters?.source && - } /> - } - - {Object.values(data.localFilter).some(v => v !== undefined) && - <> -
        - data.setLocalFilter({})} className='p-3 drop-shadow-md!' > - - } -
        -
        ; -} - -export default function SideFilters (data: { - id: string, - filters?: GameListFilterType; - setLocalFilter: (filter: GameListFilterType) => void, - localFilter: GameListFilterType, - filterValues: FrontEndFilterLists | undefined; -}) -{ - - const { ref, focusKey } = useFocusable({ focusKey: data.id }); - const globalDialog = useContext(GlobalDialogContext); - - const openSourceDialog = (focusKey: string) => - { - globalDialog.openContext({ - content: (o => ({ - content: o, - icon: sourceIconMap[o], - selected: data.localFilter.source === o, - id: `source-filter-${o}`, - type: 'primary', - action (ctx) - { - if (ctx.selected) data.setLocalFilter({ ...data.localFilter, source: undefined }); - else data.setLocalFilter({ ...data.localFilter, source: o }); - ctx.close(); - }, - })).concat({ - content: "Local Only", - icon: , - selected: data.localFilter.localOnly === true, - id: `source-filter-local`, - type: 'primary', - action (ctx) - { - if (ctx.selected) data.setLocalFilter({ ...data.localFilter, localOnly: undefined }); - else data.setLocalFilter({ ...data.localFilter, localOnly: true }); - ctx.close(); - }, - })} />, preferredChildFocusKey: `source-filter-${data.localFilter.source}` - }, focusKey); - }; - - const openGenreDialog = (focusKey: string) => - { - globalDialog.openContext({ - content: ({ - content: g, - selected: data.localFilter.genres?.includes(g), - id: `genre-filter-${g}`, - type: 'primary', - action (ctx) - { - if (ctx.selected) data.setLocalFilter({ ...data.localFilter, genres: [...data.localFilter.genres?.filter(genre => genre !== g) ?? []] }); - else data.setLocalFilter({ ...data.localFilter, genres: [...data.localFilter.genres ?? [], g] }); - ctx.close(); - }, - }))} /> - }, focusKey); - }; - - const openSortingDialog = (focusKey: string) => - { - globalDialog.openContext({ - content: }, - { stat: "activity", icon: }, - { stat: "added", icon: }, - { stat: "release", icon: }, - ] satisfies { stat: GameListFilterType['orderBy'], icon?: any; }[]) - .map(o => ({ - content: o.stat, - icon: o.icon, - selected: data.localFilter.orderBy === o.stat, - id: `sort-by-${o.stat}`, - type: 'primary', - action (ctx) - { - data.setLocalFilter({ ...data.localFilter, orderBy: o.stat }); - ctx.close(); - }, - }))} />, preferredChildFocusKey: `sort-by-${data.localFilter.orderBy}` - }, focusKey); - }; - - const openAgeRatingDialog = (focusKey: string) => - { - globalDialog.openContext({ - content: ({ - content: a, - selected: data.localFilter.age_ratings?.includes(a), - id: `age-rating-filter-${a}`, - type: 'primary', - action (ctx) - { - if (ctx.selected) data.setLocalFilter({ ...data.localFilter, age_ratings: [...data.localFilter.age_ratings?.filter(age => age !== a) ?? []] }); - else data.setLocalFilter({ ...data.localFilter, age_ratings: [...data.localFilter.age_ratings ?? [], a] }); - ctx.close(); - }, - }))} /> - }, focusKey); - }; - - return
        - - } /> - 0} icon={} /> - 0} icon={} /> - {!data.filters?.source && - } /> - } - {Object.values(data.localFilter).some(v => v !== undefined) && - <> -
        - data.setLocalFilter({})} className='p-3 drop-shadow-md!' > - - } -
        -
        ; -} \ No newline at end of file diff --git a/src/mainview/components/StatList.tsx b/src/mainview/components/StatList.tsx index bdc9a02..3ff22f9 100644 --- a/src/mainview/components/StatList.tsx +++ b/src/mainview/components/StatList.tsx @@ -29,7 +29,7 @@ export default function StatList (data: { return
          - {data.stats.flatMap((s, i) => + {data.stats.map((s, i) => { let content: any = undefined; if (s.content instanceof Array) @@ -37,9 +37,13 @@ export default function StatList (data: { content =
          {s.content.map((c, ci) => {c})}
          ; } else { - content =
          {s.icon}{s.content}
          ; + content =
          {s.icon}{s.content}
          ; } - return [
        ; diff --git a/src/mainview/components/SvgIcon.tsx b/src/mainview/components/SvgIcon.tsx index e449d84..66a5a26 100644 --- a/src/mainview/components/SvgIcon.tsx +++ b/src/mainview/components/SvgIcon.tsx @@ -1,6 +1,5 @@ import "virtual:svg-icons/register"; import { StaticAssetPath } from "../gen/static-icon-assets.gen"; -import { CSSProperties } from "react"; type OnlySvgIcon = T extends `${infer Rest}.svg` ? Rest @@ -16,19 +15,17 @@ export default function SvgIcon ({ icon, prefix = "icon", className, - style, ...props }: { icon: IconType; prefix?: string; className?: string; - style?: CSSProperties; }) { const symbolId = `#${prefix}-${icon}`; return ( -
        + return
        diff --git a/src/mainview/components/game/Achievements.tsx b/src/mainview/components/game/Achievements.tsx index 9fbe814..9296403 100644 --- a/src/mainview/components/game/Achievements.tsx +++ b/src/mainview/components/game/Achievements.tsx @@ -1,5 +1,4 @@ -import { FrontEndGameTypeDetailed, FrontEndGameTypeDetailedAchievement } from "@simeonradivoev/gameflow-sdk/shared"; import { useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { Medal } from "lucide-react"; diff --git a/src/mainview/components/game/ActionButton.tsx b/src/mainview/components/game/ActionButton.tsx index 7a6db5e..c0f3b78 100644 --- a/src/mainview/components/game/ActionButton.tsx +++ b/src/mainview/components/game/ActionButton.tsx @@ -12,11 +12,12 @@ export default function ActionButton (data: { square?: boolean, onFocus?: () => void; tooltip?: string, - tooltipType?: 'accent' | 'error'; + tooltip_type?: 'accent' | 'error'; + onAction?: () => void; disabled?: boolean; -} & InteractParams) +}) { - const { ref, focusKey } = useFocusable({ focusKey: data.id, onFocus: data.onFocus, onEnterPress: () => data.onAction?.({ focusKey }), focusable: data.disabled !== true }); + const { ref } = useFocusable({ focusKey: data.id, onFocus: data.onFocus, onEnterPress: data.onAction, focusable: data.disabled !== true }); const styles = { primary: "bg-primary text-primary-content focused:bg-base-content focused:text-base-300 focusable focusable-primary", base: " text-base-content border-dashed border-base-content/20 border-2 focused:bg-base-content focused:text-base-300 focusable focusable-primary", @@ -28,9 +29,9 @@ export default function ActionButton (data: { -
        } - {lookups?.matches.map((l, i) => - { - return - { - data.onSelect(l); - }} />; - })} - - } -
      -
  • ; -} \ No newline at end of file diff --git a/src/mainview/components/game/MainActions.tsx b/src/mainview/components/game/MainActions.tsx index 6f772af..1e82ea1 100644 --- a/src/mainview/components/game/MainActions.tsx +++ b/src/mainview/components/game/MainActions.tsx @@ -1,20 +1,18 @@ +import { Router } from "@/mainview"; import { rommApi } from "@/mainview/scripts/clientApi"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { JSX, useContext, useEffect, useRef, useState } from "react"; +import { JSX, useEffect, useRef, useState } from "react"; import { getErrorMessage } from "react-error-boundary"; import toast from "react-hot-toast"; import { useLocalStorage } from "usehooks-ts"; -import { ContextList, DialogEntry } from "../ContextDialog"; -import { Clock, Crosshair, Download, EllipsisVertical, Import, PackageOpen, Play, TriangleAlert } from "lucide-react"; +import { ContextList, DialogEntry, useContextDialog } from "../ContextDialog"; +import { Clock, Download, EllipsisVertical, Import, PackageOpen, Play, TriangleAlert } from "lucide-react"; import { gameInvalidationQuery, installMutation, playMutation } from "@/mainview/scripts/queries/romm"; import ActionButton from "./ActionButton"; -import { useNavigate, UseNavigateResult, useRouter } from "@tanstack/react-router"; -import { GamePadButtonCode, Shortcut, useShortcuts } from "@/mainview/scripts/shortcuts"; -import { CommandEntry, FrontEndGameTypeDetailed, DownloadSourceType } from "@simeonradivoev/gameflow-sdk/shared"; -import { GlobalDialogContext } from "@/mainview/scripts/contexts"; -export function usePlayMutation (navigate: UseNavigateResult) +export default function MainActions (data: { game?: FrontEndGameTypeDetailed, source: string, id: string; }) { + const installMut = useMutation(installMutation(data.source, data.id)); const playMut = useMutation({ ...playMutation, onError (error) { @@ -22,42 +20,14 @@ export function usePlayMutation (navigate: UseNavigateResult) }, onSuccess (data, { source, id }, onMutateResult, context) { - navigate({ to: '/launcher/$source/$id', params: { source: source, id: id } }); + Router.navigate({ to: '/launcher/$source/$id', params: { source: source, id: id }, replace: true }); }, }); - - return playMut; -} - -export function playGame (source: string, id: string, cmd: CommandEntry, navigate: UseNavigateResult, playMutation: (options: { source: string, id: string, command_id: string | number; }) => void) -{ - if (cmd.emulator === 'EMULATORJS') - { - const params = new URLSearchParams(Array.isArray(cmd.command) ? cmd.command[0] : cmd.command); - navigate({ to: '/embedded/$source/$id', params: { source: source, id: id }, search: Object.fromEntries(params.entries()) }); - } else - { - playMutation({ source: source, id: id, command_id: cmd.id }); - } -} - -export default function MainActions (data: { - game?: FrontEndGameTypeDetailed, - source: string, - id: string; -}) -{ - const installMut = useMutation(installMutation(data.source, data.id)); - const router = useRouter(); - - const navigate = useNavigate(); - const globalDialog = useContext(GlobalDialogContext); const ws = useRef<{ send: (data: string) => void; }>(undefined); const [progress, setProgress] = useState(undefined); const [status, setStatus] = useState(undefined); const [error, setError] = useState(undefined); const [details, setDetails] = useState(undefined); - const [installSources, setInstallSources] = useState(undefined); const [commands, setCommands] = useState(undefined); const [preferredCommand, setPreferredCommand] = useLocalStorage(`${data.game?.source ?? data.game?.id.source}-${data.game?.source_id ?? data.game?.id.id}-preferred-command`, undefined); const queryClient = useQueryClient(); @@ -68,7 +38,7 @@ export default function MainActions (data: { if (preferredCommand && c.id !== preferredCommand) return false; return true; }); - const playMut = usePlayMutation(navigate); + useEffect(() => { const sub = rommApi.api.romm.status({ source: data.source })({ id: data.id }).subscribe(); @@ -80,7 +50,6 @@ export default function MainActions (data: { setProgress((e.data as any).progress); setDetails((e.data as any).details); setCommands((e.data as any).commands); - setInstallSources((e.data as any).sources); if (e.data.status === 'refresh') { @@ -89,16 +58,17 @@ export default function MainActions (data: { { if (localId) { - router.navigate({ to: '/game/$source/$id', params: { id: String(localId), source: 'local' }, replace: true }); + Router.navigate({ to: '/game/$source/$id', params: { id: String(localId), source: 'local' }, replace: true }); } else { - router.navigate({ to: '/game/$source/$id', params: { id: data.id, source: data.source }, replace: true }); + Router.navigate({ to: '/game/$source/$id', params: { id: data.id, source: data.source }, replace: true }); } }); } else if (e.data.status === 'error') { const errorMessage = getErrorMessage(e.data.error); if (!errorMessage) return; + toast.error(errorMessage); setError(errorMessage); } }); @@ -108,7 +78,7 @@ export default function MainActions (data: { sub.close(); ws.current = undefined; }; - }, [data.source, data.id, router]); + }, [data.source, data.id]); let progressIcon: JSX.Element | undefined = undefined; switch (status) @@ -125,63 +95,56 @@ export default function MainActions (data: { } const showProgress = progress !== null && !!progressIcon; + useEffect(() => + { + if (showProgress) return; + showInstallOptions(false); + }, [showProgress]); - + const handlePlay = (cmd?: CommandEntry) => + { + if (!cmd) return; + if (cmd.emulator === 'EMULATORJS') + { + const params = new URLSearchParams(cmd.command); + Router.navigate({ to: '/embedded/$source/$id', params: { source: data.source, id: data.id }, search: Object.fromEntries(params.entries()), replace: true }); + } else + { + playMut.mutate({ source: data.source, id: data.id, command_id: cmd.id }); + } + }; let mainButton: any | undefined = undefined; - let showAllCommandsAction: ((focusKey: string) => void) | undefined; - let mainAction: () => void; if (status === 'installed') { - if (validCommands.length > 1) showAllCommandsAction = (focusKey) => globalDialog.openContext({ - content: - { - const commands: DialogEntry = { - id: String(c.id), - content: c.label ?? "", - type: 'primary', - selected: preferredCommand !== undefined ? preferredCommand === c.id : i === 0, - action (ctx) - { - setPreferredCommand(c.id); - playGame(data.source, data.id, c, navigate, playMut.mutate); - }, - }; - return commands; - })} />, - preferredChildFocusKey: String(preferredCommand) - }, focusKey); - mainAction = () => validDefaultCommand ? playGame(data.source, data.id, validDefaultCommand, navigate, playMut.mutate) : undefined; - mainButton =
    - - + mainButton =
    handlePlay(validDefaultCommand)} tooltip={validDefaultCommand?.label ?? details} + key="primary" + type='primary' + id="mainAction" + > + - + - {showAllCommandsAction && - showAllCommandsAction!('allActionsBtn')}> + {validCommands.length > 1 && + showAllCommands(true, 'allActionsBtn')}> }
    ; } else if (error) { - mainAction = () => - { - if (status === 'missing-emulator') - { - router.navigate({ to: '/settings/directories' }); - } - }; mainButton = + { + if (status === 'missing-emulator') + { + Router.navigate({ to: '/settings/directories' }); + } + }} id="mainAction"> ; @@ -191,47 +154,24 @@ export default function MainActions (data: { let icon = ; if (status === 'install') { - if (installSources && installSources.length > 1) - icon = ; - else - icon = ; - + icon = ; } else if (status === 'present') { icon = ; } - mainAction = () => - { - if (installMut.isPending) return; - switch (status) - { - case 'present': - case 'install': - if (installSources && installSources.length > 1) - { - globalDialog.openContext({ - content: ({ - content: s.name, - action (ctx) - { - installMut.mutate({ downloadId: s.id }); - ctx.close(); - }, - type: 'primary', - id: s.id - } satisfies DialogEntry)) ?? []} /> - }, 'mainAction'); - } else - { - installMut.mutate({}); - } - - break; - } - }; mainButton = + { + if (installMut.isPending) return; + switch (status) + { + case 'present': + case 'install': + installMut.mutate(); + break; + } + }} tooltip={details ?? status} type='primary' id="mainAction"> @@ -239,42 +179,42 @@ export default function MainActions (data: { ; } - useShortcuts('mainAction', () => - { - const shortcuts: Shortcut[] = [{ - button: GamePadButtonCode.A, - action: mainAction - }]; - - if (showAllCommandsAction) - shortcuts.push( + const { dialog: allCommandDialog, setOpen: showAllCommands } = useContextDialog('all-commands-dialog', { + content: + { + const commands: DialogEntry = { + id: String(c.id), + content: c.label ?? "", + type: 'primary', + selected: preferredCommand !== undefined ? preferredCommand === c.id : i === 0, + action (ctx) { - button: GamePadButtonCode.Y, - label: "All Commands", - action (e) - { - showAllCommandsAction('mainAction'); - }, - }); + setPreferredCommand(c.id); + handlePlay(c); + }, + }; + return commands; + })} />, + preferredChildFocusKey: String(preferredCommand) + }); - return shortcuts; - }, [showAllCommandsAction, mainAction]); + const { dialog: installOptionsDialog, setOpen: showInstallOptions } = useContextDialog('install-options-dialog', { + content: + }); return
    {mainButton}
    - {showProgress && globalDialog.openContext({ - content: - }, "progress")} key="progress" square tooltip={details} type="base" id="progress" > + {showProgress && showInstallOptions(true, "progress")} key="progress" square tooltip={details} type="base" id="progress" >
    {progressIcon} @@ -282,5 +222,7 @@ export default function MainActions (data: {
    } + {installOptionsDialog} + {allCommandDialog}
    ; } \ No newline at end of file diff --git a/src/mainview/components/options/Button.tsx b/src/mainview/components/options/Button.tsx index e131123..1c6e63c 100644 --- a/src/mainview/components/options/Button.tsx +++ b/src/mainview/components/options/Button.tsx @@ -7,12 +7,11 @@ import import classNames from "classnames"; import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts"; import { CSSProperties } from "react"; -import { oneShot } from "@/mainview/scripts/audio/audio"; export type ButtonStyle = 'base' | 'accent' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'; const styles = { - base: 'dark:bg-base-200 light:bg-base-100 text-base-content active:not-disabled:bg-base-300! active:not-disabled:text-base-content! active:not-disabled:ring-offset-base-content', + base: 'dark:bg-base-200 light:bg-base-300 text-base-content active:not-disabled:bg-base-300! active:not-disabled:text-base-content! active:not-disabled:ring-offset-base-content', accent: "bg-accent text-accent-content active:not-disabled:bg-base-100! active:not-disabled:text-base-content! active:ring-offset-accent", primary: "bg-primary text-primary-content active:not-disabled:bg-base-100! active:not-disabled:text-base-content! active:not-disabled:ring-offset-primary", secondary: "bg-secondary text-secondary-content active:not-disabled:bg-base-100! active:not-disabled:text-base-content! active:not-disabled:ring-offset-secondary", @@ -22,62 +21,44 @@ const styles = { error: "bg-error text-error-content active:not-disabled:bg-base-100! active:not-disabled:text-base-content! active:not-disabled:ring-offset-error", }; -const externalStyles = { - base: '', - accent: "focusable-accent", - primary: "focusable-primary", - secondary: "focusable-secondary", - info: "focusable-info", - success: "focusable-success", - warning: "focusable-warning", - error: "focusable-error", -}; - export function Button (data: { id: string, children?: any, className?: string, disabled?: boolean, - external?: boolean, type?: "reset" | "button" | "submit"; style?: ButtonStyle, shortcutLabel?: string; focusClassName?: string; cssStyle?: CSSProperties; tooltip?: string; - tooltipType?: "base" | "accent" | "error" | "warning"; + tooltipType?: "base" | "accent" | "error"; } & InteractParams & FocusParams) { - const handleAction = (event?: Event) => - { - data.onAction?.({ event, focusKey }); - oneShot('click'); - }; const { ref, focused, focusKey } = useFocusable({ focusKey: data.id, - onEnterPress: () => handleAction(), + onEnterPress: data.onAction, onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details), focusable: !data.disabled }); if (data.shortcutLabel) { - useShortcuts(focusKey, () => [{ label: data.shortcutLabel, action: handleAction, button: GamePadButtonCode.A }], [data.shortcutLabel]); + useShortcuts(focusKey, () => [{ label: data.shortcutLabel, action: data.onAction, button: GamePadButtonCode.A }], [data.shortcutLabel]); } return
    }> - {!!schema.enum && String(v))} icon={data.icon} + + {data.type === 'dropdown' && data.values && { - setLocalValue(v); + if (data.type === 'checkbox') + { + setLocalValue(v); + } else + { + setLocalValue(v); + } }} value={localValue} />} - {!schema.enum && { - setLocalValue(v); + if (data.type === 'checkbox') + { + setLocalValue(v); + } else + { + setLocalValue(v); + } }} value={localValue} />} diff --git a/src/mainview/components/options/OptionDropdown.tsx b/src/mainview/components/options/OptionDropdown.tsx index e2ed246..083b6c4 100644 --- a/src/mainview/components/options/OptionDropdown.tsx +++ b/src/mainview/components/options/OptionDropdown.tsx @@ -1,13 +1,13 @@ -import { FocusEventHandler, HTMLInputAutoCompleteAttribute, JSX, useState } from "react"; +import { FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useState } from "react"; import { twMerge } from "tailwind-merge"; import { useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { ContextDialog, ContextList, DialogEntry } from "../ContextDialog"; import { ChevronDown } from "lucide-react"; import { FOCUS_KEYS } from "@/mainview/scripts/types"; -import { oneShot } from "@/mainview/scripts/audio/audio"; export function OptionDropdown (data: { name: string; + type: HTMLInputTypeAttribute; className?: string; placeholder?: string; icon?: JSX.Element; @@ -23,7 +23,6 @@ export function OptionDropdown (data: { const handlePress = () => { setOpen(true); - oneShot('click'); }; const handleClose = () => setOpen(false); const { ref } = useFocusable({ @@ -34,7 +33,11 @@ export function OptionDropdown (data: { <> {open && ({ diff --git a/src/mainview/components/options/OptionInput.tsx b/src/mainview/components/options/OptionInput.tsx index 801e639..1f43246 100644 --- a/src/mainview/components/options/OptionInput.tsx +++ b/src/mainview/components/options/OptionInput.tsx @@ -1,10 +1,9 @@ -import { FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useEffect, useRef, useState } from "react"; +import { 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"; import { CheckIcon, X } from "lucide-react"; -import { oneShot } from "@/mainview/scripts/audio/audio"; -import { GamePadButtonCode, Shortcut, useShortcuts } from "@/mainview/scripts/shortcuts"; export function OptionInput (data: { name: string; @@ -12,15 +11,11 @@ export function OptionInput (data: { className?: string; placeholder?: string; icon?: JSX.Element; - value?: string | boolean | number; - min?: number; - max?: number; - step?: number; - defaultValue?: string | boolean | number; + value?: string | boolean; + defaultValue?: string | boolean; autocomplete?: HTMLInputAutoCompleteAttribute; - compact?: boolean; onBlur?: FocusEventHandler; - onChange?: (value: string | number | boolean) => void; + onChange?: (value: any) => void; }) { const handlePress = () => @@ -32,119 +27,48 @@ export function OptionInput (data: { { inputRef.current?.focus(); } - oneShot('click'); }; - const [inputFocused, setInputFocused] = useState(false); - const inputRef = useRef(null); - const { ref, focusKey } = useFocusable({ - focusKey: data.name, - onEnterPress: handlePress, - onBlur: () => inputRef.current?.blur() + const { ref } = useFocusable({ + focusKey: data.name, onEnterPress: handlePress }); - + const inputRef = useRef(null); const option = useOptionContext({ onOptionEnterPress: handlePress, }); - - useEffect(() => - { - if (data.type === 'range') - { - option.setFocusBoundary(inputFocused); - option.setFocusBoundaryDirections(['left', 'right']); - } - }, [inputFocused, option, data.type]); - - useShortcuts(focusKey, () => - { - - const shortcuts: Shortcut[] = []; - if (inputFocused && data.type === 'range') - { - shortcuts.push( - { - label: "Decrease", - button: GamePadButtonCode.Left, - action () - { - if (!inputRef.current) return; - inputRef.current?.stepDown(); - data.onChange?.(inputRef.current.valueAsNumber); - } - }, - { - label: "Increase", - button: GamePadButtonCode.Right, - action (e) - { - if (!inputRef.current) return; - inputRef.current?.stepUp(); - data.onChange?.(inputRef.current.valueAsNumber); - } - } - ); - } - if (inputFocused) - { - shortcuts.push({ - label: "Unfocus", - button: GamePadButtonCode.B, - action (e) - { - inputRef.current?.blur(); - } - }); - } - return shortcuts; - }, [inputFocused, data.type]); - - const handleInputFocus: FocusEventHandler = (e) => + const handleFocus = () => { option.focus(); - setInputFocused(true); - }; - - const handleInputBlur = (e: any) => - { - data.onBlur?.(e); - setInputFocused(false); + if (inputRef.current) + { + var rect = inputRef.current?.getBoundingClientRect(); + systemApi.api.system.show_keyboard.post({ + XPosition: rect.x, + YPosition: rect.y, + Width: rect.width, + Height: rect.height + }); + } }; return ( -
    } -
    +
    {data.children}
    diff --git a/src/mainview/components/options/PathSettingsOption.tsx b/src/mainview/components/options/PathSettingsOption.tsx index 7b2789f..29ba634 100644 --- a/src/mainview/components/options/PathSettingsOption.tsx +++ b/src/mainview/components/options/PathSettingsOption.tsx @@ -1,4 +1,5 @@ import { HTMLInputTypeAttribute, JSX, useEffect, useState } from "react"; +import { SettingsType } from "../../../shared/constants"; import { useMutation, useQuery } from "@tanstack/react-query"; import { OptionSpace } from "./OptionSpace"; import { OptionInput } from "./OptionInput"; @@ -8,12 +9,11 @@ import { ContextDialog } from "../ContextDialog"; import FilePicker from "../FilePicker"; import { setFocus } from "@noriginmedia/norigin-spatial-navigation"; import { getSettingQuery, setSettingMutation } from "@queries/settings"; -import { KeysWithValueAssignableTo, SettingsType } from "@simeonradivoev/gameflow-sdk/shared"; export interface PathSettingsOptionParams { label: string; - id: string; + id: KeysWithValueAssignableTo; type: HTMLInputTypeAttribute; placeholder?: string; icon?: JSX.Element; @@ -24,11 +24,10 @@ export interface PathSettingsOptionParams allowNewFolderCreation?: boolean; } -export function PathSettingsOption (data: PathSettingsOptionParams & { id: KeysWithValueAssignableTo; }) +export function PathSettingsOption (data: PathSettingsOptionParams) { const [localValue, setLocalValue] = useState(); const [dirty, setDirty] = useState(false); - const { data: defaultValue } = useQuery(getSettingQuery(data.id)); const setMutation = useMutation({ ...setSettingMutation(data.id), onSuccess: (d, v, r, cx) => @@ -45,7 +44,6 @@ export function PathSettingsOption (data: PathSettingsOptionParams & { id: KeysW save={setMutation.mutate} localValue={localValue} allowNewFolderCreation={data.allowNewFolderCreation} - defaultValue={defaultValue as any} setLocalValue={(v) => { setLocalValue(v); @@ -58,17 +56,16 @@ export function PathSettingsOptionBase (data: PathSettingsOptionParams & { localValue: string | undefined; setLocalValue: (value: string | undefined) => void; isDirty: boolean; - className?: string; - defaultValue: string | undefined; }) { const [isBrowsing, setIsBrowsing] = useState(false); - const changed = data.defaultValue !== data.localValue; + const { data: defaultValue } = useQuery(getSettingQuery(data.id)); + const changed = defaultValue !== data.localValue; useEffect(() => { - data.setLocalValue(String(data.defaultValue ?? '')); - }, [data.defaultValue]); + data.setLocalValue(String(defaultValue)); + }, [defaultValue]); const handleSelectPath = (path: string) => { @@ -83,7 +80,7 @@ export function PathSettingsOptionBase (data: PathSettingsOptionParams & { const handleCloseSeatch = () => { setIsBrowsing(false); - setFocus(`${data.id}-browse`, { instant: true }); + setFocus(`${data.id}-browse`); }; const handleInputBlur = () => @@ -95,8 +92,7 @@ export function PathSettingsOptionBase (data: PathSettingsOptionParams & { }; return ( - {data.label}{changed && }}> - + {data.label}{changed && }}> { - data.setLocalValue(String(e)); + data.setLocalValue(e); }} value={data.localValue} /> - {data.requireConfirmation === true &&
    ; } export function EmulatorsSection (data: { id: string; emulators?: FrontEndEmulator[]; - onSelect?: (em: FrontEndEmulator, focusKey: string) => void; + onSelect?: (id: string, focusKey: string) => void; header?: any; } & FocusParams) { - const router = useRouter(); const { ref, focusKey } = useFocusable({ focusKey: FOCUS_KEYS.EMULATOR_SECTION(data.id), trackChildren: true, @@ -65,12 +63,12 @@ export function EmulatorsSection (data: { {data.emulators?.map((em) => ( - data.onSelect?.(em, focusKey)} onFocus={({ node, details }) => + data.onSelect?.(em.name, focusKey)} onFocus={({ node, details }) => { scrollIntoNearestParent(node, { behavior: details.instant ? 'instant' : 'smooth' }); }} /> )) ?? Array.from({ length: 8 }).map((_, i) =>
    )} - router.navigate({ to: '/store/tab/emulators', viewTransition: { types: ['zoom-in'] } })} onFocus={({ node, instant }) => scrollIntoNearestParent(node, { behavior: instant ? 'instant' : 'smooth' })} /> + Router.navigate({ to: '/store/tab/emulators', viewTransition: { types: ['zoom-in'] } })} onFocus={({ node, instant }) => scrollIntoNearestParent(node, { behavior: instant ? 'instant' : 'smooth' })} /> diff --git a/src/mainview/components/store/GamesSection.tsx b/src/mainview/components/store/GamesSection.tsx index ccdd076..54f4057 100644 --- a/src/mainview/components/store/GamesSection.tsx +++ b/src/mainview/components/store/GamesSection.tsx @@ -10,7 +10,6 @@ import FrontEndGameCard from "../FrontEndGameCard"; import { FOCUS_KEYS } from "@/mainview/scripts/types"; import Carousel from "../Carousel"; import { twMerge } from "tailwind-merge"; -import { FrontEndGameType, FrontEndId } from "@simeonradivoev/gameflow-sdk/shared"; export function GamesSection (data: { games?: FrontEndGameType[]; @@ -31,7 +30,7 @@ export function GamesSection (data: { useEffect(() => { if (focused) - focusSelf({ instant: true }); + focusSelf(); }, [!!data.games]); return ( @@ -45,7 +44,7 @@ export function GamesSection (data: { {data.games?.map((g, i) => data.onSelect?.(g.id, FOCUS_KEYS.GAME_CARD(g.id))} onFocus={(key, node, details) => scrollIntoNearestParent(node, { behavior: details.instant ? 'instant' : 'smooth' })} diff --git a/src/mainview/components/store/InvalidStoreError.tsx b/src/mainview/components/store/InvalidStoreError.tsx deleted file mode 100644 index 646721d..0000000 --- a/src/mainview/components/store/InvalidStoreError.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ErrorComponentProps } from "@tanstack/react-router"; -import { TriangleAlert } from "lucide-react"; - -export default function Error (data: ErrorComponentProps) -{ - return
    -
    Invalid Store. Update App.
    -
    {data.error.message}
    -
    ; -} \ No newline at end of file diff --git a/src/mainview/components/store/MissingEmulatorsSection.tsx b/src/mainview/components/store/MissingEmulatorsSection.tsx index 6339dde..b064ec8 100644 --- a/src/mainview/components/store/MissingEmulatorsSection.tsx +++ b/src/mainview/components/store/MissingEmulatorsSection.tsx @@ -3,33 +3,30 @@ import useFocusable, FocusContext, } from "@noriginmedia/norigin-spatial-navigation"; -import { CircleQuestionMark, SearchAlert } from "lucide-react"; +import { Button } from "../options/Button"; +import useActiveControl from "@/mainview/scripts/gamepads"; +import { ChevronRight, CircleQuestionMark, SearchAlert } from "lucide-react"; import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts"; import { RPC_URL } from "@/shared/constants"; import { FOCUS_KEYS } from "@/mainview/scripts/types"; -import { oneShot } from "@/mainview/scripts/audio/audio"; -import { FrontEndEmulator } from "@simeonradivoev/gameflow-sdk/shared"; // ── Single missing-emulator card ─────────────────────────────────────────── interface MissingCardProps { emulator: FrontEndEmulator; - onSelect?: (em: FrontEndEmulator, focusKey: string) => void; + onSelect?: (id: string, focusKey: string) => void; } function MissingCard ({ emulator: em, onSelect }: MissingCardProps) { - const handleSelect = () => - { - onSelect?.(em, focusKey); - oneShot('click'); - }; + const handleSelect = () => onSelect?.(em.name, focusKey); const { ref, focusKey } = useFocusable({ focusKey: FOCUS_KEYS.MISSING_CARD(em.name), onEnterPress: handleSelect, }); useShortcuts(focusKey, () => [{ button: GamePadButtonCode.A, label: "Details", action: handleSelect }], [handleSelect]); + const { isMouse } = useActiveControl(); return (
    e.key === "Enter" && handleSelect} - className={"focusable focusable-accent focusable-hover cursor-pointer bg-base-100 rounded-4xl transition-all focused:animate-scale-small shadow-lg"} + className={"focusable focusable-accent bg-base-100 rounded-4xl transition-all focused:animate-scale-small shadow-lg"} >
    @@ -55,6 +52,10 @@ function MissingCard ({ emulator: em, onSelect }: MissingCardProps)

    {em.systems?.map(s => s.name).join(',')}

    +
    +

    {em.name}

    + {isMouse && } +
    ); @@ -65,7 +66,7 @@ export function MissingEmulatorsSection ({ onSelect, }: { emulators: FrontEndEmulator[]; - onSelect?: (em: FrontEndEmulator, focusKey: string) => void; + onSelect?: (id: string, focusKey: string) => void; }) { const { ref, focusKey } = useFocusable({ diff --git a/src/mainview/components/store/StoreEmulatorCard.tsx b/src/mainview/components/store/StoreEmulatorCard.tsx index 3479579..ccf81cb 100644 --- a/src/mainview/components/store/StoreEmulatorCard.tsx +++ b/src/mainview/components/store/StoreEmulatorCard.tsx @@ -1,20 +1,14 @@ import { twMerge } from "tailwind-merge"; import { RPC_URL } from "@/shared/constants"; +import { Button } from "../options/Button"; +import useActiveControl from "@/mainview/scripts/gamepads"; import { useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts"; -import { CircleFadingArrowUp, FileQuestion, IceCream2, Package, Store, WandSparkles } from "lucide-react"; +import { BadgeCheck, ChevronRight, EllipsisVertical, FileQuestion, IceCream2, Package, Sparkles, Store, WandSparkles } from "lucide-react"; import { FOCUS_KEYS } from "@/mainview/scripts/types"; import { FlatpackIcon } from "@/mainview/scripts/brandIcons"; -import { JSX, useContext } from "react"; -import { oneShot } from "@/mainview/scripts/audio/audio"; -import { useQuery } from "@tanstack/react-query"; -import { getUpdateInfoForEmulator } from "@/mainview/scripts/queries/store"; -import { FrontEndEmulator } from "@simeonradivoev/gameflow-sdk/shared"; -import { rommApi } from "@/mainview/scripts/clientApi"; -import { useNavigate } from "@tanstack/react-router"; -import { GlobalDialogContext } from "@/mainview/scripts/contexts"; -import { ContextList, DialogEntry } from "../ContextDialog"; +import { JSX } from "react"; export const emulatorStatusIcons: Record = { store: , @@ -32,12 +26,7 @@ export function StoreEmulatorCard (data: { className?: string; }) { - const navigate = useNavigate(); - const handleSelect = () => - { - data.onSelect?.(data.emulator.name, focusKey); - oneShot('click'); - }; + const handleSelect = () => data.onSelect?.(data.emulator.name, focusKey); const { ref, focusKey } = useFocusable({ focusKey: FOCUS_KEYS.EMULATOR_CARD(data.id), @@ -48,44 +37,17 @@ export function StoreEmulatorCard (data: { } }); - const { data: updateInfo } = useQuery(getUpdateInfoForEmulator(data.emulator.name)); - - const globalDialogContext = useContext(GlobalDialogContext); - useShortcuts(focusKey, () => [{ - button: GamePadButtonCode.A, - label: "Details", - action: handleSelect - - }, { - button: GamePadButtonCode.Y, - label: "Launch Emulator", - action: e => - { - const entries: DialogEntry[] = data.emulator.validSources.filter(s => s.exists).map(s => ({ - content: `Launch: ${s.type}`, - type: 'primary', - icon: emulatorStatusIcons[s.type], - action (ctx) - { - if (!data.emulator) return; - rommApi.api.romm.game({ source: 'emulator' })({ id: data.emulator.name }).play.post({ command_id: s.type }); - ctx.close(); - navigate({ to: '/launcher/$source/$id', params: { source: 'emulator', id: data.emulator.name } }); - }, id: `open-${s.type}` - } satisfies DialogEntry)); - globalDialogContext.openContext({ content: }, focusKey); - } - }], [handleSelect]); + useShortcuts(focusKey, () => [{ button: GamePadButtonCode.A, label: "Details", action: handleSelect }], [handleSelect]); + const { isMouse, isTouch } = useActiveControl(); return (
    s.exists)} - onClick={handleSelect} - className={twMerge("relative focusable focusable-info focusable-hover bg-base-100 rounded-4xl transition-shadow focused:not-control-mouse:animate-scale-small shadow-lg border border-base-content/10 active:ring-4 active:ring-base-content active:transition-none cursor-pointer", data.className)} + onClick={isTouch ? handleSelect : undefined} + className={twMerge("relative focusable focusable-info bg-base-100 rounded-4xl transition-shadow focused:not-control-mouse:animate-scale-small shadow-lg border border-base-content/10 active:ring-4 active:ring-base-content active:transition-none", data.className)} >
    @@ -113,27 +75,21 @@ export function StoreEmulatorCard (data: {
    - {updateInfo?.hasUpdate &&
    -
    - -
    + {!!data.emulator.integration && data.emulator.validSources.some(s => s.type === 'store') &&
    +
    } - {data.emulator.integrations.length > 0 &&
    i.supportLevel)} - data-full-support={data.emulator.integrations.some(i => i.supportLevel === 'full')} - className="tooltip not-aria-disabled:tooltip-primary" - data-tip={data.emulator.integrations.some(i => i.supportLevel) ? data.emulator.integrations.some(i => i.supportLevel === 'full') ? "Full Support" : "Partial SUpport" : "Can Integrate"} - > -
    -
    } - {data.emulator.validSources.slice(0, 3).map((s, i) => + {data.emulator.validSources.slice(0, 3).map(s => { - return
    -
    + return
    +
    {emulatorStatusIcons[s.type]}
    ; })} + {isMouse && <> + + } +
    diff --git a/src/mainview/emulatorjs/emulator.ts b/src/mainview/emulatorjs/emulator.ts index 48ba808..61e570b 100644 --- a/src/mainview/emulatorjs/emulator.ts +++ b/src/mainview/emulatorjs/emulator.ts @@ -10,11 +10,10 @@ Array.from(params.entries()).forEach(([key, value]) => window.addEventListener('message', (e) => { - const data = e.data as EmulatorJsMessage; - switch (data.type) + switch (e.data.type) { case 'pause': - if (data.paused) + if (e.data.data === true) { window.EJS_emulator.pause(); } else @@ -25,51 +24,14 @@ window.addEventListener('message', (e) => case 'restart': window.EJS_emulator.elements.bottomBar.restart[0].click(); break; - case 'requestSave': - window.EJS_emulator.elements.bottomBar.saveSavFiles[0].click(); - break; } }); -function postMessage (m: EmulatorJsMessage) -{ - window.parent.postMessage( - m, - "*" - ); -} - -export function loadEmulatorJSSave (save: Uint8Array) -{ - const FS = window.EJS_emulator.gameManager.FS; - const path = window.EJS_emulator.gameManager.getSaveFilePath(); - const paths = path.split("/"); - let cp = ""; - for (let i = 0; i < paths.length - 1; i++) - { - if (paths[i] === "") continue; - cp += "/" + paths[i]; - if (!FS.analyzePath(cp).exists) FS.mkdir(cp); - } - if (FS.analyzePath(path).exists) FS.unlink(path); - FS.writeFile(path, save); - window.EJS_emulator.gameManager.loadSaveFiles(); -} - -window.EJS_threads = !__PUBLIC__; +window.EJS_threads = true; window.EJS_player = "#game"; window.EJS_lightgun = false; window.EJS_startOnLoaded = true; -window.EJS_onGameStart = async () => -{ - const savesResponse = await fetch(`${RPC_URL(__HOST__)}/api/romm/emulatorjs/load?filePath=${encodeURIComponent(window.EJS_emulator.gameManager.getSaveFilePath())}`); - if (savesResponse.ok) - { - loadEmulatorJSSave(new Uint8Array(await savesResponse.arrayBuffer())); - postMessage({ type: "loaded" }); - } -}; // For core downloads, it either redirects to CDN or uses local if downloaded window.EJS_pathtodata = `${RPC_URL(__HOST__)}/api/romm/emulatorjs/data`; window.EJS_Buttons = { @@ -78,8 +40,10 @@ window.EJS_Buttons = { displayName: "Exit", callback: () => { - const saveFile = window.EJS_emulator.gameManager.getSaveFile(false); - postMessage({ type: "exit", save: saveFile ? new File([saveFile], window.EJS_emulator.gameManager.getSaveFilePath()) : undefined }); + window.parent.postMessage( + { type: "exit" }, + "*" + ); } } }; @@ -94,18 +58,7 @@ const moduleUrls = import.meta.glob import: 'default', }); -function handeSave (ctx: { save: ArrayBuffer, screenshot: ArrayBuffer | undefined, format: string; }) -{ - window.parent.postMessage({ type: 'save', save: new File([ctx.save], window.EJS_emulator.gameManager.getSaveFilePath()) }); -} - // emulatorjs expects basenames instead of paths for some reason window.EJS_paths = Object.fromEntries(await Promise.all(Object.entries(moduleUrls).map(async ([key, value]) => [basename(key), await value()]))); -window.EJS_onSaveUpdate = (ctx: { hash: string, save: ArrayBuffer, screenshot: ArrayBuffer | undefined, format: string; }) => handeSave(ctx); -window.EJS_onSaveSave = (ctx: { - save: ArrayBuffer; - screenshot: ArrayBuffer; - format: string; -}) => handeSave(ctx); await import('@emulatorjs/emulatorjs/data/loader.js' as any); \ No newline at end of file diff --git a/src/mainview/emulatorjs/types.d.ts b/src/mainview/emulatorjs/types.d.ts index 4021f5d..11b8f1f 100644 --- a/src/mainview/emulatorjs/types.d.ts +++ b/src/mainview/emulatorjs/types.d.ts @@ -14,7 +14,6 @@ export declare global EJS_cheats: string[][], EJS_fullscreenOnLoaded: boolean, EJS_startOnLoaded: boolean, - EJS_onGameStart, EJS_core: string, EJS_lightgun: boolean, EJS_biosUrl: string, @@ -57,9 +56,7 @@ export declare global EJS_browserMode, EJS_shaders, EJS_fixedSaveInterval, - EJS_onSaveUpdate, EJS_disableAutoUnload, EJS_disableBatchBootup; - EJS_onSaveSave; } } \ No newline at end of file diff --git a/src/mainview/gen/routeTree.gen.ts b/src/mainview/gen/routeTree.gen.ts index 89c6bc9..5988dfd 100644 --- a/src/mainview/gen/routeTree.gen.ts +++ b/src/mainview/gen/routeTree.gen.ts @@ -12,31 +12,22 @@ import { Route as rootRouteImport } from './../routes/__root' import { Route as GamesRouteImport } from './../routes/games' import { Route as SettingsRouteRouteImport } from './../routes/settings/route' import { Route as IndexRouteImport } from './../routes/index' -import { Route as SettingsUpdateRouteImport } from './../routes/settings/update' -import { Route as SettingsTasksRouteImport } from './../routes/settings/tasks' import { Route as SettingsPluginsRouteImport } from './../routes/settings/plugins' import { Route as SettingsInterfaceRouteImport } from './../routes/settings/interface' import { Route as SettingsEmulatorsRouteImport } from './../routes/settings/emulators' import { Route as SettingsDirectoriesRouteImport } from './../routes/settings/directories' import { Route as SettingsAccountsRouteImport } from './../routes/settings/accounts' import { Route as SettingsAboutRouteImport } from './../routes/settings/about' -import { Route as GameAddRouteImport } from './../routes/game/add' import { Route as StoreTabRouteRouteImport } from './../routes/store/tab/route' import { Route as StoreTabIndexRouteImport } from './../routes/store/tab/index' -import { Route as StoreTabPluginsRouteImport } from './../routes/store/tab/plugins' import { Route as StoreTabGamesRouteImport } from './../routes/store/tab/games' import { Route as StoreTabEmulatorsRouteImport } from './../routes/store/tab/emulators' -import { Route as StoreTabDownloadRouteImport } from './../routes/store/tab/download' -import { Route as SettingsPluginSourceRouteImport } from './../routes/settings/plugin.$source' import { Route as PlatformSourceIdRouteImport } from './../routes/platform.$source.$id' import { Route as LauncherSourceIdRouteImport } from './../routes/launcher.$source.$id' import { Route as GameSourceIdRouteImport } from './../routes/game/$source.$id' import { Route as EmbeddedSourceIdRouteImport } from './../routes/embedded.$source.$id' import { Route as CollectionSourceIdRouteImport } from './../routes/collection.$source.$id' -import { Route as StoreDetailsPluginIdRouteImport } from './../routes/store/details.plugin.$id' import { Route as StoreDetailsEmulatorIdRouteImport } from './../routes/store/details.emulator.$id' -import { Route as GameUpdateSourceIdRouteImport } from './../routes/game/update.$source.$id' -import { Route as StoreDetailsDownloadSourceIdRouteImport } from './../routes/store/details.download.$source.$id' const GamesRoute = GamesRouteImport.update({ id: '/games', @@ -53,16 +44,6 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) -const SettingsUpdateRoute = SettingsUpdateRouteImport.update({ - id: '/update', - path: '/update', - getParentRoute: () => SettingsRouteRoute, -} as any) -const SettingsTasksRoute = SettingsTasksRouteImport.update({ - id: '/tasks', - path: '/tasks', - getParentRoute: () => SettingsRouteRoute, -} as any) const SettingsPluginsRoute = SettingsPluginsRouteImport.update({ id: '/plugins', path: '/plugins', @@ -93,11 +74,6 @@ const SettingsAboutRoute = SettingsAboutRouteImport.update({ path: '/about', getParentRoute: () => SettingsRouteRoute, } as any) -const GameAddRoute = GameAddRouteImport.update({ - id: '/game/add', - path: '/game/add', - getParentRoute: () => rootRouteImport, -} as any) const StoreTabRouteRoute = StoreTabRouteRouteImport.update({ id: '/store/tab', path: '/store/tab', @@ -108,11 +84,6 @@ const StoreTabIndexRoute = StoreTabIndexRouteImport.update({ path: '/', getParentRoute: () => StoreTabRouteRoute, } as any) -const StoreTabPluginsRoute = StoreTabPluginsRouteImport.update({ - id: '/plugins', - path: '/plugins', - getParentRoute: () => StoreTabRouteRoute, -} as any) const StoreTabGamesRoute = StoreTabGamesRouteImport.update({ id: '/games', path: '/games', @@ -123,16 +94,6 @@ const StoreTabEmulatorsRoute = StoreTabEmulatorsRouteImport.update({ path: '/emulators', getParentRoute: () => StoreTabRouteRoute, } as any) -const StoreTabDownloadRoute = StoreTabDownloadRouteImport.update({ - id: '/download', - path: '/download', - getParentRoute: () => StoreTabRouteRoute, -} as any) -const SettingsPluginSourceRoute = SettingsPluginSourceRouteImport.update({ - id: '/plugin/$source', - path: '/plugin/$source', - getParentRoute: () => SettingsRouteRoute, -} as any) const PlatformSourceIdRoute = PlatformSourceIdRouteImport.update({ id: '/platform/$source/$id', path: '/platform/$source/$id', @@ -158,86 +119,52 @@ const CollectionSourceIdRoute = CollectionSourceIdRouteImport.update({ path: '/collection/$source/$id', getParentRoute: () => rootRouteImport, } as any) -const StoreDetailsPluginIdRoute = StoreDetailsPluginIdRouteImport.update({ - id: '/store/details/plugin/$id', - path: '/store/details/plugin/$id', - getParentRoute: () => rootRouteImport, -} as any) const StoreDetailsEmulatorIdRoute = StoreDetailsEmulatorIdRouteImport.update({ id: '/store/details/emulator/$id', path: '/store/details/emulator/$id', getParentRoute: () => rootRouteImport, } as any) -const GameUpdateSourceIdRoute = GameUpdateSourceIdRouteImport.update({ - id: '/game/update/$source/$id', - path: '/game/update/$source/$id', - getParentRoute: () => rootRouteImport, -} as any) -const StoreDetailsDownloadSourceIdRoute = - StoreDetailsDownloadSourceIdRouteImport.update({ - id: '/store/details/download/$source/$id', - path: '/store/details/download/$source/$id', - getParentRoute: () => rootRouteImport, - } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/settings': typeof SettingsRouteRouteWithChildren '/games': typeof GamesRoute '/store/tab': typeof StoreTabRouteRouteWithChildren - '/game/add': typeof GameAddRoute '/settings/about': typeof SettingsAboutRoute '/settings/accounts': typeof SettingsAccountsRoute '/settings/directories': typeof SettingsDirectoriesRoute '/settings/emulators': typeof SettingsEmulatorsRoute '/settings/interface': typeof SettingsInterfaceRoute '/settings/plugins': typeof SettingsPluginsRoute - '/settings/tasks': typeof SettingsTasksRoute - '/settings/update': typeof SettingsUpdateRoute '/collection/$source/$id': typeof CollectionSourceIdRoute '/embedded/$source/$id': typeof EmbeddedSourceIdRoute '/game/$source/$id': typeof GameSourceIdRoute '/launcher/$source/$id': typeof LauncherSourceIdRoute '/platform/$source/$id': typeof PlatformSourceIdRoute - '/settings/plugin/$source': typeof SettingsPluginSourceRoute - '/store/tab/download': typeof StoreTabDownloadRoute '/store/tab/emulators': typeof StoreTabEmulatorsRoute '/store/tab/games': typeof StoreTabGamesRoute - '/store/tab/plugins': typeof StoreTabPluginsRoute '/store/tab/': typeof StoreTabIndexRoute - '/game/update/$source/$id': typeof GameUpdateSourceIdRoute '/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute - '/store/details/plugin/$id': typeof StoreDetailsPluginIdRoute - '/store/details/download/$source/$id': typeof StoreDetailsDownloadSourceIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/settings': typeof SettingsRouteRouteWithChildren '/games': typeof GamesRoute - '/game/add': typeof GameAddRoute '/settings/about': typeof SettingsAboutRoute '/settings/accounts': typeof SettingsAccountsRoute '/settings/directories': typeof SettingsDirectoriesRoute '/settings/emulators': typeof SettingsEmulatorsRoute '/settings/interface': typeof SettingsInterfaceRoute '/settings/plugins': typeof SettingsPluginsRoute - '/settings/tasks': typeof SettingsTasksRoute - '/settings/update': typeof SettingsUpdateRoute '/collection/$source/$id': typeof CollectionSourceIdRoute '/embedded/$source/$id': typeof EmbeddedSourceIdRoute '/game/$source/$id': typeof GameSourceIdRoute '/launcher/$source/$id': typeof LauncherSourceIdRoute '/platform/$source/$id': typeof PlatformSourceIdRoute - '/settings/plugin/$source': typeof SettingsPluginSourceRoute - '/store/tab/download': typeof StoreTabDownloadRoute '/store/tab/emulators': typeof StoreTabEmulatorsRoute '/store/tab/games': typeof StoreTabGamesRoute - '/store/tab/plugins': typeof StoreTabPluginsRoute '/store/tab': typeof StoreTabIndexRoute - '/game/update/$source/$id': typeof GameUpdateSourceIdRoute '/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute - '/store/details/plugin/$id': typeof StoreDetailsPluginIdRoute - '/store/details/download/$source/$id': typeof StoreDetailsDownloadSourceIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -245,30 +172,21 @@ export interface FileRoutesById { '/settings': typeof SettingsRouteRouteWithChildren '/games': typeof GamesRoute '/store/tab': typeof StoreTabRouteRouteWithChildren - '/game/add': typeof GameAddRoute '/settings/about': typeof SettingsAboutRoute '/settings/accounts': typeof SettingsAccountsRoute '/settings/directories': typeof SettingsDirectoriesRoute '/settings/emulators': typeof SettingsEmulatorsRoute '/settings/interface': typeof SettingsInterfaceRoute '/settings/plugins': typeof SettingsPluginsRoute - '/settings/tasks': typeof SettingsTasksRoute - '/settings/update': typeof SettingsUpdateRoute '/collection/$source/$id': typeof CollectionSourceIdRoute '/embedded/$source/$id': typeof EmbeddedSourceIdRoute '/game/$source/$id': typeof GameSourceIdRoute '/launcher/$source/$id': typeof LauncherSourceIdRoute '/platform/$source/$id': typeof PlatformSourceIdRoute - '/settings/plugin/$source': typeof SettingsPluginSourceRoute - '/store/tab/download': typeof StoreTabDownloadRoute '/store/tab/emulators': typeof StoreTabEmulatorsRoute '/store/tab/games': typeof StoreTabGamesRoute - '/store/tab/plugins': typeof StoreTabPluginsRoute '/store/tab/': typeof StoreTabIndexRoute - '/game/update/$source/$id': typeof GameUpdateSourceIdRoute '/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute - '/store/details/plugin/$id': typeof StoreDetailsPluginIdRoute - '/store/details/download/$source/$id': typeof StoreDetailsDownloadSourceIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -277,89 +195,62 @@ export interface FileRouteTypes { | '/settings' | '/games' | '/store/tab' - | '/game/add' | '/settings/about' | '/settings/accounts' | '/settings/directories' | '/settings/emulators' | '/settings/interface' | '/settings/plugins' - | '/settings/tasks' - | '/settings/update' | '/collection/$source/$id' | '/embedded/$source/$id' | '/game/$source/$id' | '/launcher/$source/$id' | '/platform/$source/$id' - | '/settings/plugin/$source' - | '/store/tab/download' | '/store/tab/emulators' | '/store/tab/games' - | '/store/tab/plugins' | '/store/tab/' - | '/game/update/$source/$id' | '/store/details/emulator/$id' - | '/store/details/plugin/$id' - | '/store/details/download/$source/$id' fileRoutesByTo: FileRoutesByTo to: | '/' | '/settings' | '/games' - | '/game/add' | '/settings/about' | '/settings/accounts' | '/settings/directories' | '/settings/emulators' | '/settings/interface' | '/settings/plugins' - | '/settings/tasks' - | '/settings/update' | '/collection/$source/$id' | '/embedded/$source/$id' | '/game/$source/$id' | '/launcher/$source/$id' | '/platform/$source/$id' - | '/settings/plugin/$source' - | '/store/tab/download' | '/store/tab/emulators' | '/store/tab/games' - | '/store/tab/plugins' | '/store/tab' - | '/game/update/$source/$id' | '/store/details/emulator/$id' - | '/store/details/plugin/$id' - | '/store/details/download/$source/$id' id: | '__root__' | '/' | '/settings' | '/games' | '/store/tab' - | '/game/add' | '/settings/about' | '/settings/accounts' | '/settings/directories' | '/settings/emulators' | '/settings/interface' | '/settings/plugins' - | '/settings/tasks' - | '/settings/update' | '/collection/$source/$id' | '/embedded/$source/$id' | '/game/$source/$id' | '/launcher/$source/$id' | '/platform/$source/$id' - | '/settings/plugin/$source' - | '/store/tab/download' | '/store/tab/emulators' | '/store/tab/games' - | '/store/tab/plugins' | '/store/tab/' - | '/game/update/$source/$id' | '/store/details/emulator/$id' - | '/store/details/plugin/$id' - | '/store/details/download/$source/$id' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -367,16 +258,12 @@ export interface RootRouteChildren { SettingsRouteRoute: typeof SettingsRouteRouteWithChildren GamesRoute: typeof GamesRoute StoreTabRouteRoute: typeof StoreTabRouteRouteWithChildren - GameAddRoute: typeof GameAddRoute CollectionSourceIdRoute: typeof CollectionSourceIdRoute EmbeddedSourceIdRoute: typeof EmbeddedSourceIdRoute GameSourceIdRoute: typeof GameSourceIdRoute LauncherSourceIdRoute: typeof LauncherSourceIdRoute PlatformSourceIdRoute: typeof PlatformSourceIdRoute - GameUpdateSourceIdRoute: typeof GameUpdateSourceIdRoute StoreDetailsEmulatorIdRoute: typeof StoreDetailsEmulatorIdRoute - StoreDetailsPluginIdRoute: typeof StoreDetailsPluginIdRoute - StoreDetailsDownloadSourceIdRoute: typeof StoreDetailsDownloadSourceIdRoute } declare module '@tanstack/react-router' { @@ -402,20 +289,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - '/settings/update': { - id: '/settings/update' - path: '/update' - fullPath: '/settings/update' - preLoaderRoute: typeof SettingsUpdateRouteImport - parentRoute: typeof SettingsRouteRoute - } - '/settings/tasks': { - id: '/settings/tasks' - path: '/tasks' - fullPath: '/settings/tasks' - preLoaderRoute: typeof SettingsTasksRouteImport - parentRoute: typeof SettingsRouteRoute - } '/settings/plugins': { id: '/settings/plugins' path: '/plugins' @@ -458,13 +331,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsAboutRouteImport parentRoute: typeof SettingsRouteRoute } - '/game/add': { - id: '/game/add' - path: '/game/add' - fullPath: '/game/add' - preLoaderRoute: typeof GameAddRouteImport - parentRoute: typeof rootRouteImport - } '/store/tab': { id: '/store/tab' path: '/store/tab' @@ -479,13 +345,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof StoreTabIndexRouteImport parentRoute: typeof StoreTabRouteRoute } - '/store/tab/plugins': { - id: '/store/tab/plugins' - path: '/plugins' - fullPath: '/store/tab/plugins' - preLoaderRoute: typeof StoreTabPluginsRouteImport - parentRoute: typeof StoreTabRouteRoute - } '/store/tab/games': { id: '/store/tab/games' path: '/games' @@ -500,20 +359,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof StoreTabEmulatorsRouteImport parentRoute: typeof StoreTabRouteRoute } - '/store/tab/download': { - id: '/store/tab/download' - path: '/download' - fullPath: '/store/tab/download' - preLoaderRoute: typeof StoreTabDownloadRouteImport - parentRoute: typeof StoreTabRouteRoute - } - '/settings/plugin/$source': { - id: '/settings/plugin/$source' - path: '/plugin/$source' - fullPath: '/settings/plugin/$source' - preLoaderRoute: typeof SettingsPluginSourceRouteImport - parentRoute: typeof SettingsRouteRoute - } '/platform/$source/$id': { id: '/platform/$source/$id' path: '/platform/$source/$id' @@ -549,13 +394,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof CollectionSourceIdRouteImport parentRoute: typeof rootRouteImport } - '/store/details/plugin/$id': { - id: '/store/details/plugin/$id' - path: '/store/details/plugin/$id' - fullPath: '/store/details/plugin/$id' - preLoaderRoute: typeof StoreDetailsPluginIdRouteImport - parentRoute: typeof rootRouteImport - } '/store/details/emulator/$id': { id: '/store/details/emulator/$id' path: '/store/details/emulator/$id' @@ -563,20 +401,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof StoreDetailsEmulatorIdRouteImport parentRoute: typeof rootRouteImport } - '/game/update/$source/$id': { - id: '/game/update/$source/$id' - path: '/game/update/$source/$id' - fullPath: '/game/update/$source/$id' - preLoaderRoute: typeof GameUpdateSourceIdRouteImport - parentRoute: typeof rootRouteImport - } - '/store/details/download/$source/$id': { - id: '/store/details/download/$source/$id' - path: '/store/details/download/$source/$id' - fullPath: '/store/details/download/$source/$id' - preLoaderRoute: typeof StoreDetailsDownloadSourceIdRouteImport - parentRoute: typeof rootRouteImport - } } } @@ -587,9 +411,6 @@ interface SettingsRouteRouteChildren { SettingsEmulatorsRoute: typeof SettingsEmulatorsRoute SettingsInterfaceRoute: typeof SettingsInterfaceRoute SettingsPluginsRoute: typeof SettingsPluginsRoute - SettingsTasksRoute: typeof SettingsTasksRoute - SettingsUpdateRoute: typeof SettingsUpdateRoute - SettingsPluginSourceRoute: typeof SettingsPluginSourceRoute } const SettingsRouteRouteChildren: SettingsRouteRouteChildren = { @@ -599,9 +420,6 @@ const SettingsRouteRouteChildren: SettingsRouteRouteChildren = { SettingsEmulatorsRoute: SettingsEmulatorsRoute, SettingsInterfaceRoute: SettingsInterfaceRoute, SettingsPluginsRoute: SettingsPluginsRoute, - SettingsTasksRoute: SettingsTasksRoute, - SettingsUpdateRoute: SettingsUpdateRoute, - SettingsPluginSourceRoute: SettingsPluginSourceRoute, } const SettingsRouteRouteWithChildren = SettingsRouteRoute._addFileChildren( @@ -609,18 +427,14 @@ const SettingsRouteRouteWithChildren = SettingsRouteRoute._addFileChildren( ) interface StoreTabRouteRouteChildren { - StoreTabDownloadRoute: typeof StoreTabDownloadRoute StoreTabEmulatorsRoute: typeof StoreTabEmulatorsRoute StoreTabGamesRoute: typeof StoreTabGamesRoute - StoreTabPluginsRoute: typeof StoreTabPluginsRoute StoreTabIndexRoute: typeof StoreTabIndexRoute } const StoreTabRouteRouteChildren: StoreTabRouteRouteChildren = { - StoreTabDownloadRoute: StoreTabDownloadRoute, StoreTabEmulatorsRoute: StoreTabEmulatorsRoute, StoreTabGamesRoute: StoreTabGamesRoute, - StoreTabPluginsRoute: StoreTabPluginsRoute, StoreTabIndexRoute: StoreTabIndexRoute, } @@ -633,16 +447,12 @@ const rootRouteChildren: RootRouteChildren = { SettingsRouteRoute: SettingsRouteRouteWithChildren, GamesRoute: GamesRoute, StoreTabRouteRoute: StoreTabRouteRouteWithChildren, - GameAddRoute: GameAddRoute, CollectionSourceIdRoute: CollectionSourceIdRoute, EmbeddedSourceIdRoute: EmbeddedSourceIdRoute, GameSourceIdRoute: GameSourceIdRoute, LauncherSourceIdRoute: LauncherSourceIdRoute, PlatformSourceIdRoute: PlatformSourceIdRoute, - GameUpdateSourceIdRoute: GameUpdateSourceIdRoute, StoreDetailsEmulatorIdRoute: StoreDetailsEmulatorIdRoute, - StoreDetailsPluginIdRoute: StoreDetailsPluginIdRoute, - StoreDetailsDownloadSourceIdRoute: StoreDetailsDownloadSourceIdRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/mainview/gen/static-icon-assets.gen.ts b/src/mainview/gen/static-icon-assets.gen.ts index 1d1a4aa..cb3fe1b 100644 --- a/src/mainview/gen/static-icon-assets.gen.ts +++ b/src/mainview/gen/static-icon-assets.gen.ts @@ -464,7 +464,7 @@ const assets = new Set([ ]); // Store basePath resolved from Vite config -const BASE_PATH = "/"; +const BASE_PATH = "./"; /** diff --git a/src/mainview/index.css b/src/mainview/index.css index 332862e..eb09eb3 100644 --- a/src/mainview/index.css +++ b/src/mainview/index.css @@ -1,15 +1,13 @@ @import "tailwindcss"; @import 'animate.css'; @plugin "daisyui"; -@plugin "@tailwindcss/typography"; @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); @custom-variant light (&:where([data-theme=light], [data-theme=light] *)); @theme { --breakpoint-sm: 0px; - --breakpoint-md: 1024px; - --breakpoint-lg: 1280px; + --breakpoint-md: 1280px; --page-scroll-bg: transparent; --animation-size: 1; diff --git a/src/mainview/index.tsx b/src/mainview/index.tsx index 166cc4f..c76e8e9 100644 --- a/src/mainview/index.tsx +++ b/src/mainview/index.tsx @@ -8,24 +8,16 @@ import RouterProvider, } from "@tanstack/react-router"; import { routeTree } from "./gen/routeTree.gen"; -import { QueryClient } from "@tanstack/react-query"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { RPC_URL } from "../shared/constants"; import "./scripts/gamepads"; import "./scripts/windowEvents"; +import { client as rommClient } from "../clients/romm/client.gen"; import "./scripts/spatialNavigation"; import NotFound from "./components/NotFound"; import Error from "./components/Error"; import serviceWorker from './scripts/serviceWorker?worker&url'; -import App from "./App"; -import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; -import { createStore, get, set, del } from "idb-keyval"; -import -{ - PersistedClient, - Persister, -} from '@tanstack/react-query-persist-client'; -import pkg from '../../package.json'; - -const idbStore = createStore("tanstack-query", "cache"); +import { getCurrentFocusKey, setFocus } from "@noriginmedia/norigin-spatial-navigation"; if ('serviceWorker' in navigator) { @@ -34,31 +26,13 @@ if ('serviceWorker' in navigator) const hashHistory = createHashHistory({}); -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - gcTime: 1000 * 60 * 60 * 24 * 5, // 5 days - } - } +rommClient.setConfig({ + baseUrl: `${RPC_URL(__HOST__)}/api/romm`, + credentials: "include", + mode: "cors", }); -export function createIDBPersister (idbValidKey: IDBValidKey = 'reactQuery'): Persister -{ - return { - persistClient: async (client: PersistedClient) => - { - await set(idbValidKey, client, idbStore); - }, - restoreClient: async () => - { - return await get(idbValidKey, idbStore); - }, - removeClient: async () => - { - await del(idbValidKey, idbStore); - }, - } satisfies Persister; -} +const queryClient = new QueryClient(); export interface RouterContext { @@ -92,6 +66,25 @@ export const Router = createRouter({ } }); +const focusMap = new Map(); +export const focusQueue: string[] = []; + +Router.history.subscribe((op) => +{ + if (op.action.type === 'PUSH') + { + focusMap.set(op.location.state.__TSR_index - 1, getCurrentFocusKey()); + } else if (op.action.type === 'BACK') + { + if (focusMap.has(op.location.state.__TSR_index)) + { + focusQueue.pop(); + focusQueue.push(focusMap.get(op.location.state.__TSR_index)!); + focusMap.delete(op.location.state.__TSR_index); + } + } +}); + // Register things for typesafety declare module "@tanstack/react-router" { interface Register @@ -107,11 +100,9 @@ if (!rootElement.innerHTML) const root = createRoot(rootElement); root.render( - - - - - + + + , ); } diff --git a/src/mainview/preload.tsx b/src/mainview/preload.tsx index add9d87..c95a05b 100644 --- a/src/mainview/preload.tsx +++ b/src/mainview/preload.tsx @@ -1,7 +1,6 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; -import LoadingScreen from "./components/LoadingScreen"; const rootElement = document.getElementById("preload")!; @@ -10,11 +9,13 @@ if (!rootElement.innerHTML) const root = createRoot(rootElement); root.render( -
    - - Loading Gameflow - +
    + +
    +
    +
    + Loading Gameflow
    - + , ); } diff --git a/src/mainview/query-options.ts b/src/mainview/query-options.ts index 879d632..a52c649 100644 --- a/src/mainview/query-options.ts +++ b/src/mainview/query-options.ts @@ -1,7 +1,6 @@ import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { getRomApiRomsIdGetOptions, getRomsApiRomsGetOptions } from "../clients/romm/@tanstack/react-query.gen"; -import { DefaultRommStaleTime } from "../shared/constants"; -import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared'; +import { DefaultRommStaleTime, GameListFilterType } from "../shared/constants"; export function gamesQueryOptions (filter?: GameListFilterType) { diff --git a/src/mainview/routes/__root.tsx b/src/mainview/routes/__root.tsx index cafbab4..345a707 100644 --- a/src/mainview/routes/__root.tsx +++ b/src/mainview/routes/__root.tsx @@ -4,11 +4,11 @@ import Notifications from "../components/Notifications"; import { Toaster } from "react-hot-toast"; import { mobileCheck, useLocalSetting } from "../scripts/utils"; import useActiveControl from "../scripts/gamepads"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { SystemInfoContext } from "../scripts/contexts"; +import { SystemInfoType } from "@/shared/constants"; +import { systemApi } from "../scripts/clientApi"; import AppCommunication from "../components/AppCommunication"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; -import GlobalContextDialog from "../components/GlobalContextDialog"; export const Route = createRootRouteWithContext()({ component: RootComponent, @@ -35,20 +35,19 @@ function RootComponent () }, [theme]); - const queryDevOptions = useLocalSetting('showQueryDevOptions'); - const routerDevOptions = useLocalSetting('showRouterDevOptions'); - return (
    - - - - - + + + - {queryDevOptions && } - {routerDevOptions && } + {/*import.meta.env.DEV && !isMobile && + <> + + + + */}
    ); } diff --git a/src/mainview/routes/collection.$source.$id.tsx b/src/mainview/routes/collection.$source.$id.tsx index a08c164..2f62d91 100644 --- a/src/mainview/routes/collection.$source.$id.tsx +++ b/src/mainview/routes/collection.$source.$id.tsx @@ -6,14 +6,10 @@ import { AnimatedBackgroundContext } from '../scripts/contexts'; import { getCollectionQuery } from '@queries/romm'; import { zodValidator } from '@tanstack/zod-adapter'; import z from 'zod'; -import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared'; -import { useLocalStorage } from 'usehooks-ts'; export const Route = createFileRoute('/collection/$source/$id')({ component: RouteComponent, - validateSearch: zodValidator(z.object({ - countHint: z.number().optional() - })) + validateSearch: zodValidator(z.object({ countHint: z.number().optional() })) }); function RouteComponent () @@ -22,16 +18,8 @@ function RouteComponent () const { countHint } = Route.useSearch(); const { data: collection } = useQuery(getCollectionQuery(source, id)); const animatedBgContext = useContext(AnimatedBackgroundContext); - const [filter, setFilter] = useLocalStorage("collection-filter", {}); return ( - {collection?.name}
    } - filters={{ collection_id: Number(id), collection_source: source }} - /> + {collection?.name}
    } filters={{ collection_id: Number(id), collection_source: source }} /> ); } diff --git a/src/mainview/routes/embedded.$source.$id.tsx b/src/mainview/routes/embedded.$source.$id.tsx index b9a39b6..9d67605 100644 --- a/src/mainview/routes/embedded.$source.$id.tsx +++ b/src/mainview/routes/embedded.$source.$id.tsx @@ -1,29 +1,23 @@ import { RPC_URL, SERVER_URL } from '@/shared/constants'; -import { createFileRoute, useRouter } from '@tanstack/react-router'; +import { createFileRoute } from '@tanstack/react-router'; import { zodValidator } from '@tanstack/zod-adapter'; import z from 'zod'; import { RefObject, useEffect, useRef, useState } from 'react'; +import { Router } from '..'; import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; import { ButtonStyle } from '../components/options/Button'; -import { CloudDownload, DoorOpen, RefreshCw, Undo } from 'lucide-react'; -import { GamePadButtonCode, useShortcuts } from '../scripts/shortcuts'; -import { FloatingShortcuts } from '../components/Shortcuts'; +import { DoorOpen, RefreshCw, Undo } from 'lucide-react'; +import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts'; +import Shortcuts from '../components/Shortcuts'; import { useEventListener } from 'usehooks-ts'; import useActiveControl from '../scripts/gamepads'; import { twMerge } from 'tailwind-merge'; import { HeaderAccounts, HeaderStatusBar } from '../components/Header'; import { RoundButton } from '../components/RoundButton'; import { gameQuery } from '@queries/romm'; -import { rommApi } from '../scripts/clientApi'; -import toast from 'react-hot-toast'; -import { getErrorMessage } from 'react-error-boundary'; export const Route = createFileRoute('/embedded/$source/$id')({ component: RouteComponent, - staticData: { - enterSound: 'launch', - missNavSound: false - }, loader: async (ctx) => { const data = await ctx.context.queryClient.fetchQuery(gameQuery(ctx.params.source, ctx.params.id)); @@ -49,7 +43,7 @@ function OverlayButton (data: { function Overlay (data: { open: boolean; - postMessage: (m: EmulatorJsMessage) => void; + iframeRef: RefObject; close: () => void; goBack: () => void; }) @@ -63,11 +57,12 @@ function Overlay (data: { { if (data.open) { - focusSelf({ instant: true }); + focusSelf(); } }, [data.open]); const { isPointer } = useActiveControl(); + const handleEvent = (type: string, value?: any) => data.iframeRef.current?.contentWindow?.postMessage({ type, data: value }); return
    @@ -81,7 +76,7 @@ function Overlay (data: { { data.close(); - data.postMessage({ type: 'restart' }); + handleEvent('restart'); }} > @@ -104,7 +99,7 @@ function Frame (data: { ref: RefObject; }) const search = Route.useSearch(); search['gameName'] = game.name; - search['backgroundImage'] = `${RPC_URL(__HOST__)}${game.path_covers[0]}`; + search['backgroundImage'] = `${RPC_URL(__HOST__)}${game.path_cover}`; search['backgroundBlur'] = "true"; if (!__PUBLIC__) @@ -127,7 +122,6 @@ function Frame (data: { ref: RefObject; }) function RouteComponent () { - const router = useRouter(); const { ref, focusSelf, focusKey } = useFocusable({ focusKey: 'emulatorjs', preferredChildFocusKey: 'frame', @@ -135,39 +129,18 @@ function RouteComponent () }); const iframeRef = useRef(null); const [overlayOpen, setOverlayOpen] = useState(false); - const postMessage = (m: EmulatorJsMessage) => iframeRef.current?.contentWindow?.postMessage(m); const { source, id } = Route.useParams(); function HandleGoBack () { - if (router.history.canGoBack()) - { - router.history.back(); - } else - { - router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id }, replace: true }); - } + Router.navigate({ to: '/game/$source/$id', params: { source, id }, replace: true }); } useEventListener('message', e => { - const data = e.data as EmulatorJsMessage; - switch (data.type) + if (e.data.type === 'exit') { - case "exit": - rommApi.api.romm.emulatorjs.post_play({ source })({ id }).post({ save: data.save }); - HandleGoBack(); - break; - case "loaded": - toast.success("Save Loaded", { icon: }); - break; - case "save": - rommApi.api.romm.emulatorjs.save.put({ save: data.save }).then(r => - { - if (r.error) toast.error(getErrorMessage(r.error.value) ?? "Error While Saving"); - else toast.success("Save Backed Up"); - }); - break; + HandleGoBack(); } }); @@ -191,15 +164,16 @@ function RouteComponent () const setPaused = (paused: boolean) => { - if (paused) postMessage({ type: 'pause', paused: true }); + if (paused) iframeRef.current?.contentWindow?.postMessage({ type: 'pause', data: true }); else { // we want to prevent input from closing the overlay spilling - setTimeout(() => postMessage({ type: 'pause', paused: false }), 100); + setTimeout(() => iframeRef.current?.contentWindow?.postMessage({ type: 'pause', data: false }), 100); } }; useEffect(() => setPaused(overlayOpen), [overlayOpen]); - useEffect(() => { if (!overlayOpen) focusSelf({ instant: true }); }, [overlayOpen]); + const { shortcuts } = useShortcutContext(); + useEffect(() => { if (!overlayOpen) focusSelf(); }, [overlayOpen]); function handleClose () { setOverlayOpen(false); @@ -209,9 +183,11 @@ function RouteComponent ()
    - + +
    +
    +
    -
    ; } \ No newline at end of file diff --git a/src/mainview/routes/game/$source.$id.tsx b/src/mainview/routes/game/$source.$id.tsx index 13ea8ac..6f97fbc 100644 --- a/src/mainview/routes/game/$source.$id.tsx +++ b/src/mainview/routes/game/$source.$id.tsx @@ -1,15 +1,16 @@ -import { createFileRoute, ErrorComponentProps, useRouter } from "@tanstack/react-router"; +import { createFileRoute, ErrorComponentProps } from "@tanstack/react-router"; import { RPC_URL } from "@shared/constants"; -import { useRef, useState } from "react"; -import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; -import { Calendar, Folder, Gamepad2, Image, Info, TriangleAlert, Trophy } from "lucide-react"; -import { HeaderUI, StickyHeaderUI } from "../../components/Header"; +import { useEffect, useRef, useState } from "react"; +import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; +import { Calendar, Clock, Folder, Gamepad2, Image, Info, Store, TriangleAlert, Trophy } from "lucide-react"; +import { HeaderUI } from "../../components/Header"; import { AnimatedBackground } from "../../components/AnimatedBackground"; import { useQuery } from "@tanstack/react-query"; -import { FloatingShortcuts } from "../../components/Shortcuts"; -import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts"; +import { Router } from "../.."; +import Shortcuts from "../../components/Shortcuts"; +import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts"; import Screenshots from "@/mainview/components/Screenshots"; -import { HandleGoBack, scrollIntoViewHandler, useOnNavigateBack } from "@/mainview/scripts/utils"; +import { HandleGoBack, scrollIntoViewHandler, useStickyDataAttr } from "@/mainview/scripts/utils"; import { FilterUI } from "@/mainview/components/Filters"; import StatList, { StatEntry } from "@/mainview/components/StatList"; import { useIntersectionObserver, useLocalStorage } from "usehooks-ts"; @@ -20,11 +21,8 @@ import Achievements from "@/mainview/components/game/Achievements"; import { GameDetailsContext } from "@/mainview/scripts/contexts"; import { gameQuery, gamesRecommendedBasedOnGameQuery } from "@queries/romm"; import { GamesSection } from "@/mainview/components/store/GamesSection"; -import Details from "@/mainview/components/game/Details"; +import Details, { DetailElement } from "@/mainview/components/game/Details"; import { AutoFocus } from "@/mainview/components/AutoFocus"; -import SelectMenu from "@/mainview/components/SelectMenu"; -import { IGDBIcon } from "@/mainview/scripts/brandIcons"; -import { FrontEndGameTypeDetailed } from "@simeonradivoev/gameflow-sdk/shared"; export const Route = createFileRoute("/game/$source/$id")({ loader: async ({ params, context }) => @@ -33,13 +31,7 @@ export const Route = createFileRoute("/game/$source/$id")({ }, component: RouteComponent, errorComponent: Error, - validateSearch: zodValidator(z.object({ - focus: z.string().optional(), - })), - staticData: { - enterSound: 'openDetails', - goBackSound: "returnDetails" - }, + validateSearch: zodValidator(z.object({ focus: z.string().optional() })) }); function useDetailsSection () @@ -51,8 +43,12 @@ function Error (data: ErrorComponentProps) { const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details-error", preferredChildFocusKey: "main-details" }); - const router = useRouter(); - useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: (e) => HandleGoBack(router, e) }]); + useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]); + const { shortcuts } = useShortcutContext(); + useEffect(() => + { + focusSelf(); + }, []); return
    @@ -64,9 +60,14 @@ function Error (data: ErrorComponentProps)
    {JSON.stringify(data.error, null, 3)}
    +
    + +
    + +
    +
    -
    ; } @@ -100,20 +101,14 @@ function Stats (data: { game: FrontEndGameTypeDetailed | undefined; }) { if (data.game.path_fs) stats.push({ label: "Location", content: data.game.path_fs, icon: }); - if (data.game.metadata.companies) - stats.push({ label: "Companies", content: data.game.metadata.companies }); - if (data.game.metadata.genres) - stats.push({ label: 'Genres', content: data.game.metadata.genres }); - if (data.game.metadata.first_release_date) - stats.push({ label: "Release Date", content: data.game.metadata.first_release_date.toLocaleDateString(), icon: }); + if (data.game.companies) + stats.push({ label: "Companies", content: data.game.companies }); + if (data.game.genres) + stats.push({ label: 'Genres', content: data.game.genres }); + if (data.game.release_date) + stats.push({ label: "Release Date", content: data.game.release_date.toLocaleDateString(), icon: }); if (data.game.emulators) stats.push({ label: "Emulators", content: data.game.emulators.map(e => e.name) }); - if (data.game.igdb_id) - stats.push({ label: "IGDB", icon: IGDBIcon, content: String(data.game.igdb_id) }); - if (data.game.source) - stats.push({ label: "Source", content: `${data.game.source} - ${data.game.source_id}` }); - const integrations = new Set(data.game.emulators?.flatMap(e => e.integrations).flatMap(i => i.capabilities).filter(c => !!c)); - stats.push({ label: "Integrations", content: Array.from(integrations) }); } return ; @@ -144,23 +139,22 @@ function Divider (data: { rootFocusKey: string; showShortcuts: boolean; game: Fr function RouteComponent () { - const router = useRouter(); const [recommendedGamesVisible, setRecommendedGamesVisible] = useState(false); const { source, id } = Route.useParams(); const { data } = useQuery(gameQuery(source, id)); + const { focus } = Route.useSearch(); const [, setUpdate] = useState(0); const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details", forceFocus: true }); const headerRef = useRef(null); - const backgroundImage = data ? new URL(`${RPC_URL(__HOST__)}${data.path_covers[0]}`) : undefined; + const sentinelRef = useRef(null); + const backgroundImage = data ? new URL(`${RPC_URL(__HOST__)}${data.path_cover}`) : undefined; const { data: recommendedGames } = useQuery({ ...gamesRecommendedBasedOnGameQuery(data?.id.source ?? source, data?.id.id ?? id), enabled: !!data && recommendedGamesVisible }); - useShortcuts(focusKey, () => [{ - label: "Back", button: GamePadButtonCode.B, action: (e) => HandleGoBack(router, e) - }], [router]); + useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]); + const { shortcuts } = useShortcutContext(); - useOnNavigateBack((s) => s.sound = 'returnDetails'); - - const recommendedEmulators = data?.emulators?.filter(e => e.validSources.some(em => em.exists) || e.source === 'store'); + useStickyDataAttr(headerRef, sentinelRef, ref); + const recommendedEmulators = data?.emulators?.filter(e => e.validSources.some(em => em.exists)); const { ref: intersct } = useIntersectionObserver({ onChange: (isIntersecting, entry) => @@ -171,12 +165,16 @@ function RouteComponent () return ( + setUpdate(v => v + 1) }} >
    - +
    +
    + +
    @@ -190,10 +188,9 @@ function RouteComponent () Related Emulators } onFocus={scrollIntoViewHandler({ block: 'center' })} - onSelect={(em, focus) => + onSelect={(id, focus) => { - if (em.source === 'local') return; - router.navigate({ to: '/store/details/emulator/$id', params: { id: em.name } }); + Router.navigate({ to: '/store/details/emulator/$id', params: { id } }); }} emulators={recommendedEmulators} />} @@ -209,16 +206,16 @@ function RouteComponent ()
    { - router.navigate({ to: '/game/$source/$id', params: { id: id.id, source: id.source } }); + Router.navigate({ to: '/game/$source/$id', params: { id: id.id, source: id.source } }); }} onFocus={scrollIntoViewHandler({ block: 'center', inline: 'nearest' })} games={recommendedGames} />
    -
    - +
    + +
    - ); } \ No newline at end of file diff --git a/src/mainview/routes/game/add.tsx b/src/mainview/routes/game/add.tsx deleted file mode 100644 index 6399cd0..0000000 --- a/src/mainview/routes/game/add.tsx +++ /dev/null @@ -1,425 +0,0 @@ -import { AutoFocus } from '@/mainview/components/AutoFocus'; -import { OptionElement } from '@/mainview/components/ContextDialog'; -import GameLookupElement from '@/mainview/components/game/GameLookup'; -import { StickyHeaderUI } from '@/mainview/components/Header'; -import LoadingScreen from '@/mainview/components/LoadingScreen'; -import { Button } from '@/mainview/components/options/Button'; -import { PathSettingsOptionBase } from '@/mainview/components/options/PathSettingsOption'; -import SelectMenu from '@/mainview/components/SelectMenu'; -import { FloatingShortcuts } from '@/mainview/components/Shortcuts'; -import { oneShot } from '@/mainview/scripts/audio/audio'; -import { rommApi } from '@/mainview/scripts/clientApi'; -import { addManualGameMutation, allGamesInvalidateQuery, gameLookupDetails, platformLookupMatchQuery } from '@/mainview/scripts/queries/romm'; -import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts'; -import { HandleGoBack } from '@/mainview/scripts/utils'; -import { isUrl } from '@/shared/utils'; -import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { createFileRoute, useNavigate, useRouter } from '@tanstack/react-router'; -import { zodValidator } from '@tanstack/zod-adapter'; -import { ArrowBigRightDash, Check, CirclePlus, CircleQuestionMark, CircleX, File, FileSearch, FolderOpen, Globe, HardDrive, Link, Save } from 'lucide-react'; -import { basename } from 'pathe'; -import prettyBytes from 'pretty-bytes'; -import { JSX, useState } from 'react'; -import toast from 'react-hot-toast'; -import { twMerge } from 'tailwind-merge'; -import z from 'zod'; - - -const StateSchema = z.object({ - step: z.number().default(0), - gameLocation: z.string().optional(), - selectedGame: z.object({ source: z.string(), id: z.string() }).optional(), - platformId: z.number().optional(), - search: z.string().optional() -}); - -export const Route = createFileRoute('/game/add')({ - component: RouteComponent, - validateSearch: zodValidator(StateSchema) -}); - -function FileSelectionField (data: { location: string | undefined, setLocation: (location: string | undefined) => void; }) -{ - const [localLocation, setLocalLocation] = useState(data.location); - const navigate = useNavigate(); - return - - ; -} - -const TAG_REGEX = /\(([^)]+)\)|\[([^\]]+)\]/g; -const EXTENSION_REGEX = /\.(([a-z]+\.)*\w+)$/g; -const LEADING_ARTICLE_PATTERN = /^(a|an|the)\b/g; -const COMMA_ARTICLE_PATTERN = /,\s(a|an|the)\b(?=\s*[^\w\s]|$)/g; -const NON_WORD_SPACE_PATTERN = /[^\w\s]/g; -const MULTIPLE_SPACE_PATTERN = /\s+/g; - -function BuildSearch (filePath: string) -{ - const name = basename(filePath); - const nameWithoutExt = name.replace(EXTENSION_REGEX, "").trim(); - if (!nameWithoutExt) return undefined; - const nameWithoutTags = nameWithoutExt.replaceAll(TAG_REGEX, "").trim(); - if (TAG_REGEX.test(nameWithoutExt)) console.log("match"); - if (!nameWithoutTags) return undefined; - - // Lower and replace underscores with spaces - let finalSearch = nameWithoutTags.toLowerCase().replace("_", " "); - - // Remove articles (combined if possible) - finalSearch = finalSearch.replaceAll(LEADING_ARTICLE_PATTERN, ''); - finalSearch = finalSearch.replaceAll(COMMA_ARTICLE_PATTERN, ''); - - // Remove punctuation and normalize spaces in one step - finalSearch = finalSearch.replaceAll(NON_WORD_SPACE_PATTERN, ''); - finalSearch = finalSearch.replaceAll(MULTIPLE_SPACE_PATTERN, ''); - - return nameWithoutTags; -} - -const typeIconMap: Record = { - new: , - existing: , - unknown: -}; - -function Overview (data: {}) -{ - const navigate = useNavigate(); - const router = useRouter(); - const state = Route.useSearch(); - const linkInfo = useQuery({ - enabled (query) - { - return isUrl(query.queryKey[1]); - }, - queryKey: ['dl-link-info', state.gameLocation], - queryFn: async () => - { - return rommApi.api.romm.download.file.info.get({ query: { file_url: state.gameLocation! } }); - } - }); - const { data: game } = useQuery(gameLookupDetails(state.selectedGame?.source, state.selectedGame?.id)); - const { data: platform } = useQuery(platformLookupMatchQuery(state.selectedGame?.source, state.platformId)); - const addGame = useMutation({ - ...addManualGameMutation, - onError (error, variables, onMutateResult, context) - { - toast.error(error.message); - }, - async onSuccess (data, variables, onMutateResult, context) - { - if (data.id === null || isUrl(state.gameLocation)) return; - await context.client.invalidateQueries(allGamesInvalidateQuery); - navigate({ - to: '/game/$source/$id', params: { - source: data.source, id: String(data.id) - }, replace: true - }); - }, - }); - - if (!game) return
    Select A Game
    ; - - return
    -
    Preview
    -
    -
    {!!game[0].coverUrl && }
    -
    -
    {game[0].name}
    -
    {game[0].summary}
    -
    -
    {platform?.details.name}
    - -
    - {!!platform?.match.coverUrl && } -
    {platform?.match.name}
    -
    {platform?.match.family_name}
    - - {!!platform?.match.type && typeIconMap[platform?.match.type]} -
    {platform?.match.type}
    -
    -
    -
    {isUrl(state.gameLocation) ? : }{state.gameLocation}
    -
    - - {linkInfo.isFetching ? : (linkInfo.data?.data?.size && prettyBytes(linkInfo.data.data.size))} - - {linkInfo.isFetching ? : (linkInfo.data?.data?.content_type && linkInfo.data.data.content_type)} -
    -
    -
    -
    Actions
    -
    - - -
    -
    ; -} - -function PlatformEntry (data: { - id: string, - displayName: string, - platformSource: string, - platformId: number; -}) -{ - const state = Route.useSearch(); - const { data: match, isFetching: matchIsFetching } = useQuery({ ...platformLookupMatchQuery(data.platformSource, data.platformId), staleTime: 1000 * 60 * 60 }); - const navigate = useNavigate(); - const handleAction = () => - { - navigate({ to: '/game/add', search: { ...state, platformId: data.platformId, step: 3 }, replace: true }); - oneShot('openGeneric'); - }; - - return -
    {data.displayName}
    -
    - {matchIsFetching ? : match && <> - - {match.match.coverUrl ? : } -
    {match.match.name} - {!!match.match.type && typeIconMap[match.match.type]} {match.match.type}
    - } -
    - } type={'primary'} />; -} - -function PlatformSelection (data: {}) -{ - const state = Route.useSearch(); - const { data: game, isFetching } = useQuery({ ...gameLookupDetails(state.selectedGame?.source, state.selectedGame?.id), staleTime: 1000 * 60 * 60 }); - if (isFetching) return ; - if (!game) return
    Select A Game
    ; - return
      - {game[0].platforms.map((p, i) => )} -
    ; -} - -function Lookup () -{ - const state = Route.useSearch(); - const [search, setSearch] = useState(state.search); - const navigate = useNavigate(); - const handleSetSelectedGame = (source: string, id: string) => - { - navigate({ to: '/game/add', search: { ...state, selectedGame: { source, id }, platformId: undefined, search, step: 2 }, replace: true }); - oneShot('openGeneric'); - }; - return - { - handleSetSelectedGame(l.source, l.id); - }} />; -} - -const StepDetails = [{ label: "Select Location" }, { label: "Find Match" }, { label: "Select Platform" }, { label: "Confirm" }]; - -function Location () -{ - const state = Route.useSearch(); - const navigate = useNavigate(); - const handleSetLocation = (location: string | undefined) => - { - if (!location) return; - navigate({ - to: '/game/add', search: { - ...state, - gameLocation: location, - search: BuildSearch(location), - selectedGame: undefined, - platformId: undefined, - step: 1 - }, replace: true - }); - oneShot('openGeneric'); - }; - return
    -
    Select Game Rom
    - -
    - Select The Rom File from your local storage or use a link -
    -
    ; -} - -function Details (data: {}) -{ - const { ref, focusKey } = useFocusable({ focusKey: 'add-game-details-section' }); - const state = Route.useSearch(); - const step = state.step ?? 0; - return
    - - {step === 0 && } - {step === 1 && } - {step === 2 && } - {step === 3 && } - - -
    ; -} - -function getStepDetails (index: number, state: z.infer) -{ - let completed = index < state.step; - if (index === 0 && state.gameLocation) completed = true; - if (index === 1 && state.selectedGame) completed = true; - if (index === 2 && state.platformId) completed = true; - if (index === 3 && state.gameLocation && state.selectedGame && state.platformId) completed = true; - let canNavigate = index <= state.step; - if (index === 1 && state.gameLocation) canNavigate = true; - if (index === 2 && state.selectedGame) canNavigate = true; - if (index === 3 && state.platformId) canNavigate = true; - return { completed, canNavigate }; -} - -function Step (data: { index: number; label: string; }) -{ - const navigate = useNavigate(); - const handleGoToStep = (step: number) => - { - navigate({ to: '/game/add', search: { ...state, step: step }, replace: true }); - oneShot('openGeneric'); - }; - const state = Route.useSearch(); - const step = state.step ?? 0; - const { canNavigate, completed } = getStepDetails(data.index, state); - - const { ref } = useFocusable({ - focusKey: `step-${data.index}`, - focusable: canNavigate, - onFocus: () => - { - if (step === data.index) return; - navigate({ to: '/game/add', search: { ...state, step: data.index }, replace: true }); - oneShot('openGeneric'); - } - }); - return
  • - { - if (!canNavigate) return; - handleGoToStep(data.index); - }} className={twMerge("step not-aria-disabled:cursor-pointer", data.index <= step ? "step-primary" : "")}> - {completed ? : } - {data.label} -
  • ; -} - -function Steps () -{ - const state = Route.useSearch(); - const step = state.step ?? 0; - const { ref, focusKey } = useFocusable({ focusKey: "steps", preferredChildFocusKey: `step-${step}`, saveLastFocusedChild: false }); - return
    - -
      - - {StepDetails.map((s, i) => )} - -
    ; -} - -function RouteComponent () -{ - const navigate = useNavigate(); - const state = Route.useSearch(); - const step = state.step ?? 0; - const router = useRouter(); - const queryClient = useQueryClient(); - const isAddingGame = queryClient.isMutating(addManualGameMutation) > 0; - - const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'add-game-page', preferredChildFocusKey: 'steps' }); - - const handleReturnStep = (e: Event) => - { - if (step <= 0) - { - HandleGoBack(router, e); - } else - { - const newStep = step - 1; - navigate({ to: '/game/add', search: { ...state, step: newStep }, replace: true }); - } - }; - - const handleStepNavigation = (newStep: number) => - { - if (step === newStep) return; - const { canNavigate } = getStepDetails(newStep, state); - if (!canNavigate) return; - navigate({ to: '/game/add', search: { ...state, step: newStep }, replace: true }); - oneShot('openGeneric'); - }; - - useShortcuts(focusKey, () => [ - { button: GamePadButtonCode.B, label: step === 0 ? "Cancel" : "Prev Step", action: handleReturnStep }, - { button: GamePadButtonCode.Y, label: "Cancel", action: e => HandleGoBack(router, e) }, - { - button: GamePadButtonCode.L1, label: "Prev Step", action (e) - { - handleStepNavigation(Math.max(step - 1, 0)); - }, - }, - { - button: GamePadButtonCode.R1, label: "Next Step", action (e) - { - handleStepNavigation(Math.min(step + 1, 3)); - }, - } - ], [step]); - - return
    - -
    - - -
    - -
    - - - {isAddingGame && -
    - -
    Adding Game
    -
    -
    } - -
    -
    ; -} diff --git a/src/mainview/routes/game/update.$source.$id.tsx b/src/mainview/routes/game/update.$source.$id.tsx deleted file mode 100644 index 0a6ef83..0000000 --- a/src/mainview/routes/game/update.$source.$id.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { AnimatedBackground } from '@/mainview/components/AnimatedBackground'; -import { AutoFocus } from '@/mainview/components/AutoFocus'; -import GameLookupElement from '@/mainview/components/game/GameLookup'; -import { HeaderUI } from '@/mainview/components/Header'; -import { FloatingShortcuts } from '@/mainview/components/Shortcuts'; -import { customUpdateMutation, gameInvalidationQuery, gameQuery } from '@/mainview/scripts/queries/romm'; -import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts'; -import { HandleGoBack } from '@/mainview/scripts/utils'; -import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { createFileRoute, useRouter } from '@tanstack/react-router'; -import { useEffect, useState } from 'react'; -import toast from 'react-hot-toast'; - -export const Route = createFileRoute('/game/update/$source/$id')({ - component: RouteComponent, -}); - -function RouteComponent () -{ - const { source, id } = Route.useParams(); - const [search, setSearch] = useState(undefined); - - const router = useRouter(); - const { data: game } = useQuery(gameQuery(source, id)); - const update = useMutation({ - ...customUpdateMutation, - async onSuccess (data, variables, onMutateResult, context) - { - toast.success("Updated Metadata"); - await context.client.invalidateQueries(gameInvalidationQuery(source, id)); - router.history.back(); - }, - }); - - const { ref, focusKey, focusSelf } = useFocusable({ focusKey: `custom-update-page`, preferredChildFocusKey: 'search-field-section' }); - - useShortcuts(focusKey, () => [{ button: GamePadButtonCode.B, label: "Return", action (e) { HandleGoBack(router, e); }, }]); - useEffect(() => - { - if (search) return; - setSearch(game?.name ?? undefined); - }, [game]); - - return - - -
    - - - update.mutate({ source, id, destination: l.source, destinationId: l.id })} - /> - - -
    -
    -
    ; -} diff --git a/src/mainview/routes/games.tsx b/src/mainview/routes/games.tsx index bd0fc8e..d1071fa 100644 --- a/src/mainview/routes/games.tsx +++ b/src/mainview/routes/games.tsx @@ -1,45 +1,16 @@ -import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import { createFileRoute } from '@tanstack/react-router'; import { CollectionsDetail } from '../components/CollectionsDetail'; import { zodValidator } from '@tanstack/zod-adapter'; import z from 'zod'; -import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared'; -import { useSessionStorage } from 'usehooks-ts'; -import HeaderSearchField from '../components/HeaderSearchField'; -import { useEffect } from 'react'; -import { RoundButton } from '../components/RoundButton'; -import { Plus } from 'lucide-react'; export const Route = createFileRoute('/games')({ component: RouteComponent, - validateSearch: zodValidator(z.object({ - focus: z.string().optional(), - search: z.string().optional() - })) + validateSearch: zodValidator(z.object({ focus: z.string().optional() })) }); function RouteComponent () { const { focus } = Route.useSearch(); - const { search } = Route.useSearch(); - const [filter, setFilter] = useSessionStorage('all-games-filters', {}); - const navigate = useNavigate(); - useEffect(() => - { - setFilter(v => ({ ...v, search })); - }, [search]); - - return - { - navigate({ to: '/game/add' }); - }} >, - setFilter({ ...filter, search: v })} search={filter.search} id='search-filter' />] - } - localFilter={filter} - setLocalFilter={setFilter} - focus={focus} - id='all-games' - />; + return ; } \ No newline at end of file diff --git a/src/mainview/routes/index.tsx b/src/mainview/routes/index.tsx index 8ae562a..945b6d7 100644 --- a/src/mainview/routes/index.tsx +++ b/src/mainview/routes/index.tsx @@ -3,19 +3,17 @@ import { Gamepad2, Settings, + MessageSquare, + Image, Search, Power, OctagonAlert, Maximize, Store, - LayoutGrid, - LucideIcon, } from "lucide-react"; import { createFileRoute, - useNavigate, - useRouter, } from "@tanstack/react-router"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import @@ -35,27 +33,18 @@ import { AutoFocus } from "../components/AutoFocus"; import SaveScroll from "../components/SaveScroll"; import { ErrorBoundary, useErrorBoundary } from "react-error-boundary"; import { twMerge } from "tailwind-merge"; +import Shortcuts from "../components/Shortcuts"; import { PlatformsList } from "../components/PlatformsList"; -import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts"; +import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts"; import z from "zod"; +import { Router } from ".."; import CollectionList from "../components/CollectionList"; import { zodValidator } from '@tanstack/zod-adapter'; -import { mobileCheck, scrollIntoViewHandler, useDragScroll } from "../scripts/utils"; -import { AnimatedBackgroundContext, GlobalDialogContext } from "../scripts/contexts"; +import { mobileCheck, useDragScroll } from "../scripts/utils"; +import { AnimatedBackgroundContext } from "../scripts/contexts"; import Carousel from "../components/Carousel"; import { closeMutation } from "@queries/system"; import { gameQuery } from "../scripts/queries/romm"; -import { oneShot } from "../scripts/audio/audio"; -import { FloatingShortcuts } from "../components/Shortcuts"; -import SelectMenu from "../components/SelectMenu"; -import HeaderSearchField from "../components/HeaderSearchField"; -import CardElement from "../components/CardElement"; -import { Router } from ".."; -import { FrontEndId } from "@simeonradivoev/gameflow-sdk/shared"; -import { playGame, usePlayMutation } from "../components/game/MainActions"; -import { rommApi } from "../scripts/clientApi"; -import { ContextList, DialogEntry } from "../components/ContextDialog"; -import { FOCUS_KEYS } from "../scripts/types"; export const Route = createFileRoute("/")({ component: ConsoleHomeUI, @@ -99,56 +88,20 @@ function HomeListError (data: { focused: boolean; })
    ; } -function Preview (data: { index: number; children?: any; }) +function ShowAllGamesCard () { - const isMobile = mobileCheck(); - return
    - {data.children} -
    ; -} - -function AdditionalCard (data: { - id: string, - route: keyof typeof Router.routesByPath, - title: string, - subTitle: string, - index: number, - actionLabel: string; - icon: LucideIcon | string; - badgeIcon?: LucideIcon; -}) -{ - const router = useRouter(); - const handleNavigate = () => { - router.navigate({ to: data.route as any }); + Router.navigate({ to: '/games', viewTransition: { types: ['zoom-in'] } }); }; - useShortcuts(data.id, () => [{ label: data.actionLabel, button: GamePadButtonCode.A, action: handleNavigate }]); - return ] : undefined} onAction={handleNavigate} title={data.title} subtitle={data.subTitle} preview={ - {typeof data.icon === 'string' ? - : - - } - } focusKey={data.id} index={0} id={data.id} />; + const { ref } = useFocusable({ focusKey: 'all-games-btn', onEnterPress: handleNavigate }); + return
    All Games
    ; } function HomeList (data: { selectedFilter: string; }) { - const router = useRouter(); const queryClient = useQueryClient(); const [initFocus, setInitFocus] = useState(false); const bg = useContext(AnimatedBackgroundContext); @@ -157,9 +110,6 @@ function HomeList (data: { focusKey: "home-list", preferredChildFocusKey: `${data.selectedFilter}-list` }); - const navigate = useNavigate(); - const playGameMut = usePlayMutation(navigate); - const globalDialog = useContext(GlobalDialogContext); const handleNodeFocus = (id: string, node: HTMLElement, details: FocusDetails) => { @@ -174,55 +124,9 @@ function HomeList (data: { function handleGameSelect (id: FrontEndId, source: string | null, sourceId: string | null) { - router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } }); + Router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } }); }; - async function handleGamePlay (id: FrontEndId, source: string | null, sourceId: string | null) - { - const finalSource = source ?? id.source; - const finalId = String(sourceId ?? id.id); - - const validCommands = await rommApi.api.romm.game({ source: finalSource })({ id: finalId }).commands.get(); - if (validCommands.data) - { - const preferredCommand = localStorage.getItem(`${finalSource}-${finalId}-preferred-command`); - if (preferredCommand) - { - playGame(finalSource, finalId, validCommands.data.commands[JSON.parse(preferredCommand)], navigate, playGameMut.mutate); - } else - { - if (validCommands.data.commands.length > 1) - { - globalDialog.openContext({ - content: - { - const option: DialogEntry = { - id: String(c.id), - content: c.label ?? String(c.id), - type: "primary", - action (ctx) - { - localStorage.setItem(`${finalSource}-${finalId}-preferred-command`, JSON.stringify(i)); - ctx.close(); - playGame(finalSource, finalId, validCommands.data.commands[0], navigate, playGameMut.mutate); - }, - }; - - return option; - }) - } /> - }, FOCUS_KEYS.GAME_LIST_CARD('games-list', id)); - } else if (validCommands.data.commands.length === 1) - { - playGame(finalSource, finalId, validCommands.data.commands[0], navigate, playGameMut.mutate); - } - - } - } - - } - let activeList: JSX.Element; switch (data.selectedFilter) { @@ -244,11 +148,10 @@ function HomeList (data: { activeList = <> { - const [source, id] = d.id?.split('@', 2); + const [source, id] = l.split('@'); queryClient.prefetchQuery(gameQuery(source, id)); handleNodeFocus(l, n, d); }} @@ -257,13 +160,7 @@ function HomeList (data: { id="games-list" setBackground={bg.setBackground} filters={{ limit: 12, orderBy: 'activity' }} - finalElement={[ - , - - ]} - emptyElement={[ - - ]} + finalElement={} /> ; @@ -316,11 +213,9 @@ function HomeList (data: { function MainMenu () { - const router = useRouter(); const { ref, focusKey } = useFocusable({ focusKey: `main-menu`, trackChildren: true, - focusBoundaryDirections: ['up', 'down'] }); return (
      router.navigate({ to: "/games", state: { eventType: e?.event?.type } })} + action={() => Router.navigate({ to: "/games" })} icon={} label="Home" type="secondary" /> - } onAction={(e) => router.navigate({ to: "/store/tab", state: { eventType: e?.event?.type } })} label="Shop" /> + } label="News" /> + } action={() => Router.navigate({ to: "/store/tab" })} label="Shop" /> + } label="Album" /> + icon={} + label="Controllers" + /> + { - router.navigate({ to: '/settings/interface', state: { eventType: e?.event?.type } }); + Router.navigate({ to: '/settings/accounts' }); }} icon={} label="Settings" @@ -352,21 +253,17 @@ function MainMenu () } function CircleIcon (data: { + action?: () => void; type?: "secondary" | "accent" | "info"; label?: string; icon?: JSX.Element; -} & InteractParams) +}) { - const handleAction = (event?: Event) => - { - data.onAction?.({ event, focusKey }); - oneShot('click'); - }; const { ref, focusKey } = useFocusable({ - focusKey: `menu-navigation-icon-${data.label}`, - onEnterPress: handleAction, + focusKey: `navigation-icon-${data.label}`, + onEnterPress: data.action, }); - useShortcuts(focusKey, () => [{ label: data.label, action: handleAction, button: GamePadButtonCode.A }]); + useShortcuts(focusKey, () => [{ label: data.label, action: (e) => data.action?.(), button: GamePadButtonCode.A }]); const typeClasses = { secondary: "bg-secondary text-secondary-content", accent: "bg-accent text-accent-content", @@ -376,8 +273,7 @@ function CircleIcon (data: { return (
    • handleAction(e.nativeEvent)} + onClick={data.action} className={twMerge( `portrait:sm:size-12 sm:w-14 sm:h-10 menu-icon text-base-300 md:w-20 md:h-20 rounded-full flex items-center justify-center drop-shadow-lg cursor-pointer transition-all focusable focusable-primary focused:drop-shadow-2xl focused:animate-scale focusable-hover bg-base-content border-6 md:border-12 border-base-content focused:border-0 hover:border-0 z-1 active:border-0 active:bg-base-300 active:text-base-content active:transition-none`, typeClasses[data.type ?? 'none'])} > @@ -391,7 +287,7 @@ export default function ConsoleHomeUI () const { filter } = Route.useSearch(); const close = useMutation(closeMutation); - const router = useRouter(); + const { ref, focusKey } = useFocusable({ forceFocus: true, autoRestoreFocus: false, @@ -400,19 +296,17 @@ export default function ConsoleHomeUI () preferredChildFocusKey: `home-list`, }); - const setFilter = (filter: string) => router.navigate({ to: '/', search: { filter }, viewTransition: false, replace: true }); + const setFilter = (filter: string) => Router.navigate({ to: '/', search: { filter }, viewTransition: false, replace: true }); + const { shortcuts } = useShortcutContext(); const headerButtons: HeaderButton[] = []; if (mobileCheck()) headerButtons.push({ id: "fullscreen", icon: , action: handleFullscreen }); headerButtons.push( - { id: "power-button", icon: , external: true, action: () => close.mutate(), className: "focusable-error!" }, - { id: "settings-header-button", icon: , external: true, action: () => router.navigate({ to: "/settings/accounts" }) } + { id: "search-header-button", icon: }, + { id: "power-button", icon: , external: true, action: () => close.mutate() }, + { id: "settings-header-button", icon: , external: true, action: () => Router.navigate({ to: "/settings/accounts" }) } ); - const handleSearch = (search: string | undefined) => - { - router.navigate({ to: '/games', search: { search } }); - }; return ( @@ -430,7 +324,7 @@ export default function ConsoleHomeUI () />
    - } /> +
    - + - + ); diff --git a/src/mainview/routes/launcher.$source.$id.tsx b/src/mainview/routes/launcher.$source.$id.tsx index 78df354..89c4a65 100644 --- a/src/mainview/routes/launcher.$source.$id.tsx +++ b/src/mainview/routes/launcher.$source.$id.tsx @@ -1,73 +1,59 @@ import { AnimatedBackground } from '@/mainview/components/AnimatedBackground'; -import { createFileRoute, useBlocker, useRouter } from '@tanstack/react-router'; +import { createFileRoute } from '@tanstack/react-router'; import DotsLoading from '../components/backgrounds/dots'; -import { GamePadButtonCode, useShortcuts } from '../scripts/shortcuts'; +import { Router } from '..'; +import { useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts'; import { useFocusable } from '@noriginmedia/norigin-spatial-navigation'; -import { FloatingShortcuts } from '../components/Shortcuts'; -import { useJobStatus } from '../scripts/utils'; -import { useRef } from 'react'; +import Shortcuts from '../components/Shortcuts'; +import { gameQuery } from '@queries/romm'; +import { rommApi } from '../scripts/clientApi'; export const Route = createFileRoute('/launcher/$source/$id')({ component: RouteComponent, - staticData: { - enterSound: 'launch', - missNavSound: false - }, }); -const stateLookup: Record = { - saves: "Syncing Saves" -}; - function RouteComponent () { - const router = useRouter(); function HandleGoBack () { - if (router.history.canGoBack()) - { - router.history.back(); - } else - { - router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id }, replace: true }); - } + Router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id }, replace: true }); } - const progressRef = useRef(null); const { source, id } = Route.useParams(); const { ref, focusKey } = useFocusable({ focusKey: `launching-${source}-${id}` }); + const { data } = useQuery(gameQuery(source, id)); useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]); + const { shortcuts } = useShortcutContext(); - const { state, data } = useJobStatus('launch-game', { - onProgress (process, data) - { - if (progressRef.current) - progressRef.current.value = process; - }, - onEnded (data) - { - HandleGoBack(); - }, - onWaiting () - { - HandleGoBack(); - }, - }, [progressRef.current, HandleGoBack]); + useEffect(() => + { + if (!data) return; + const sub = rommApi.api.romm.status({ source: data.id.source })({ id: data.id.id }).subscribe(); + sub.subscribe((e) => + { + if (e.data.status !== 'playing') + { + HandleGoBack(); + } + }); - useBlocker({ shouldBlockFn: () => !!data }); + return () => + { + sub.close(); + }; + }, [data?.id]); return
    - {!!state && !!stateLookup[state] ? - <> -

    Launching {data?.name} ...

    - - : -

    Launching {data?.name} ...

    } +

    Launching {data?.name} ...

    +
    +
    +
    -
    ; } diff --git a/src/mainview/routes/platform.$source.$id.tsx b/src/mainview/routes/platform.$source.$id.tsx index bc35faf..4f5347d 100644 --- a/src/mainview/routes/platform.$source.$id.tsx +++ b/src/mainview/routes/platform.$source.$id.tsx @@ -1,23 +1,14 @@ -import { createFileRoute, useRouter } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; import { CollectionsDetail } from "../components/CollectionsDetail"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { RPC_URL } from "../../shared/constants"; -import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared'; -import { deletePlatformMutation, localPlatformFilter, platformQuery, updatePlatformMutation } from "@queries/romm"; +import { platformQuery } from "@queries/romm"; import { zodValidator } from "@tanstack/zod-adapter"; import z from "zod"; -import { useLocalStorage } from "usehooks-ts"; -import { RefreshCcw, Settings2 } from "lucide-react"; -import { ContextList, DialogEntry } from "../components/ContextDialog"; -import toast from "react-hot-toast"; -import { useContext } from "react"; -import { GlobalDialogContext } from "../scripts/contexts"; export const Route = createFileRoute("/platform/$source/$id")({ component: RouteComponent, - validateSearch: zodValidator(z.object({ - countHint: z.number().optional() - })) + validateSearch: zodValidator(z.object({ countHint: z.number().optional() })) }); function PlatformTitle (data: {}) @@ -25,7 +16,7 @@ function PlatformTitle (data: {}) const { source, id } = Route.useParams(); const { data: platform } = useQuery(platformQuery(source, id)); - return
    + return
    {!!platform && } @@ -37,74 +28,12 @@ function PlatformTitle (data: {}) function RouteComponent () { const { source, id } = Route.useParams(); - const router = useRouter(); const { countHint } = Route.useSearch(); - const { data: platform } = useQuery(platformQuery(source, id)); - const [filter, setFilter] = useLocalStorage("platforms-filters", {}); - const updatePlatform = useMutation({ - ...updatePlatformMutation(source, id), onSuccess (data, variables, onMutateResult, context) - { - context.client.invalidateQueries(localPlatformFilter(id)); - }, - }); - const globalDialog = useContext(GlobalDialogContext); - const deletePlatform = useMutation({ - ...deletePlatformMutation(id), - onError (error, variables, onMutateResult, context) - { - toast.error(error.message); - }, - onSuccess (data, variables, onMutateResult, context) - { - context.client.invalidateQueries(localPlatformFilter(id)); - router.history.back(); - }, - }); - const settingsOptions: DialogEntry[] = []; - if (source === 'local' || platform?.hasLocal) - { - settingsOptions.push({ - id: 'update-platform', - type: "primary", - content: "Update Platform", - icon: updatePlatform.isPending ? : , - async action (ctx) - { - await updatePlatform.mutateAsync(); - ctx.close(); - router.navigate({ replace: true }); - }, - }); - } - - if (source === 'local') - { - settingsOptions.push({ - id: 'delete-platform', - type: "error", - content: "Delete", - icon: deletePlatform.isPending ? : , - action (ctx) - { - deletePlatform.mutateAsync(); - }, - }); - } return (
    , - action () - { - globalDialog.openContext({ content: }, 'open-platform-settings-btn'); - }, - }]} - countHint={countHint} + countHit={countHint} title={} filters={{ platform_id: Number(id), platform_source: source }} /> diff --git a/src/mainview/routes/settings/about.tsx b/src/mainview/routes/settings/about.tsx index da450d4..d30b291 100644 --- a/src/mainview/routes/settings/about.tsx +++ b/src/mainview/routes/settings/about.tsx @@ -1,6 +1,5 @@ -import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; import { systemInfoQuery } from '@queries/system'; import { useQuery } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; @@ -13,68 +12,58 @@ export const Route = createFileRoute('/settings/about')({ function RouteComponent () { const { data: systemInfo } = useQuery(systemInfoQuery); - const { ref, focusKey } = useFocusable({ focusKey: 'about-section' }); - - - return - - - - - - - - - - - - {/* row 2 */} - - - - - - - - - - - - - {/* row 3 */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + return
    Version{systemInfo?.data?.version}
    Agent{navigator.userAgent}
    Platform{navigator.platform}
    Resolution{screen.width}x{screen.height}
    Window{window.innerWidth}x{window.innerHeight}
    User{systemInfo?.data?.user}
    Architecture{systemInfo?.data?.arch}
    System{systemInfo?.data?.platform}
    Hostname{systemInfo?.data?.hostname}
    Machine{systemInfo?.data?.machine}
    SizesCache: {prettyBytes(systemInfo?.data?.cacheSize ?? 0)}, Store: {prettyBytes(systemInfo?.data?.storeSize ?? 0)}
    Source{systemInfo?.data?.source}
    Steam Deck{systemInfo?.data?.steamDeck ?? 'false'}
    + + + + + + {/* row 2 */} + + + + + + + + + + + + + {/* row 3 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Agent{navigator.userAgent}
    Platform{navigator.platform}
    Resolution{screen.width}x{screen.height}
    Window{window.innerWidth}x{window.innerHeight}
    User{systemInfo?.data?.user}
    Architecture{systemInfo?.data?.arch}
    System{systemInfo?.data?.platform}
    Hostname{systemInfo?.data?.hostname}
    Machine{systemInfo?.data?.machine}
    SizesCache: {prettyBytes(systemInfo?.data?.cacheSize ?? 0)}, Store: {prettyBytes(systemInfo?.data?.storeSize ?? 0)}
    Source{systemInfo?.data?.source}
    Steam Deck{systemInfo?.data?.steamDeck ?? 'false'}
    ; } diff --git a/src/mainview/routes/settings/accounts.tsx b/src/mainview/routes/settings/accounts.tsx index eacd432..7c0faf5 100644 --- a/src/mainview/routes/settings/accounts.tsx +++ b/src/mainview/routes/settings/accounts.tsx @@ -5,16 +5,15 @@ import useFocusable, } from "@noriginmedia/norigin-spatial-navigation"; import { useIsMutating, useMutation, useQuery } from "@tanstack/react-query"; -import { createFileRoute, useRouter } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router"; import classNames from "classnames"; -import { Info, Key, Link, Lock, LogIn, LogOut, ScanQrCode, User, X } from "lucide-react"; +import { Key, Link, Lock, LogIn, LogOut, Save, ScanQrCode, Trash, User, X } from "lucide-react"; import { useEffect, useRef, } from "react"; -import { RPC_URL } from "@shared/constants"; -import { RommLoginDataSchema } from '@simeonradivoev/gameflow-sdk/shared'; +import { RommLoginDataSchema, RPC_URL } from "@shared/constants"; import toast from "react-hot-toast"; import { OptionSpace } from "../../components/options/OptionSpace"; import { useSettingsForm, useSettingsFormContext } from "../../components/options/SettingsAppForm"; @@ -25,15 +24,11 @@ import { useJobStatus } from "@/mainview/scripts/utils"; import { useInterval } from "usehooks-ts"; import { TwitchIcon } from "@/mainview/scripts/brandIcons"; import { twitchLoginMutation, twitchLoginVerificationQuery, twitchLogoutMutation } from "@queries/settings"; -import { rommGetOptionsQuery, rommLoggedInQuery, rommHostnameQuery, rommLoginMutation, rommLogoutMutation, rommQrLoginMutation, rommUsernameQuery, rommUserQuery, invalidateLogin } from "@queries/romm"; +import { rommGetOptionsQuery, rommLoggedInQuery, rommHostnameQuery, rommLoginMutation, rommLogoutMutation, rommQrLoginMutation, rommUsernameQuery, rommUserQuery } from "@queries/romm"; import { systemApi } from "@/mainview/scripts/clientApi"; -import z from "zod"; export const Route = createFileRoute("/settings/accounts")({ component: RouteComponent, - validateSearch: z.object({ - focus: z.string().optional() - }), }); function LoginQR (data: { id: string, isOpen: boolean, cancel: () => void, url: string; endsAt: Date; startedAt: Date; code?: string; }) @@ -64,7 +59,10 @@ function TwitchLogin () { const loginStatus = useQuery(twitchLoginVerificationQuery); - const loginMutation = useMutation(twitchLoginMutation); + const loginMutation = useMutation({ + ...twitchLoginMutation, + onSuccess: () => loginStatus.refetch() + }); const logoutMutation = useMutation({ ...twitchLogoutMutation, onSuccess: () => loginStatus.refetch() }); @@ -92,7 +90,6 @@ function TwitchLogin () function LoginControls (data: {}) { const user = useQuery(rommUserQuery); - const router = useRouter(); const loginMutation = useMutation(rommQrLoginMutation); const { data: statusValue, wsRef } = useJobStatus('login-job'); const { data: loginStatusData } = useQuery(rommLoggedInQuery); @@ -102,9 +99,8 @@ function LoginControls (data: {}) ...rommLogoutMutation, onSuccess: async (d, v, r, c) => { - await user.refetch(); - await invalidateLogin(c.client); - await router.navigate({ replace: true }); + user.refetch(); + c.client.invalidateQueries({ queryKey: ["romm", "auth"] }); } }); return
    @@ -175,7 +171,7 @@ function RouteComponent () { if (focus) { - focusSelf({ instant: true }); + focusSelf(); } }, [focus]); @@ -226,8 +222,6 @@ function RouteComponent () } type="text" />} /> } type="password" placeholder="Password" />} /> - -
    For Romm Client API Token open plugin settings
    } /> diff --git a/src/mainview/routes/settings/directories.tsx b/src/mainview/routes/settings/directories.tsx index 5adee26..5a32eec 100644 --- a/src/mainview/routes/settings/directories.tsx +++ b/src/mainview/routes/settings/directories.tsx @@ -13,13 +13,9 @@ import { systemApi } from '@/mainview/scripts/clientApi'; import useActiveControl from '@/mainview/scripts/gamepads'; import { changeDownloadsMutation } from '@queries/settings'; import { downloadDrivesQuery } from '@/mainview/scripts/queries/system'; -import { DownloadsDrive } from '@simeonradivoev/gameflow-sdk/shared'; -import { zodValidator } from '@tanstack/zod-adapter'; -import z from 'zod'; export const Route = createFileRoute('/settings/directories')({ component: RouteComponent, - validateSearch: zodValidator(z.object({ focus: z.string().optional() })) }); function DriveComponent (data: { drive: DownloadsDrive; downloadsSize: number; refetchDrives: () => void; }) diff --git a/src/mainview/routes/settings/emulators.tsx b/src/mainview/routes/settings/emulators.tsx index 17344df..4d7c177 100644 --- a/src/mainview/routes/settings/emulators.tsx +++ b/src/mainview/routes/settings/emulators.tsx @@ -1,15 +1,14 @@ -import { createFileRoute, useRouter } from '@tanstack/react-router'; +import { createFileRoute } from '@tanstack/react-router'; import { OptionSpace } from '../../components/options/OptionSpace'; import { OptionInput } from '../../components/options/OptionInput'; import { useMutation, useQuery } from '@tanstack/react-query'; -import { useCallback, useEffect, useState } from 'react'; +import { JSX, useCallback, useEffect, useState } from 'react'; import { Button } from '../../components/options/Button'; -import { Check, ChevronDown, FolderSearch, HardDrive, Plug, SearchAlert, Store, Trash } from 'lucide-react'; +import { Check, ChevronDown, FileQuestion, FolderSearch, Plug, SearchAlert, Store, Trash, TriangleAlert } from 'lucide-react'; import { ContextDialog, ContextList, DialogEntry, OptionElement } from '../../components/ContextDialog'; import classNames from 'classnames'; import { twMerge } from 'tailwind-merge'; import { RPC_URL } from '../../../shared/constants'; -import { SettingsSchema } from '@simeonradivoev/gameflow-sdk/shared'; import emulators from '@emulators'; import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts'; @@ -20,16 +19,11 @@ import Carousel from '@/mainview/components/Carousel'; import { FOCUS_KEYS } from '@/mainview/scripts/types'; import { scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from '@/mainview/scripts/utils'; import { SettingsOption } from '@/mainview/components/options/SettingsOption'; -import { SettingsDropdown } from '@/mainview/components/options/SettingsDropdown'; -import { FrontEndEmulator } from '@simeonradivoev/gameflow-sdk/shared'; -import { zodValidator } from '@tanstack/zod-adapter'; -import z from 'zod'; -import { isUrl } from '@/shared/utils'; +import { Router } from '@/mainview'; export const Route = createFileRoute('/settings/emulators')({ component: RouteComponent, pendingComponent: EmulatorsPending, - validateSearch: zodValidator(z.object({ focus: z.string().optional() })) }); function EmulatorsPending () @@ -82,14 +76,11 @@ function NewEmulatorPath (data: { addOverride: (emulator: string) => void; isAdd const handleCloseContext = () => { setNewEmulatorTypeOpen(false); - setFocus('emulator', { instant: true }); + setFocus('emulator'); }; - return -
    Custom Emulator Path
    -
    Manually Pick a path to an emulator if not automatically found.
    -
    }> + return - ; -} - -function PluginOption (data: { name: string, title?: string, prop: JSONSchema7; }) -{ - const { source: sourceRaw } = Route.useParams(); - const source = decodeURIComponent(sourceRaw); - const { data: value, refetch: refetchValue } = useQuery(getPluginSettingQuery(source, data.name)); - const setValue = useMutation({ - ...setPluginSettingMutation(source, data.name), - onError (error, variables, onMutateResult, context) - { - toast.error(error.message); - }, - onSuccess (data, variables, onMutateResult, context) - { - refetchValue(); - }, - }); - let input: any = undefined; - switch (data.prop.type) - { - case "string": - if (Array.isArray(data.prop.examples)) - { - input = !!e).map(e => e!.toString())} onChange={v => setValue.mutate(v)} value={value?.value as any} />; - } else - { - input = setValue.mutate(v)} type="text" name={data.name} />; - } - break; - - case "boolean": - input = setValue.mutate(v)} type='checkbox' name={data.name} />; - break; - } - return -
    {data.title ?? data.name}
    -
    {data.prop.description}
    -
    }> - {input} - ; -} - -function Settings (data: { update: PluginUpdateCheck | undefined; }) -{ - const { definitions, actions } = Route.useLoaderData(); - const { source: sourceRaw } = Route.useParams(); - const source = decodeURIComponent(sourceRaw); - const queryClient = useQueryClient(); - const navigate = useNavigate(); - const update = useMutation({ - ...updatePluginMutation(source), - onSuccess (data, variables, onMutateResult, context) - { - context.client.invalidateQueries(allPluginsFilter); - navigate({ to: '/settings/plugin/$source', params: { source: encodeURIComponent(source) }, replace: true }); - }, - }); - const handleReload = () => - { - queryClient.refetchQueries(getPluginSettingsDefinitionQuery(source)); - queryClient.refetchQueries(getPluginActionsQuery(source)); - }; - const { ref, focusKey } = useFocusable({ - focusKey: 'plugin-settings', - focusable: (definitions?.properties && Object.keys(definitions?.properties).length > 0) || actions.length > 0 || !!data.update - }); - return
    - - {!!definitions?.properties && Object.entries(Object.groupBy(Object.entries(definitions?.properties) - .filter(([key, prop]) => typeof prop === 'object'), ([key, prop]) => - { - const schema = prop as JSONSchema7; - if (schema.$comment) - { - const meta = JSON.parse(schema.$comment); - return meta.category; - } - return "settings"; - })).map(([cat, data]) => - { - return
    -
    {cat !== "settings" ? cat : <> Settings}
    - {data?.map(([key, prop]) => - { - const schema = prop as JSONSchema7; - return ; - })} -
    ; - - })} -
    Actions
    - {!!data.update && -
    Update
    -
    {data?.update?.current} {'>'} {data?.update?.new}
    -
    }> - - } - {actions?.map(a => )} - -
    ; -} - -function RouteComponent () -{ - const { source: sourceRaw } = Route.useParams(); - const source = decodeURIComponent(sourceRaw); - - const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'plugins' }); - const { data } = useQuery(getPluginDetailsQuery(source)); - const navigate = useNavigate(); - const handleReturn = () => navigate({ to: '/settings/plugins', replace: true, viewTransition: { types: ['slide-up'] } }); - useShortcuts(focusKey, () => [{ label: "Return", button: GamePadButtonCode.B, action: handleReturn }]); - - return
    - - -
    -
    - - -
    {data?.displayName}
    -
    {data?.version}
    - {!!data?.update &&
    {data?.update.new}
    } -
    -
      {data?.keywords?.map((k, i) =>
    • {k}
    • )}
    -
    {data?.description}
    -
    - -
    - -
    ; -} diff --git a/src/mainview/routes/settings/plugins.tsx b/src/mainview/routes/settings/plugins.tsx index c55fc8f..7b1a8da 100644 --- a/src/mainview/routes/settings/plugins.tsx +++ b/src/mainview/routes/settings/plugins.tsx @@ -1,15 +1,10 @@ -import { AutoFocus } from '@/mainview/components/AutoFocus'; -import { pluginCategoryIcons, pluginCategoryPriorities } from '@/mainview/components/Constants'; +import { Button } from '@/mainview/components/options/Button'; import { OptionInput } from '@/mainview/components/options/OptionInput'; import { OptionSpace } from '@/mainview/components/options/OptionSpace'; -import { RoundButton } from '@/mainview/components/RoundButton'; -import { enablePluginMutation, getAllPluginsQuery, uninstallPluginMutation } from '@/mainview/scripts/queries/plugins'; -import { GamePadButtonCode, Shortcut } from '@/mainview/scripts/shortcuts'; -import { FrontendPlugin } from '@simeonradivoev/gameflow-sdk/shared'; -import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; +import { enablePluginMutation, getAllPluginsQuery } from '@/mainview/scripts/queries/plugins'; import { useMutation, useQuery } from '@tanstack/react-query'; -import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { CircleFadingArrowUp, Eye, Puzzle, Settings2, Trash } from 'lucide-react'; +import { createFileRoute } from '@tanstack/react-router'; +import { Puzzle, Search } from 'lucide-react'; export const Route = createFileRoute('/settings/plugins')({ component: RouteComponent, @@ -24,52 +19,23 @@ function Plugin (data: { setEnabled: (enabled: boolean) => void; }) { - const shortcuts: Shortcut[] = []; - const navigate = useNavigate(); - if (data.plugin.hasSettings) - shortcuts.push({ - button: GamePadButtonCode.Y, label: "Details", action (e) - { - - }, - }); - const uninstall = useMutation(uninstallPluginMutation(data.plugin.name)); - const handleUninstall = () => uninstall.mutate(); - const handleDetails = () => navigate({ to: '/settings/plugin/$source', params: { source: encodeURIComponent(data.plugin.name) }, replace: true, viewTransition: { types: ['slide-up'] } }); - - return -
    - {data.plugin.icon ? : } -
    -
    -
    {data.plugin.displayName ?? data.plugin.name}
    -
    -
    {data.plugin.name} ({data.plugin.version})
    - {data.plugin.hasSettings && } - {data.plugin.update &&
    - -
    } -
    -
    -
    - } - className='flex p-4 bg-base-200 rounded-3xl scroll-m-12' - shortcuts={shortcuts} - > -
    - {data.plugin.hasSettings ? : } - {data.plugin.canUninstall && {uninstall.isPending ? : }} - {data.plugin.canDisable && data.setEnabled(!!v)} value={data.plugin.enabled} name={data.plugin.name} type="checkbox" />} + return +
    + {data.plugin.icon ? : }
    +
    +
    {data.plugin.displayName}
    +
    {data.plugin.name} ({data.plugin.version})
    +
    +
    } className='flex p-4 bg-base-200 rounded-3xl'> + + ; } function RouteComponent () { const { data: plugins, refetch: refetchPlugins } = useQuery(getAllPluginsQuery); - const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'plugins' }); const pluginMutation = useMutation({ ...enablePluginMutation, onSuccess (data, variables, onMutateResult, context) { @@ -77,21 +43,15 @@ function RouteComponent () }, }); - return
    - - {!!plugins && Object.entries(Object.groupBy(plugins, p => p.category)) - .filter(([cat, plugins]) => !!plugins) - .toSorted(([catA], [catB]) => pluginCategoryPriorities[catB] - pluginCategoryPriorities[catA]) - .map(([cat, plugins]) => - { - return
    -
    {pluginCategoryIcons[cat]}{cat}
    -
    - {plugins!.map(p => pluginMutation.mutate({ id: p.name, enabled: v })} />)} -
    -
    ; - })} - -
    -
    ; + return <> + {!!plugins && Object.entries(Object.groupBy(plugins, p => p.source)).map(([source, plugins]) => + { + return <> +
    {source === 'builtin' ? "Built In" : "Store"}
    +
    + {plugins.map(p => pluginMutation.mutate({ id: p.name, enabled: v })} />)} +
    + ; + })} + ; } diff --git a/src/mainview/routes/settings/route.tsx b/src/mainview/routes/settings/route.tsx index 625e884..c6e8198 100644 --- a/src/mainview/routes/settings/route.tsx +++ b/src/mainview/routes/settings/route.tsx @@ -7,69 +7,58 @@ import { Outlet, createFileRoute, - useMatchRoute, - useRouter, - useRouterState, + useMatch, } from "@tanstack/react-router"; import { ViewTransitionOptions } from "@tanstack/router-core"; import classNames from "classnames"; import { ArrowBigLeft, - Cog, FingerprintPattern, HardDrive, Info, Joystick, MonitorCog, Puzzle, - RefreshCcw, } from "lucide-react"; -import { JSX, useMemo } from "react"; +import { JSX, useEffect } from "react"; import { twMerge } from "tailwind-merge"; -import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts"; +import z from "zod"; +import { SettingsSchema } from "../../../shared/constants"; +import { Router } from "../.."; +import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts"; import Shortcuts from "@/mainview/components/Shortcuts"; import { HandleGoBack } from "@/mainview/scripts/utils"; -import { AutoFocus } from "@/mainview/components/AutoFocus"; -import { oneShot } from "@/mainview/scripts/audio/audio"; -import SelectMenu from "@/mainview/components/SelectMenu"; export const Route = createFileRoute("/settings")({ component: SettingsUI, - staticData: { - enterSound: 'openSettings' - } + validateSearch: z.object({ + focus: z.keyof(SettingsSchema).optional() + }) }); function MenuItem (data: { route: string; - matchRoutes?: string[]; return?: boolean; viewTransition?: boolean | ViewTransitionOptions; icon: JSX.Element; focusSelect?: boolean; className?: string; linkClassName?: string; - active?: boolean; label: string; }) { - const router = useRouter(); - const routerState = useRouterState(); - const matchRoute = useMatchRoute(); - - const acitve = useMemo(() => data.matchRoutes ? data.matchRoutes.some(r => !!matchRoute({ to: r })) : !!router.matchRoute({ to: data.route }), - [routerState, matchRoute, data.matchRoutes, data.route]); - const handleNonFocusSelect = (e?: Event) => + const acitve = !!useMatch({ from: data.route as any, shouldThrow: false });; + const handleNonFocusSelect = () => { if (data.return) { - HandleGoBack(router, e); + HandleGoBack(); } else if (!acitve) { - router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true }); + Router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true }); } - oneShot('click'); + }; const { ref, focusSelf } = useFocusable({ focusKey: `menu-item-${data.route}`, @@ -78,7 +67,7 @@ function MenuItem (data: { { if (data.focusSelect && !acitve) { - router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true }); + Router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true }); } (ref.current as HTMLElement).scrollIntoView({ inline: 'center' }); }, @@ -92,8 +81,7 @@ function MenuItem (data: {
  • handleNonFocusSelect(e.nativeEvent)} + onClick={data.focusSelect ? focusSelf : handleNonFocusSelect} onFocus={focusSelf} className={twMerge("flex group-focusable cursor-pointer", data.className)} > @@ -118,11 +106,10 @@ function MenuItem (data: { function SettingsMenu (data: {}) { - const router = useRouter(); const { ref, focusKey } = useFocusable({ focusable: true, focusKey: 'settings-menu', - preferredChildFocusKey: `menu-item-${router.history.location.pathname}` + preferredChildFocusKey: location.hash.replaceAll(/#|(\?.+)/g, '') }); return
      } /> - } - /> - } - /> } + route="/settings/plugins" + label="Plugins" + icon={} /> [{ label: "Back", button: GamePadButtonCode.B, action: (e) => HandleGoBack(router, e) }], [router]); + useEffect(() => + { + focusSelf(); + }, []); + + useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]); + const { shortcuts } = useShortcutContext(); return ( @@ -213,14 +192,10 @@ export function SettingsUI ()
  • -
    -
    - } /> +
    +
    - - ); } diff --git a/src/mainview/routes/settings/tasks.tsx b/src/mainview/routes/settings/tasks.tsx deleted file mode 100644 index afc6f54..0000000 --- a/src/mainview/routes/settings/tasks.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { Button } from '@/mainview/components/options/Button'; -import { jobsApi } from '@/mainview/scripts/clientApi'; -import { FrontEndJob } from '@simeonradivoev/gameflow-sdk/shared'; -import { createFileRoute } from '@tanstack/react-router'; -import { Ban, Clock, Cog, Download, DownloadCloud, Gauge } from 'lucide-react'; -import prettyBytes from 'pretty-bytes'; -import { useEffect, useRef, useState } from 'react'; - -export const Route = createFileRoute('/settings/tasks')({ - component: RouteComponent, -}); - -function RouteComponent () -{ - - const [activeJobs, setActiveJobs] = useState([]); - const [queuedJobs, setQueuedJobs] = useState([]); - const wsRef = useRef<{ send: (data: any) => void; }>(null); - - useEffect(() => - { - const sub = jobsApi.api.jobs.list.subscribe(); - wsRef.current = { - send (data) - { - sub.ws.send(JSON.stringify(data)); - }, - }; - sub.on('message', e => - { - switch (e.data.type) - { - case 'allJobs': - setActiveJobs(e.data.active); - setQueuedJobs(e.data.queued); - break; - - case 'aborted': - const abortedJobId = e.data.id; - setActiveJobs(jobs => jobs.map(j => j.id === abortedJobId ? { ...j, status: 'aborted' } : j)); - setQueuedJobs(jobs => jobs.filter(j => j.id !== abortedJobId)); - break; - - case 'queued': - const queuedJob = e.data.job; - setQueuedJobs(jobs => [...jobs, queuedJob]); - break; - - case 'progress': - const progressJob = e.data.job; - setActiveJobs(jobs => jobs.map(j => j.id === progressJob.id ? progressJob : j)); - break; - - case 'started': - const newJob = e.data.job; - setActiveJobs(jobs => [newJob, ...jobs]); - setQueuedJobs(jobs => jobs.filter(j => j.id !== newJob.id)); - break; - - case 'ended': - const endedJobId = e.data.id; - setActiveJobs(jobs => jobs.filter(j => j.id !== endedJobId)); - break; - } - }); - - return () => - { - sub.close(); - wsRef.current = null; - }; - }, []); - - const handleCancel = (id: string) => - { - wsRef.current?.send({ type: 'cancel', id: id }); - }; - - return
    -
    Active
    -
      - {activeJobs.map((job, i) =>
    • -
      -
      - {job.data.preview_url ? : } -
      -
      {job.data.name ?? job.id}
      -
      -
      -
      -
      -
      {job.state}
      -
      {job.progress.toFixed(1)}%
      -
      - -
      - {job.data.downloaded != null && job.data.total != null &&
      {prettyBytes(job.data.downloaded)}/{prettyBytes(job.data.total)}
      } - {job.data.speed != null &&
      {prettyBytes(job.data.speed)}/s
      } -
      -
      - -
      -
    • )} -
    -
    Queued
    -
      - {queuedJobs.map((job, i) =>
    • -
      -
      -
      {job.data.name ?? job.id}
      -
      -
      -
      -
      - {job.data.total !== undefined &&
      {prettyBytes(job.data.total)}
      } -
      -
      - -
      -
    • )} -
    -
    ; -} diff --git a/src/mainview/routes/settings/update.tsx b/src/mainview/routes/settings/update.tsx deleted file mode 100644 index 3f27639..0000000 --- a/src/mainview/routes/settings/update.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { AutoFocus } from '@/mainview/components/AutoFocus'; -import DotsLoading from '@/mainview/components/backgrounds/dots'; -import { Button } from '@/mainview/components/options/Button'; -import { checkUpdateMutation, hasUpdateQuery, updateMutation } from '@/mainview/scripts/queries/system'; -import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; -import { useMutation } from '@tanstack/react-query'; -import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { CircleFadingArrowUp, RefreshCcw } from 'lucide-react'; -import { MarkdownAsync } from 'react-markdown'; - -export const Route = createFileRoute('/settings/update')({ - component: RouteComponent, - pendingComponent: Loading, - async loader (ctx) - { - const data = await ctx.context.queryClient.fetchQuery(hasUpdateQuery); - return { data: data }; - }, -}); - -function Loading () -{ - const { ref, focusSelf } = useFocusable({ focusKey: 'updates' }); - return <> - - - ; -} - -function RouteComponent () -{ - const { data } = Route.useLoaderData(); - const navigate = useNavigate(); - const update = useMutation(updateMutation); - const forceCheckUpdate = useMutation({ - ...checkUpdateMutation, - onSuccess (data, variables, onMutateResult, context) - { - context.client.invalidateQueries(hasUpdateQuery); - navigate({ to: '/settings/update', replace: true }); - }, - }); - const { ref, focusKey } = useFocusable({ focusKey: 'updates' }); - return
    - -

    Version: {data.version}

    -
    - { - data.hasUpdate > 0 ? - : - - } - {} -
    -
    Version Info
    -
    - {children}; - }, - }} >{data.info} -
    -
    -
    ; -} diff --git a/src/mainview/routes/store/details.download.$source.$id.tsx b/src/mainview/routes/store/details.download.$source.$id.tsx deleted file mode 100644 index 2100442..0000000 --- a/src/mainview/routes/store/details.download.$source.$id.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { AutoFocus } from '@/mainview/components/AutoFocus'; -import DotsLoading from '@/mainview/components/backgrounds/dots'; -import { ContextList, DialogEntry } from '@/mainview/components/ContextDialog'; -import { StickyHeaderUI } from '@/mainview/components/Header'; -import { Button } from '@/mainview/components/options/Button'; -import Screenshots from '@/mainview/components/Screenshots'; -import SelectMenu from '@/mainview/components/SelectMenu'; -import { FloatingShortcuts } from '@/mainview/components/Shortcuts'; -import { GlobalDialogContext } from '@/mainview/scripts/contexts'; -import { downloadLookupQuery } from '@/mainview/scripts/queries/romm'; -import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts'; -import { HandleGoBack } from '@/mainview/scripts/utils'; -import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; -import { createFileRoute, useNavigate, useRouter } from '@tanstack/react-router'; -import { Download } from 'lucide-react'; -import prettyBytes from 'pretty-bytes'; -import { useContext } from 'react'; - -export const Route = createFileRoute('/store/details/download/$source/$id')({ - component: RouteComponent, - pendingComponent: Loading, - async loader (ctx) - { - const data = await ctx.context.queryClient.fetchQuery(downloadLookupQuery(decodeURIComponent(ctx.params.source), decodeURIComponent(ctx.params.id))); - return { data }; - } -}); - -function Loading () -{ - const { ref, focusSelf } = useFocusable({ focusKey: 'download-details' }); - return <> - - - ; -} - -const imagesMap = new Set(['JPEG', 'PNG', 'Motion JPEG', 'Item Image']); -const videoFormat = new Set(['h.264']); -const downloadsBlacklist = new Set(['JPEG Thumb', 'Metadata', 'Thumbnail', 'Item Tile', 'Archive BitTorrent', ...videoFormat, ...imagesMap]); - -function Details (data: { onDownload: (focusKey: string) => void; }) -{ - const { data: download } = Route.useLoaderData(); - const screenshots = download.files.filter(f => f.format && imagesMap.has(f.format)).map(f => f.download_url); - if (screenshots.length <= 0 && download.cover_url) screenshots.push(download.cover_url); - return
    - -
    -
    -
    - {!!download.cover_url && } -
    -
    {download.name}
    -
    -
    {download.date?.toDateString()}
    -
    -
    {download.source}
    -
    -
    -
    -
    - -
    -
    -
    - {!!download.summary &&
    -
    -
    } -
    -
    Downloads
    -
      - {download.files.filter(f => f.format && !downloadsBlacklist.has(f.format)).map(f =>
    • - {f.id} - {!!f.size && prettyBytes(f.size)} -
    • )} -
    -
    - -
    -
    -
    ; -} - -function RouteComponent () -{ - const navigate = useNavigate(); - const router = useRouter(); - const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'download-details', preferredChildFocusKey: 'download-btn' }); - const { data } = Route.useLoaderData(); - const globalDialog = useContext(GlobalDialogContext); - - useShortcuts(focusKey, () => [{ - label: "Return", - action: (e) => HandleGoBack(router, e), - button: GamePadButtonCode.B - }], [router]); - - return
    - - -
    globalDialog.openContext({ - content: f.format && !downloadsBlacklist.has(f.format)).map(f => - { - const option: DialogEntry = { - id: f.id, - content: f.id, - type: 'primary', - action (ctx) - { - navigate({ - to: '/game/add', search: { - gameLocation: f.download_url, - search: data.name, - step: 1 - } - }); - }, - }; - - return option; - })} /> - }, focusKey)} /> - - - - -
    ; -} diff --git a/src/mainview/routes/store/details.emulator.$id.tsx b/src/mainview/routes/store/details.emulator.$id.tsx index d8c8372..1593bd9 100644 --- a/src/mainview/routes/store/details.emulator.$id.tsx +++ b/src/mainview/routes/store/details.emulator.$id.tsx @@ -1,17 +1,18 @@ -import { useContext, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useFocusable, FocusContext, } from "@noriginmedia/norigin-spatial-navigation"; -import { createFileRoute, useNavigate, useRouter } from "@tanstack/react-router"; -import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts"; -import { FloatingShortcuts } from "@/mainview/components/Shortcuts"; +import { createFileRoute } from "@tanstack/react-router"; +import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts"; +import { Router } from "@/mainview"; +import Shortcuts from "@/mainview/components/Shortcuts"; import { AnimatedBackground } from "@/mainview/components/AnimatedBackground"; -import { rommApi, systemApi } from "@/mainview/scripts/clientApi"; +import { systemApi } from "@/mainview/scripts/clientApi"; import { Button } from "@/mainview/components/options/Button"; -import { ChevronDown, CircleFadingArrowUp, CloudUpload, Cpu, Download, Fullscreen, Gamepad2, Info, Monitor, Puzzle, Settings, Settings2, Terminal, Trash2, TriangleAlert, WandSparkles } from "lucide-react"; -import { ContextList, DialogEntry } from "@/mainview/components/ContextDialog"; +import { ChevronDown, Cpu, Download, Gamepad2, Info, Puzzle, Settings, Trash2, TriangleAlert, WandSparkles } from "lucide-react"; +import { ContextList, DialogEntry, useContextDialog } from "@/mainview/components/ContextDialog"; import { RPC_URL } from "@/shared/constants"; import Screenshots from "@/mainview/components/Screenshots"; import { StickyHeaderUI } from "@/mainview/components/Header"; @@ -26,11 +27,6 @@ import { GamesSection } from "@/mainview/components/store/GamesSection"; import { deleteBiosMutation, downloadBiosMutation, installEmulatorMutation, storeEmulatorDeleteMutation, storeEmulatorDetailsQuery, storeEmulatorsRecommendedQuery } from "@queries/store"; import { gamesRecommendedBasedOnEmulatorQuery } from "@queries/romm"; import FocusTooltip from "@/mainview/components/FocusTooltip"; -import { AutoFocus } from "@/mainview/components/AutoFocus"; -import { FilterUI } from "@/mainview/components/Filters"; -import Markdown from "react-markdown"; -import { FrontEndEmulatorDetailed } from "@simeonradivoev/gameflow-sdk/shared"; -import { GlobalDialogContext } from "@/mainview/scripts/contexts"; export const Route = createFileRoute('/store/details/emulator/$id')({ component: RouteComponent, @@ -39,10 +35,6 @@ export const Route = createFileRoute('/store/details/emulator/$id')({ ctx.context.queryClient.prefetchQuery(storeEmulatorDetailsQuery(ctx.params.id)); ctx.context.queryClient.prefetchQuery(storeEmulatorsRecommendedQuery(ctx.params.id)); ctx.context.queryClient.prefetchQuery(gamesRecommendedBasedOnEmulatorQuery(ctx.params.id)); - }, - staticData: { - enterSound: "openDetails", - goBackSound: "returnDetails" } }); @@ -63,11 +55,8 @@ function HomePageLink (data: { homepage?: string; }) function TitleArea (data: { emulator?: FrontEndEmulatorDetailed; onInstall: (source: string) => void; - onUpdate: (source: string) => void; }) { - const globalDialog = useContext(GlobalDialogContext); - const navigation = useNavigate(); const queryClient = useQueryClient(); const deleteMutation = useMutation({ ...storeEmulatorDeleteMutation, @@ -77,7 +66,6 @@ function TitleArea (data: { }, }); const downloadBios = useMutation(downloadBiosMutation(data.emulator?.name ?? '')); - const updateToVersion = data.emulator?.downloads.find(d => d.version === data.emulator!.storeDownloadInfo?.type)?.version ?? data.emulator?.downloads[0]?.version; const deleteBios = useMutation({ ...deleteBiosMutation, onSuccess (data, variables, onMutateResult, context) @@ -130,7 +118,7 @@ function TitleArea (data: { const isInstalling = !!installJob || !!biosInstallJob; const options: DialogEntry[] = []; - const installedFromStore = !!data.emulator?.validSources.find(s => s.type === 'store' && s.exists); + const installedFromStore = !!data.emulator?.sources.find(s => s.type === 'store' && s.exists); if (data.emulator) { if (!isInstalling && !installedFromStore) @@ -163,22 +151,6 @@ function TitleArea (data: { id: "delete" }); - if ((!data.emulator.storeDownloadInfo || data.emulator.storeDownloadInfo.hasUpdate)) - { - options.push({ - content: `Update ${data.emulator.storeDownloadInfo?.type}: ${data.emulator.storeDownloadInfo?.version ?? "Unknown"} > ${updateToVersion}`, - type: 'warning', - icon: , - action (ctx) - { - const source = data.emulator?.storeDownloadInfo?.type ?? data.emulator?.downloads[0]?.type; - if (source) data.onUpdate(source); - ctx.close(); - }, - id: 'update' - }); - } - if (!data.emulator.bios || data.emulator.bios.length <= 0) { options.push({ @@ -207,16 +179,8 @@ function TitleArea (data: { id: "download-bios" }); } - } - options.push(...data.emulator.validSources.filter(s => s.exists).map(s => ({ - content: `Launch: ${s.type}`, type: 'primary', icon: emulatorStatusIcons[s.type], action (ctx) - { - if (!data.emulator) return; - rommApi.api.romm.game({ source: 'emulator' })({ id: data.emulator.name }).play.post({ command_id: s.type }); - navigation({ to: '/launcher/$source/$id', params: { source: 'emulator', id: data.emulator.name } }); - }, id: `open-${s.type}` - } satisfies DialogEntry))); + } } const { ref, focusKey, hasFocusedChild } = useFocusable({ @@ -255,12 +219,14 @@ function TitleArea (data: { installButtonContent = <>Unsupported; } - const openOptionsDialog = (focusKey: string) => globalDialog.openContext({ content: }, focusKey); + const { dialog: installOptionsDialog, setOpen } = useContextDialog("install-context-menu", { + content: + }); const handleOptionsOpen = () => { - if (isInstalling || !data.emulator) return false; - openOptionsDialog('install-btn'); + if (isInstalling || !data.emulator || data.emulator.downloads.length <= 0) return false; + setOpen(true, 'install-btn'); }; return
    @@ -283,21 +249,15 @@ function TitleArea (data: { {!!data.emulator?.bios?.[0] &&
    } - {data.emulator && data.emulator.integrations.length > 0 &&
    + {data.emulator && !!data.emulator.integration && data.emulator.validSources.some(s => s.type === 'store') &&
    } - {data.emulator?.integrations.some(s => s.capabilities?.includes('saves')) &&
    -
    -
    }
    - {(data.emulator?.storeDownloadInfo?.hasUpdate || !data.emulator?.storeDownloadInfo) && installedFromStore && !!updateToVersion &&
    - -
    } {(!data.emulator?.bios || data.emulator.bios.length <= 0) && (data.emulator?.biosRequirement === 'required') && installedFromStore &&
    - +
    }
    + {installOptionsDialog}
    ; } @@ -324,28 +285,10 @@ function Description (data: { emulator?: FrontEndEmulatorDetailed; }) ; } -const capabilityIconMap: Record = { - saves: , - fullscreen: , - resolution: , - config: , - batch: -}; - -function InfoTabs (data: { tabs: Record, selectTab: (v: string) => void; }) -{ - const { ref, focusKey } = useFocusable({ focusKey: 'emulator-info-tabs-section', onFocus: (l, p, d) => scrollIntoViewHandler({ block: 'start' })(focusKey, ref.current, d) }); - return
    - - data.selectTab(v)} /> - -
    ; -} - export function RouteComponent () { const { id } = Route.useParams(); - const router = useRouter(); + const { ref, focusKey, focusSelf } = useFocusable({ focusKey: `GAME_DETAIL_${id}`, trackChildren: true, @@ -355,72 +298,54 @@ export function RouteComponent () const { data: emulator, isPending: isEmulatorPending } = useQuery(storeEmulatorDetailsQuery(id)); const { data: recommendedEmulators } = useQuery(storeEmulatorsRecommendedQuery(id)); const { data: recommendedGames } = useQuery(gamesRecommendedBasedOnEmulatorQuery(id)); - const [infoTab, setInfoTab] = useState("stats"); useShortcuts(focusKey, () => [{ label: "Return", - action: (e) => HandleGoBack(router, e), + action: HandleGoBack, button: GamePadButtonCode.B - }], [router]); + }]); const installMutation = useMutation({ - ...installEmulatorMutation(id), - onSuccess: (data, variables, onMutateResult, context) => context.client.refetchQueries(storeEmulatorDetailsQuery(id)), + ...installEmulatorMutation(id), onSuccess: (data, variables, onMutateResult, context) => context.client.refetchQueries(storeEmulatorDetailsQuery(id)), }); - const stats: StatEntry[] = []; + useEffect(() => + { + focusSelf(); + }, []); + const { shortcuts } = useShortcutContext(); + + + const stats: StatEntry[] = []; if (emulator) { if (emulator.keywords) stats.push({ label: "Tags", content: emulator.keywords }); - if (emulator.storeDownloadInfo) - stats.push({ label: "Version", content: `${emulator.storeDownloadInfo.version ?? "Unknown"} (${emulator.storeDownloadInfo.type})` }); stats.push({ label: "Systems", content: emulator.systems.map(s => s.name) }); - stats.push(...emulator.validSources.flatMap(s => [{ - label: "Source", content:
    -
    -
    {emulatorStatusIcons[s.type]}{s.type}
    -
    {s.binPath}
    -
    - {emulator.integrations.some(i => i.source?.type === s.type) &&
    } - {emulator.integrations.filter(i => i.source?.type === s.type).map(i => - { - return
    -
    - -
    {i.id}
    -
    -
    - {i.capabilities?.map(c => <>
    {capabilityIconMap[c]}{c}
    )} -
    -
    ; - })} + stats.push(...emulator.sources.flatMap(s => [{ + label: "Source", content:
    +
    {emulatorStatusIcons[s.type]}{s.type}:
    +
    {s.binPath}
    }])); if (emulator.bios) stats.push({ label: "Bios", content: emulator.bios && emulator.bios.length > 0 ? emulator.bios :
    Missing
    }); - } - - const infoTabs: Record = { - stats: { label: "Stats", selected: infoTab === 'stats', icon: }, - }; - - if (emulator?.storeDownloadInfo?.hasUpdate) - { - infoTabs.update = { label: "Update", icon: , selected: infoTab === 'update' }; + if (emulator.integration) + { + stats.push({ label: "Integration", icon: , content: `${emulator.integration.name} (${emulator.integration.version})` }); + } } return ( -
    - installMutation.mutate({ source: s, isUpdate: false })} onUpdate={s => installMutation.mutate({ source: s, isUpdate: true })} /> +
    @@ -432,9 +357,8 @@ export function RouteComponent ()
    - - {infoTab === 'stats' && } - {infoTab === 'update' && {emulator?.storeDownloadInfo?.description}} +
    Stats
    + {recommendedEmulators &&
    } onFocus={scrollIntoViewHandler({ block: 'center' })} - onSelect={(em, focus) => + onSelect={(id, focus) => { - if (em.source === 'local') return; - router.navigate({ - to: '/store/details/emulator/$id', params: { id: em.name } + Router.navigate({ + to: '/store/details/emulator/$id', params: { id } }); }} emulators={recommendedEmulators} /> @@ -463,13 +386,13 @@ export function RouteComponent ()
    { - router.navigate({ + Router.navigate({ to: '/game/$source/$id', params: { id: id.id, source: id.source } }); }} games={recommendedGames} />
    }
    - +
    diff --git a/src/mainview/routes/store/details.plugin.$id.tsx b/src/mainview/routes/store/details.plugin.$id.tsx deleted file mode 100644 index 9e35cdb..0000000 --- a/src/mainview/routes/store/details.plugin.$id.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { AutoFocus } from '@/mainview/components/AutoFocus'; -import DotsLoading from '@/mainview/components/backgrounds/dots'; -import { StickyHeaderUI } from '@/mainview/components/Header'; -import LoadingScreen from '@/mainview/components/LoadingScreen'; -import { Button } from '@/mainview/components/options/Button'; -import { FloatingShortcuts } from '@/mainview/components/Shortcuts'; -import StatList, { StatEntry } from '@/mainview/components/StatList'; -import { installPluginMutation, pluginFilter, uninstallPluginMutation, updatePluginMutation } from '@/mainview/scripts/queries/plugins'; -import { pluginDetailsQuery } from '@/mainview/scripts/queries/store'; -import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts'; -import { HandleGoBack } from '@/mainview/scripts/utils'; -import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; -import { QueryClient, useMutation } from '@tanstack/react-query'; -import { createFileRoute, useNavigate, useRouter } from '@tanstack/react-router'; -import { ArrowRight, CircleFadingArrowUp, Download, Settings, Trash } from 'lucide-react'; -import prettyBytes from 'pretty-bytes'; -import { Suspense } from 'react'; - -export const Route = createFileRoute('/store/details/plugin/$id')({ - component: RouteComponent, - pendingComponent: Loading, - async loader (ctx) - { - const id = decodeURIComponent(ctx.params.id); - const data = await ctx.context.queryClient.fetchQuery(pluginDetailsQuery(id)); - return { data }; - }, -}); - -function Loading () -{ - const { ref, focusSelf } = useFocusable({ focusKey: 'plugin-details' }); - return <> - - - ; -} - -function Details () -{ - const { id } = Route.useParams(); - const plugin = decodeURIComponent(id); - const { data } = Route.useLoaderData(); - const navigate = useNavigate(); - const handleRefresh = (client: QueryClient) => - { - client.invalidateQueries(pluginFilter(plugin)); - navigate({ to: '/store/details/plugin/$id', params: { id: encodeURIComponent(id) }, replace: true }); - }; - const update = useMutation({ - ...updatePluginMutation(plugin), - onSuccess (data, variables, onMutateResult, context) - { - handleRefresh(context.client); - }, - }); - const install = useMutation({ - ...installPluginMutation(plugin), - onSuccess (data, variables, onMutateResult, context) - { - handleRefresh(context.client); - }, - }); - const uninstall = useMutation({ - ...uninstallPluginMutation(plugin), - onSuccess (data, variables, onMutateResult, context) - { - handleRefresh(context.client); - }, - }); - - const stats: StatEntry[] = []; - if (data.devDependencies) - { - stats.push({ content: Object.keys(data.devDependencies), label: "Dev Dependecies" }); - } - if (data.dependencies) - { - stats.push({ content: Object.keys(data.dependencies), label: "Dependecies" }); - } - if (data.maintainers) - { - stats.push({ content: data.maintainers.map(m => m.name), label: "Maintainers" }); - } - if (data.dist) - { - stats.push({ content: prettyBytes(data.dist.unpackedSize), label: "Size" }); - } - if (data.license) - { - stats.push({ content: data.license, label: "License" }); - } - return <> - -
    -
    -
    {data.name}
    -
    -
    - {data.update ? <> -
    {data.update.from}
    - -
    {data.version}
    - : -
    {data.version}
    } - -
    - by {data.author?.name ?? data._npmUser?.name}
    -
    -
    - {data.installed && <> - {!!data.update && } - - - - } - {!data.installed && } - -
    -
    -
    Details
    -
    -
    {data.description}
    - -
    -
    Keywords
    -
    - {data.keywords.map(k =>
  • {k}
  • )} -
    - ; -} - -function RouteComponent () -{ - const router = useRouter(); - const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'plugin-details' }); - useShortcuts(focusKey, () => [{ - label: "Return", button: GamePadButtonCode.B, action (e) - { - HandleGoBack(router, e); - }, - }]); - return
    - - - }> -
    - - - - -
    ; -} diff --git a/src/mainview/routes/store/tab/download.tsx b/src/mainview/routes/store/tab/download.tsx deleted file mode 100644 index a1f0172..0000000 --- a/src/mainview/routes/store/tab/download.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import DotsLoading from '@/mainview/components/backgrounds/dots'; -import LoadMoreButton from '@/mainview/components/LoadMoreButton'; -import { Button } from '@/mainview/components/options/Button'; -import { SideDownloadFilters } from '@/mainview/components/SideFilters'; -import { downloadLookupFiltersQuery, downloadsLookupQuery } from '@/mainview/scripts/queries/romm'; -import { scrollIntoViewHandler } from '@/mainview/scripts/utils'; -import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; -import { DownloadLookupEntry, DownloadsLookupFilter } from '@simeonradivoev/gameflow-sdk/shared'; -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; -import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { ArrowRight, DownloadIcon, Eye, MessageCircle, Puzzle, Save, Star } from 'lucide-react'; -import prettyBytes from 'pretty-bytes'; -import { useSessionStorage } from 'usehooks-ts'; - -export const Route = createFileRoute('/store/tab/download')({ - component: RouteComponent, -}); - -function Download (data: { focusKey: string, match: DownloadLookupEntry; }) -{ - const navigate = useNavigate(); - const handleAction = () => navigate({ - to: '/store/details/download/$source/$id', params: { - source: encodeURIComponent(data.match.source), - id: encodeURIComponent(data.match.id) - } - }); - const { ref, focusKey } = useFocusable({ - focusKey: data.focusKey, - onFocus: (l, p, d) => scrollIntoViewHandler({ behavior: "smooth", block: "center", inline: "center" })(focusKey, ref.current, d), - onEnterPress: handleAction - }); - return
  • - {!!data.match.cover_url && } -
    -
    {data.match.name}
    -
    {data.match.date?.toDateString()}
    -
      - {!!data.match.size &&
    • {prettyBytes(data.match.size)}
    • } - {!!data.match.download_count &&
    • {data.match.download_count}
    • } - {!!data.match.view_count &&
    • {data.match.view_count}
    • } - {!!data.match.comment_count &&
    • {data.match.comment_count}
    • } - {!!data.match.rating &&
    • {data.match.rating}
    • } -
    -
    -
  • ; -} - -function Downloads (data: { - pages: { - data: DownloadLookupEntry[]; - totalCount: number; - hadMatchers: boolean; - nextPage: number; - }[]; - hasNextPage: boolean, - isFetchingNextPage: boolean, - isFetching: boolean, - fetchNextPage: () => void, - error: string | undefined; -}) -{ - const navigate = useNavigate(); - const { ref, focusKey } = useFocusable({ focusKey: 'downloads-list' }); - return
      - - {data.pages.flatMap((page, p) => page.data.map((match, i) => ))} - {!data.pages[0].hadMatchers &&
      } - {data.hasNextPage && data.pages[0].hadMatchers &&
      -
    ; -} - -function RouteComponent () -{ - const [search] = useSessionStorage(`${Route.to}-search`, undefined); - const [filter, setFilter] = useSessionStorage('store-download-lookup-filters', {}); - const { data, error, isPending, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery({ - ...downloadsLookupQuery({ ...filter, search }), - maxPages: 10, - refetchOnMount: false - }); - const { ref, focusKey } = useFocusable({ - focusKey: "main-area", - preferredChildFocusKey: "downloads-list" - }); - - const { data: lookupFilters } = useQuery(downloadLookupFiltersQuery); - - return
    - -
    - {isFetching && } - Results - {isPending ? : {data?.pages[0].totalCount}} -
    - {isPending && } - {data && } -
    - -
    - -
    -
    ; -} diff --git a/src/mainview/routes/store/tab/emulators.tsx b/src/mainview/routes/store/tab/emulators.tsx index 1fb831f..7fd1e0f 100644 --- a/src/mainview/routes/store/tab/emulators.tsx +++ b/src/mainview/routes/store/tab/emulators.tsx @@ -1,6 +1,6 @@ -import { createFileRoute } from '@tanstack/react-router'; +import { createFileRoute, useSearch } from '@tanstack/react-router'; import { Joystick } from 'lucide-react'; import { useContext, useEffect } from 'react'; import { FocusContext, getCurrentFocusKey, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; @@ -9,33 +9,20 @@ import { StoreContext } from '@/mainview/scripts/contexts'; import { GetFocusedElement } from '@/mainview/scripts/spatialNavigation'; import { useQuery } from '@tanstack/react-query'; import { storeEmulatorsQuery } from '@queries/store'; -import InvalidStoreError from '@/mainview/components/store/InvalidStoreError'; -import { useSessionStorage } from 'usehooks-ts'; -import { zodValidator } from '@tanstack/zod-adapter'; -import z from 'zod'; export const Route = createFileRoute('/store/tab/emulators')({ component: RouteComponent, - errorComponent: InvalidStoreError, - validateSearch: zodValidator(z.object({ - search: z.string().optional() - })) }); function RouteComponent () { - const { focus } = Route.useSearch(); - const [search] = useSessionStorage(`${Route.to}-search`, undefined); + const { focus } = useSearch({ from: '/store/tab' }); const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "main-area", preferredChildFocusKey: focus }); const storeContext = useContext(StoreContext); - const { data: emulators } = useQuery({ - ...storeEmulatorsQuery({ search }), - retry: false, - throwOnError: true - }); + const { data: emulators } = useQuery(storeEmulatorsQuery); useEffect(() => { @@ -71,7 +58,6 @@ function RouteComponent () /> )) ?? Array.from({ length: 10 }).map((_, i) =>
    )}
    - ; diff --git a/src/mainview/routes/store/tab/games.tsx b/src/mainview/routes/store/tab/games.tsx index 9176c20..7bbf93e 100644 --- a/src/mainview/routes/store/tab/games.tsx +++ b/src/mainview/routes/store/tab/games.tsx @@ -1,44 +1,25 @@ import { FocusContext, getCurrentFocusKey, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; -import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { Gamepad2, HardDrive } from 'lucide-react'; -import { JSX, useEffect } from 'react'; -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { createFileRoute, useSearch } from '@tanstack/react-router'; +import { Gamepad2 } from 'lucide-react'; +import { useContext, useEffect } from 'react'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import FrontEndGameCard from '@/mainview/components/FrontEndGameCard'; import { GetFocusedElement } from '@/mainview/scripts/spatialNavigation'; import LoadMoreButton from '@/mainview/components/LoadMoreButton'; import { storeGamesInfiniteQuery } from '@queries/store'; -import InvalidStoreError from '@/mainview/components/store/InvalidStoreError'; -import { CardList, GameMetaExtra } from '@/mainview/components/CardList'; -import { RPC_URL } from '@/shared/constants'; -import { GameListFilterType, FrontEndGameType } from '@simeonradivoev/gameflow-sdk/shared'; -import { useSessionStorage } from 'usehooks-ts'; -import { zodValidator } from '@tanstack/zod-adapter'; -import z from 'zod'; -import SideFilters from '@/mainview/components/SideFilters'; -import { gameFiltersQuery } from '@/mainview/scripts/queries/romm'; -import { isUrl } from '@/shared/utils'; +import { StoreContext } from '@/mainview/scripts/contexts'; export const Route = createFileRoute('/store/tab/games')({ - component: RouteComponent, - errorComponent: InvalidStoreError, - validateSearch: zodValidator(z.object({ - search: z.string().optional() - })) + component: RouteComponent }); function RouteComponent () { - const { focus } = Route.useSearch(); - const [search] = useSessionStorage(`${Route.to}-search`, undefined); - const navigator = useNavigate(); - const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "main-area", preferredChildFocusKey: focus ?? 'store-games' }); - const [filter, setFilter] = useSessionStorage('store-games-filters', {}); - const { data, fetchNextPage, isFetchingNextPage, isFetching } = useInfiniteQuery(storeGamesInfiniteQuery(filter)); - const { data: gameFilters } = useQuery(gameFiltersQuery({ source: 'store' })); + const { focus } = useSearch({ from: '/store/tab' }); + const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "main-area", preferredChildFocusKey: focus }); - useEffect(() => - { - setFilter(v => ({ ...v, search })); - }, [search]); + const { data, fetchNextPage, isFetchingNextPage, isFetching } = useInfiniteQuery(storeGamesInfiniteQuery); + const storeContext = useContext(StoreContext); useEffect(() => { @@ -55,11 +36,6 @@ function RouteComponent () node.scrollIntoView({ behavior: details.instant ? 'instant' : 'smooth', block: 'center' }); }; - function handleDefaultSelect (g: FrontEndGameType) - { - navigator({ to: '/game/$source/$id', params: { id: g.id.id, source: g.id.source } }); - }; - return <>
    @@ -69,9 +45,19 @@ function RouteComponent () Games -
    -
    -
    - + }} />
    diff --git a/src/mainview/routes/store/tab/index.tsx b/src/mainview/routes/store/tab/index.tsx index dcb53c8..b596807 100644 --- a/src/mainview/routes/store/tab/index.tsx +++ b/src/mainview/routes/store/tab/index.tsx @@ -15,8 +15,6 @@ import { GetFocusedElement } from '@/mainview/scripts/spatialNavigation'; import { useQuery } from '@tanstack/react-query'; import { autoEmulatorsQuery } from '@queries/settings'; import { storeEmulatorsRecommendedQuery, storeFeaturedGamesQuery } from '@queries/store'; -import ImageWithFallbacks from '@/mainview/components/ImageWithFallbacks'; -import { FrontEndGameTypeDetailed } from '@simeonradivoev/gameflow-sdk/shared'; export const Route = createFileRoute('/store/tab/')({ component: RouteComponent @@ -52,37 +50,40 @@ function Main (data: { games?: FrontEndGameTypeDetailed[]; }) }, 10); const storeContext = useContext(StoreContext); - const previewUrls = data.games?.[selectedGame] ? data.games[selectedGame].path_covers.map(c => - { - const url = new URL(`${RPC_URL(__HOST__)}${c}`); - url.searchParams.set('blur', '16'); - return url; - }) : undefined; - + const previewUrl = data.games ? new URL(`${RPC_URL(__HOST__)}${data.games[selectedGame].path_cover}`) : undefined; + previewUrl?.searchParams.set('blur', '16'); return
    - {game ?
    + {game ?
    - + + { + e.currentTarget.dataset.loaded = "true"; + e.currentTarget.classList.toggle('scale-110', false); + }} + />
    -
    -
    - {!!data.games && } +
    +
    + {!!data.games && }
    -

    {game.name}

    -

    {game.summary}

    +

    {game.name}

    +

    {game.summary}

    - +
    :
    } @@ -127,19 +128,19 @@ export function RouteComponent () {
    } {!!crucialEmulators && crucialEmulators?.length > 0 && storeContext.showDetails('emulator', em.source, em.name, focus)} + onSelect={(id, focus) => storeContext.showDetails('emulator', 'store', id, focus)} emulators={crucialEmulators} />}
    storeContext.showDetails('emulator', em.source, em.name, focus)} + onSelect={(id, focus) => storeContext.showDetails('emulator', 'store', id, focus)} onFocus={scrollIntoViewHandler({ block: 'end' })} emulators={recommendedEmulators} />
    -
    +

    Featured Games diff --git a/src/mainview/routes/store/tab/plugins.tsx b/src/mainview/routes/store/tab/plugins.tsx deleted file mode 100644 index 36fd70a..0000000 --- a/src/mainview/routes/store/tab/plugins.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { allPluginsFilter, installPluginMutation, uninstallPluginMutation, updatePluginMutation } from '@/mainview/scripts/queries/plugins'; -import { pluginsQuery } from '@/mainview/scripts/queries/store'; -import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts'; -import { FOCUS_KEYS } from '@/mainview/scripts/types'; -import { PluginEntryType } from '@simeonradivoev/gameflow-sdk/shared'; -import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; -import { QueryClient, useMutation, useQuery } from '@tanstack/react-query'; -import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { CircleFadingArrowUp, Dot, Download, HardDrive, Puzzle } from 'lucide-react'; -import prettyMilliseconds from 'pretty-ms'; -import { useSessionStorage } from 'usehooks-ts'; - -export const Route = createFileRoute('/store/tab/plugins')({ - component: RouteComponent -}); - -function PluginCard (data: { plugin: PluginEntryType; }) -{ - const navigate = useNavigate(); - const onAction = () => - { - navigate({ to: '/store/details/plugin/$id', params: { id: decodeURIComponent(data.plugin.package.name) } }); - }; - const { ref, focusKey } = useFocusable({ focusKey: FOCUS_KEYS.PLUGIN_ENTRY(data.plugin.package.sanitized_name), onEnterPress: onAction }); - const handleRefresh = (client: QueryClient) => - { - client.invalidateQueries(allPluginsFilter); - navigate({ to: '/store/tab/plugins', replace: true }); - }; - const update = useMutation({ - ...updatePluginMutation(data.plugin.package.name), - onSuccess (data, variables, onMutateResult, context) - { - handleRefresh(context.client); - }, - }); - const install = useMutation({ - ...installPluginMutation(data.plugin.package.name), - onSuccess (f, variables, onMutateResult, context) - { - handleRefresh(context.client); - } - }); - const uninstall = useMutation({ - ...uninstallPluginMutation(data.plugin.package.name), - onSuccess (f, variables, onMutateResult, context) - { - handleRefresh(context.client); - } - }); - useShortcuts(focusKey, () => - { - const shortcuts: Shortcut[] = [{ - label: "Details", button: GamePadButtonCode.A, action (e) - { - onAction(); - }, - }]; - - if (data.plugin.installed) - { - shortcuts.push({ - label: "Uninstall", - button: GamePadButtonCode.X, - action (e) - { - uninstall.mutate(); - }, - }); - - if (data.plugin.update) - { - shortcuts.push({ - label: "Update", - button: GamePadButtonCode.Y, - action (e) - { - update.mutate(); - }, - }); - } - - } else - { - shortcuts.push({ - label: "Install", - button: GamePadButtonCode.X, - action (e) - { - install.mutate(); - }, - }); - } - return shortcuts; - }, [data.plugin.installed, install.isPending, uninstall.isPending]); - return
    -
    -
    - {data.plugin.installed && } - {data.plugin.update && } - {data.plugin.package.name} - {(install.isPending || uninstall.isPending) && } -
    -
    {data.plugin.package.description}
    -
      {data.plugin.package.keywords.concat(...data.plugin.installed ? ["installed"] : []).map((k, i) =>
    • {k}
    • )}
    -
      -
    • {data.plugin.package.publisher.username}
    • - -
    • {data.plugin.package.version}
    • - -
    • {prettyMilliseconds(new Date().getTime() - data.plugin.package.date.getTime(), { hideSeconds: true })}
    • - -
    • {data.plugin.package.license}
    • - {install.isPending && <> - -
    • installing
    • - } - {uninstall.isPending && <> - -
    • uninstalling
    • - } -
    -
    -
    -
    - - {data.plugin.downloads.monthly} -
    -
    -
    ; -} - -function RouteComponent () -{ - const [search] = useSessionStorage(`${Route.to}-search`, undefined); - const { data: plugins } = useQuery(pluginsQuery(search)); - const { ref, focusKey } = useFocusable({ focusKey: "plugins-store" }); - return
    - -
    {plugins?.total} Plugins
    -
    - {plugins?.objects.map((p, i) => )} -
    -
    -
    ; -} diff --git a/src/mainview/routes/store/tab/route.tsx b/src/mainview/routes/store/tab/route.tsx index 81a5a01..e656b8e 100644 --- a/src/mainview/routes/store/tab/route.tsx +++ b/src/mainview/routes/store/tab/route.tsx @@ -1,31 +1,24 @@ -import { AutoFocus } from '@/mainview/components/AutoFocus'; +import { Router } from '@/mainview'; import { FilterUI } from '@/mainview/components/Filters'; import { HeaderUI } from '@/mainview/components/Header'; -import HeaderSearchField from '@/mainview/components/HeaderSearchField'; -import SelectMenu from '@/mainview/components/SelectMenu'; -import { FloatingShortcuts } from '@/mainview/components/Shortcuts'; +import Shortcuts from '@/mainview/components/Shortcuts'; import { StoreContext } from '@/mainview/scripts/contexts'; import { gameQuery } from '@/mainview/scripts/queries/romm'; import { storeEmulatorDetailsQuery } from '@/mainview/scripts/queries/store'; -import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts'; -import { HandleGoBack, mobileCheck, useStickyDataAttr } from '@/mainview/scripts/utils'; +import { GamePadButtonCode, useShortcutContext, useShortcuts } from '@/mainview/scripts/shortcuts'; +import { mobileCheck, useStickyDataAttr } from '@/mainview/scripts/utils'; import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; import { useQueryClient } from '@tanstack/react-query'; -import { useMatchRoute, useRouter } from '@tanstack/react-router'; +import { useMatchRoute } from '@tanstack/react-router'; import { createFileRoute, Outlet } from '@tanstack/react-router'; import { zodValidator } from '@tanstack/zod-adapter'; -import { DownloadCloud, Gamepad2, Home, Joystick, Puzzle } from 'lucide-react'; -import { useRef } from 'react'; -import { useSessionStorage } from 'usehooks-ts'; +import { Settings } from 'lucide-react'; +import { useEffect, useRef } from 'react'; import z from 'zod'; export const Route = createFileRoute('/store/tab')({ component: RouteComponent, - validateSearch: zodValidator(z.object({ focus: z.string().optional() })), - staticData: { - enterSound: 'openStore', - enterHaptic: 'navigateStore' - } + validateSearch: zodValidator(z.object({ focus: z.string().optional() })) }); function useIsSettings (subPath: string) @@ -40,7 +33,6 @@ function useIsSettings (subPath: string) function TopArea (data: { filters: Record; }) { - const router = useRouter(); const { ref, focusKey } = useFocusable({ focusKey: 'top-area', preferredChildFocusKey: `store-tabs`, @@ -52,13 +44,13 @@ function TopArea (data: { filters: Record; }) useShortcuts("STORE_ROOT", () => [{ label: "Return", - action: (e) => HandleGoBack(router, e), + action: () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-out'] } }), button: GamePadButtonCode.B - }], [router]); + }], []); const handleNavigate = (s: string) => { - router.navigate({ to: `/store/tab/${s === 'home' ? '' : s}`, viewTransition: { types: ['slide-up'] }, replace: true }); + Router.navigate({ to: `/store/tab/${s === 'home' ? '' : s}`, viewTransition: { types: ['slide-up'] }, replace: true }); }; return
    @@ -84,7 +76,6 @@ function StoreOutlet () function RouteComponent () { // Root spatial nav container - const router = useRouter(); const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "STORE_ROOT", preferredChildFocusKey: 'top-area', @@ -94,26 +85,33 @@ function RouteComponent () const headerRef = useRef(null); const sentinelRef = useRef(null); const filters: Record = { - home: { label: "Home", icon: , selected: useIsSettings(''), }, - emulators: { label: "Emulators", icon: , selected: useIsSettings('emulators') }, - games: { label: "Games", icon: , selected: useIsSettings('games') }, - download: { label: "Download", icon: , selected: useIsSettings('download') }, - plugins: { label: "Plugins", icon: , selected: useIsSettings('plugins') } + home: { label: "Home", selected: useIsSettings(''), }, + emulators: { label: "Emulators", selected: useIsSettings('emulators') }, + games: { label: "Games", selected: useIsSettings('games') } }; - const [search, setSearch] = useSessionStorage(`${router.history.location.pathname}-search`, undefined); - const [, setGamesSearch] = useSessionStorage(`/store/tab/games-search`, undefined); + + const { shortcuts } = useShortcutContext(); + const { focus } = Route.useSearch(); + + useEffect(() => + { + if (!focus) + { + focusSelf(); + } + }, []); const handleDetails = (type: string, source: string, id: string, focus: string) => { if (type === 'emulator') { - if (!source || source === 'local') return; - router.navigate({ to: '/store/details/emulator/$id', params: { id } }); + Router.navigate({ to: '/store/details/emulator/$id', params: { id } }); } else if (type === 'game') { - router.navigate({ to: '/game/$source/$id', params: { source: source, id: id } }); + Router.navigate({ to: '/game/$source/$id', params: { source: source, id: id } }); } + }; const handlePrefetch = (type: string, source: string, id: string) => @@ -128,19 +126,6 @@ function RouteComponent () } }; - const handleSearch = (search: string | undefined) => - { - if (filters['home'].selected) - { - setGamesSearch(search); - router.navigate({ to: '/store/tab/games', replace: true, viewTransition: { types: ['slide-up'] } }); - } else - { - setSearch(search); - } - - }; - const isMobile = mobileCheck(); useStickyDataAttr(headerRef, sentinelRef, ref); @@ -150,20 +135,20 @@ function RouteComponent ()
    - } /> +
    +
    + +
    {!isMobile && <>
    }
    - - -
    ; } diff --git a/src/mainview/scripts/audio/audio.ts b/src/mainview/scripts/audio/audio.ts deleted file mode 100644 index 430e4de..0000000 --- a/src/mainview/scripts/audio/audio.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Howl } from 'howler'; -import sounds from '../../assets/sounds.ogg'; -import soundSprites from '../../assets/sounds.json'; -import { getLocalSetting } from '../utils'; -import { hapticMap } from '../gamepads'; -import { soundMap, SoundMapEntry } from './audioConstants'; - -const timingMap = new Map(); - -// Browsers need input to start any sound, so intro doesn't auto play. -/*const introSound = new Howl({ - src: [intro], - volume: getLocalSetting("soundEffectsVolume") / 100, - autoplay: true, -});*/ - -const sound = new Howl({ - src: [sounds], - sprite: soundSprites.sprite as any, - volume: getLocalSetting("soundEffectsVolume") / 100, -}); - -import.meta.hot?.dispose(() => { sound.unload(); }); - -declare module '@tanstack/react-router' { - interface StaticDataRouteOption - { - enterSound?: keyof typeof soundMap | null; - enterHaptic?: keyof typeof hapticMap | null; - goBackSound?: keyof typeof soundMap | null; - missNavSound?: boolean; - } -} - -function sinRandom () -{ - return Math.sin(new Date().getMilliseconds() / 1000 * Math.PI); -} - - -function random () -{ - return Math.random() * 2 - 1; -} - -export function oneShot (id: keyof typeof soundMap, options?: { volume?: number; }) -{ - const currentDate = timingMap.get(id); - if (!getLocalSetting('soundEffects')) return; - const soundValue = soundMap[id] as SoundMapEntry; - const maxDelay = soundValue.maxDelay ?? 100; - if (currentDate && new Date().getTime() - currentDate.getTime() <= maxDelay) return; - - const instanceId = sound.play(soundValue.key); - const baseVolume = getLocalSetting("soundEffectsVolume") / 100; - sound.volume(Math.min(baseVolume * (soundValue.volume ?? 1) * (options?.volume ?? 1) * (1 + random() * (soundValue.volumeVariation ?? 0), 1)), instanceId); - sound.rate(1 + sinRandom() * (soundValue.rateVariation ?? 0), instanceId); - timingMap.set(id, new Date()); -} - diff --git a/src/mainview/scripts/audio/audioConstants.ts b/src/mainview/scripts/audio/audioConstants.ts deleted file mode 100644 index 25c85c6..0000000 --- a/src/mainview/scripts/audio/audioConstants.ts +++ /dev/null @@ -1,38 +0,0 @@ -import soundSprites from '../../assets/sounds.json'; - -const volumeVariation = 0.05; -const rateVariation = 0.02; - -export interface SoundMapEntry -{ - key: keyof typeof soundSprites.sprite; - rateVariation?: number; - volumeVariation?: number; - volume?: number; - maxDelay?: number; -} - -export const soundMap = { - openDetails: { key: 'Classic UI SFX - Chords #2' }, - returnGeneric: { key: 'Classic UI SFX - Short - Low #2' }, - returnDetails: { key: 'Classic UI SFX - Short - Low #5' }, - openGeneric: { key: 'Classic UI SFX - Short - High #9' }, - select: { key: "UI_TwoNote Up_Set 11_01", rateVariation, volumeVariation }, - selectAlt: { key: "UI_TwoNote Up_Set 11_01", rateVariation, volumeVariation }, - selectMenu: { key: "UI_TwoNote Up_Set 11_02", rateVariation, volumeVariation }, - selectFilter: { key: 'Classic UI SFX - Short - High #3', volumeVariation }, - closeContext: { key: 'Classic UI SFX - Short - High #19' }, - openContext: { key: 'Classic UI SFX - Short - High #22' }, - openKeyboard: { key: 'Classic UI SFX - Short - High #25' }, - openStore: { key: 'Classic UI SFX - Chords #16' }, - openSettings: { key: 'Classic UI SFX - Short - High #8' }, - click: { key: "UI_Single_Set 16_03", rateVariation, volumeVariation }, - clickAlt: { key: "UI_Single_Set 16_01", rateVariation, volumeVariation }, - keyPress: { key: "UI_Single_Set 5_02", rateVariation, volumeVariation }, - keyPressReturn: { key: "UI_Single_Set 5_04", rateVariation, volumeVariation }, - keyPressSpace: { key: "UI_Single_Set 5_03", rateVariation, volumeVariation }, - keyPressBackspace: { key: "UI_Single_Set 5_01", rateVariation, volumeVariation }, - keyHover: { key: "UI_Single_Set 11_02", rateVariation, volumeVariation, volume: 0.5, maxDelay: 60 }, - invalidNavigation: { key: "Classic UI SFX - Short - Low #6", rateVariation, volumeVariation }, - launch: { key: "UI SFX_InGameMenu_Open" } -} satisfies Record; \ No newline at end of file diff --git a/src/mainview/scripts/brandIcons.tsx b/src/mainview/scripts/brandIcons.tsx index bf2e576..ef35534 100644 --- a/src/mainview/scripts/brandIcons.tsx +++ b/src/mainview/scripts/brandIcons.tsx @@ -3,8 +3,4 @@ export const TwitchIcon = ; -export const IGDBIcon = IGDB; - -export const Rclone = Rclone; - export const FlatpackIcon = Flathub; \ No newline at end of file diff --git a/src/mainview/scripts/contexts.ts b/src/mainview/scripts/contexts.ts index 0c958b1..3257bfb 100644 --- a/src/mainview/scripts/contexts.ts +++ b/src/mainview/scripts/contexts.ts @@ -1,7 +1,6 @@ -import { SystemInfoType, Drive, AppInfoContext } from '@simeonradivoev/gameflow-sdk/shared'; -import { Direction, FocusDetails } from "@noriginmedia/norigin-spatial-navigation"; +import { SystemInfoType } from "@/shared/constants"; +import { FocusDetails } from "@noriginmedia/norigin-spatial-navigation"; import { createContext } from "react"; -import { Shortcut } from "./shortcuts"; export const StoreContext = createContext({} as { showDetails: (type: 'emulator' | 'game', source: string, id: string, focusSource: string) => void; @@ -21,8 +20,6 @@ export const OptionContext = createContext( focused: boolean; focus: (focusDetails?: FocusDetails | undefined) => void; eventTarget: EventTarget; - setFocusBoundary: (b: boolean) => void; - setFocusBoundaryDirections: (dirs: Direction[]) => void; }, ); @@ -37,24 +34,8 @@ export const FilePickerContext = createContext<{ activeDrive: Drive | undefined; }>({} as any); -export const ShortcutsContext = createContext({} as { - shortcuts: ({ - key: string; - } & Shortcut)[] | undefined; -}); - export const SystemInfoContext = createContext({} as SystemInfoType | undefined); -export const AppContext = createContext({} as AppInfoContext); - -export const GlobalDialogContext = createContext({} as { - openContext: (options: { - content: any; - preferredChildFocusKey?: string; - onClose?: () => void; - }, focusKey: string) => void; -}); - export const GameDetailsContext = createContext<{ update: () => void; }>({} as any); \ No newline at end of file diff --git a/src/mainview/scripts/feedbackCallbacks.ts b/src/mainview/scripts/feedbackCallbacks.ts deleted file mode 100644 index f103d68..0000000 --- a/src/mainview/scripts/feedbackCallbacks.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Router } from "@/mainview"; -import { soundMap } from "./audio/audioConstants"; -import { oneShotRumble } from "./gamepads"; -import { oneShot } from "./audio/audio"; - -export default function load () -{ - let lastLocationPath: string | undefined; - const unsub = Router.history.subscribe((op) => - { - if (op.action.type === 'PUSH') - { - lastLocationPath = op.location.pathname; - - const routes = Router.matchRoutes(op.location.pathname); - const soundRoute = routes.find(r => r.staticData.enterSound !== undefined); - if (soundRoute) - { - oneShot(soundRoute.staticData.enterSound!); - } else - { - oneShot("openGeneric"); - } - - if (op.location.state.eventType === 'gamepadbuttondown') - { - const hapticRoute = routes.find(r => r.staticData.enterHaptic !== undefined); - if (hapticRoute) oneShotRumble(hapticRoute.staticData.enterHaptic!, { all: true }); - else oneShotRumble('navigateForward', { all: true }); - } - } else if (op.action.type === 'BACK') - { - if (lastLocationPath) - { - const soundRoutes = Router.matchRoutes(lastLocationPath); - const soundRoute = soundRoutes.find(r => r.staticData.goBackSound !== undefined); - if (soundRoute) - { - if (soundRoute.staticData.goBackSound) oneShot(soundRoute.staticData.goBackSound); - } else - { - oneShot("returnGeneric"); - } - } else - { - oneShot("returnGeneric"); - } - - lastLocationPath = op.location.state.key; - } - }); - - let focusChangeDebounced: undefined | NodeJS.Timeout; - - const focuschangedHandler = (e: CustomEvent) => - { - clearTimeout(focusChangeDebounced); - if (!e.detail.focusKeyChanged) return; - - if (e.detail.nativeEvent || e.detail.event) - { - let sound: keyof typeof soundMap; - if (e.detail.node && e.detail.node.matches('[data-sound-category="menu"]')) - { - sound = 'selectMenu'; - - } else if (e.detail.node && e.detail.node.matches('[data-sound-category="filter"]')) - { - sound = "selectFilter"; - } - else if (e.detail.node && e.detail.node.matches('[data-sound-category="emulator"]')) - { - sound = "selectAlt"; - } - else if (!e.detail.node || !e.detail.node.matches('[data-sound-disable="focus"]')) - { - sound = e.detail.sound as any ?? 'select'; - } - - setTimeout(() => - { - if (e.detail.nativeEvent || e.detail.event) - { - oneShot(sound); - oneShotRumble('select', { event: e.detail.event }); - } - }, 10); - } - }; - - window.addEventListener('focuschanged', focuschangedHandler as any); - - return { - cleanup: () => - { - unsub(); - window.removeEventListener('focuschanged', focuschangedHandler as any); - } - }; -} \ No newline at end of file diff --git a/src/mainview/scripts/gamepads.ts b/src/mainview/scripts/gamepads.ts index 5959d90..fed3f3d 100644 --- a/src/mainview/scripts/gamepads.ts +++ b/src/mainview/scripts/gamepads.ts @@ -1,9 +1,7 @@ import { getCurrentFocusKey, navigateByDirection } from "@noriginmedia/norigin-spatial-navigation"; import { GetFocusedElement } from "./spatialNavigation"; import { useEffect, useState } from "react"; -import { getLocalSetting, isTextInputFocused, mobileCheck } from "./utils"; -import { oneShot } from "./audio/audio"; -import { Router } from "@/mainview"; +import { mobileCheck } from "./utils"; let loopStarted = false; let isTouching = false; @@ -98,11 +96,6 @@ const throttleMap = new Map(); const throttleAcceleration = new Map(); function throttleNav (key: string, dir: string, event: Event) { - if (isTextInputFocused()) - { - return false; - } - const minSpeed = 150; const maxSpeed = 300; const currentDate = new Date(); @@ -111,16 +104,7 @@ function throttleNav (key: string, dir: string, event: Event) const speed = Math.max(maxSpeed - (maxSpeed - minSpeed) * (acceleration / 6), minSpeed); if ((currentDate.getTime() - (lastTime ?? 0) > speed)) { - const currentFocusKey = getCurrentFocusKey(); navigateByDirection(dir, { event }); - if (currentFocusKey === getCurrentFocusKey()) - { - const routes = Router.matchRoutes(Router.history.location.pathname); - if (!routes.some(r => r.staticData.missNavSound === false)) - { - oneShot('invalidNavigation'); - } - } throttleMap.set(key, currentDate.getTime()); throttleAcceleration.set(key, acceleration + 1); return true; @@ -292,45 +276,4 @@ function updateStatus () } requestAnimationFrame(updateStatus); -} - -export const hapticMap = { - select: [{ duration: 50, strongMagnitude: 0, weakMagnitude: 1 }], - navigateForward: [{ duration: 50, strongMagnitude: 0.2, weakMagnitude: 0.2 }, { duration: 100, strongMagnitude: 0.5, weakMagnitude: 0.5 }], - navigateBack: [{ duration: 100, strongMagnitude: 0.5, weakMagnitude: 0.5 }, { duration: 50, strongMagnitude: 0.2, weakMagnitude: 0.2 }], - navigateStore: [{ duration: 200, strongMagnitude: 0.5, weakMagnitude: 0.5 }, { duration: 300, strongMagnitude: 0.2, weakMagnitude: 0.2 }], - openContext: [{ duration: 50, strongMagnitude: 0.5, weakMagnitude: 0.5 }, { duration: 50, strongMagnitude: 0.0, weakMagnitude: 0.0 }, { duration: 50, strongMagnitude: 0.2, weakMagnitude: 0.2 }], -} satisfies Record; - -let lastRumble: AbortController; - -export function oneShotRumble (effect: keyof typeof hapticMap, init?: { event?: Event, all?: boolean; }) -{ - if (!getLocalSetting('hapticsEffects')) return; - - async function play (g: Gamepad) - { - lastRumble = new AbortController(); - for (const e of hapticMap[effect]) - { - await new Promise(resolve => - { - g.vibrationActuator.playEffect('dual-rumble', e); - const timeout = setTimeout(() => resolve(true), e.duration + 50); - lastRumble.signal.onabort = () => clearTimeout(timeout); - if (lastRumble.signal.aborted) resolve(false); - }); - - if (lastRumble.signal.aborted) return; - } - } - - if (lastRumble) lastRumble.abort(); - if (init?.event instanceof GamepadEvent || init?.event instanceof GamepadButtonEvent) - { - if (init?.event.gamepad) play(init?.event.gamepad); - } else if (init?.all) - { - navigator.getGamepads().filter(g => !!g).forEach(g => play(g)); - } } \ No newline at end of file diff --git a/src/mainview/scripts/queries/plugins.ts b/src/mainview/scripts/queries/plugins.ts index 8e6e948..6be97dc 100644 --- a/src/mainview/scripts/queries/plugins.ts +++ b/src/mainview/scripts/queries/plugins.ts @@ -1,4 +1,4 @@ -import { mutationOptions, QueryFilters, queryOptions } from "@tanstack/react-query"; +import { mutationOptions, queryOptions } from "@tanstack/react-query"; import { pluginsApi } from "../clientApi"; export const getAllPluginsQuery = queryOptions({ @@ -11,64 +11,11 @@ export const getAllPluginsQuery = queryOptions({ } }); -export const getPluginDetailsQuery = (source: string) => queryOptions({ - queryKey: ['plugins', source], queryFn: async () => - { - const { data, error } = await pluginsApi.plugins({ id: encodeURIComponent(source) }).get(); - if (error) throw error; - return data; - } -}); - export const enablePluginMutation = mutationOptions({ mutationKey: ['plugin', 'enable'], mutationFn: async (vars: { id: string, enabled: boolean; }) => { - const { error } = await pluginsApi.plugins({ id: encodeURIComponent(vars.id) }).post({ enabled: vars.enabled }); + const { error } = await pluginsApi.plugins({ id: vars.id }).post({ enabled: vars.enabled }); if (error) throw error; } -}); - -export const installPluginMutation = (id: string) => mutationOptions({ - mutationKey: ['plugin', 'install', id], - mutationFn: async () => - { - const { data, error } = await pluginsApi.plugins.install.post({ id }); - if (error) throw error; - return data; - } -}); - -export const updatePluginMutation = (id: string) => mutationOptions({ - mutationKey: ['plugin', 'update', id], - mutationFn: async () => - { - const { data, error } = await pluginsApi.plugins.update.post({ id }); - if (error) throw error; - return data; - } -}); - -export const uninstallPluginMutation = (id: string) => mutationOptions({ - mutationKey: ['plugin', 'uninstall', id], - mutationFn: async () => - { - const { data, error } = await pluginsApi.plugins.uninstall.post({ id: id }); - if (error) throw error; - return data; - } -}); - -export const pluginFilter = (id: string): QueryFilters => ({ - predicate (query) - { - return query.queryKey.includes(id); - }, -}); - -export const allPluginsFilter: QueryFilters = ({ - predicate (query) - { - return query.queryKey.includes('plugin') || query.queryKey.includes('plugins'); - }, }); \ No newline at end of file diff --git a/src/mainview/scripts/queries/romm.ts b/src/mainview/scripts/queries/romm.ts index cf059f3..4ecf6ca 100644 --- a/src/mainview/scripts/queries/romm.ts +++ b/src/mainview/scripts/queries/romm.ts @@ -1,7 +1,6 @@ -import { DefaultRommStaleTime } from "@/shared/constants"; -import { GameListFilterType, RommLoginDataSchema, FrontEndId, DownloadLookupEntry, DownloadsLookupFilter } from '@simeonradivoev/gameflow-sdk/shared'; +import { DefaultRommStaleTime, GameListFilterType, RommLoginDataSchema } from "@/shared/constants"; import { rommApi, settingsApi } from "../clientApi"; -import { infiniteQueryOptions, InvalidateQueryFilters, mutationOptions, QueryClient, QueryFilters, queryOptions } from "@tanstack/react-query"; +import { mutationOptions, QueryFilters, queryOptions } from "@tanstack/react-query"; import z from "zod"; import { statsApiStatsGetOptions } from "@/clients/romm/@tanstack/react-query.gen"; @@ -24,20 +23,6 @@ export const gameQuery = (source: string, id: string) => queryOptions({ }, }); export const rommLogoutMutation = mutationOptions({ mutationKey: ["romm", "auth", "logout"], mutationFn: () => rommApi.api.romm.logout.romm.post() }); -export const invalidateLogin = (client: QueryClient) => -{ - return client.invalidateQueries({ - predicate (query) - { - return query.queryKey.includes('auth') - || query.queryKey.includes('games') - || query.queryKey.includes('game') - || query.queryKey.includes('platform') - || query.queryKey.includes('platforms') - || query.queryKey.includes('collections'); - }, - }); -}; export const rommQrLoginMutation = mutationOptions({ mutationKey: ['login', 'qr', 'cancel'], mutationFn: async () => @@ -45,11 +30,7 @@ export const rommQrLoginMutation = mutationOptions({ const { data, error } = await rommApi.api.romm.login.romm.qr.post(); if (error) throw error; return data; - }, - onSuccess: (d, v, r, c) => - { - invalidateLogin(c.client); - }, + } }); export const rommLoginMutation = mutationOptions({ mutationKey: ["romm", "login"], @@ -60,7 +41,7 @@ export const rommLoginMutation = mutationOptions({ }, onSuccess: (d, v, r, c) => { - invalidateLogin(c.client); + c.client.invalidateQueries({ queryKey: ['romm', 'auth'] }); }, onError: (e) => { @@ -91,8 +72,8 @@ export const rommLoggedInQuery = queryOptions({ return data; } }); -export const rommHostnameQuery = queryOptions({ queryKey: ['romm', 'auth', 'hostname'], queryFn: () => settingsApi.api.settings({ source: 'local' })({ id: 'rommAddress' }).get().then(d => d.data?.value as string) }); -export const rommUsernameQuery = queryOptions({ queryKey: ['romm', 'auth', 'username'], queryFn: () => settingsApi.api.settings({ source: 'local' })({ id: 'rommUser' }).get().then(d => d.data?.value as string) }); +export const rommHostnameQuery = queryOptions({ queryKey: ['romm', 'auth', 'hostname'], queryFn: () => settingsApi.api.settings({ id: 'rommAddress' }).get().then(d => d.data?.value as string) }); +export const rommUsernameQuery = queryOptions({ queryKey: ['romm', 'auth', 'username'], queryFn: () => settingsApi.api.settings({ id: 'rommUser' }).get().then(d => d.data?.value as string) }); export const deleteGameMutation = (id: FrontEndId) => mutationOptions({ mutationKey: ['delete', id], mutationFn: () => rommApi.api.romm.game({ source: id.source })({ id: id.id }).delete() @@ -126,9 +107,9 @@ export const platformQuery = (source: string, id: string) => queryOptions({ }); export const installMutation = (source: string, id: string) => mutationOptions({ mutationKey: ['install', source, id], - mutationFn: async (init: { downloadId?: string; }) => + mutationFn: async () => { - const { data, error } = await rommApi.api.romm.game({ source })({ id }).install.post({ downloadId: init.downloadId }); + const { data, error } = await rommApi.api.romm.game({ source })({ id }).install.post(); if (error) throw error; return data; } @@ -167,9 +148,6 @@ export const gamesRecommendedBasedOnGameQuery = (source: string, id: string) => return data; } }); -export const allGamesInvalidateQuery: QueryFilters = { - queryKey: ['games'] -}; export const gameInvalidationQuery = (source: string, id: string): QueryFilters => ({ predicate (query) { @@ -177,157 +155,4 @@ export const gameInvalidationQuery = (source: string, id: string): QueryFilters if (query.queryKey.includes(source) && query.queryKey.includes(id)) return true; return false; }, -}); -export const validateSourceQuery = (source: string, id: string) => queryOptions({ - queryKey: ["game", source, id, "validate"], queryFn: async () => - { - const { data } = await rommApi.api.romm.game({ source })({ id }).validate.get(); - return data; - } -}); -export const fixSourceMutation = mutationOptions({ - mutationKey: ['game', "fix_source"], mutationFn: async ({ source, id }: { source: string, id: string; }) => - { - const { data, error } = await rommApi.api.romm.game({ source })({ id }).fix_source.post(); - if (error) throw error; - return data; - } -}); -export const updateSourceMutation = mutationOptions({ - mutationKey: ['game', "update_source"], mutationFn: async ({ source, id }: { source: string, id: string; }) => - { - const { data, error } = await rommApi.api.romm.game({ source })({ id }).update.post({ - source: source, - id: id - }); - if (error) throw error; - return data; - } -}); -export const updatePlatformMutation = (source: string, id: string) => mutationOptions({ - mutationKey: ['platform', source, 'update', id], - mutationFn: async () => - { - const { data, error } = await rommApi.api.romm.platform({ source })({ id }).update.post(); - if (error) throw error; - return data; - } -}); -export const deletePlatformMutation = (id: string) => mutationOptions({ - mutationKey: ['platform', 'local', 'delete', id], - mutationFn: async () => - { - const { data, error } = await rommApi.api.romm.platform.local({ id }).delete(); - if (error) throw error; - return data; - } -}); -export const localPlatformFilter = (id: string) => ({ - predicate (query) - { - return query.queryKey.includes('platform') && ((query.queryKey.includes('local') && query.queryKey.includes(id)) || query.queryKey.includes('all')); - }, -} satisfies InvalidateQueryFilters as InvalidateQueryFilters); - -export const gameFiltersQuery = (filters: { source?: string; }) => queryOptions({ - queryKey: ['game', 'filters', filters], queryFn: async () => - { - const { data, error } = await rommApi.api.romm.games.filters.get({ query: { source: filters.source } }); - if (error) throw error; - return data; - } -}); - -export const gameLookupQuery = (search: string | undefined) => queryOptions({ - queryKey: ['game', 'lookup', search], - queryFn: async () => - { - if (!search) return []; - const { data, error } = await rommApi.api.romm.lookup.get({ query: { search } }); - if (error) throw error; - return data; - } -}); - -export const gameLookupDetails = (source: string | undefined, id: string | undefined) => queryOptions({ - enabled: !!source && !!id, - queryKey: ['game', 'lookup', source, id], - queryFn: async () => - { - const { data, error } = await rommApi.api.romm.lookup({ source: source! })({ id: id! }).get(); - if (error) throw error; - return data; - } -}); - -export const platformLookupMatchQuery = (source: string | undefined, id: number | undefined) => queryOptions({ - enabled: !!source && !!id, - queryKey: ['platform', 'lookup', 'match', source, id], - queryFn: async () => - { - const { data, error } = await rommApi.api.romm.platform.lookup.match({ source: source! })({ id: id! }).get(); - if (error) throw error; - return data; - } -}); - -export const customUpdateMutation = mutationOptions({ - mutationKey: ['game', 'custom-update'], mutationFn: async (args: { source: string, id: string, destination: string, destinationId: string; }) => - { - const { data, error } = await rommApi.api.romm.game({ source: args.source })({ id: args.id }).update.post({ source: args.destination, id: args.destinationId }); - if (error) throw error; - return data; - } -}); - -export const addManualGameMutation = mutationOptions({ - mutationKey: ['game', 'custom-add'], - mutationFn: async (args: { source: string, id: string, gamePath: string, platformId: number; }) => - { - const { data, error } = await rommApi.api.romm.add.custom.post({ - source: args.source, - id: args.id, - gamePath: args.gamePath, - platformId: args.platformId - }); - if (error) throw error; - return data; - } -}); - -export const downloadsLookupQuery = (filter: DownloadsLookupFilter) => infiniteQueryOptions<{ - data: DownloadLookupEntry[], - totalCount: number, - nextPage: number; - hadMatchers: boolean; -}>({ - initialPageParam: 1, - queryKey: ["downloads", filter], - getNextPageParam: (lastPage, pages) => lastPage.nextPage, - queryFn: async (params) => - { - const pageParam = params.pageParam as number; - const { data, error } = await rommApi.api.romm.downloads.lookup.get({ query: { ...filter, page: pageParam } }); - if (error) throw error; - return { data: data.matches, totalCount: data.totalCount, hadMatchers: data.hadMatchers, nextPage: pageParam + 1 }; - } -}); - -export const downloadLookupQuery = (source: string, id: string) => queryOptions({ - queryKey: ["downloads", source, id], - queryFn: async () => - { - const { data, error } = await rommApi.api.romm.download.lookup({ source: encodeURIComponent(source) })({ id: encodeURIComponent(id) }).get(); - if (error) throw error; - return data; - } -}); - -export const downloadLookupFiltersQuery = queryOptions({ - queryKey: ['game', 'filters'], queryFn: async () => - { - const { data, error } = await rommApi.api.romm.download.lookup.filters.get(); - if (error) throw error; - return data; - } }); \ No newline at end of file diff --git a/src/mainview/scripts/queries/settings.ts b/src/mainview/scripts/queries/settings.ts index 03956af..186b224 100644 --- a/src/mainview/scripts/queries/settings.ts +++ b/src/mainview/scripts/queries/settings.ts @@ -2,7 +2,6 @@ import { mutationOptions, queryOptions } from "@tanstack/react-query"; import { getErrorMessage } from "react-error-boundary"; import toast from "react-hot-toast"; import { rommApi, settingsApi } from "../clientApi"; -import { invalidateLogin } from "./romm"; export const changeDownloadsMutation = mutationOptions({ mutationKey: ["setting", "downloads"], @@ -30,25 +29,21 @@ export const autoEmulatorsQuery = queryOptions({ } }); export const twitchLogoutMutation = mutationOptions({ - mutationKey: ['twitch', 'auth', 'logout'], + mutationKey: ['twitch', 'logout'], mutationFn: () => { return rommApi.api.romm.logout.twitch.post(); } }); export const twitchLoginMutation = mutationOptions({ - mutationKey: ['twitch', 'auth', 'login'], + mutationKey: ['twitch', 'login'], mutationFn: (openInBrowser: boolean) => { return rommApi.api.romm.login.twitch.post({ openInBrowser }); - }, - onSuccess (data, variables, onMutateResult, context) - { - invalidateLogin(context.client); - }, + } }); export const twitchLoginVerificationQuery = queryOptions({ - queryKey: ['twitch', 'login', 'status', 'auth'], + queryKey: ['twitch', 'login', 'status'], retry (failureCount, error) { if ((error as any).status === 404) @@ -118,7 +113,7 @@ export const setSettingMutation = (id?: string) => mutationOptions({ mutationKey: ["setting", id], mutationFn: async (value: any) => { - const response = await settingsApi.api.settings.local({ id: id! }).post({ value }); + const response = await settingsApi.api.settings({ id: id! }).post({ value }); if (response.error) throw response.error; return response.data; } @@ -128,58 +123,9 @@ export const getSettingQuery = (id: string | undefined) => queryOptions({ queryKey: ["setting", id], queryFn: async () => { - const { data: value, error } = await settingsApi.api.settings.local({ id: id! }).get(); + const { data: value, error } = await settingsApi.api.settings({ id: id! }).get(); if (error) throw error; return value.value; }, -}); -export const getPluginSettingsDefinitionQuery = (source: string) => queryOptions({ - queryKey: ['settings', source, 'definitions'], - queryFn: async () => - { - const { data: value, error } = await settingsApi.api.settings.definitions({ source: encodeURIComponent(source) }).get(); - if (error) throw error; - - return value; - } -}); -export const getPluginSettingQuery = (source: string, id: string) => queryOptions({ - queryKey: ["setting", source, id], - queryFn: async () => - { - const { data, error } = await settingsApi.api.settings({ source: encodeURIComponent(source) })({ id: encodeURIComponent(id) }).get(); - if (error) throw error; - - return data; - }, -}); -export const setPluginSettingMutation = (source: string, id: string) => mutationOptions({ - mutationKey: ["setting", source, id], - mutationFn: async (value: any) => - { - const { data, error } = await settingsApi.api.settings({ source: encodeURIComponent(source) })({ id: encodeURIComponent(id) }).put({ value }); - if (error) throw error; - - return data; - }, -}); -export const getPluginActionsQuery = (source: string) => queryOptions({ - queryKey: ['plugin', source, 'actions'], queryFn: async () => - { - const { data, error } = await settingsApi.api.settings.actions({ source: encodeURIComponent(source) }).get(); - if (error) throw error; - - return data; - } -}); -export const pluginActionMutation = (source: string, id: string) => mutationOptions({ - mutationKey: ["plugin", source, "action"], - mutationFn: async () => - { - const { data, error, response } = await settingsApi.api.settings.actions({ source: encodeURIComponent(source) })({ id: encodeURIComponent(id) }).post(); - if (error) throw error; - - return { data: data as any, response }; - }, }); \ No newline at end of file diff --git a/src/mainview/scripts/queries/store.ts b/src/mainview/scripts/queries/store.ts index a428f40..e7b84f1 100644 --- a/src/mainview/scripts/queries/store.ts +++ b/src/mainview/scripts/queries/store.ts @@ -1,12 +1,12 @@ import { infiniteQueryOptions, mutationOptions, queryOptions } from "@tanstack/react-query"; import { rommApi, storeApi } from "../clientApi"; -import { GameListFilterType, FrontEndGameType } from '@simeonradivoev/gameflow-sdk/shared'; -export const storeEmulatorsQuery = (filters: { search?: string; }) => queryOptions({ - queryKey: ['store-emulators', filters], queryFn: async () => + +export const storeEmulatorsQuery = queryOptions({ + queryKey: ['store-emulators'], queryFn: async () => { - const { data, error } = await storeApi.api.store.emulators.get({ query: { search: filters.search } }); - if (error) throw new Error(JSON.stringify(error.value)); + const { data, error } = await storeApi.api.store.emulators.get(); + if (error) throw error; return data; } }); @@ -42,20 +42,18 @@ export const storeEmulatorDeleteMutation = mutationOptions({ if (error) throw error; } }); - -export const storeGamesInfiniteQuery = (filter: GameListFilterType) => infiniteQueryOptions<{ data: FrontEndGameType[], nextPage: number; }>({ +export const storeGamesInfiniteQuery = infiniteQueryOptions<{ data: FrontEndGameType[], nextPage: number; }>({ initialPageParam: 0, - queryKey: ['store-games', filter], + queryKey: ['store-games'], getNextPageParam: (lastPage, pages) => lastPage.nextPage, queryFn: async (data) => { const pageParam = data.pageParam as number; - const { data: games, error } = await rommApi.api.romm.games.get({ query: { ...filter, source: 'store', offset: pageParam * 10, limit: 10 } }); + const { data: games, error } = await rommApi.api.romm.games.get({ query: { source: 'store', offset: pageParam * 10, limit: 10 } }); if (error) throw error; return { data: games.games, nextPage: pageParam + 1 }; } }); - export const storeGetStatsQuery = queryOptions({ queryKey: ['store', 'stats'], queryFn: async () => { @@ -66,9 +64,9 @@ export const storeGetStatsQuery = queryOptions({ }); export const installEmulatorMutation = (id: string) => mutationOptions({ mutationKey: ['install', 'emulator', id], - mutationFn: async (ctx: { source: string, isUpdate: boolean; }) => + mutationFn: async (source: string) => { - const { data, error } = await storeApi.api.store.install.emulator({ id })({ source: ctx.source }).post({ isUpdate: ctx.isUpdate }); + const { data, error } = await storeApi.api.store.install.emulator({ id })({ source }).post(); if (error) throw error; return data; } @@ -87,30 +85,4 @@ export const deleteBiosMutation = mutationOptions({ const { error } = await storeApi.api.store.bios({ id }).delete(); if (error) throw error; } -}); -export const getUpdateInfoForEmulator = (id: string) => queryOptions({ - queryKey: ['emulator', 'update'], queryFn: async () => - { - const { data, error } = await storeApi.api.store.emulator({ id }).update.get(); - if (error) throw error; - return data; - } -}); -export const pluginsQuery = (search?: string) => queryOptions({ - queryKey: ['plugins', 'store', search ?? 'all'], - queryFn: async () => - { - const { data, error } = await storeApi.api.store.plugins.get({ query: { search } }); - if (error) throw error; - return data; - } -}); -export const pluginDetailsQuery = (id: string) => queryOptions({ - queryKey: ['plugin', 'store', id], - queryFn: async () => - { - const { data, error } = await storeApi.api.store.plugin.get({ query: { plugin: id } }); - if (error) throw error; - return data; - } }); \ No newline at end of file diff --git a/src/mainview/scripts/queries/system.ts b/src/mainview/scripts/queries/system.ts index ffc503f..853e3a9 100644 --- a/src/mainview/scripts/queries/system.ts +++ b/src/mainview/scripts/queries/system.ts @@ -46,34 +46,4 @@ export const closeMutation = mutationOptions({ const { error } = await systemApi.api.system.exit.post(); if (error) throw error; } -}); -export const hasUpdateQuery = queryOptions({ - queryKey: ['update'], - queryFn: async () => - { - const { data, error } = await systemApi.api.system.update.get(); - if (error) throw error; - return data; - }, - staleTime: 1000 * 60 * 30 -}); - -export const checkUpdateMutation = mutationOptions({ - mutationKey: ['update', 'check'], - mutationFn: async () => - { - const { data, error } = await systemApi.api.system.update.check.post(); - if (error) throw error; - return data; - }, -}); - -export const updateMutation = mutationOptions({ - mutationKey: ['update'], - mutationFn: async () => - { - const { data, error } = await systemApi.api.system.update.post(); - if (error) throw error; - return data; - }, }); \ No newline at end of file diff --git a/src/mainview/scripts/shortcuts.ts b/src/mainview/scripts/shortcuts.ts index c947d23..5defdf7 100644 --- a/src/mainview/scripts/shortcuts.ts +++ b/src/mainview/scripts/shortcuts.ts @@ -2,7 +2,6 @@ import { DependencyList, useEffect, useState } from "react"; import { GamepadButtonEvent } from "./gamepads"; import { dispatchFocusedEvent, GetFocusedTree } from "./spatialNavigation"; import { getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation"; -import { isTextInputFocused } from "./utils"; const shortcutMap = new Map Shortcut[])[]>(); const conflictSet = new Set(); @@ -35,7 +34,6 @@ export interface Shortcut button: GamePadButtonCode; heldTime?: number; action?: (e: GamepadButtonEvent) => void; - side?: "left" | "right"; } let isDirty = false; @@ -124,21 +122,12 @@ export function useShortcutContext () if (e.key === 'Escape') { shortcuts.get(GamePadButtonCode.B)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.B })); - } else + } else if (e.key === 'Backspace') { - // We use backspace and space in typing - if (isTextInputFocused()) - { - return false; - } - - if (e.key === 'Backspace') - { - shortcuts.get(GamePadButtonCode.X)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.X })); - } else if (e.key === ' ') - { - shortcuts.get(GamePadButtonCode.Y)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.Y })); - } + shortcuts.get(GamePadButtonCode.X)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.X })); + } else if (e.key === ' ') + { + shortcuts.get(GamePadButtonCode.Y)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.Y })); } }; @@ -201,7 +190,7 @@ export function useShortcutContext () return { shortcuts: array }; } -export function useShortcuts (focusKey: string, build: () => Shortcut[], deps?: DependencyList) +export function useShortcuts (focusKey: string, build: () => Shortcut[], ...deps: DependencyList) { useEffect(() => { @@ -221,6 +210,6 @@ export function useShortcuts (focusKey: string, build: () => Shortcut[], deps?: markDirtyThrottled(); }; - }, [focusKey, ...deps ?? []]); + }, [...deps, focusKey]); } \ No newline at end of file diff --git a/src/mainview/scripts/spatialNavigation.ts b/src/mainview/scripts/spatialNavigation.ts index d076df4..3d4b182 100644 --- a/src/mainview/scripts/spatialNavigation.ts +++ b/src/mainview/scripts/spatialNavigation.ts @@ -1,5 +1,6 @@ import { + FocusDetails, getCurrentFocusKey, init, SpatialNavigation, @@ -8,7 +9,7 @@ import UseFocusableResult, } from "@noriginmedia/norigin-spatial-navigation"; import { RefObject, useEffect, useState } from "react"; -import { focusQueue } from "../App"; +import { focusQueue, Router } from ".."; init({ shouldFocusDOMNode: false, @@ -32,7 +33,7 @@ export function GetFocusedElement (focusKey: string) export function GetFocusedTree (leaf: string): string[] { - const tree: string[] = ["window"]; + const tree: string[] = []; let component = (SpatialNavigation as any).focusableComponents[leaf]; while (component) { @@ -96,21 +97,13 @@ SpatialNavigation.updateLayout = (focusKey) => SpatialNavigation.setFocus = (newFocusKey, focusDetails) => { setFocus(newFocusKey, focusDetails); + dispatchFocusedEvent(new CustomEvent('focuschanged', { bubbles: true, detail: focusDetails })); }; SpatialNavigation.setCurrentFocusedKey = (newFocusKey, focusDetails) => { - const details: FocusEventDetails = { - ...focusDetails, - focusKey: newFocusKey, - focusKeyChanged: newFocusKey !== getCurrentFocusKey(), - node: GetFocusedElement(newFocusKey) - }; setCurrentFocusedKey(newFocusKey, focusDetails); - (GetFocusedElement(newFocusKey) ?? window).dispatchEvent(new CustomEvent('focuschanged', { - bubbles: true, - detail: details - })); + window.dispatchEvent(new CustomEvent('focuschanged', { bubbles: true, detail: focusDetails })); }; SpatialNavigation.updateFocusable = (key, data) => diff --git a/src/mainview/scripts/types.ts b/src/mainview/scripts/types.ts index fbfcb36..ca39657 100644 --- a/src/mainview/scripts/types.ts +++ b/src/mainview/scripts/types.ts @@ -1,5 +1,3 @@ -import { FrontEndId } from "@simeonradivoev/gameflow-sdk/shared"; - export const FOCUS_KEYS = { NAV_CATEGORIES: "NAV_CATEGORIES", NAV_CATEGORY: (cat: string) => `NAV_CAT_${cat}`, @@ -8,13 +6,8 @@ export const FOCUS_KEYS = { EMULATOR_SECTION: (id: string) => `EMULATOR_SECTION_${id}`, EMULATOR_CUSTOM_PATH: (id: string) => `EMULATOR_CUSTOM_PATH_${id}`, CONTEXT_DIALOG_OPTION: (contextId: string, id: string) => `${contextId}_LIST_OPTION${id}`, - CONTEXT_DIALOG: (contextId: string) => `${contextId}_CONTEXT_DIALOG`, EMULATOR_CARD: (id: string) => `EMULATOR_${id}`, GAME_SECTION: "GAME_SECTION", GAME_CARD: (id: FrontEndId) => `GAME_${id.source}_${id.id}`, - GAME_LIST_CARD: (list: string, id: FrontEndId) => `LIST_${list}_GAME_${id.source}_${id.id}`, - GAME_MATCH: (id: FrontEndId) => `GAME_${id.source}_${id.id}`, STATS_SECTION: "STATS_SECTION", - PLUGIN_ENTRY: (id: string) => `PLUGIN_${id}`, - DOWNLOAD_ENTRY: (source: string, id: string) => `DOWNLOAD_${source}_${id}` } as const; \ No newline at end of file diff --git a/src/mainview/scripts/utils.ts b/src/mainview/scripts/utils.ts index 6635bd6..fd2572b 100644 --- a/src/mainview/scripts/utils.ts +++ b/src/mainview/scripts/utils.ts @@ -1,11 +1,9 @@ -import { LocalSettingsSchema, LocalSettingsType } from '@simeonradivoev/gameflow-sdk/shared'; -import { DependencyList, RefObject, useEffect, useRef, useState } from "react"; +import { LocalSettingsSchema, LocalSettingsType } from "@/shared/constants"; +import { RefObject, useEffect, useRef, useState } from "react"; import { useLocalStorage } from "usehooks-ts"; -import { jobsApi, systemApi } from "./clientApi"; +import { jobsApi } from "./clientApi"; import { JobsAPIType } from "@/bun/api/rpc"; -import { AnyRouter, useRouter } from "@tanstack/react-router"; -import { soundMap } from "./audio/audioConstants"; -import { GamepadButtonEvent, oneShotRumble } from "./gamepads"; +import { Router } from ".."; export type ScrollSaveParams = { id: string; @@ -13,12 +11,6 @@ export type ScrollSaveParams = { storage?: "session" | "local"; shouldSave?: boolean; }; - -export function isTextInputFocused () -{ - return document.activeElement && document.activeElement instanceof HTMLInputElement; -} - export function useScrollSave (data: ScrollSaveParams) { useEffect(() => @@ -67,13 +59,6 @@ export function mobileCheck () return check; }; -export function getLocalSetting (key: TKey): LocalSettingsType[TKey] -{ - const localValueRaw = localStorage.getItem(key); - if (!localValueRaw) return LocalSettingsSchema.shape[key].parse(undefined) as any; - return LocalSettingsSchema.shape[key].parse(JSON.parse(localValueRaw)) as any; -} - export function useLocalSetting (key: TKey) { const [localValue] = useLocalStorage(key, LocalSettingsSchema.shape[key].parse(undefined), { deserializer: (value) => LocalSettingsSchema.shape[key].parse(JSON.parse(value)) }); @@ -233,7 +218,7 @@ export function scrollIntoViewHandler (params?: ScrollIntoViewOptions) return (focusKey: string, node: HTMLElement, details: any) => { if (details.nativeEvent instanceof PointerEvent) return; - node.scrollIntoView({ ...params, behavior: details.instant || !details.event ? 'instant' : 'smooth' }); + node.scrollIntoView({ ...params, behavior: details.instant ? 'instant' : 'smooth' }); }; } @@ -274,13 +259,10 @@ export function useJobStatus, onProgress?: (process: number, data: ExtractField, "data" | "started" | "progress" | "completed" | "ended", 'data'>) => void, - onWaiting?: () => void, onEnded?: (data: ExtractField, "completed" | "ended", 'data'>) => void; onCompleted?: (data: ExtractField, "completed" | "ended", 'data'>) => void; onError?: (error: string) => void; - onClosed?: () => void; - }, - deps?: DependencyList + } ) { type Response = JobResponse; @@ -296,7 +278,6 @@ export function useJobStatus init?.onClosed?.()); sub.subscribe(({ data }) => { switch (data.type) @@ -317,11 +298,6 @@ export function useJobStatus) => void) -{ - const router = useRouter(); - const prevIndex = useRef(router.history.location.state.__TSR_index); - - useEffect(() => - { - const unsub = router.history.subscribe(() => - { - const currentIndex = router.history.location.state.__TSR_index; - const isBack = currentIndex < prevIndex.current; - - if (isBack) - { - callback(router.history.location.state); - } - - prevIndex.current = currentIndex; - }); - - return unsub; - }, [router]); -} - -export function showKeyboardHandler (activeControl: string | undefined, node?: HTMLInputElement) -{ - if (node && node.type !== 'checkbox' && (activeControl === 'gamepad' || activeControl === 'touch')) - { - var rect = node.getBoundingClientRect(); - systemApi.api.system.show_keyboard.post({ - XPosition: rect.x, - YPosition: rect.y, - Width: rect.width, - Height: rect.height - }); - } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/mainview/scripts/windowEvents.ts b/src/mainview/scripts/windowEvents.ts index 88dd926..c6f7ab8 100644 --- a/src/mainview/scripts/windowEvents.ts +++ b/src/mainview/scripts/windowEvents.ts @@ -2,7 +2,7 @@ import { settingsApi } from "./clientApi"; const handleResize = () => { - settingsApi.api.settings.local({ id: 'windowSize' }).post({ value: { width: window.innerWidth, height: window.innerHeight } }); + settingsApi.api.settings({ id: 'windowSize' }).post({ value: { width: window.innerWidth, height: window.innerHeight } }); }; window.addEventListener("resize", handleResize); import.meta.hot?.dispose(() => window.removeEventListener('resize', handleResize)); @@ -13,7 +13,7 @@ var screenPositionInternal: NodeJS.Timeout = setInterval(() => { if (lastWindowPosX != window.screenX || lastWindowPosY != window.screenY) { - settingsApi.api.settings.local({ id: 'windowPosition' }).post({ value: { x: window.screenX, y: window.screenY } }); + settingsApi.api.settings({ id: 'windowPosition' }).post({ value: { x: window.screenX, y: window.screenY } }); } lastWindowPosX = window.screenX; diff --git a/src/mainview/types.d.ts b/src/mainview/types.d.ts index 2a0c101..699ca3c 100644 --- a/src/mainview/types.d.ts +++ b/src/mainview/types.d.ts @@ -1,6 +1,5 @@ declare const __HOST__: string; declare const __PUBLIC__: boolean; -declare const __FLATPAK__: boolean; declare const __EMULATORS__: Record; declare module "@emulators" { const data: Record; @@ -17,60 +16,16 @@ declare global "save-scroll"?: boolean; } } - - module "@noriginmedia/norigin-spatial-navigation" { - declare interface FocusDetails - { - instant?: boolean; - sound?: string; - } - } - - module "@tanstack/react-router" { - declare interface HistoryState - { - eventType?: string; - } - } - } -declare interface FocusEventDetails -{ - focusKey: string; - instant?: boolean; - sound?: string; - nativeEvent?: any; - event?: Event; - node: HTMLElement | undefined; - focusKeyChanged: boolean; -} - -declare interface GameMeta extends FocusParams -{ - id: string, - onSelect?: () => void, - onQuickAction?: () => void, - title: string, - subtitle?: any, - previewUrls?: string | URL[]; - previewSrcset?: string; -}; - declare interface FocusParams { onFocus?: (focusKey: string, node: HTMLElement, details: Record) => void; } -declare interface InteractParamsArgs -{ - event?: Event, - focusKey?: string; -} - declare interface InteractParams { - onAction?: (ctx: InteractParamsArgs) => void; + onAction?: (e?: Event) => void; } declare interface FilterOption extends FocusParams, InteractParams @@ -78,11 +33,4 @@ declare interface FilterOption extends FocusParams, InteractParams label: string; selected: boolean; icon?: any; -} - -declare type EmulatorJsMessage = { type: 'restart'; } | -{ type: 'pause'; paused: boolean; } | -{ type: 'exit'; save?: File; } | -{ type: 'save', save: File, screenshot?: File, type: string; } | -{ type: 'loaded'; } | -{ type: 'requestSave'; }; \ No newline at end of file +} \ No newline at end of file diff --git a/src/packages/gameflow-sdk/README.md b/src/packages/gameflow-sdk/README.md deleted file mode 100644 index 8338233..0000000 --- a/src/packages/gameflow-sdk/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Gameflow Deck SDK - -This is the type definitions for Gameflow Deck plugins. - -## Developing a plugin - -The plugin must have a default export class of type `PluginType`. It exposes the context and all the hooks to be tapped. -Gameflow uses the [Tapable Hooks](https://github.com/webpack/tapable). - -The package must expose a main script gameflow will import and validate. It must implement the type fields on `PluginDescriptionType`. - -## Publishing - -For the plugin to show up in the UI for download. It must be published to NPM with the `gameflow-plugin` keyword. Gameflow uses bun to install plugins as packages from npmjs. -Follow publishing instruction check the [NPM Docs](https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry) - -## Dependencies - -Peer dependencies will not be installed when the run adds the plugin package. They are provided by gameflow. -All peer dependencies can be marked as external as gameflow provides it. There is a helper build script that does all that for you, to run it use. - -`bunx gameflow-build --entry=index.ts` - -supported arguments are -`--entry` the entry of the app to build -`--outdir` Where to build. Default is 'dist' -`--minify` Minify the code. Default is 'false' -`--sourcemap` Include a source map. Default is 'none' - -If you want to include dependencies that gameflow does not provide you have to bundle them in. Gameflow does not load dependencies for you. diff --git a/src/packages/gameflow-sdk/build.ts b/src/packages/gameflow-sdk/build.ts deleted file mode 100755 index 9f18b88..0000000 --- a/src/packages/gameflow-sdk/build.ts +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bun - -import pkg from './package.json'; - -import { parseArgs } from "util"; - -const { values } = parseArgs({ - args: Bun.argv.slice(2), - options: { - outdir: { type: "string", default: "dist" }, - minify: { type: "boolean", default: false }, - sourcemap: { type: "string", default: "none" }, // "none" | "inline" | "external" - entry: { type: "string", default: "src/index.ts" }, - }, - allowPositionals: true, -}); - -await Bun.build({ - entrypoints: [values.entry], - outdir: values.outdir, - minify: values.minify, - sourcemap: values.sourcemap as any, - external: [...Object.keys(pkg.peerDependencies), pkg.name], - target: "bun", -}); - -console.log(`✅ Built to ${values.outdir}`); \ No newline at end of file diff --git a/src/packages/gameflow-sdk/hooks/app.ts b/src/packages/gameflow-sdk/hooks/app.ts deleted file mode 100644 index 1b73daa..0000000 --- a/src/packages/gameflow-sdk/hooks/app.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { AsyncSeriesBailHook } from "tapable"; -import AuthHooks from "./auth"; -import EmulatorHooks from "./emulators"; -import GameHooks from "./games"; -import StoreHooks from "./store"; -import { DownloadFileEntry, ProgressStats } from "../shared"; - -export class GameflowHooks -{ - games = new GameHooks(); - emulators = new EmulatorHooks(); - auth = new AuthHooks(); - store = new StoreHooks(); - /** Download the given files and return their final paths. */ - downloadFiles = new AsyncSeriesBailHook<[ctx: { - /** Unique ID of the download */ - id: string, - /** The root download path. Each file has it's own download sub path */ - downloadPath: string, - abortSignal?: AbortSignal, - /** Authentication needed for download. Should be put in the headers. */ - auth?: string, - /** The files to download */ - files: DownloadFileEntry[]; - /** Call it to update progress in the UI */ - updateProgress: (stats: ProgressStats) => void; - - }], { - /** What downloaded the files. Will be passed to {@link postDownloadFiles} files hook. */ - source: string, - /** The file paths ot the downloaded files. */ - files: string[]; - } | undefined>(['ctx']); - /** Called after {@link downloadFiles} has finished downloading. - * @returns The modified file paths. - */ - postDownloadFiles = new AsyncSeriesBailHook<[ctx: { - /** Who downloaded the files. Passed from the {@link downloadFiles} hook. */ - source: string; - /** Can be directories or files */ - files: string[]; - /** The root downloads folder. */ - downloadPath: string, - /** The sub path where the archive should be extracted to. This will be a sub path of `path_fs` */ - extract_path?: string; - /** This is the parent path for the extracted files. */ - path_fs?: string; - }], string[] | undefined>(['ctx']); -} \ No newline at end of file diff --git a/src/packages/gameflow-sdk/hooks/emulators.ts b/src/packages/gameflow-sdk/hooks/emulators.ts deleted file mode 100644 index d852d06..0000000 --- a/src/packages/gameflow-sdk/hooks/emulators.ts +++ /dev/null @@ -1,42 +0,0 @@ - -import { EmulatorPostInstallContextType } from "../index"; -import { DownloadFileEntry, EmulatorSourceEntryType, EmulatorSystem } from "../shared"; -import { AsyncSeriesBailHook, AsyncSeriesHook } from "tapable"; - -export default class EmulatorHooks -{ - /** Download emulator bios files */ - fetchBiosDownload = new AsyncSeriesBailHook<[ctx: { - emulator: string; - systems: EmulatorSystem[]; - biosFolder: string; - }], { auth?: string, files: DownloadFileEntry[]; } | undefined>(['ctx']); - - /** - * Triggered when emulator is downloaded or updated - */ - emulatorPostInstall = new AsyncSeriesHook<[ctx: EmulatorPostInstallContextType], { emulator: string; }>(['ctx']); - /** Find locations of emulators on the system. Be it already installed ones or ones downloaded by the store. */ - findEmulatorSource = new AsyncSeriesHook<[ctx: { emulator: string; sources: EmulatorSourceEntryType[]; }]>(['ctx']); - /** Match emulators for a given system */ - findEmulatorForSystem = new AsyncSeriesHook<[ctx: { system: string; emulators: string[]; }]>(['ctx']); - - constructor() - { - this.emulatorPostInstall.intercept({ - register (tap) - { - return { - ...tap, - fn: async (ctx: EmulatorPostInstallContextType, ...rest: any[]) => - { - if (ctx.emulator === tap.emulator) - { - tap.fn(ctx, ...rest); - } - } - }; - }, - }); - } -} \ No newline at end of file diff --git a/src/packages/gameflow-sdk/hooks/games.ts b/src/packages/gameflow-sdk/hooks/games.ts deleted file mode 100644 index 314b138..0000000 --- a/src/packages/gameflow-sdk/hooks/games.ts +++ /dev/null @@ -1,202 +0,0 @@ - -import { EmulatorPackageType, GameListFilterType, CommandEntry, DownloadInfo, EmulatorSourceEntryType, EmulatorSupport, EmulatorSystem, FrontEndCollection, FrontEndFilterSets, FrontEndGameType, FrontEndGameTypeDetailed, FrontEndGameTypeWithIds, FrontEndId, FrontEndPlatformType, GameLookup, SaveFileChange, SaveSlots, DownloadLookupEntry, DownloadLookupDetails, DownloadsLookupFilterValues, DownloadsLookupFilter } from '../shared'; -import { SyncBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } from 'tapable'; - -export default class GameHooks -{ - /** Build commands the game can be launched with. */ - buildLaunchCommands = new AsyncSeriesBailHook<[ctx: { - source: string | null; - sourceId: string | null; - id: FrontEndId; - systemSlug: string; - gamePath: string | null, - /** The glob pattern for the main executable of the game */ - mainGlob?: string | null, - }], CommandEntry[] | Error | undefined>(['ctx']); - /** override the launch command for an emulator - * @returns The argument list to be used when running the emulator. - * If no emulator bin in the command entry is found the actual command will be used as the bin. - */ - emulatorLaunch = new AsyncSeriesBailHook<[ctx: { - /** The auto generated command for example based on the ES-DE listing */ - autoValidCommand: CommandEntry; - /** Don't actually launch just see if it can be launched */ - dryRun: boolean, - game: { - /** The source of the game */ - source?: string; - /** The ID of the source. This could be for example the ROMM ID the game was */ - sourceId?: string; - id: FrontEndId; - platformSlug?: string; - }; - }], { args: string[], savesPath?: SaveSlots; env?: Record; } | undefined, { emulator: string; }>(['ctx']); - /** - * Is the given emulator for the given command supported - * @returns The current support level. Partial means it can affect some functionality. Full means fully integrated for example with portable ones where you can control all aspects. - * - */ - emulatorLaunchSupport = new SyncBailHook<[ctx: { - emulator: string; - source?: EmulatorSourceEntryType; - }], EmulatorSupport | undefined, { emulator: string; }>(['ctx']); - /** - * Fetches and returns a list of games converted to frontend. - */ - fetchGames = new AsyncSeriesHook<[ctx: { - query: GameListFilterType; - games: FrontEndGameTypeWithIds[]; - }]>(['ctx']); - /** Return all filters the users can apply for a give source. */ - fetchFilters = new AsyncSeriesHook<[ctx: { - source?: string; - filters: FrontEndFilterSets; - }]>(['ctx']); - /** Get game metadata */ - fetchGame = new AsyncSeriesBailHook<[ctx: { - source: string; - localGame?: FrontEndGameTypeDetailed; - id: string; - }], FrontEndGameTypeDetailed | undefined>(['ctx']); - /** Search for a given game based on the igdb id or ra id. */ - searchGame = new AsyncSeriesBailHook<[ctx: { - source: string; - igdb_id?: number; - ra_id?: number; - }], FrontEndGameTypeDetailed | undefined>(['ctx']); - /** Get download file URLs */ - fetchDownloads = new AsyncSeriesBailHook<[ctx: { - source: string; - id: string; - /** If there are multiple downloads, use the one with same ID */ - downloadId?: string; - }], DownloadInfo[] | undefined>(['ctx']); - /** Get the paths to rom files. This is mainly used for emulator js. */ - fetchRomFiles = new AsyncSeriesBailHook<[ctx: { - source: string; - id: string; - }], string[] | undefined>(['ctx']); - fetchRecommendedGamesForGame = new AsyncSeriesHook<[ctx: { - game: FrontEndGameTypeDetailed, - games: (FrontEndGameType & { metadata?: any; })[]; - }]>(['ctx']); - fetchRecommendedGamesForEmulator = new AsyncSeriesHook<[cts: { - emulator: EmulatorPackageType; - systems: EmulatorSystem[]; - games: FrontEndGameType[]; - }]>(['ctx']); - fetchPlatform = new AsyncSeriesBailHook<[ctx: { - source: string; - id: string; - }], FrontEndPlatformType | undefined>(['ctx']); - /** Lookup a given platform with a given slug or id. This may or may not exist. */ - platformLookup = new AsyncSeriesBailHook<[ctx: { - source?: string; - id?: string; - slug?: string; - }], { - slug: string; - url_logo?: string | null; - name?: string; - family_name?: string; - } | undefined>(['ctx']); - /** Lookup downloads based on a search pattern. - * This is just downloads. Doesn't actually have to be a game. - * This is mainly used to manually add games from outside sources */ - downloadsLookup = new AsyncSeriesWaterfallHook<[matches: Map, ctx: { - page?: number; - rows?: number; - } & DownloadsLookupFilter]>(['matches', 'ctx']); - /** List all available filters */ - downloadsLookupFilters = new AsyncSeriesHook<[ctx: { - filters: DownloadsLookupFilterValues; - }]>(['ctx']); - /** Look for the files for a download the user can pick from */ - downloadLookup = new AsyncSeriesBailHook<[ctx: { source: string, id: string; }], DownloadLookupDetails | undefined>(['ctx']); - /** Look up game metadata based on a search */ - gameLookup = new AsyncSeriesWaterfallHook<[matches: Map, ctx: { - source?: string, - id?: string; - search?: string; - }]>(['matches', 'ctx']); - fetchPlatforms = new AsyncSeriesHook<[ctx: { - platforms: FrontEndPlatformType[]; - }]>(['ctx']); - /** Called before the game is played. */ - prePlay = new AsyncSeriesHook<[ctx: { - source: string, - id: string; - saveFolderSlots: Record; - setProgress: (progress: number, state: string) => void, - command: CommandEntry; - gameInfo: { - platformSlug?: string; - }; - }]>(["ctx"]); - /** - * Called after the game process has finished. - */ - postPlay = new AsyncSeriesHook<[ctx: { - source: string, - id: string; - saveFolderSlots?: SaveSlots; - /** Auto detected changed files. This is mainly used to see what changed during gameplay */ - changedSaveFiles: { subPath: string, cwd: string; }[], - /** This will be final valid changes to be saved using save integrations like rclone */ - validChangedSaveFiles: Record, - /** The command that was used to launch the game */ - command: CommandEntry; - gameInfo: { - platformSlug?: string; - }; - }]>(["ctx"]); - /** Called after game install - * This includes game being downloaded and registered in the database. - */ - postInstall = new AsyncSeriesHook<[ctx: { - source: string, - id: string; - files: string[]; - info: DownloadInfo; - }]>(['ctx']); - fetchCollections = new AsyncSeriesHook<[ctx: { collections: FrontEndCollection[]; }]>(['ctx']); - fetchCollection = new AsyncSeriesBailHook<[ctx: { source: string, id: string; }], FrontEndCollection | undefined>(['ctx']); - - constructor() - { - this.emulatorLaunchSupport.intercept({ - register (tap) - { - return { - ...tap, - fn: (e: any, ...rest: any[]) => - { - if (e.emulator === tap.emulator) - { - return tap.fn(e, ...rest); - } - } - }; - }, - }); - this.emulatorLaunch.intercept({ - register (tap) - { - return { - ...tap, - fn: async (e: any, ...rest: any[]) => - { - if ((e.autoValidCommand as CommandEntry).emulator === tap.emulator) - { - return tap.fn(e, ...rest); - } - } - }; - }, - }); - } -} \ No newline at end of file diff --git a/src/packages/gameflow-sdk/hooks/store.ts b/src/packages/gameflow-sdk/hooks/store.ts deleted file mode 100644 index c7f43e5..0000000 --- a/src/packages/gameflow-sdk/hooks/store.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FrontEndEmulator, FrontEndEmulatorDetailed, FrontEndGameTypeDetailed, EmulatorDownloadInfoType } from "../shared"; -import { AsyncSeriesBailHook, AsyncSeriesHook } from "tapable"; - -export default class StoreHooks -{ - fetchFeaturedGames = new AsyncSeriesHook<[ctx: { games: FrontEndGameTypeDetailed[]; }]>(['ctx']); - fetchEmulators = new AsyncSeriesHook<[ctx: { emulators: FrontEndEmulator[]; search?: string; }]>(['ctx']); - fetchEmulator = new AsyncSeriesBailHook<[ctx: { id: string; }], FrontEndEmulatorDetailed | undefined>(['ctx']); - fetchDownload = new AsyncSeriesBailHook<[ctx: { id: string; }], (EmulatorDownloadInfoType & { hasUpdate: boolean; }) | undefined>(['ctx']); -} \ No newline at end of file diff --git a/src/packages/gameflow-sdk/index.ts b/src/packages/gameflow-sdk/index.ts deleted file mode 100644 index c78c757..0000000 --- a/src/packages/gameflow-sdk/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -import z from "zod"; -import { GameflowHooks } from "./hooks/app"; -import { EmulatorDownloadInfoSchema, EmulatorPackageSchema, FrontendNotification, SettingsType } from "./shared"; -import { $ZodRegistry } from "zod/v4/core"; -import Conf from "conf"; -import { EventEmitter } from 'node:events'; -import { TaskQueue } from "./task-queue"; - -export * from "./hooks/app"; -export * from "./task-queue"; - -export interface AppEventMap -{ - exitapp: []; - notification: [FrontendNotification]; - focus: []; -} - -export const PluginContextSchema = z.object({ - hooks: z.instanceof(GameflowHooks) -}); - -export const PluginLoadingContextSchema = z.object({ - setProgress: z.function().input([z.number(), z.string()]).output(z.void()), - config: z.instanceof(Conf).describe("Per plugin config. It will use the settings schema defined in the plugin class"), - zodRegistry: z.instanceof($ZodRegistry).describe("Used by the settings to register metadata for the UI"), - app: z.object({ - config: z.instanceof(Conf), - events: z.instanceof(EventEmitter), - taskQueue: z.instanceof(TaskQueue) - }) -}).extend(PluginContextSchema.shape); - -export const PluginDescriptionSchema = z.object({ - name: z.string(), - displayName: z.string().optional(), - version: z.string(), - description: z.string().optional(), - icon: z.url().optional().describe("Can be an external URL to an image or a data url"), - keywords: z.array(z.string()).optional(), - peerDependencies: z.record(z.string(), z.string()).optional(), - category: z.string().default("other"), - main: z.string().describe("The main entry. It must export a default class implementing PluginType"), - canDisable: z.boolean().default(true).optional().describe("Can the plugin be disabled or enabled by the user"), - autoUpdate: z.boolean().optional().describe("Should the plugin auto update to latest version") -}); - -export const PluginSchema = z.object({ - load: z.function().input([PluginLoadingContextSchema]).output(z.promise(z.void())).describe("Called when the plugin is loaded or reloaded"), - cleanup: z.function().output(z.promise(z.void())).optional().describe("Called when the plugin is unloaded or before it's reloaded"), - settingsSchema: z.instanceof(z.ZodObject).optional().describe("The settings schema. Gameflow will show settings in the UI."), - settingsMigrations: z.record(z.string(), z.function().input([z.instanceof(Conf)]).output(z.void())).optional(), - eventsNames: z.object({ - id: z.string(), - title: z.string().optional(), - description: z.string().optional(), - action: z.string() - }).array().optional().describe("Events will be called when the user presses the button in plugin settings. Each event creates a button."), - onEvent: z.function().input([z.string()]).output(z.object({ - openTab: z.string().optional(), - reload: z.boolean().optional() - }).or(z.record(z.string(), z.any()))).optional() -}); - -export const ActiveGameSchema = z.object({ - process: z.any().optional(), - gameId: z.object({ id: z.string(), source: z.string() }), - source: z.string().optional(), - sourceId: z.string().optional(), - name: z.string(), - command: z.object({ command: z.string().or(z.string().array()), startDir: z.string().optional() }) -}); - -export const EmulatorPostInstallContextSchema = z.object({ - emulator: z.string(), - emulatorPackage: EmulatorPackageSchema.optional(), - path: z.string(), - update: z.boolean(), - info: EmulatorDownloadInfoSchema, -}); - -export type ActiveGameType = z.infer; -export type PluginDescriptionType = z.infer; -export type PluginContextType = z.infer; -export type PluginLoadingContextType = Record> = z.infer & { - config: Conf; -}; -export type PluginType = Record> = Omit, "load" | 'settingsMigrations'> & { - load: (ctx: PluginLoadingContextType) => Promise; - settingsMigrations?: Record) => void>; -}; -export type EmulatorPostInstallContextType = z.infer; - diff --git a/src/packages/gameflow-sdk/package.json b/src/packages/gameflow-sdk/package.json deleted file mode 100644 index 4e4ce28..0000000 --- a/src/packages/gameflow-sdk/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@simeonradivoev/gameflow-sdk", - "version": "1.6.0", - "types": "index.d.ts", - "description": "plugin SDK for the Gameflow Deck Launcher", - "exports": { - ".": "./index.ts", - "./shared": "./shared.ts" - }, - "bin": { - "gameflow-build": "build.ts" - }, - "peerDependencies": { - "7zip-bin": "^5.2.0", - "@auth/core": "^0.34.3", - "cheerio": "^1.2.0", - "conf": "^15.1.0", - "drizzle-orm": "^0.45.2", - "elysia": "^1.4.28", - "fs-extra": "^11.3.5", - "get-folder-size": "^5.0.0", - "ini": "^6.0.0", - "jimp": "^1.6.1", - "mustache": "^4.2.0", - "node-7z": "^3.0.0", - "node-disk-info": "^1.3.0", - "node-downloader-helper": "^2.1.11", - "node-stream-zip": "^1.15.0", - "node-unrar-js": "^2.0.2", - "open": "^11.0.0", - "p-queue": "^9.2.0", - "pathe": "^2.0.3", - "slugify": "^1.6.9", - "smol-toml": "^1.6.1", - "tapable": "^2.3.3", - "unzip-stream": "^0.3.4", - "zod": "^4.4.3" - }, - "keywords": [ - "gameflow", - "sdk" - ] -} \ No newline at end of file diff --git a/src/packages/gameflow-sdk/sdk.tsconfig.json b/src/packages/gameflow-sdk/sdk.tsconfig.json deleted file mode 100644 index 707544a..0000000 --- a/src/packages/gameflow-sdk/sdk.tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "useDefineForClassFields": true, - "lib": [ - "ES2024" - ], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "emitDeclarationOnly": true, - "declaration": true, - "strict": true, - "outDir": "../../dist-sdk", - "types": [ - "node" - ] - } -} \ No newline at end of file diff --git a/src/packages/gameflow-sdk/shared.ts b/src/packages/gameflow-sdk/shared.ts deleted file mode 100644 index 96ddb01..0000000 --- a/src/packages/gameflow-sdk/shared.ts +++ /dev/null @@ -1,710 +0,0 @@ -import * as z from "zod"; - -export const settingRegistry = z.registry<{ - dev?: boolean; -}>(); - -export const SettingsSchema = z.object({ - rommAddress: z.url().optional(), - rommUser: z.string().default('admin').optional(), - windowSize: z.object({ width: z.number(), height: z.number() }).optional(), - windowPosition: z.object({ x: z.number(), y: z.number() }).optional(), - downloadPath: z.string(), - launchInFullscreen: z.boolean().default(true), - disabledPlugins: z.array(z.string()).default([]), - emulatorResolution: z.enum(['720p', '1080p', '1440p', '4k']).default('720p'), - emulatorWidescreen: z.boolean().default(true) -}); export const LocalSettingsSchema = z.object({ - backgroundBlur: z.boolean().default(true).meta({ title: "Background Blur" }), - backgroundAnimation: z.boolean().default(true).meta({ title: "Background Animation" }), - theme: z.enum(['dark', 'light', 'auto']).default('auto').meta({ title: "Theme" }), - soundEffects: z.boolean().default(true).meta({ title: "Sounds" }), - soundEffectsVolume: z.number().min(0).max(100).default(50).meta({ title: "Sound Volume" }), - hapticsEffects: z.boolean().default(true).meta({ title: "Haptics" }), - showRouterDevOptions: z.boolean().default(false).meta({ title: "Show Router Options" }).register(settingRegistry, { dev: true }), - showQueryDevOptions: z.boolean().default(false).meta({ title: "Show Query Options" }).register(settingRegistry, { dev: true }), - useGameflowKeyboard: z.boolean().default(true).describe("Show the gameflow on screen keyboard when using a controller").meta({ title: "Use Gameflow Keyboard" }), - autoKeybaord: z.boolean().default(true).describe("Open on screen keybaord automatically").meta({ title: "Auto Keyboard" }) -}); -export const GameListFilterSchema = z.object({ - platform_source: z.string().optional(), - platform_slug: z.string().optional(), - platform_id: z.coerce.number().optional(), - collection_id: z.coerce.number().optional(), - collection_source: z.string().optional(), - limit: z.coerce.number().optional(), - search: z.string().optional(), - offset: z.coerce.number().optional(), - source: z.string().optional(), - localOnly: z.coerce.boolean().optional(), - orderBy: z.literal(['added', 'activity', 'name', 'release']).optional(), - age_ratings: z.union([z.string().array(), z.string().transform(v => [v])]).optional(), - genres: z.union([z.string().array(), z.string().transform(v => [v])]).optional(), - keywords: z.union([z.string().array(), z.string().transform(v => [v])]).optional(), -}); -export const DownloadSourceSchema = z.object({ - id: z.string(), - name: z.string() -}); -export const RommLoginDataSchema = z.object({ hostname: z.url(), username: z.string(), password: z.string() }); -export type GameListFilterType = z.infer; -export const DirSchema = z.object({ name: z.string(), parentPath: z.string(), isDirectory: z.boolean() }); -export type DirType = z.infer; -export const CustomEmulatorSchema = z.record(z.string(), z.string()); -export const GithubManifestSchema = z.object({ - sha: z.hash('sha1'), - url: z.url(), - tree: z.array(z.object({ - path: z.string(), - mode: z.string(), - type: z.enum(['blob', 'tree']), - sha: z.hash('sha1'), - url: z.url() - })) -}); -export const StoreGameSaveSchema = z.object({ - cwd: z.string(), - globs: z.string().array() -}); -export const StoreDownloadSchema = z.discriminatedUnion('type', [ - z.object({ - type: z.literal('direct'), - url: z.url(), - name: z.string().optional(), - system: z.string(), - main: z.string().optional(), - saves: z.record(z.string(), StoreGameSaveSchema).optional() - }), - z.object({ - type: z.literal("itch"), - path: z.string(), - name: z.string().optional(), - system: z.string(), - saves: z.record(z.string(), StoreGameSaveSchema).optional() - }) -]); -export const NewGameSchema = z.object({ - name: z.string(), - summary: z.string(), - genres: z.string().regex(/^$|^(\s*\S[^,]*)(\s*,\s*\S[^,]*)*\s*$/, { - message: "Must be a comma-separated list", - }) -}); -export const StoreGameSchema = z.object({ - name: z.string(), - description: z.string(), - version: z.string(), - homepage: z.string().optional(), - keywords: z.string().array().optional(), - genres: z.string().array().optional(), - companies: z.string().array().optional(), - screenshots: z.string().array().optional(), - covers: z.string().array().optional(), - igdb_id: z.number().optional(), - ra_id: z.number().optional(), - sgdb_id: z.number().optional(), - first_release_date: z.union([z.number(), z.date()]).optional(), - player_count: z.string().optional(), - saves: z.record(z.string(), z.record(z.string(), StoreGameSaveSchema)).optional(), - downloads: z.record(z.string(), StoreDownloadSchema) -}); -export const EmulatorPackageSchema = z.object({ - name: z.string(), - description: z.string(), - homepage: z.url(), - logo: z.url(), - type: z.enum(['emulator']), - os: z.array(z.enum(['darwin', 'linux', 'win32', 'android'])), - keywords: z.array(z.string()).optional(), - downloads: z.record(z.string(), z.array(z.discriminatedUnion('type', [ - z.object({ - type: z.literal(['github', 'gitlab']), - pattern: z.string(), - path: z.string(), - bin: z.string().optional() - }), - z.object({ - type: z.literal('direct'), - url: z.url(), - bin: z.string().optional() - }), - z.object({ - type: z.literal('scoop'), - url: z.url(), - bin: z.string().optional() - }) - ]))).optional(), - systems: z.array(z.string()), - bios: z.literal(["required", "optional"]).optional() -}); -export const ScoopPackageSchema = z.object({ - version: z.string(), - url: z.url().optional(), - description: z.string(), - bin: z.string().optional(), - architecture: z.record(z.string(), z.object({ - url: z.url(), - hash: z.string().optional(), - extract_dir: z.string().optional() - })).optional() -}); -export const SystemInfoSchema = z.object({ - battery: z.object({ - percent: z.number(), - isCharging: z.boolean(), - acConnected: z.boolean(), - hasBattery: z.boolean() - }), - wifiConnections: z.array(z.object({ signalLevel: z.number() })), - bluetoothDevices: z.array(z.object({ connected: z.boolean() })) -}); -export const GithubReleaseSchema = z.object({ - id: z.number(), - tag_name: z.string().optional(), - url: z.url(), - body: z.string(), - assets: z.array(z.object({ - name: z.string(), - browser_download_url: z.url(), - content_type: z.string().optional() - })) -}); -export const EmulatorDownloadInfoSchema = z.object({ - id: z.string(), - version: z.string().optional(), - url: z.url().optional(), - description: z.string().optional(), - downloadDate: z.coerce.date(), - type: z.string() -}); -export const PluginEntrySchema = z.object({ - downloads: z.object({ - monthly: z.number(), - weekly: z.number() - }), - searchScore: z.number(), - installed: z.boolean(), - update: z.object({ from: z.string() }).optional(), - package: z.object({ - name: z.string(), - keywords: z.string().array(), - version: z.string(), - description: z.string().optional(), - sanitized_name: z.string(), - license: z.string().optional(), - publisher: z.object({ - email: z.string(), - username: z.string(), - trustedPublisher: z.object({ - id: z.string(), - oidcConfigId: z.string() - }).optional() - }), - date: z.coerce.date(), - links: z.object({ - homepage: z.string().optional(), - repository: z.string().optional(), - bugs: z.string().optional(), - npm: z.url() - }) - }) -}); -export const PluginBunDetailsSchema = z.object({ - name: z.string(), - keywords: z.string().array(), - version: z.string(), - author: z.object({ name: z.string().optional() }).optional(), - license: z.string().optional(), - devDependencies: z.record(z.string(), z.string()).optional(), - dependencies: z.record(z.string(), z.string()).optional(), - maintainers: z.object({ name: z.string() }).array().optional(), - dist: z.object({ unpackedSize: z.number() }), - description: z.string().optional(), - _npmUser: z.object({ name: z.string() }).optional() -}); -export type EmulatorPackageType = z.infer; -export type StoreGameType = z.infer; -export type StoreDownloadType = z.infer; -export type SettingsType = z.infer; -export type LocalSettingsType = z.infer; -export const PlatformSchema = z.object({ slug: z.string() }); -export type SystemInfoType = z.infer; -export type EmulatorDownloadInfoType = z.infer; -export type DownloadSourceType = z.infer; -export type PluginEntryType = z.infer; -export type PluginBunDetailsType = z.infer; - -export interface SaveFileChange -{ - subPath: string | string[]; - isGlob?: true; - cwd: string; - shared: boolean; - fixedSize?: boolean; -} - -export type EmulatorSourceType = 'custom' | 'store' | 'registry' | 'system' | 'static' | 'embedded'; - -export interface EmulatorSourceEntryType -{ - binPath: string; - rootPath?: string; - type: EmulatorSourceType; - /** Does the emulator exist in the file system */ - exists: boolean; -} - -export interface FrontEndEmulator -{ - name: string; - source: string; - logo: string; - systems: EmulatorSystem[]; - description?: string; - gameCount: number; - validSources: EmulatorSourceEntryType[]; - integrations: EmulatorSupport[]; -} - -export interface EmulatorSystem { id: string, romm_slug?: string, name: string, iconUrl: string; } - -export interface FrontEndEmulatorDetailedDownload -{ - name: string; - type: string | undefined; - version?: string; -} - -export interface FrontEndEmulatorDetailed extends FrontEndEmulator -{ - homepage: string; - description: string; - downloads: FrontEndEmulatorDetailedDownload[]; - keywords?: string[]; - screenshots: string[]; - biosRequirement?: "required" | "optional"; - bios?: string[]; - storeDownloadInfo?: { hasUpdate: boolean; version?: string, type: string; description?: string; }; -} - -export interface FrontEndGameTypeDetailedAchievement -{ - id: string; - title: string; - description?: string; - date?: Date; - date_hardcode?: Date; - badge_url?: string; - display_order: number; - type?: string; -} - -export interface FrontEndGameTypeDetailedEmulator extends FrontEndEmulator -{ - -} - -export interface FrontEndGameTypeDetailed extends Exclude -{ - summary: string | null; - fs_size_bytes: number | null; - missing: boolean; - local: boolean; - version?: string | null; - version_system?: string | null; - version_source?: string | null; - metadata: FrontEndGameMetadataDetailed, - emulators?: FrontEndGameTypeDetailedEmulator[], - achievements?: { - unlocked: number; - total: number; - entires: FrontEndGameTypeDetailedAchievement[]; - }; -}; - -export interface Drive -{ - parent: string | null; - device: string; - label: string; - mountPoint: string | null; - type: string; - size: number; - used: number; - isRemovable: boolean; - interfaceType: string | null; - hasWriteAccess: boolean; - hasReadAccess: boolean; -} - -export interface DownloadsDrive -{ - device: string; - label: string; - mountPoint: string | null; - isRemovable: boolean; - size: number; - used: number; - isCurrentlyUsed: boolean; - unusableReason: 'not_enough_space' | 'already_used' | null; -} - -export interface FrontendNotification -{ - title?: string; - message: string; - type: 'success' | 'error' | 'info' | 'custom'; - icon?: "save" | "upload" | "clock"; - duration?: number; -} - -export interface CommandEntry -{ - /** The ID of the command. Could be just an index or a string */ - id: string | number; - /** The front end label for the command. Mainly gotten from ES-DE list */ - label?: string; - /** Compiled command to be executed */ - command: string | string[]; - /** Environment variables */ - env?: Record, - /** The path the spawned process will start at */ - startDir?: string; - /** Is the command valid, for example does the executable exists */ - valid: boolean; - /** Run the command as shell. Defaults is true */ - shell?: boolean; - /** For what emulator is the command */ - emulator?: string; - /** Where the emulator came from */ - emulatorSource?: EmulatorSourceType; - /** Metadata for the command */ - metadata: { - romPath?: string; - emulatorBin?: string; - /** The root directory of the emulator */ - emulatorDir?: string; - }; -} - -export interface FrontEndId -{ - id: string; - source: string; -} - -// Stuff stored in the local sqlite metadata field -export interface LocalGameMetadata -{ - genres?: string[], - companies?: string[], - game_modes?: string[], - age_ratings?: string[]; - player_count?: string; - first_release_date?: number; - average_rating?: number; -} - -export interface FrontEndPlatformType -{ - id: FrontEndId; - slug: string; - name: string; - family_name?: string | null; - path_cover: string | null; - game_count: number; - updated_at: Date; - hasLocal: boolean; - paths_screenshots: string[]; -} - -export interface FrontEndGameTypeWithIds extends FrontEndGameType -{ - igdb_id: number | null; - ra_id: number | null; -} - -export interface FrontEndFilterSets -{ - age_ratings: Set, - player_counts: Set, - languages: Set, - companies: Set, - genres: Set; -} - -export interface FrontEndFilterLists -{ - age_ratings: string[], - player_counts: string[], - languages: string[], - companies: string[], - genres: string[]; -} - -export interface FrontEndGameMetadata -{ - first_release_date: Date | null; -} - -export interface FrontEndGameMetadataDetailed extends FrontEndGameMetadata -{ - genres: string[], - companies: string[], - game_modes: string[], - age_ratings: string[]; - player_count: string | null; - average_rating: number | null; -} - -export interface FrontEndGameType -{ - platform_display_name: string | null, - path_platform_cover: string | null; - id: FrontEndId, - source: string | null, - source_id: string | null, - path_fs: string | null, - path_covers: string[], - last_played: Date | null, - updated_at: Date, - metadata: FrontEndGameMetadata, - slug: string | null, - name: string | null, - platform_id: number | null, - platform_slug: string | null, - paths_screenshots: string[]; -}; - -export type GameStatusType = 'installed' | 'missing-emulator' | 'error' | 'install' | 'download' | 'extract' | 'playing' | 'queued'; - -export interface GameInstallProgress -{ - progress?: number; - status?: GameStatusType; - details?: string; - commands?: CommandEntry[]; - error?: any; -} - -export type JobStatus = 'completed' | 'error' | 'running' | 'queued' | 'aborted'; -export type GameInstallProgressEvent = 'refresh'; - -export interface FrontEndJob -{ - id: string; - data: any; - progress: number; - state?: string; - status: string; -} - -export interface FrontendPlugin -{ - name: string; - displayName?: string; - description?: string; - category: string; - enabled: boolean; - canDisable: boolean; - canUninstall: boolean; - source: PluginSourceType; - hasSettings: boolean; - version: string; - icon?: string; - update?: PluginUpdateCheck; -} - -export interface PluginUpdateCheck -{ - current: string; - new: string; -} - -export type PluginSourceType = "builtin" | "store"; - -export type KeysWithValueAssignableTo = { - [K in keyof T]: Exclude extends Value ? K : never; -}[keyof T]; - -export interface DownloadInfo -{ - id: string; - screenshotUrls: string[]; - coverUrl: string; - platform?: DownloadPlatform; - slug?: string; - path_fs?: string; - main_glob?: string; - summary?: string; - name: string; - last_played?: Date; - igdb_id?: number; - ra_id?: number; - source_id: string; - system_slug: string; - extract_path?: string; - metadata?: any; - files: DownloadFileEntry[]; - auth?: string; - version?: string; - version_source?: string; - version_system?: string; -} - -export interface DownloadPlatform -{ - id: string; - source: string; - igdb_id?: number; - igdb_slug?: string; - ra_id?: number; - moby_id?: number; - slug: string; - name: string; - /** Like Sony or Nintendo */ - family_name?: string; -} - -export interface DownloadFileEntry -{ - url: URL; - /** The path of the file, excluding the name */ - file_path: string; - /** Just the name of the file including the extension */ - file_name: string; - /** Checksum of the file */ - sha1?: string; - /** Size in bytes */ - size?: number; -} - -export interface LocalDownloadFileEntry extends DownloadFileEntry -{ - /** Exists on the file system */ - exists: boolean; - /** Matches the checksum */ - matches: boolean; -} - -export interface FrontEndCollection -{ - id: FrontEndId; - name: string; - description: string; - path_platform_cover: string | null; - game_count: number; -} - -export type EmulatorCapabilities = "saves" | "fullscreen" | "resolution" | "batch" | "states" | "config"; - -export interface EmulatorSupport -{ - id: string; - source?: EmulatorSourceEntryType; - supportLevel?: "partial" | "full"; - capabilities?: EmulatorCapabilities[]; -} - -export interface GameLookup -{ - source: string; - id: string; - coverUrl: string | null | undefined; - slug: string | null | undefined; - screenshotUrls: string[]; - name: string; - summary: string | null | undefined; - genres: string[]; - companies: string[]; - game_modes: string[]; - age_ratings: string[]; - player_count: string | undefined; - first_release_date: number | undefined; - average_rating: number | undefined; - keywords: string[]; - igdb_id: number | undefined; - platforms: { - id: number; - name?: string | null; - displayName: string; - slug: string; - }[]; -} - -export interface DownloadLookupEntry -{ - source: string; - id: string; - cover_url: string | null | undefined; - name: string; - summary: string | null | undefined; - size: number | null | undefined; - date: Date | null | undefined; - rating: number | null | undefined; - view_count: number | null | undefined; - download_count: number | null | undefined; - comment_count: number | null | undefined; -} - -export interface DownloadLookupDetailsFile -{ - id: string; - format: string | null | undefined; - mtime: Date | null | undefined; - size: number | null | undefined; - download_url: string; -} - -export interface DownloadLookupDetails -{ - source: string; - id: string; - cover_url: string | null | undefined; - name: string; - summary: string | null | undefined; - date: Date | null | undefined; - files: DownloadLookupDetailsFile[]; -} - -export interface AutoSaveChange -{ - subPath: string; - cwd: string; -} - -export interface AppInfoContext -{ - activeTaskProgress: number | null; -} - -export type SaveSlots = Record; - -/** Jobs that are downloading stuff can implement this data interface to show up in the downloads screen */ -export interface DownloadJobData extends Partial> -{ - preview_url?: string | null; - name?: string; -} - -export interface ProgressStats -{ - progress: number; - speed: number; - total: number; - downloaded: number; -} - -export interface DownloadsLookupFilter -{ - source?: string, - orderBy?: string, - search?: string; - sortDirection?: "desc" | "asc"; -} - -export interface DownloadsLookupFilterValues -{ - orderBy: string[], - source: string[]; -} \ No newline at end of file diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 3c9d776..66e3a78 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,3 +1,9 @@ + + +import { FocusDetails } from '@noriginmedia/norigin-spatial-navigation'; +import { JSX } from 'react'; +import * as z from 'zod'; + export const LOGIN_PORT = 5196; export const OAUTH_REDIRECT_PORT = 5194; export const SERVER_PORT = 5173; @@ -8,6 +14,128 @@ export const RPC_PORT = 8787; export const RPC_URL = (host: string) => `http://${host}:${RPC_PORT}`; export const EMULATORJS_URL = (host: string) => `http://${host}:${EMULATORJS_PORT}`; export const SOCKETS_URL = (host: string) => `ws://${host}:${RPC_PORT}`; +export const STORE_VERSION = "^0"; export const DefaultRommStaleTime = 60 * 1000; // A minute -export const PluginRegistry = process.env.STORE_REGISTRY ?? "https://registry.npmjs.org"; \ No newline at end of file +export interface GameMeta +{ + id: string, + onSelect?: () => void, + onFocus?: (details: FocusDetails) => void, + title: string, + subtitle: string | JSX.Element, + previewUrl?: string; + previewSrcset?: string; +}; + +export const SettingsSchema = z.object({ + rommAddress: z.url().optional(), + rommUser: z.string().default('admin').optional(), + windowSize: z.object({ width: z.number(), height: z.number() }).optional(), + windowPosition: z.object({ x: z.number(), y: z.number() }).optional(), + downloadPath: z.string(), + launchInFullscreen: z.boolean().default(true), + disabledPlugins: z.array(z.string()).default([]) +}); + +export const LocalSettingsSchema = z.object({ + backgroundBlur: z.stringbool().or(z.boolean()).default(true), + backgroundAnimation: z.stringbool().or(z.boolean()).default(true), + theme: z.enum(['dark', 'light', 'auto']).default('auto') +}); + +export const GameListFilterSchema = z.object({ + platform_source: z.string().optional(), + platform_slug: z.string().optional(), + platform_id: z.coerce.number().optional(), + collection_id: z.coerce.number().optional(), + collection_source: z.string().optional(), + limit: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + source: z.string().optional(), + orderBy: z.literal(['added', 'activity', 'name']).optional() +}); + +export const RommLoginDataSchema = z.object({ hostname: z.url(), username: z.string(), password: z.string() }); + +export type GameListFilterType = z.infer; + +export const DirSchema = z.object({ name: z.string(), parentPath: z.string(), isDirectory: z.boolean() }); +export type DirType = z.infer; + +export const CustomEmulatorSchema = z.record(z.string(), z.string()); + +export const GithubManifestSchema = z.object({ + sha: z.hash('sha1'), + url: z.url(), + tree: z.array(z.object({ + path: z.string(), + mode: z.string(), + type: z.enum(['blob', 'tree']), + sha: z.hash('sha1'), + url: z.url() + })) +}); + +export const StoreGameSchema = z.object({ + system: z.string(), + title: z.string(), + url: z.string().optional(), + file: z.url(), + description: z.string(), + pictures: z.object({ + screenshots: z.array(z.string()), + titlescreens: z.array(z.string()) + }), + tags: z.array(z.string()) +}); + +export const EmulatorPackageSchema = z.object({ + name: z.string(), + description: z.string(), + homepage: z.url(), + logo: z.url(), + type: z.enum(['emulator']), + os: z.array(z.enum(['darwin', 'linux', 'win32', 'android'])), + keywords: z.array(z.string()).optional(), + downloads: z.record(z.string(), z.array(z.discriminatedUnion('type', [ + z.object({ + type: z.literal(['github', 'gitlab']), + pattern: z.string(), + path: z.string() + }), + z.object({ + type: z.literal('direct'), + url: z.url(), + }) + ]))).optional(), + systems: z.array(z.string()), + bios: z.literal(["required", "optional"]).optional() +}); + +export const SystemInfoSchema = z.object({ + battery: z.object({ + percent: z.number(), + isCharging: z.boolean(), + acConnected: z.boolean(), + hasBattery: z.boolean() + + }), + wifiConnections: z.array(z.object({ signalLevel: z.number() })), + bluetoothDevices: z.array(z.object({ connected: z.boolean() })) +}); + +export const GithubReleaseSchema = z.object({ + assets: z.array(z.object({ + name: z.string(), + browser_download_url: z.url(), + content_type: z.string().optional() + })) +}); + +export type EmulatorPackageType = z.infer; +export type StoreGameType = z.infer; +export type SettingsType = z.infer; +export type LocalSettingsType = z.infer; +export const PlatformSchema = z.object({ slug: z.string() }); +export type SystemInfoType = z.infer; diff --git a/src/shared/types..d.ts b/src/shared/types..d.ts new file mode 100644 index 0000000..760fc7f --- /dev/null +++ b/src/shared/types..d.ts @@ -0,0 +1,267 @@ +declare type EmulatorSourceType = 'custom' | 'store' | 'registry' | 'system' | 'static' | 'embedded'; + +declare interface EmulatorSourceEntryType +{ + binPath: string; + rootPath?: string; + type: EmulatorSourceType; + exists: boolean; +} + +declare interface FrontEndEmulator +{ + name: string; + logo: string; + systems: EmulatorSystem[]; + description?: string; + gameCount: number; + validSources: EmulatorSourceEntryType[]; + integration?: { + name: string; + version: string; + }; +} + +declare interface EmulatorSystem { id: string, romm_slug?: string, name: string, iconUrl: string; } + +declare interface FrontEndEmulatorDetailedDownload +{ + name: string; + type: string | undefined; +} + +declare interface FrontEndEmulatorDetailed extends FrontEndEmulator +{ + homepage: string; + description: string; + downloads: FrontEndEmulatorDetailedDownload[]; + keywords?: string[]; + screenshots: string[]; + sources: EmulatorSourceEntryType[]; + biosRequirement?: "required" | "optional"; + bios?: string[]; +} + +declare interface FrontEndGameTypeDetailedAchievement +{ + id: string; + title: string; + description?: string; + date?: Date; + date_hardcode?: Date; + badge_url?: string; + display_order: number; + type?: string; +} + +declare interface FrontEndGameTypeDetailedEmulator extends FrontEndEmulator +{ + +} + +declare interface FrontEndGameTypeDetailed extends FrontEndGameType +{ + summary: string | null; + fs_size_bytes: number | null; + missing: boolean; + local: boolean; + genres?: string[]; + companies?: string[]; + release_date?: Date; + emulators?: FrontEndGameTypeDetailedEmulator[], + achievements?: { + unlocked: number; + total: number; + entires: FrontEndGameTypeDetailedAchievement[]; + }; +}; + +declare interface Drive +{ + parent: string | null; + device: string; + label: string; + mountPoint: string | null; + type: string; + size: number; + used: number; + isRemovable: boolean; + interfaceType: string | null; + hasWriteAccess: boolean; + hasReadAccess: boolean; +} + +declare interface DownloadsDrive +{ + device: string; + label: string; + mountPoint: string | null; + isRemovable: boolean; + size: number; + used: number; + isCurrentlyUsed: boolean; + unusableReason: 'not_enough_space' | 'already_used' | null; +} + +declare interface FrontendNotification +{ + title?: string; + message: string; + type: 'success' | 'error' | 'info'; + duration?: number; +} + +declare interface CommandEntry +{ + /** The ID of the command. Could be just an index or a string */ + id: string | number; + /** The front end label for the command. Mainly gotten from ES-DE list */ + label?: string; + /** Compiled command to be executed */ + command: string; + /** The path the spawned process will start at */ + startDir?: string; + /** Is the command valid, for example does the executable exists */ + valid: boolean; + /** For what emulator is the command */ + emulator?: string; + /** Where the emulator came from */ + emulatorSource?: EmulatorSourceType; + /** Metadata for the command */ + metadata: { + romPath: string; + emulatorBin?: string; + /** The root directory of the emulator */ + emulatorDir?: string; + }; +} + +declare interface FrontEndId +{ + id: string; + source: string; +} + +declare interface FrontEndPlatformType +{ + id: FrontEndId; + slug: string; + name: string; + family_name?: string | null; + path_cover: string | null; + game_count: number; + updated_at: Date; + hasLocal: boolean; + paths_screenshots: string[]; +} + +declare interface FrontEndGameType +{ + platform_display_name: string | null, + path_platform_cover: string | null; + id: FrontEndId, + source: string | null, + source_id: string | null, + path_fs: string | null, + path_cover: string | null, + last_played: Date | null, + updated_at: Date, + slug: string | null, + name: string | null, + platform_id: number | null, + platform_slug: string | null, + paths_screenshots: string[]; +}; + +declare type GameStatusType = 'installed' | 'missing-emulator' | 'error' | 'install' | 'download' | 'extract' | 'playing' | 'queued'; + +declare interface GameInstallProgress +{ + progress?: number; + status?: GameStatusType; + details?: string; + commands?: CommandEntry[]; + error?: any; +} + +declare type JobStatus = 'completed' | 'error' | 'running' | 'queued' | 'aborted'; +declare type GameInstallProgressEvent = 'refresh'; + +declare interface FrontendPlugin +{ + name: string; + displayName: string; + description: string; + enabled: boolean; + source: PluginSourceType; + version: string; + icon?: string; +} + +declare type PluginSourceType = "builtin"; + +declare type KeysWithValueAssignableTo = { + [K in keyof T]: Exclude extends Value ? K : never; +}[keyof T]; + +declare interface DownloadInfo +{ + screenshotUrls: string[]; + coverUrl: string; + platform?: DownloadPlatform; + slug?: string; + path_fs?: string; + summary?: string; + name: string; + last_played?: Date; + igdb_id?: number; + ra_id?: number; + source_id: string; + system_slug: string; + extract_path?: string; + metadata?: any; + files: DownloadFileEntry[]; + auth?: string; +} + +declare interface DownloadPlatform +{ + igdb_id?: number; + igdb_slug?: string; + ra_id?: number; + moby_id?: number; + slug: string; + name: string; + /** Like Sony or Nintendo */ + family_name?: string; +} + +declare interface DownloadFileEntry +{ + url: URL; + /** The path of the file, excluding the name */ + file_path: string; + /** Just the name of the file including the extension */ + file_name: string; + /** Checksum of the file */ + sha1?: string; + /** Size in bytes */ + size?: number; +} + +declare interface LocalDownloadFileEntry extends DownloadFileEntry +{ + /** Exists on the file system */ + exists: boolean; + /** Matches the checksum */ + matches: boolean; +} + +declare interface FrontEndCollection +{ + id: FrontEndId; + name: string; + description: string; + path_platform_cover: string | null; + game_count: number; +} \ No newline at end of file diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 2b3623d..1e128cf 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -22,12 +22,4 @@ export async function delay (delay: number | Date, signal?: AbortSignal) } }); -}; - -const urlRegex = /^https?:\/\//; - -export function isUrl (value: string | undefined) -{ - if (!value) return false; - return urlRegex.test(value); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/sounds/Classic UI SFX - Chords #1.wav b/src/sounds/Classic UI SFX - Chords #1.wav deleted file mode 100644 index eef68fe..0000000 --- a/src/sounds/Classic UI SFX - Chords #1.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cce497234f22db3e9d1c0e720c36242b3e0d68a8e5ebed0d99df9dbfb5a7ac85 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #10.wav b/src/sounds/Classic UI SFX - Chords #10.wav deleted file mode 100644 index dff9ef9..0000000 --- a/src/sounds/Classic UI SFX - Chords #10.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f5e37d4692690781115bef69f033c371c89fc5e3be3415c01bcfb47f5b03c9f -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #11.wav b/src/sounds/Classic UI SFX - Chords #11.wav deleted file mode 100644 index 975f24d..0000000 --- a/src/sounds/Classic UI SFX - Chords #11.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:61c21f7ec7440719aef486730104cc1110c881de5a38e58151101088b7f63e89 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #12.wav b/src/sounds/Classic UI SFX - Chords #12.wav deleted file mode 100644 index b6db6ee..0000000 --- a/src/sounds/Classic UI SFX - Chords #12.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:80bbc3cddf2f040a96a989e4febf66885a031a42438e705267ecad60e69caf23 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #13.wav b/src/sounds/Classic UI SFX - Chords #13.wav deleted file mode 100644 index bdd6474..0000000 --- a/src/sounds/Classic UI SFX - Chords #13.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:532526fee3cea70f093cceef20d1a2587e334f13c293461c152b9e4faf5c8ab5 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #14.wav b/src/sounds/Classic UI SFX - Chords #14.wav deleted file mode 100644 index 86c4cf1..0000000 --- a/src/sounds/Classic UI SFX - Chords #14.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ca7fadf4951a5beace5c5db4a24fd86f8907d0071a7f9a3bc600e973399b001f -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #15.wav b/src/sounds/Classic UI SFX - Chords #15.wav deleted file mode 100644 index 40c31fb..0000000 --- a/src/sounds/Classic UI SFX - Chords #15.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:54ed638f5ecca2615e0ac4bd42031efa89de810f18b0b3c0b0f2920bfaf021f6 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #16.wav b/src/sounds/Classic UI SFX - Chords #16.wav deleted file mode 100644 index c019363..0000000 --- a/src/sounds/Classic UI SFX - Chords #16.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e67390430ce0b19689f5322ad6ff78cd0a7f01cea6b55d02fef19c72ea77a653 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #17.wav b/src/sounds/Classic UI SFX - Chords #17.wav deleted file mode 100644 index e3f84cb..0000000 --- a/src/sounds/Classic UI SFX - Chords #17.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:90fe98d297a58235bda0a9a4bfc367914f052a550a7289fe652617f30f0b5e32 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #18.wav b/src/sounds/Classic UI SFX - Chords #18.wav deleted file mode 100644 index 8883284..0000000 --- a/src/sounds/Classic UI SFX - Chords #18.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb19e2996301ccfcfdf328c2f2b553ff9aa0dfe1ff4982ae1c54c9d6a2ba2438 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #19.wav b/src/sounds/Classic UI SFX - Chords #19.wav deleted file mode 100644 index 0edb657..0000000 --- a/src/sounds/Classic UI SFX - Chords #19.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:31630542c8751e4fcdd0a9ab211816f09aea6c4bf6069694e291b18b0774d7df -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #2.wav b/src/sounds/Classic UI SFX - Chords #2.wav deleted file mode 100644 index 4b38c6b..0000000 --- a/src/sounds/Classic UI SFX - Chords #2.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5dc42fc6845b2ef1ee3db50d81335e93e97902cde85fcf8fe3e5ee29c83163e9 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #20.wav b/src/sounds/Classic UI SFX - Chords #20.wav deleted file mode 100644 index 7d61fa7..0000000 --- a/src/sounds/Classic UI SFX - Chords #20.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:52d3abeb064f3e749d0f4fd29f92b19e54e1477ec80155aca906ae8975e2709f -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #3.wav b/src/sounds/Classic UI SFX - Chords #3.wav deleted file mode 100644 index a9778fb..0000000 --- a/src/sounds/Classic UI SFX - Chords #3.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1295f494ec9d295710e7bb1cf467fb118e102891dc9009b86b0ae9960d32e382 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #4.wav b/src/sounds/Classic UI SFX - Chords #4.wav deleted file mode 100644 index b2f258e..0000000 --- a/src/sounds/Classic UI SFX - Chords #4.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c5e387f33652387eda2189fc9f6c2194b36a9aac272b36ccc64c4e468b77e214 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #5.wav b/src/sounds/Classic UI SFX - Chords #5.wav deleted file mode 100644 index 6f2dc11..0000000 --- a/src/sounds/Classic UI SFX - Chords #5.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:739c571b6b1ab60a002b9b357878ae91bb9df576d52e5480f07c17886a53f805 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #6.wav b/src/sounds/Classic UI SFX - Chords #6.wav deleted file mode 100644 index fc36385..0000000 --- a/src/sounds/Classic UI SFX - Chords #6.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d80b6464291c88b7f33748d4a4e93ed07ee1756d5c623b3862bd3847767d7b54 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #7.wav b/src/sounds/Classic UI SFX - Chords #7.wav deleted file mode 100644 index 33f80af..0000000 --- a/src/sounds/Classic UI SFX - Chords #7.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:796918fa72911e50c7d762f0fb599427c5924d53b627a1b735f16ca1a9f54fe3 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #8.wav b/src/sounds/Classic UI SFX - Chords #8.wav deleted file mode 100644 index 047aa68..0000000 --- a/src/sounds/Classic UI SFX - Chords #8.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0439d8fbe9c4734acd7b40e440027d2a7a1c1ae20218f1ca2e3dacd318d776f9 -size 769182 diff --git a/src/sounds/Classic UI SFX - Chords #9.wav b/src/sounds/Classic UI SFX - Chords #9.wav deleted file mode 100644 index 7368828..0000000 --- a/src/sounds/Classic UI SFX - Chords #9.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe8e930484f41092b1518f0f3839b617561f1c62ce4b7532158c0cf484fd4d6f -size 769182 diff --git a/src/sounds/Classic UI SFX - Short - High #1.wav b/src/sounds/Classic UI SFX - Short - High #1.wav deleted file mode 100644 index a29dbce..0000000 --- a/src/sounds/Classic UI SFX - Short - High #1.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aa96866433176c69bf23254badee5abd87741816fe833a07a26d6031020464a2 -size 489182 diff --git a/src/sounds/Classic UI SFX - Short - High #10.wav b/src/sounds/Classic UI SFX - Short - High #10.wav deleted file mode 100644 index 8c9c510..0000000 --- a/src/sounds/Classic UI SFX - Short - High #10.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e58d993037d6c05797c1ecadd738f484d2c1428db6d96381522ef1254b6f794f -size 490182 diff --git a/src/sounds/Classic UI SFX - Short - High #11.wav b/src/sounds/Classic UI SFX - Short - High #11.wav deleted file mode 100644 index 558786a..0000000 --- a/src/sounds/Classic UI SFX - Short - High #11.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d648dc7030de8bc59abc70a0153e37b08cf4267228aab41a321eeaa3f03fb2fc -size 562182 diff --git a/src/sounds/Classic UI SFX - Short - High #12.wav b/src/sounds/Classic UI SFX - Short - High #12.wav deleted file mode 100644 index cbd310c..0000000 --- a/src/sounds/Classic UI SFX - Short - High #12.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1b4c11124a6a9542a3e672e4707d9dd67ea5f805ec239c310218c53d61634d4e -size 562182 diff --git a/src/sounds/Classic UI SFX - Short - High #13.wav b/src/sounds/Classic UI SFX - Short - High #13.wav deleted file mode 100644 index fa8a598..0000000 --- a/src/sounds/Classic UI SFX - Short - High #13.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5911be904de2655606bd16834391d21f56313d6fce28ed5cfaaf560331a4be80 -size 576182 diff --git a/src/sounds/Classic UI SFX - Short - High #14.wav b/src/sounds/Classic UI SFX - Short - High #14.wav deleted file mode 100644 index e7e21ca..0000000 --- a/src/sounds/Classic UI SFX - Short - High #14.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:815699bd8283ecd8af0cdfcf384a97a6a61a8aa6ab9400d0fa079267eee0567e -size 538182 diff --git a/src/sounds/Classic UI SFX - Short - High #15.wav b/src/sounds/Classic UI SFX - Short - High #15.wav deleted file mode 100644 index 142bc6f..0000000 --- a/src/sounds/Classic UI SFX - Short - High #15.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:95b2b932aade06ea1575755fa516aec836224397145d6b22270096ae74078b7c -size 523182 diff --git a/src/sounds/Classic UI SFX - Short - High #16.wav b/src/sounds/Classic UI SFX - Short - High #16.wav deleted file mode 100644 index d37ab0c..0000000 --- a/src/sounds/Classic UI SFX - Short - High #16.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8340972a758f262ff71b319a43bcdebb685d3c7036a169276b095b726c22000b -size 562182 diff --git a/src/sounds/Classic UI SFX - Short - High #17.wav b/src/sounds/Classic UI SFX - Short - High #17.wav deleted file mode 100644 index 112b03e..0000000 --- a/src/sounds/Classic UI SFX - Short - High #17.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0fbe6ca64226581e8bafa0891fabfd465d9045b395c4a21534a04af435342971 -size 553182 diff --git a/src/sounds/Classic UI SFX - Short - High #18.wav b/src/sounds/Classic UI SFX - Short - High #18.wav deleted file mode 100644 index 34ef574..0000000 --- a/src/sounds/Classic UI SFX - Short - High #18.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:95539682288858aaf0b37bc35715b71060e76bf15b81db3898a0f739f062b243 -size 453182 diff --git a/src/sounds/Classic UI SFX - Short - High #19.wav b/src/sounds/Classic UI SFX - Short - High #19.wav deleted file mode 100644 index b165ece..0000000 --- a/src/sounds/Classic UI SFX - Short - High #19.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d312f707829b975fb625bed15879118479a7927d9e29bd62d2c1c716d35b367f -size 586182 diff --git a/src/sounds/Classic UI SFX - Short - High #2.wav b/src/sounds/Classic UI SFX - Short - High #2.wav deleted file mode 100644 index f6d5fa2..0000000 --- a/src/sounds/Classic UI SFX - Short - High #2.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:89c235075af8db515f97c2bfd50f5350c28c2ba2079e6fad8fdfb963f24a12a1 -size 546182 diff --git a/src/sounds/Classic UI SFX - Short - High #20.wav b/src/sounds/Classic UI SFX - Short - High #20.wav deleted file mode 100644 index 7e23a1e..0000000 --- a/src/sounds/Classic UI SFX - Short - High #20.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:61a585819286d0f11314813ee89bc8f121ca4b362d1721e2edfe14172a6358e7 -size 387182 diff --git a/src/sounds/Classic UI SFX - Short - High #21.wav b/src/sounds/Classic UI SFX - Short - High #21.wav deleted file mode 100644 index 49cb131..0000000 --- a/src/sounds/Classic UI SFX - Short - High #21.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:480c9e31c1a033c959888bd2a2691c772ef80bbec1c5f8dc3fccb7e86da6c153 -size 385182 diff --git a/src/sounds/Classic UI SFX - Short - High #22.wav b/src/sounds/Classic UI SFX - Short - High #22.wav deleted file mode 100644 index acced97..0000000 --- a/src/sounds/Classic UI SFX - Short - High #22.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8ab262921ec2763a84c25d3ec00e1ade68e96393fbd475c28659aa661af9d41f -size 478182 diff --git a/src/sounds/Classic UI SFX - Short - High #23.wav b/src/sounds/Classic UI SFX - Short - High #23.wav deleted file mode 100644 index 4d106a7..0000000 --- a/src/sounds/Classic UI SFX - Short - High #23.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0f09d67685a7cf0b82b66f082ad77cf0b536557306fbf6862ba3ef0b92b8280 -size 472182 diff --git a/src/sounds/Classic UI SFX - Short - High #24.wav b/src/sounds/Classic UI SFX - Short - High #24.wav deleted file mode 100644 index e3d0dc8..0000000 --- a/src/sounds/Classic UI SFX - Short - High #24.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fd3520550af14b8d19b275bba18fb7f2610f156e6eea780837748a068d6a858d -size 402182 diff --git a/src/sounds/Classic UI SFX - Short - High #25.wav b/src/sounds/Classic UI SFX - Short - High #25.wav deleted file mode 100644 index 6632b69..0000000 --- a/src/sounds/Classic UI SFX - Short - High #25.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f18ca1455d31a7c430572c64b85c0c600a0c77986e6411a0f9cff783445393c3 -size 385182 diff --git a/src/sounds/Classic UI SFX - Short - High #3.wav b/src/sounds/Classic UI SFX - Short - High #3.wav deleted file mode 100644 index 767c819..0000000 --- a/src/sounds/Classic UI SFX - Short - High #3.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:140b58fa531bb20dcb284e8babea35ef55fa5d58aff16e8d98b505ccf02115e5 -size 550182 diff --git a/src/sounds/Classic UI SFX - Short - High #4.wav b/src/sounds/Classic UI SFX - Short - High #4.wav deleted file mode 100644 index 5eb2d78..0000000 --- a/src/sounds/Classic UI SFX - Short - High #4.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d11499b350e8e95990d298dc96ce01026973ccf5bd58891c9b505d3ba32b1a82 -size 582182 diff --git a/src/sounds/Classic UI SFX - Short - High #5.wav b/src/sounds/Classic UI SFX - Short - High #5.wav deleted file mode 100644 index 8cc1019..0000000 --- a/src/sounds/Classic UI SFX - Short - High #5.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e92d3e60c23ea4927444a28174a53fcb668d83f850df2494f53d5870613143a -size 499182 diff --git a/src/sounds/Classic UI SFX - Short - High #6.wav b/src/sounds/Classic UI SFX - Short - High #6.wav deleted file mode 100644 index 3e496cb..0000000 --- a/src/sounds/Classic UI SFX - Short - High #6.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2ec68d1b1549fed1bccaf62ea4fd80c5c59b9b50750fa324b858e1c370344270 -size 466182 diff --git a/src/sounds/Classic UI SFX - Short - High #7.wav b/src/sounds/Classic UI SFX - Short - High #7.wav deleted file mode 100644 index 8fae195..0000000 --- a/src/sounds/Classic UI SFX - Short - High #7.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:14f2dd1c42c354343f80c631721c8dceadb80b8e03ecec4e93294c8ffb21cfcf -size 474182 diff --git a/src/sounds/Classic UI SFX - Short - High #8.wav b/src/sounds/Classic UI SFX - Short - High #8.wav deleted file mode 100644 index a25618d..0000000 --- a/src/sounds/Classic UI SFX - Short - High #8.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:683fc09fbf580efa6990504bd17861f48495555ac81f4ed2b9e85f14628348fe -size 560182 diff --git a/src/sounds/Classic UI SFX - Short - High #9.wav b/src/sounds/Classic UI SFX - Short - High #9.wav deleted file mode 100644 index 9ee3b5f..0000000 --- a/src/sounds/Classic UI SFX - Short - High #9.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b60cd47d7591fc5e2cf49c8139781479d3833d621bba129903d78843d7929380 -size 432182 diff --git a/src/sounds/Classic UI SFX - Short - Low #1.wav b/src/sounds/Classic UI SFX - Short - Low #1.wav deleted file mode 100644 index dba2e40..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #1.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:419e970729e384fcba3a9c4bb3c55cec906dfd20f969c1cf0cbf2ffcbc00638e -size 386182 diff --git a/src/sounds/Classic UI SFX - Short - Low #10.wav b/src/sounds/Classic UI SFX - Short - Low #10.wav deleted file mode 100644 index 4eebb5b..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #10.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9749c07cacebb0bfe9924bbf8f0142630c96c0d4d5d0e727f5daeb354f9505a8 -size 580182 diff --git a/src/sounds/Classic UI SFX - Short - Low #11.wav b/src/sounds/Classic UI SFX - Short - Low #11.wav deleted file mode 100644 index d4cbb0d..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #11.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a4b7c87005eae8d0b0d329329135a67cd318a775cf2c75dd5ae68974537f9b7c -size 472182 diff --git a/src/sounds/Classic UI SFX - Short - Low #12.wav b/src/sounds/Classic UI SFX - Short - Low #12.wav deleted file mode 100644 index 65b3517..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #12.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:96018d0ee84d1e27e0fc655c03d4afd3fa0839aecca09fe7ef2f0f104f424e60 -size 557182 diff --git a/src/sounds/Classic UI SFX - Short - Low #13.wav b/src/sounds/Classic UI SFX - Short - Low #13.wav deleted file mode 100644 index ac2b8bf..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #13.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a3de21306c039abc81ace6a9e2e18f581c4993c532c8120126df055be7c9767 -size 546182 diff --git a/src/sounds/Classic UI SFX - Short - Low #14.wav b/src/sounds/Classic UI SFX - Short - Low #14.wav deleted file mode 100644 index ce57f07..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #14.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:848195440e0a64566d175a62aa13685872fdb012bf491e5742984d5059524f5f -size 602182 diff --git a/src/sounds/Classic UI SFX - Short - Low #15.wav b/src/sounds/Classic UI SFX - Short - Low #15.wav deleted file mode 100644 index e8218d5..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #15.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aee89025f7fb8517deb610d2814b941136fda09ebcbbacc78cddfe834c5b348a -size 519182 diff --git a/src/sounds/Classic UI SFX - Short - Low #16.wav b/src/sounds/Classic UI SFX - Short - Low #16.wav deleted file mode 100644 index 187b096..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #16.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4a0bafde7f6a2b75589faac13b7d2b929239189ebc554c338fd2741e3ea46e78 -size 552182 diff --git a/src/sounds/Classic UI SFX - Short - Low #17.wav b/src/sounds/Classic UI SFX - Short - Low #17.wav deleted file mode 100644 index 4a4608d..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #17.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7b2daee3db271eb4244dd40ea3abb5a7f036e69e637222f7a611c89a5f0a9a3e -size 562182 diff --git a/src/sounds/Classic UI SFX - Short - Low #18.wav b/src/sounds/Classic UI SFX - Short - Low #18.wav deleted file mode 100644 index efbf4cc..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #18.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3bda2874577cb666819141de8eba8375bddcb07bf1e813d33ad718f6828227f1 -size 587182 diff --git a/src/sounds/Classic UI SFX - Short - Low #19.wav b/src/sounds/Classic UI SFX - Short - Low #19.wav deleted file mode 100644 index 6efa7e3..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #19.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f577ca8f4b9d172c1e8d10553451128ea424a0cd4e8fc2ba217f1703de74eedc -size 475182 diff --git a/src/sounds/Classic UI SFX - Short - Low #2.wav b/src/sounds/Classic UI SFX - Short - Low #2.wav deleted file mode 100644 index 07e9d9a..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #2.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:36527b664e371bc952fa2ef6cd6f5479fcfd183144812091caf234be45dd8d3f -size 496182 diff --git a/src/sounds/Classic UI SFX - Short - Low #20.wav b/src/sounds/Classic UI SFX - Short - Low #20.wav deleted file mode 100644 index c8d717c..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #20.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:598800de963759dbe0bca69f66facbe31546c0e39f76f0fe59950ac3dd8f1c08 -size 483182 diff --git a/src/sounds/Classic UI SFX - Short - Low #21.wav b/src/sounds/Classic UI SFX - Short - Low #21.wav deleted file mode 100644 index ea90758..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #21.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ca62b2181585cbc11f1c66ba1e1d83b2987e50f9a5adb236c9c80dc29a43675b -size 500182 diff --git a/src/sounds/Classic UI SFX - Short - Low #22.wav b/src/sounds/Classic UI SFX - Short - Low #22.wav deleted file mode 100644 index 3505329..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #22.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:49ad78c33edc1c0bcfdb5d9d2d6d559b7530403cec0d7ccfb770fb4fb3356527 -size 582182 diff --git a/src/sounds/Classic UI SFX - Short - Low #23.wav b/src/sounds/Classic UI SFX - Short - Low #23.wav deleted file mode 100644 index 5cac1d4..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #23.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e3c1f824ac3a97213b56c1493b4493ef11d3292bff84eac4eeb9134b9546941c -size 564182 diff --git a/src/sounds/Classic UI SFX - Short - Low #24.wav b/src/sounds/Classic UI SFX - Short - Low #24.wav deleted file mode 100644 index d3b0245..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #24.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bb1948113b38e53a46ca720b3096a6b991c4dce76f9b3dbdec2268566587242b -size 501182 diff --git a/src/sounds/Classic UI SFX - Short - Low #25.wav b/src/sounds/Classic UI SFX - Short - Low #25.wav deleted file mode 100644 index aa73af8..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #25.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:44dc28db05d46c9eba8710a3f9fade5722551f7bbbd4318f391a78cfc2995538 -size 504182 diff --git a/src/sounds/Classic UI SFX - Short - Low #3.wav b/src/sounds/Classic UI SFX - Short - Low #3.wav deleted file mode 100644 index 9d62309..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #3.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b78146a7c13a9dc54279b0d6909ce7d33bd8521f53d8f70d5cc35941b04be055 -size 543182 diff --git a/src/sounds/Classic UI SFX - Short - Low #4.wav b/src/sounds/Classic UI SFX - Short - Low #4.wav deleted file mode 100644 index 42499ed..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #4.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bfcb2172fc599b189deecc57f0ceaa8fa54059442194829e5ae5ef78bb4960a5 -size 502182 diff --git a/src/sounds/Classic UI SFX - Short - Low #5.wav b/src/sounds/Classic UI SFX - Short - Low #5.wav deleted file mode 100644 index 6092153..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #5.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d120f6502806f3b7c1259d89aa90a0cf851f7f4826d454bd34b740f40b73f59 -size 607182 diff --git a/src/sounds/Classic UI SFX - Short - Low #6.wav b/src/sounds/Classic UI SFX - Short - Low #6.wav deleted file mode 100644 index 8708e29..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #6.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:70ba897bce8a0ec2ce6c0c2b16dfb2fd1b5cefa3eb00c152a489de35188bbf83 -size 448182 diff --git a/src/sounds/Classic UI SFX - Short - Low #7.wav b/src/sounds/Classic UI SFX - Short - Low #7.wav deleted file mode 100644 index 64a7cce..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #7.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8d465d28fc3c3c9a1dc2992724b3a674ff4d31aa9721c222cd00434ba53f4086 -size 487182 diff --git a/src/sounds/Classic UI SFX - Short - Low #8.wav b/src/sounds/Classic UI SFX - Short - Low #8.wav deleted file mode 100644 index 0ed12c7..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #8.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:62dc712e485e785716d944f0764c0588f00dd64185e4a205393f7a7df651ccfb -size 505182 diff --git a/src/sounds/Classic UI SFX - Short - Low #9.wav b/src/sounds/Classic UI SFX - Short - Low #9.wav deleted file mode 100644 index 18c0b1a..0000000 --- a/src/sounds/Classic UI SFX - Short - Low #9.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c2b0db41db5cddc7e8d6884c856b86f0b77e771958a1da9867455a6212407074 -size 518182 diff --git a/src/sounds/UI SFX_InGameMenu_Open.ogg b/src/sounds/UI SFX_InGameMenu_Open.ogg deleted file mode 100644 index bc0c8b8..0000000 --- a/src/sounds/UI SFX_InGameMenu_Open.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:95a5270d6d8be097815eb5dfe8db69c48b15b4202be308ea627fe73405ac6304 -size 47490 diff --git a/src/sounds/UI_Flourish Down_Set 14_01.wav b/src/sounds/UI_Flourish Down_Set 14_01.wav deleted file mode 100644 index f7e23e4..0000000 --- a/src/sounds/UI_Flourish Down_Set 14_01.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:81e6141d86e6f8baacc7767840861b5868b9bf4f3ea23e2c05136f44440c39ac -size 182602 diff --git a/src/sounds/UI_Flourish Up_Set 14_01.wav b/src/sounds/UI_Flourish Up_Set 14_01.wav deleted file mode 100644 index 3b9616d..0000000 --- a/src/sounds/UI_Flourish Up_Set 14_01.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:862a1190a73b987f928cbff1d03c197ae8f25d1acfb2e4052210ee0564394d2c -size 182602 diff --git a/src/sounds/UI_Single_Set 11_01.wav b/src/sounds/UI_Single_Set 11_01.wav deleted file mode 100644 index 6aae99a..0000000 --- a/src/sounds/UI_Single_Set 11_01.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d285c7ef4c2624c83deed49249b4123f7491485673bfda244fd850c8b65dc6c7 -size 16690 diff --git a/src/sounds/UI_Single_Set 11_02.wav b/src/sounds/UI_Single_Set 11_02.wav deleted file mode 100644 index 8b4c034..0000000 --- a/src/sounds/UI_Single_Set 11_02.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7aed91f6dfacc52821f0ab664fc6002090925ebdfde7dd30ebc1b2319742cc4 -size 16690 diff --git a/src/sounds/UI_Single_Set 11_03.wav b/src/sounds/UI_Single_Set 11_03.wav deleted file mode 100644 index daaa072..0000000 --- a/src/sounds/UI_Single_Set 11_03.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:003320de3e37afd144c16248464e728f28a2140121d042b0b241329236b5ae0a -size 16690 diff --git a/src/sounds/UI_Single_Set 16_01.ogg b/src/sounds/UI_Single_Set 16_01.ogg deleted file mode 100644 index b6c70e0..0000000 --- a/src/sounds/UI_Single_Set 16_01.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d04ded8bae2d120e79e7969910c7016dc00ec8b22680ab4ed5842d257616c348 -size 6983 diff --git a/src/sounds/UI_Single_Set 16_02.ogg b/src/sounds/UI_Single_Set 16_02.ogg deleted file mode 100644 index 1cdb93a..0000000 --- a/src/sounds/UI_Single_Set 16_02.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dee515a756b38ea169ebfabd91320a5d50873993765095920ec2f9e142dfd00d -size 7069 diff --git a/src/sounds/UI_Single_Set 16_03.ogg b/src/sounds/UI_Single_Set 16_03.ogg deleted file mode 100644 index 4bdfd18..0000000 --- a/src/sounds/UI_Single_Set 16_03.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d8d972d72b3570f0638e094e84791ad0aa8a39b9d4806b8a5b441802a73bf3ac -size 7156 diff --git a/src/sounds/UI_Single_Set 5_01.wav b/src/sounds/UI_Single_Set 5_01.wav deleted file mode 100644 index fa2df4b..0000000 --- a/src/sounds/UI_Single_Set 5_01.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7dfe353747423192f9f92c3427d4f14d79b1889f0c3396f15c3072164c7d9883 -size 155042 diff --git a/src/sounds/UI_Single_Set 5_02.wav b/src/sounds/UI_Single_Set 5_02.wav deleted file mode 100644 index f3628cc..0000000 --- a/src/sounds/UI_Single_Set 5_02.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4d589b9af08bb0e18efbd04f262d009fb3ee9d73374921af6578d92d49899632 -size 155042 diff --git a/src/sounds/UI_Single_Set 5_03.wav b/src/sounds/UI_Single_Set 5_03.wav deleted file mode 100644 index 07a0a76..0000000 --- a/src/sounds/UI_Single_Set 5_03.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4d7df23402238e2bb14cc7cc225d4b3997fd0647ec9119d6e08d27444039d4d3 -size 94402 diff --git a/src/sounds/UI_Single_Set 5_04.wav b/src/sounds/UI_Single_Set 5_04.wav deleted file mode 100644 index e5f50d1..0000000 --- a/src/sounds/UI_Single_Set 5_04.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5912777aae29c0ffadac28527d05699f690d10e0be634f6fedd0b1ce97954081 -size 94402 diff --git a/src/sounds/UI_TwoNote Down_Set 11_01.wav b/src/sounds/UI_TwoNote Down_Set 11_01.wav deleted file mode 100644 index 350a1a7..0000000 --- a/src/sounds/UI_TwoNote Down_Set 11_01.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:344e7d622e651e1b969c793585665a823ed14343cc0cebbd053428d00d01719c -size 77342 diff --git a/src/sounds/UI_TwoNote Down_Set 14_01.wav b/src/sounds/UI_TwoNote Down_Set 14_01.wav deleted file mode 100644 index 53b56e8..0000000 --- a/src/sounds/UI_TwoNote Down_Set 14_01.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f40883f32c6b5de586299a123e800cb3fa32d65d20a5bb50d69d3fcd8ed661f3 -size 94402 diff --git a/src/sounds/UI_TwoNote Up_Set 11_01.wav b/src/sounds/UI_TwoNote Up_Set 11_01.wav deleted file mode 100644 index 4040311..0000000 --- a/src/sounds/UI_TwoNote Up_Set 11_01.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a61d664dee7dd1e796fa16c0e2b5265bdd3741956d4b73b9c76271b3a4a90400 -size 22946 diff --git a/src/sounds/UI_TwoNote Up_Set 11_02.wav b/src/sounds/UI_TwoNote Up_Set 11_02.wav deleted file mode 100644 index e1d9377..0000000 --- a/src/sounds/UI_TwoNote Up_Set 11_02.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c1851531d1a0bf95f78a8ce8acfb02aa8e726cca3c934f165f1727340f2e1b2 -size 44262 diff --git a/src/sounds/UI_TwoNote Up_Set 11_03.wav b/src/sounds/UI_TwoNote Up_Set 11_03.wav deleted file mode 100644 index ac9a7a4..0000000 --- a/src/sounds/UI_TwoNote Up_Set 11_03.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f5a95e063e3368d24e44a4952f39e2949fdf278791b505ce2400ed1f6f7c05e -size 71826 diff --git a/src/sounds/UI_TwoNote Up_Set 14_01.wav b/src/sounds/UI_TwoNote Up_Set 14_01.wav deleted file mode 100644 index bc92f9a..0000000 --- a/src/sounds/UI_TwoNote Up_Set 14_01.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3131ea87c8893a87f6964f07728154e91115093313e55fa6c04f1028000af50a -size 88890 diff --git a/src/sounds/UI_TwoNote_Set 15_01.ogg b/src/sounds/UI_TwoNote_Set 15_01.ogg deleted file mode 100644 index 7d8a3c2..0000000 --- a/src/sounds/UI_TwoNote_Set 15_01.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ad955c552cc1a111a9885fcb09f460e03f6b31ef9ecd1637a49970c0edc5fef2 -size 7624 diff --git a/src/sounds/UI_TwoNote_Set 15_02.ogg b/src/sounds/UI_TwoNote_Set 15_02.ogg deleted file mode 100644 index 6a5a4c9..0000000 --- a/src/sounds/UI_TwoNote_Set 15_02.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:78e1b6c3fcff1f9f4a3a2247b8b835d26585b089ea36f21ba212d99ea6f60ba2 -size 7596 diff --git a/src/tests/downloads.test.ts b/src/tests/downloads.test.ts index 7be5dd0..3c68d35 100644 --- a/src/tests/downloads.test.ts +++ b/src/tests/downloads.test.ts @@ -1,10 +1,9 @@ -import { expect, test, describe, afterAll, beforeAll, jest } from 'bun:test'; +import { expect, test, describe, beforeEach, afterAll, beforeAll, jest } from 'bun:test'; import { client } from './client'; import * as app from '@/bun/api/app'; import fs from 'node:fs/promises'; import path from "node:path"; import AdmZip from "adm-zip"; -import { DownloadInfo } from '@simeonradivoev/gameflow-sdk/shared'; describe("Download Tests", () => { @@ -51,18 +50,17 @@ describe("Download Tests", () => { const mock = jest.fn(); app.plugins.hooks.games.fetchDownloads.tap('test2', mock); - app.plugins.hooks.games.fetchDownloads.tapPromise('test', async ({ source }) => + app.plugins.hooks.games.fetchDownloads.tapPromise('test', async ({ source, id }) => { if (source !== 'test') return; - return [{ + return { files: [{ file_name: "Test File.txt", file_path: 'test/files', url: new URL(`${server.url.href}download/single_file.txt`) }], coverUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/SIPI_Jelly_Beans_4.1.07.tiff/lossy-page1-256px-SIPI_Jelly_Beans_4.1.07.tiff.jpg", name: "Test Game", screenshotUrls: [], system_slug: 'ps2', - source_id: "0", - id: 'test' - } satisfies DownloadInfo]; + source_id: "0" + }; }); const res = await client.rommApi.api.romm.game({ source: 'test' })({ id: '0' }).install.post(); @@ -79,7 +77,7 @@ describe("Download Tests", () => app.plugins.hooks.games.fetchDownloads.tapPromise('test', async ({ source, id }) => { if (source !== 'test') return; - return [{ + return { files: [ { file_name: "Test File.txt", file_path: 'test/files', url: new URL(`${server.url.href}download/single_file.txt`) }, { file_name: "Test File 2.txt", file_path: 'test/files', url: new URL(`${server.url.href}download/single_file_2.txt`) }], @@ -87,9 +85,8 @@ describe("Download Tests", () => name: "Test Game", screenshotUrls: [], system_slug: 'ps2', - source_id: "0", - id: 'test' - } satisfies DownloadInfo]; + source_id: "0" + }; }); const res = await client.rommApi.api.romm.game({ source: 'test' })({ id: '0' }).install.post(); @@ -107,7 +104,7 @@ describe("Download Tests", () => app.plugins.hooks.games.fetchDownloads.tapPromise('test', async ({ source, id }) => { if (source !== 'test') return; - return [{ + return { files: [ { file_name: "zip_file_with_single_file.zip", file_path: 'test', url: new URL(`${server.url.href}download/zip_file_with_single_file.zip`) }], coverUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/SIPI_Jelly_Beans_4.1.07.tiff/lossy-page1-256px-SIPI_Jelly_Beans_4.1.07.tiff.jpg", @@ -115,9 +112,8 @@ describe("Download Tests", () => screenshotUrls: [], system_slug: 'ps2', source_id: "0", - extract_path: 'test/files', - id: 'test' - } satisfies DownloadInfo]; + extract_path: 'test/files' + }; }); const res = await client.rommApi.api.romm.game({ source: 'test' })({ id: '0' }).install.post(); diff --git a/src/tests/game-launching.test.ts b/src/tests/game-launching.test.ts index a84e6d5..3ac7d8a 100644 --- a/src/tests/game-launching.test.ts +++ b/src/tests/game-launching.test.ts @@ -1,35 +1,24 @@ import { expect, test } from 'bun:test'; import path, { resolve } from 'node:path'; import * as app from '@/bun/api/app'; -import * as appSchema from '@/bun/api/schema/app'; -import { } from 'node:test'; test("uses custom emulator", async () => { app.customEmulators.set('PCSX2', resolve("./src/tests/mock-roms/mock-emulator.exe")); - const mockPlatform: typeof appSchema.platforms.$inferInsert = { - name: 'Test', - slug: 'ps2', - }; - await app.db.insert(appSchema.platforms).values(mockPlatform); - const mockGame: typeof appSchema.games.$inferInsert = { - platform_id: 1, - path_fs: './mock-rom.iso' - }; - await app.db.insert(appSchema.games).values(mockGame); + + const { getValidLaunchCommands: getLaunchCommands } = await import('@/bun/api/games/services/launchGameService'); + const commands = await getLaunchCommands({ + systemSlug: 'ps2', + gamePath: './mock-rom.iso' + }); await Bun.write(path.join(app.config.get('downloadPath'), 'mock-rom.iso'), "This is a mock Rom"); await Bun.write(path.join(app.config.get('downloadPath'), 'mock-emulator.exe'), "This is a mock Emulator"); - const { getValidLaunchCommandsForGame } = await import('@/bun/api/games/services/statusService'); - const commands = await getValidLaunchCommandsForGame('local', '1'); - expect(commands) .toSatisfy((d) => { - if (d instanceof Error) return false; - if (!d) return false; - const validCommand = d.commands.find(c => + const validCommand = d.find(c => c?.command.includes("mock-rom.iso") && c.command.includes("mock-emulator.exe") ); diff --git a/src/tests/mock-roms/mock-emulator.exe b/src/tests/mock-roms/mock-emulator.exe deleted file mode 100644 index 8a4beb5..0000000 --- a/src/tests/mock-roms/mock-emulator.exe +++ /dev/null @@ -1 +0,0 @@ -This is a mock Emulator \ No newline at end of file diff --git a/src/tests/mock-roms/mock-rom.iso b/src/tests/mock-roms/mock-rom.iso deleted file mode 100644 index 802b307..0000000 --- a/src/tests/mock-roms/mock-rom.iso +++ /dev/null @@ -1 +0,0 @@ -This is a mock Rom \ No newline at end of file diff --git a/src/tests/preload.ts b/src/tests/preload.ts index 40cf49d..b71e8e0 100644 --- a/src/tests/preload.ts +++ b/src/tests/preload.ts @@ -1,20 +1,19 @@ -import { beforeAll, beforeEach, afterEach } from 'bun:test'; +import { afterAll, beforeAll, beforeEach, afterEach } from 'bun:test'; import { resolve } from 'node:path'; import * as app from '@/bun/api/app'; -import { ensureDir, remove } from 'fs-extra'; +import { remove } from 'fs-extra'; +import { spawnSync } from "child_process"; export async function LoadApp () { console.log("Loading App"); await app.load(); - await app.taskQueue.waitForAll(); } export async function CleanupApp () { console.log("Cleaning Up App"); await app.cleanup(); - await app.resetCleanup(); } beforeAll(async () => @@ -22,7 +21,6 @@ beforeAll(async () => process.env.CUSTOM_STORE_PATH = resolve('./src/tests/mock-store'); process.env.CONFIG_CWD = resolve('./src/tests/mock-config'); process.env.DEFAULT_DOWNLOAD_PATH = resolve('./src/tests/mock-roms'); - process.env.PLUGIN_BLACKLIST = 'com.simeonradivoev.gameflow.rclone,@simeonradivoev/gameflow-store,com.simeonradivoev.gameflow.romm,com.simeonradivoev.gameflow.igdb,@simeonradivoev/gameflow-sdk'; }); async function FileCleanup () diff --git a/tsconfig.json b/tsconfig.json index 500e8f7..49e40c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,6 @@ "noUnusedLocals": true, "noFallthroughCasesInSwitch": true, "paths": { - "@simeonradivoev/gameflow-sdk/*": ["./src/packages/gameflow-sdk/*"], "@/*": [ "./src/*" ], @@ -42,7 +41,6 @@ }, "include": [ "src", - "scripts", "vite.config.ts", "vite-env-override.d.ts" ] diff --git a/vendors/es-de/emulators.darwin.x64.sqlite b/vendors/es-de/emulators.darwin.x64.sqlite index ca00b69..b669d19 100644 Binary files a/vendors/es-de/emulators.darwin.x64.sqlite and b/vendors/es-de/emulators.darwin.x64.sqlite differ diff --git a/vendors/es-de/emulators.haiku.x64.sqlite b/vendors/es-de/emulators.haiku.x64.sqlite index 3f406ab..556e0a8 100644 Binary files a/vendors/es-de/emulators.haiku.x64.sqlite and b/vendors/es-de/emulators.haiku.x64.sqlite differ diff --git a/vendors/es-de/emulators.linux.arm.sqlite b/vendors/es-de/emulators.linux.arm.sqlite index 289e182..349ec0f 100644 Binary files a/vendors/es-de/emulators.linux.arm.sqlite and b/vendors/es-de/emulators.linux.arm.sqlite differ diff --git a/vendors/es-de/emulators.linux.x64.sqlite b/vendors/es-de/emulators.linux.x64.sqlite index f08e28f..56fe557 100644 Binary files a/vendors/es-de/emulators.linux.x64.sqlite and b/vendors/es-de/emulators.linux.x64.sqlite differ diff --git a/vendors/es-de/emulators.win32.x64.sqlite b/vendors/es-de/emulators.win32.x64.sqlite index f9eb11d..02a6f03 100644 Binary files a/vendors/es-de/emulators.win32.x64.sqlite and b/vendors/es-de/emulators.win32.x64.sqlite differ diff --git a/vendors/romm/custom-overrides.json b/vendors/romm/custom-overrides.json index e7380fe..af1795e 100644 --- a/vendors/romm/custom-overrides.json +++ b/vendors/romm/custom-overrides.json @@ -1,15 +1,11 @@ { - "atarist": "atari-st", - "flash ": "browser", - "dragon32": "dragon-32-slash-64", - "scv": "epoch-cassette-vision", - "megaduck": "mega-duck-slash-cougar-boy", - "odyssey-2": "odyssey2", - "pico8": "pico", - "mark3": "sms", - "windows": [ - "windows-apps", - "win" - ], - "linux": "desktop" + "atari-st": "atarist", + "browser": "flash", + "dragon-32-slash-64": "dragon32", + "epoch-cassette-vision": "scv", + "mega-duck-slash-cougar-boy": "megaduck", + "odyssey2": "odyssey-2", + "pico": "pico8", + "sms": "mark3", + "windows-apps": "windows" } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index b396eae..7029442 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -102,8 +102,7 @@ export default defineConfig(({ command }) => }, define: { __HOST__: JSON.stringify(host), - __PUBLIC__: process.env.PUBLIC_ACCESS ? true : false, - __FLATPAK__: process.env.FLATPAK_BUILD ? true : false + __PUBLIC__: process.env.PUBLIC_ACCESS ? true : false } }; }); \ No newline at end of file