feat: Implemented launching and downloading of roms
This is just an initial implementation lots of kings to iron out
This commit is contained in:
parent
ef08fa6114
commit
f15bf9a1e0
117 changed files with 37776 additions and 1073 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -22,3 +22,5 @@ dist-*
|
|||
settings.local.json
|
||||
.tanstack
|
||||
artifacts
|
||||
trace
|
||||
downloads
|
||||
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
|
|
@ -16,7 +16,7 @@
|
|||
"internalConsoleOptions": "neverOpen",
|
||||
"request": "attach",
|
||||
"name": "Attach Bun",
|
||||
"url": "ws://127.0.0.1:9229/7lt63qegtr8",
|
||||
"url": "ws://127.0.0.1:9229/54esztvxlfe",
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"stopOnEntry": false,
|
||||
}
|
||||
|
|
@ -24,7 +24,10 @@
|
|||
"compounds": [
|
||||
{
|
||||
"name": "Attach Debug App",
|
||||
"configurations": ["Attach Bun", "Attach to Edge"],
|
||||
"configurations": [
|
||||
"Attach Bun",
|
||||
"Attach to Edge"
|
||||
],
|
||||
"stopAll": true,
|
||||
"preLaunchTask": "bun: dev"
|
||||
}
|
||||
|
|
|
|||
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
|
|
@ -10,7 +10,6 @@
|
|||
"search.exclude": {
|
||||
"**/*.gen.ts": true,
|
||||
"src/mainview/gen/*": true,
|
||||
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"[typescriptreact]": {
|
||||
|
|
@ -22,6 +21,13 @@
|
|||
"editor.formatOnSave": true
|
||||
},
|
||||
"cSpell.words": [
|
||||
"elysia"
|
||||
"elysia",
|
||||
"elysiajs",
|
||||
"gameflow",
|
||||
"hackolade",
|
||||
"keytar",
|
||||
"norigin",
|
||||
"noriginmedia",
|
||||
"romm"
|
||||
]
|
||||
}
|
||||
10
README.md
10
README.md
|
|
@ -17,6 +17,8 @@ Focused on building a simple user experience and intuitive UI.
|
|||
- Not tested on Mac yet
|
||||
- **Steam Deck Support**: Extensively tested with the steam deck. It can use flatpak installed browsers.
|
||||
- **Great for Controllers**: The UI is inspired by the switch and works great with joysticks and dpads.
|
||||
- **Automatic Download** Downloads roms from ROMM automatically
|
||||
- **Automatic Emulator Discovery** Using the configs of the excellent ES-DE to discover installed emulators and launch games.
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
|
@ -47,6 +49,14 @@ Focused on building a simple user experience and intuitive UI.
|
|||
```
|
||||
Builds will go in `/builds/<platform>`.
|
||||
|
||||
4. Additional Commands:
|
||||
- ```bun run mappings:generate``` converts the es-de configs into local sqlite configs with mappings to rom systems
|
||||
- ```bun run drizzle:generate``` generates sqlite migrations based on the app schema
|
||||
- ```bun run openapi-ts``` generated the openapi client calls from romm's API
|
||||
- ```bun run package``` builds an executable
|
||||
- ```bun run package:auto-prod``` builds and executable for production
|
||||
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- [Bun](https://bun.com/) for the backend
|
||||
|
|
|
|||
370
bun.lock
370
bun.lock
|
|
@ -9,10 +9,15 @@
|
|||
"@elysiajs/cors": "^1.4.1",
|
||||
"@elysiajs/eden": "^1.4.6",
|
||||
"@elysiajs/static": "^1.4.7",
|
||||
"@hackolade/keytar": "^7.9.0-7",
|
||||
"@rcompat/webview": "^0.18.0",
|
||||
"cheerio": "^1.2.0",
|
||||
"conf": "^15.0.2",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"elysia": "^1.4.22",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"node-downloader-helper": "^2.1.10",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"open": "^11.0.0",
|
||||
"pathe": "^2.0.3",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"tough-cookie-file-store": "^3.3.0",
|
||||
|
|
@ -39,12 +44,16 @@
|
|||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"daisyui": "^5.5.14",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"dts-bundle-generator": "^9.5.1",
|
||||
"eden-tanstack-query": "^0.0.9",
|
||||
"lucide-react": "^0.563.0",
|
||||
"pretty-bytes": "^7.1.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-error-boundary": "^6.1.0",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"sass-embedded": "^1.97.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
|
@ -53,6 +62,7 @@
|
|||
"vite": "^7.3.1",
|
||||
"vite-plugin-svg-icons-ng": "^1.5.2",
|
||||
"vite-static-assets-plugin": "^1.2.2",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -103,6 +113,10 @@
|
|||
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
|
||||
|
||||
"@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="],
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="],
|
||||
|
||||
"@elysiajs/eden": ["@elysiajs/eden@1.4.6", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-Tsa4NwXEWg/u73vWiYZQ3L5/ecgZSxqiEjYwpS+4qBKXeTZqZKl2hcgHJSVBL+InEDMi35Xugct7qyAXE5oM4Q=="],
|
||||
|
|
@ -111,69 +125,61 @@
|
|||
|
||||
"@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@hackolade/keytar": ["@hackolade/keytar@7.9.0-7", "", { "dependencies": { "@hackolade/keytar-darwin-arm64": "7.9.0-7", "@hackolade/keytar-darwin-x64": "7.9.0-7", "@hackolade/keytar-linux-arm64": "7.9.0-7", "@hackolade/keytar-linux-x64": "7.9.0-7", "@hackolade/keytar-win32-x64": "7.9.0-7" } }, "sha512-1U4Wfo3dbP63Dcl+SZyHgy3Q+sOdKzvZjuEu01BxDq4A/gtB/1e3Q9HouWUY37xdcTRSG1rD4iYirbQ79RN2iQ=="],
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@hackolade/keytar-darwin-arm64": ["@hackolade/keytar-darwin-arm64@7.9.0-7", "", {}, "sha512-A4rE3nAnjtJ0JaKfXoYLGDHky0JbIK2AqyZUcpSTIfVo28rBhqbfY8JPqEfgwibInKO1vjDUeizlhSOP0Q5RQA=="],
|
||||
|
||||
"@hackolade/keytar-darwin-x64": ["@hackolade/keytar-darwin-x64@7.9.0-7", "", {}, "sha512-xc/B1MrTD9cfxArBVto2dL9d9noRHgT/ZDZfBlfCEfb2pmbfg83WipDKxuu0nvL2Pzs2Ob26sBbFAxQE5djWIQ=="],
|
||||
|
||||
"@hackolade/keytar-linux-arm64": ["@hackolade/keytar-linux-arm64@7.9.0-7", "", {}, "sha512-G99cXS3li/mnW3qtncLAsDNPpx6Jqut1HnRgJVnO1RNotGdGU6EDcTog4pPHy7TVSwsc3QZ3Jay5wfJ0meXnSQ=="],
|
||||
|
||||
"@hackolade/keytar-linux-x64": ["@hackolade/keytar-linux-x64@7.9.0-7", "", {}, "sha512-Zx2e4aSbt3Ti0727GQMohlMqBOc7KElXOpZeW+F822U67CMckulJO0D4jcRKov5Kg3eO0nqCVT0GAnvLX6xtXw=="],
|
||||
|
||||
"@hackolade/keytar-win32-x64": ["@hackolade/keytar-win32-x64@7.9.0-7", "", {}, "sha512-tsSNLp5N8w7c3cauMxOpO5/ZVnEQ5TRDiAwZAQxI1TRJ3zerS7GmDbUhriZPU22d1qp4q9Fd+nQFhOe4NvJ8Lg=="],
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.6.0", "", { "dependencies": { "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-O6TRP3g1188gdpZC9yio64KkvWD97QpLjaCFnwTgykTZtzQXZbkyw+lYPVfZ7Ee+m7eLau1/jEJmQcUY9G0sLw=="],
|
||||
|
||||
|
|
@ -213,6 +219,34 @@
|
|||
|
||||
"@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="],
|
||||
|
||||
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="],
|
||||
|
||||
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="],
|
||||
|
||||
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="],
|
||||
|
||||
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="],
|
||||
|
||||
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="],
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
||||
|
||||
"@rcompat/assert": ["@rcompat/assert@0.6.0", "", { "dependencies": { "@rcompat/is": "^0.4.0", "@rcompat/type": "^0.9.0" } }, "sha512-V8YrttJqBNsLo9DQkXGpPt09LqfUGvwA30q8Tf+uukEhE6Nraw4jM4Nq0c5yKQF0IWv1eSoPUJyCO4W4neS5IA=="],
|
||||
|
||||
"@rcompat/dict": ["@rcompat/dict@0.3.1", "", { "dependencies": { "@rcompat/assert": "^0.6.0", "@rcompat/is": "^0.4.2" } }, "sha512-eWZ4ACk0DpT8PS+umVlp/TmFfWAD0yqkGxfvvtfL/9fqPEh1bcCFtGMySCwmTGx/FU8sPnxwnSiZGZmN36gTBQ=="],
|
||||
|
|
@ -429,6 +463,10 @@
|
|||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="],
|
||||
|
||||
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||
|
|
@ -445,6 +483,8 @@
|
|||
|
||||
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
|
||||
|
||||
"colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="],
|
||||
|
||||
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
|
||||
|
||||
"concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
|
||||
|
|
@ -507,19 +547,31 @@
|
|||
|
||||
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="],
|
||||
|
||||
"dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="],
|
||||
|
||||
"eden-tanstack-query": ["eden-tanstack-query@0.0.9", "", { "peerDependencies": { "@elysiajs/eden": ">=1.0.0", "@tanstack/query-core": "^5.90.16" } }, "sha512-EYnFasVEFHFZ9aoI2TDFHU+q0gRdvnFYvX8QzzjXww4dLy0qXDNTGvywmLuiqfiICDC2y4oh2/ZpIPFkGTLNPQ=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.277", "", {}, "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw=="],
|
||||
|
||||
"elysia": ["elysia@1.4.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
|
|
@ -553,12 +605,16 @@
|
|||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-folder-size": ["get-folder-size@5.0.0", "", { "bin": { "get-folder-size": "bin/get-folder-size.js" } }, "sha512-+fgtvbL83tSDypEK+T411GDBQVQtxv+qtQgbV+HVa/TYubqDhNd5ghH/D6cOHY9iC5/88GtOZB7WI8PXy2A3bg=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
|
||||
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
|
||||
|
||||
"goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
|
@ -567,8 +623,14 @@
|
|||
|
||||
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
|
||||
|
||||
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"immutable": ["immutable@5.1.4", "", {}, "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA=="],
|
||||
|
||||
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
|
@ -659,12 +721,18 @@
|
|||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"node-downloader-helper": ["node-downloader-helper@2.1.10", "", { "bin": { "ndh": "bin/ndh" } }, "sha512-8LdieUd4Bqw/CzfZLf30h+1xSAq3riWSDfWKsPJYz8EULoWxjS1vw6BGLYFZDxQgXjDR7UmC9UpQ0oV93U98Fg=="],
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"node-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"node-stream-zip": ["node-stream-zip@1.15.0", "", {}, "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
|
@ -679,6 +747,12 @@
|
|||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
|
||||
|
||||
"parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
|
@ -739,6 +813,48 @@
|
|||
|
||||
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"sass": ["sass@1.97.3", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg=="],
|
||||
|
||||
"sass-embedded": ["sass-embedded@1.97.3", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-all-unknown": "1.97.3", "sass-embedded-android-arm": "1.97.3", "sass-embedded-android-arm64": "1.97.3", "sass-embedded-android-riscv64": "1.97.3", "sass-embedded-android-x64": "1.97.3", "sass-embedded-darwin-arm64": "1.97.3", "sass-embedded-darwin-x64": "1.97.3", "sass-embedded-linux-arm": "1.97.3", "sass-embedded-linux-arm64": "1.97.3", "sass-embedded-linux-musl-arm": "1.97.3", "sass-embedded-linux-musl-arm64": "1.97.3", "sass-embedded-linux-musl-riscv64": "1.97.3", "sass-embedded-linux-musl-x64": "1.97.3", "sass-embedded-linux-riscv64": "1.97.3", "sass-embedded-linux-x64": "1.97.3", "sass-embedded-unknown-all": "1.97.3", "sass-embedded-win32-arm64": "1.97.3", "sass-embedded-win32-x64": "1.97.3" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-eKzFy13Nk+IRHhlAwP3sfuv+PzOrvzUkwJK2hdoCKYcWGSdmwFpeGpWmyewdw8EgBnsKaSBtgf/0b2K635ecSA=="],
|
||||
|
||||
"sass-embedded-all-unknown": ["sass-embedded-all-unknown@1.97.3", "", { "dependencies": { "sass": "1.97.3" }, "cpu": [ "!arm", "!x64", "!arm64", ] }, "sha512-t6N46NlPuXiY3rlmG6/+1nwebOBOaLFOOVqNQOC2cJhghOD4hh2kHNQQTorCsbY9S1Kir2la1/XLBwOJfui0xg=="],
|
||||
|
||||
"sass-embedded-android-arm": ["sass-embedded-android-arm@1.97.3", "", { "os": "android", "cpu": "arm" }, "sha512-cRTtf/KV/q0nzGZoUzVkeIVVFv3L/tS1w4WnlHapphsjTXF/duTxI8JOU1c/9GhRPiMdfeXH7vYNcMmtjwX7jg=="],
|
||||
|
||||
"sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.97.3", "", { "os": "android", "cpu": "arm64" }, "sha512-aiZ6iqiHsUsaDx0EFbbmmA0QgxicSxVVN3lnJJ0f1RStY0DthUkquGT5RJ4TPdaZ6ebeJWkboV4bra+CP766eA=="],
|
||||
|
||||
"sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.97.3", "", { "os": "android", "cpu": "none" }, "sha512-zVEDgl9JJodofGHobaM/q6pNETG69uuBIGQHRo789jloESxxZe82lI3AWJQuPmYCOG5ElfRthqgv89h3gTeLYA=="],
|
||||
|
||||
"sass-embedded-android-x64": ["sass-embedded-android-x64@1.97.3", "", { "os": "android", "cpu": "x64" }, "sha512-3ke0le7ZKepyXn/dKKspYkpBC0zUk/BMciyP5ajQUDy4qJwobd8zXdAq6kOkdiMB+d9UFJOmEkvgFJHl3lqwcw=="],
|
||||
|
||||
"sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.97.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fuqMTqO4gbOmA/kC5b9y9xxNYw6zDEyfOtMgabS7Mz93wimSk2M1quQaTJnL98Mkcsl2j+7shNHxIS/qpcIDDA=="],
|
||||
|
||||
"sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.97.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-b/2RBs/2bZpP8lMkyZ0Px0vkVkT8uBd0YXpOwK7iOwYkAT8SsO4+WdVwErsqC65vI5e1e5p1bb20tuwsoQBMVA=="],
|
||||
|
||||
"sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.97.3", "", { "os": "linux", "cpu": "arm" }, "sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA=="],
|
||||
|
||||
"sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.97.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-IP1+2otCT3DuV46ooxPaOKV1oL5rLjteRzf8ldZtfIEcwhSgSsHgA71CbjYgLEwMY9h4jeal8Jfv3QnedPvSjg=="],
|
||||
|
||||
"sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.97.3", "", { "os": "linux", "cpu": "arm" }, "sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg=="],
|
||||
|
||||
"sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.97.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw=="],
|
||||
|
||||
"sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.97.3", "", { "os": "linux", "cpu": "none" }, "sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA=="],
|
||||
|
||||
"sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.97.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw=="],
|
||||
|
||||
"sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.97.3", "", { "os": "linux", "cpu": "none" }, "sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA=="],
|
||||
|
||||
"sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.97.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg=="],
|
||||
|
||||
"sass-embedded-unknown-all": ["sass-embedded-unknown-all@1.97.3", "", { "dependencies": { "sass": "1.97.3" }, "os": [ "!linux", "!win32", "!darwin", "!android", ] }, "sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q=="],
|
||||
|
||||
"sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.97.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-RDGtRS1GVvQfMGAmVXNxYiUOvPzn9oO1zYB/XUM9fudDRnieYTcUytpNTQZLs6Y1KfJxgt5Y+giRceC92fT8Uw=="],
|
||||
|
||||
"sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.97.3", "", { "os": "win32", "cpu": "x64" }, "sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
|
@ -773,6 +889,10 @@
|
|||
|
||||
"svgo": ["svgo@3.3.2", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, "bin": "./bin/svgo" }, "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw=="],
|
||||
|
||||
"sync-child-process": ["sync-child-process@1.0.2", "", { "dependencies": { "sync-message-port": "^1.0.0" } }, "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA=="],
|
||||
|
||||
"sync-message-port": ["sync-message-port@1.2.0", "", {}, "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg=="],
|
||||
|
||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||
|
|
@ -807,6 +927,8 @@
|
|||
|
||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||
|
||||
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
||||
|
|
@ -817,6 +939,8 @@
|
|||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
|
@ -829,14 +953,22 @@
|
|||
|
||||
"usehooks-ts": ["usehooks-ts@3.1.1", "", { "dependencies": { "lodash.debounce": "^4.0.8" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA=="],
|
||||
|
||||
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
|
||||
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
|
||||
"vite-plugin-svg-icons-ng": ["vite-plugin-svg-icons-ng@1.5.2", "", { "dependencies": { "fast-glob": "^3.3.3", "fs-extra": "^11.3.2", "node-html-parser": "^7.0.1", "svgo": "^3.3.2" }, "peerDependencies": { "vite": ">=5.0.0" } }, "sha512-A68obs8XDT+q8q8dKyjrT/v0qw8h5pEBKXJ27aUXjARYeJ6MNvhIhRLLiUwnSrbn/B4TBF4UVaWRXKftAqP7+A=="],
|
||||
|
||||
"vite-static-assets-plugin": ["vite-static-assets-plugin@1.2.2", "", { "dependencies": { "chalk": "^5.4.1", "chokidar": "^3.5.3", "minimatch": "^10.0.1" }, "peerDependencies": { "typescript": "^5.0.0", "vite": "^6.2.0" } }, "sha512-0mzHsxFa46Np5AixQcdWYLVH6eJxeok7qL7tXmxYavg/Uo0e5z+J6gavJ0TJ6dmJSe2Z+gwmDb64bCCZfg+gqA=="],
|
||||
|
||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
|
||||
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||
|
||||
"when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
|
@ -859,6 +991,8 @@
|
|||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
|
@ -889,24 +1023,184 @@
|
|||
|
||||
"elysia/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="],
|
||||
|
||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"sass/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||
|
||||
"vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||
|
||||
"vite-static-assets-plugin/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
|
||||
"sass/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
||||
|
||||
"vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
||||
|
||||
"vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
||||
|
||||
"vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
bunfig.toml
Normal file
3
bunfig.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[test]
|
||||
# Load these modules before running tests.
|
||||
preload = ["./src/tests/preload.ts"]
|
||||
11
drizzle.config.ts
Normal file
11
drizzle.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'dotenv/config';
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
out: './drizzle',
|
||||
schema: './src/bun/api/schema/app.ts',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: {
|
||||
url: "./games.db"
|
||||
}
|
||||
});
|
||||
65
drizzle/0000_pretty_harry_osborn.sql
Normal file
65
drizzle/0000_pretty_harry_osborn.sql
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
CREATE TABLE `collections` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `collections_games` (
|
||||
`collection_id` integer NOT NULL,
|
||||
`game_id` integer NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
FOREIGN KEY (`collection_id`) REFERENCES `collections`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||
FOREIGN KEY (`game_id`) REFERENCES `games`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `games` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`source_id` integer,
|
||||
`source` text,
|
||||
`igdb_id` integer,
|
||||
`name` text,
|
||||
`ra_id` integer,
|
||||
`path_fs` text,
|
||||
`last_played` integer,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`metadata` text DEFAULT '{}',
|
||||
`slug` text,
|
||||
`platform_id` integer NOT NULL,
|
||||
`cover` blob,
|
||||
`type` text,
|
||||
`summary` text,
|
||||
FOREIGN KEY (`platform_id`) REFERENCES `platforms`(`id`) ON UPDATE cascade ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `games_source_id_unique` ON `games` (`source_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `games_igdb_id_unique` ON `games` (`igdb_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `games_ra_id_unique` ON `games` (`ra_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `games_slug_unique` ON `games` (`slug`);--> statement-breakpoint
|
||||
CREATE TABLE `platforms` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`igdb_id` integer,
|
||||
`igdb_slug` text,
|
||||
`moby_id` integer,
|
||||
`name` text NOT NULL,
|
||||
`es_slug` text,
|
||||
`ra_id` integer,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`slug` text NOT NULL,
|
||||
`metadata` text,
|
||||
`cover` blob,
|
||||
`type` text,
|
||||
`family_name` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `platforms_igdb_id_unique` ON `platforms` (`igdb_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `platforms_igdb_slug_unique` ON `platforms` (`igdb_slug`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `platforms_moby_id_unique` ON `platforms` (`moby_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `platforms_es_slug_unique` ON `platforms` (`es_slug`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `platforms_ra_id_unique` ON `platforms` (`ra_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `platforms_slug_unique` ON `platforms` (`slug`);--> statement-breakpoint
|
||||
CREATE TABLE `screenshots` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`game_id` integer,
|
||||
`content` blob NOT NULL,
|
||||
`type` text,
|
||||
FOREIGN KEY (`game_id`) REFERENCES `games`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
458
drizzle/meta/0000_snapshot.json
Normal file
458
drizzle/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "673fe5dc-58a5-495b-8fb1-104e7945e90b",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"collections": {
|
||||
"name": "collections",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"collections_games": {
|
||||
"name": "collections_games",
|
||||
"columns": {
|
||||
"collection_id": {
|
||||
"name": "collection_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"game_id": {
|
||||
"name": "game_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"collections_games_collection_id_collections_id_fk": {
|
||||
"name": "collections_games_collection_id_collections_id_fk",
|
||||
"tableFrom": "collections_games",
|
||||
"tableTo": "collections",
|
||||
"columnsFrom": [
|
||||
"collection_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
},
|
||||
"collections_games_game_id_games_id_fk": {
|
||||
"name": "collections_games_game_id_games_id_fk",
|
||||
"tableFrom": "collections_games",
|
||||
"tableTo": "games",
|
||||
"columnsFrom": [
|
||||
"game_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"games": {
|
||||
"name": "games",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"source_id": {
|
||||
"name": "source_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"source": {
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"igdb_id": {
|
||||
"name": "igdb_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ra_id": {
|
||||
"name": "ra_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path_fs": {
|
||||
"name": "path_fs",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_played": {
|
||||
"name": "last_played",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'{}'"
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"platform_id": {
|
||||
"name": "platform_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cover": {
|
||||
"name": "cover",
|
||||
"type": "blob",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"summary": {
|
||||
"name": "summary",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"games_source_id_unique": {
|
||||
"name": "games_source_id_unique",
|
||||
"columns": [
|
||||
"source_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"games_igdb_id_unique": {
|
||||
"name": "games_igdb_id_unique",
|
||||
"columns": [
|
||||
"igdb_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"games_ra_id_unique": {
|
||||
"name": "games_ra_id_unique",
|
||||
"columns": [
|
||||
"ra_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"games_slug_unique": {
|
||||
"name": "games_slug_unique",
|
||||
"columns": [
|
||||
"slug"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"games_platform_id_platforms_id_fk": {
|
||||
"name": "games_platform_id_platforms_id_fk",
|
||||
"tableFrom": "games",
|
||||
"tableTo": "platforms",
|
||||
"columnsFrom": [
|
||||
"platform_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"platforms": {
|
||||
"name": "platforms",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"igdb_id": {
|
||||
"name": "igdb_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"igdb_slug": {
|
||||
"name": "igdb_slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"moby_id": {
|
||||
"name": "moby_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"es_slug": {
|
||||
"name": "es_slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ra_id": {
|
||||
"name": "ra_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cover": {
|
||||
"name": "cover",
|
||||
"type": "blob",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"family_name": {
|
||||
"name": "family_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"platforms_igdb_id_unique": {
|
||||
"name": "platforms_igdb_id_unique",
|
||||
"columns": [
|
||||
"igdb_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"platforms_igdb_slug_unique": {
|
||||
"name": "platforms_igdb_slug_unique",
|
||||
"columns": [
|
||||
"igdb_slug"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"platforms_moby_id_unique": {
|
||||
"name": "platforms_moby_id_unique",
|
||||
"columns": [
|
||||
"moby_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"platforms_es_slug_unique": {
|
||||
"name": "platforms_es_slug_unique",
|
||||
"columns": [
|
||||
"es_slug"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"platforms_ra_id_unique": {
|
||||
"name": "platforms_ra_id_unique",
|
||||
"columns": [
|
||||
"ra_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"platforms_slug_unique": {
|
||||
"name": "platforms_slug_unique",
|
||||
"columns": [
|
||||
"slug"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"screenshots": {
|
||||
"name": "screenshots",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"game_id": {
|
||||
"name": "game_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "blob",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"screenshots_game_id_games_id_fk": {
|
||||
"name": "screenshots_game_id_games_id_fk",
|
||||
"tableFrom": "screenshots",
|
||||
"tableTo": "games",
|
||||
"columnsFrom": [
|
||||
"game_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1771508990238,
|
||||
"tag": "0000_pretty_harry_osborn",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
23
package.json
23
package.json
|
|
@ -12,20 +12,28 @@
|
|||
"build:dev": "NODE_ENV=development bun run build",
|
||||
"package": "bun run build && bun run ./scripts/package-bun.ts",
|
||||
"package:auto-prod": "bun run build:pro && NODE_ENV=production bun run ./scripts/package-bun.ts",
|
||||
"package:linux": "bun run build && TARGET=bun-linux-x64 bun run ./scripts/package-bun.ts",
|
||||
"package:linux": "bun run build && NODE_ENV=development TARGET=bun-linux-x64 bun run ./scripts/package-bun.ts",
|
||||
"openapi-ts": "bun run ./scripts/romm/openapi-ts.ts",
|
||||
"run:build-action": "act workflow_dispatch --artifact-server-path artifacts --env ACTIONS_RUNTIME_TOKEN=foo -W .github/workflows/build.yml",
|
||||
"hmr": "vite --port 5173"
|
||||
"run:build-action": "act workflow_dispatch --artifact-server-path artifacts --env ACTIONS_RUNTIME_TOKEN=foo -W .forgejo/workflows/build.yml",
|
||||
"hmr": "vite --port 5173",
|
||||
"drizzle:generate": "bunx drizzle-kit generate",
|
||||
"test": "bun test",
|
||||
"mappings:generate": "bun run drizzle-kit generate --dialect=sqlite --schema=./src/bun/api/schema/emulators.ts --out=./scripts/drizzle/es-de && bun run ./scripts/generate-es-de-mapping.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.34.3",
|
||||
"@elysiajs/cors": "^1.4.1",
|
||||
"@elysiajs/eden": "^1.4.6",
|
||||
"@elysiajs/static": "^1.4.7",
|
||||
"@hackolade/keytar": "^7.9.0-7",
|
||||
"@rcompat/webview": "^0.18.0",
|
||||
"cheerio": "^1.2.0",
|
||||
"conf": "^15.0.2",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"elysia": "^1.4.22",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"node-downloader-helper": "^2.1.10",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"open": "^11.0.0",
|
||||
"pathe": "^2.0.3",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"tough-cookie-file-store": "^3.3.0",
|
||||
|
|
@ -52,12 +60,16 @@
|
|||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"daisyui": "^5.5.14",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"dts-bundle-generator": "^9.5.1",
|
||||
"eden-tanstack-query": "^0.0.9",
|
||||
"lucide-react": "^0.563.0",
|
||||
"pretty-bytes": "^7.1.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-error-boundary": "^6.1.0",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"sass-embedded": "^1.97.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
|
@ -65,6 +77,7 @@
|
|||
"usehooks-ts": "^3.1.1",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-svg-icons-ng": "^1.5.2",
|
||||
"vite-static-assets-plugin": "^1.2.2"
|
||||
"vite-static-assets-plugin": "^1.2.2",
|
||||
"vite-tsconfig-paths": "^6.1.1"
|
||||
}
|
||||
}
|
||||
160
scripts/generate-es-de-mapping.ts
Normal file
160
scripts/generate-es-de-mapping.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { getSupportedPlatformsEndpointApiPlatformsSupportedGet } from '../src/clients/romm';
|
||||
import customMappings from '../vendors/romm/custom-overrides.json';
|
||||
import { Database } from "bun:sqlite";
|
||||
import * as schema from '../src/bun/api/schema/emulators';
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
|
||||
/** get all latest supported romm platforms */
|
||||
const rommPlatforms = await getSupportedPlatformsEndpointApiPlatformsSupportedGet({ baseUrl: "https://demo.romm.app" });
|
||||
|
||||
/** a matrix for supported platforms and architectures */
|
||||
const platforms: [NodeJS.Platform, NodeJS.Architecture][] = [['linux', 'x64'], ['win32', 'x64'], ['darwin', 'x64'], ['haiku', 'x64'], ['linux', 'arm']];
|
||||
|
||||
/** Save client minimal info for emulator names and descriptions */
|
||||
await Promise.all(platforms.map(async ([platform, arch]) =>
|
||||
{
|
||||
const rules = await Bun.file(`./vendors/es-de/systems/${mapSystem(platform, arch)}/es_find_rules.xml`).arrayBuffer();
|
||||
const $r = cheerio.load(Buffer.from(rules));
|
||||
const es_emulators = $r('ruleList emulator');
|
||||
|
||||
const emulators = Object.fromEntries(es_emulators.toArray().map(system =>
|
||||
{
|
||||
const $system = $r(system);
|
||||
const key = $system.attr('name');
|
||||
const comment = $system.contents().toArray().find((node) => node.type === 'comment');
|
||||
return [key, comment?.data.trim() ?? key];
|
||||
}));
|
||||
|
||||
await Bun.write(`./vendors/es-de/emulators.${platform}.${arch}.json`, JSON.stringify(emulators, null, 3));
|
||||
}));
|
||||
|
||||
/** Delete old databases, we recreate them each time */
|
||||
await Promise.all(platforms.map(async ([platform, arch]) =>
|
||||
{
|
||||
const sqlitePath = `./vendors/es-de/emulators.${platform}.${arch}.sqlite`;
|
||||
if (await fs.exists(sqlitePath))
|
||||
await fs.rm(sqlitePath);
|
||||
}));
|
||||
|
||||
await Promise.all(platforms.map(async ([platform, arch]) =>
|
||||
{
|
||||
const systems = await Bun.file(`./vendors/es-de/systems/${mapSystem(platform, arch)}/es_systems.xml`).arrayBuffer();
|
||||
const $s = cheerio.load(Buffer.from(systems));
|
||||
const rules = await Bun.file(`./vendors/es-de/systems/${mapSystem(platform, arch)}/es_find_rules.xml`).arrayBuffer();
|
||||
const $r = cheerio.load(Buffer.from(rules));
|
||||
|
||||
const sqlitePath = `./vendors/es-de/emulators.${platform}.${arch}.sqlite`;
|
||||
const sqlite = new Database(sqlitePath, { create: true, readwrite: true });
|
||||
const db = drizzle(sqlite, { schema });
|
||||
migrate(db, { migrationsFolder: "./scripts/drizzle/es-de" });
|
||||
|
||||
/** Save the ruleset for emulators */
|
||||
await db.insert(schema.emulators).values($r('ruleList emulator').toArray().map(s =>
|
||||
{
|
||||
const $emulator = $r(s);
|
||||
const $systempath = $emulator.find('rule[type=systempath] entry');
|
||||
const $staticpath = $emulator.find('rule[type=staticpath] entry');
|
||||
const $corepath = $emulator.find('rule[type=corepath] entry');
|
||||
const $androidpackage = $emulator.find('rule[type=androidpackage] entry');
|
||||
const $winregistrypath = $emulator.find('rule[type=winregistrypath] entry');
|
||||
|
||||
const emulatorName = $emulator.attr('name');
|
||||
const emulator: typeof schema.emulators.$inferInsert = {
|
||||
name: emulatorName!,
|
||||
systempath: $systempath.toArray().map(p => $r(p).text()),
|
||||
staticpath: $staticpath.toArray().map(p => $r(p).text()),
|
||||
corepath: $corepath.toArray().map(p => $r(p).text()),
|
||||
androidpackage: $androidpackage.toArray().map(p => $r(p).text()),
|
||||
winregistrypath: $winregistrypath.toArray().map(p => $r(p).text()),
|
||||
};
|
||||
return emulator;
|
||||
}));
|
||||
|
||||
/** Save the systems like ps2 or psp */
|
||||
await Promise.all($s(`systemList system`).toArray().map(async s =>
|
||||
{
|
||||
const name = $s(s).find("name").text();
|
||||
const fullname = $s(s).find("fullname").text();
|
||||
const rommMapping = rommPlatforms.data?.find(p =>
|
||||
p.slug === (customMappings as any)[name] ||
|
||||
p.slug === name ||
|
||||
p.igdb_slug === name ||
|
||||
p.hltb_slug === name ||
|
||||
p.moby_slug === name ||
|
||||
p.display_name === fullname
|
||||
);
|
||||
|
||||
const system: typeof schema.systems.$inferInsert = {
|
||||
name,
|
||||
fullname,
|
||||
path: $s(s).find("path").text(),
|
||||
extension: $s(s).find("extension").text().replaceAll('.', '').split(' ')
|
||||
};
|
||||
|
||||
/** Store mappings to all other sources for easy reference */
|
||||
db.transaction(async (tx) =>
|
||||
{
|
||||
await tx.insert(schema.systems).values(system);
|
||||
if (rommMapping)
|
||||
{
|
||||
const sources: [string, keyof typeof rommMapping | null, keyof typeof rommMapping | null][] = [
|
||||
['ra', 'ra_id', null],
|
||||
['ss', 'ss_id', null],
|
||||
['hltb', null, 'hltb_slug'],
|
||||
['moby', 'moby_id', 'moby_slug'],
|
||||
['launchbox', 'launchbox_id', null],
|
||||
['sgdb', 'sgdb_id', null],
|
||||
['tgdb', 'tgdb_id', null],
|
||||
['hasheous', 'hasheous_id', null],
|
||||
['flashpoint', 'flashpoint_id', null],
|
||||
['romm', null, 'slug'],
|
||||
['igdb', 'igdb_id', 'igdb_slug']
|
||||
];
|
||||
|
||||
await tx.insert(schema.systemMappings)
|
||||
.values(sources.map(([source, sourceId, sourceSlug]) => ({
|
||||
source,
|
||||
sourceId: sourceId ? rommMapping[sourceId] as number : null,
|
||||
sourceSlug: sourceSlug ? rommMapping[sourceSlug] as string : null,
|
||||
system: system.name
|
||||
} satisfies typeof schema.systemMappings.$inferInsert))
|
||||
.filter(m => m.sourceId !== null || m.sourceSlug !== null));
|
||||
}
|
||||
});
|
||||
|
||||
await db.insert(schema.commands).values($s(s).find("command").toArray().map(c =>
|
||||
{
|
||||
const command: typeof schema.commands.$inferInsert = {
|
||||
label: $s(c).attr('label'),
|
||||
command: $s(c).text(),
|
||||
system: system.name
|
||||
};
|
||||
|
||||
return command;
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
|
||||
/** map from bun platform to es-de folder naming */
|
||||
function mapSystem (platform: NodeJS.Platform, arch: NodeJS.Architecture)
|
||||
{
|
||||
let system: string | undefined = undefined;
|
||||
if (platform === 'darwin')
|
||||
{
|
||||
system = 'macos';
|
||||
} else if (platform === 'win32')
|
||||
{
|
||||
system = 'windows';
|
||||
} else if (platform === 'linux' && arch === 'arm')
|
||||
{
|
||||
system = 'linuxarm';
|
||||
}
|
||||
else
|
||||
{
|
||||
system = platform;
|
||||
}
|
||||
return system;
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path, { } from "node:path";
|
||||
import os from "node:os";
|
||||
import { Glob } from "bun";
|
||||
|
||||
const buildSubDir = process.env.BUILD_DIR ?? `./build/${os.platform()}`;
|
||||
const system = getPlatform();
|
||||
const buildSubDir = process.env.BUILD_DIR ?? `./build/${system.platform}`;
|
||||
|
||||
const compileOption: Bun.CompileBuildOptions = {
|
||||
outfile: "gameflow",
|
||||
|
|
@ -19,7 +21,7 @@ if (process.env.TARGET)
|
|||
}
|
||||
|
||||
await Bun.build({
|
||||
entrypoints: ["./src/bun/index.ts", "./src/bun/webview-worker.ts"],
|
||||
entrypoints: ["./src/bun/index.ts", `./src/bun/webview/${system.platform}.ts`],
|
||||
metafile: true,
|
||||
compile: compileOption,
|
||||
outdir: buildSubDir,
|
||||
|
|
@ -27,8 +29,8 @@ await Bun.build({
|
|||
define: {
|
||||
"process.env.IS_BINARY": "true"
|
||||
},
|
||||
minify: true,
|
||||
sourcemap: "linked",
|
||||
minify: process.env.NODE_ENV !== 'development',
|
||||
sourcemap: process.env.NODE_ENV === 'development' ? 'inline' : "linked",
|
||||
target: 'bun',
|
||||
format: 'esm',
|
||||
loader: {
|
||||
|
|
@ -52,7 +54,32 @@ await Bun.build({
|
|||
build.onEnd(async () =>
|
||||
{
|
||||
await fs.cp('./dist', `${buildSubDir}/dist`, { recursive: true });
|
||||
await fs.cp('./drizzle', `${buildSubDir}/drizzle`, { recursive: true });
|
||||
await fs.cp(`./vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, `${buildSubDir}/vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, { recursive: true });
|
||||
});
|
||||
},
|
||||
}]
|
||||
});
|
||||
|
||||
function getPlatform ()
|
||||
{
|
||||
if (process.env.TARGET)
|
||||
{
|
||||
const arch = process.env.TARGET.includes('arm') ? 'arm' : 'x64';
|
||||
let platform = os.platform();
|
||||
if (platform.includes('windows'))
|
||||
{
|
||||
platform = 'win32';
|
||||
} else if (platform.includes('darwin'))
|
||||
{
|
||||
platform = 'darwin';
|
||||
} else
|
||||
{
|
||||
platform = 'linux';
|
||||
}
|
||||
return { platform, arch };
|
||||
} else
|
||||
{
|
||||
return { platform: os.platform(), arch: os.arch() };
|
||||
}
|
||||
}
|
||||
76
src/bun/api/app.ts
Normal file
76
src/bun/api/app.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
|
||||
import { TaskQueue } from "./task-queue";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { CookieJar } from 'tough-cookie';
|
||||
import FileCookieStore from 'tough-cookie-file-store';
|
||||
import path from 'node:path';
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import Conf from "conf";
|
||||
import projectPackage from '~/package.json';
|
||||
import { SERVER_URL, SettingsSchema, SettingsType } from "../../shared/constants";
|
||||
import { client } from "@clients/romm/client.gen";
|
||||
import * as schema from "./schema/app";
|
||||
import * as emulatorSchema from "./schema/emulators";
|
||||
import { login, logout } from "./auth";
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import { ActiveGame } from "../types/types";
|
||||
import EventEmitter from "node:events";
|
||||
import { ErrorLike } from "bun";
|
||||
|
||||
export const config = new Conf<SettingsType>({
|
||||
projectName: projectPackage.name,
|
||||
projectSuffix: 'bun',
|
||||
schema: Object.fromEntries(Object.entries(SettingsSchema.shape).map(([key, schema]) => [key, schema.toJSONSchema() as any])) as any,
|
||||
defaults: SettingsSchema.parse({}),
|
||||
});
|
||||
export const customEmulators = new Conf<Record<string, string>>({
|
||||
projectName: projectPackage.name,
|
||||
projectSuffix: 'bun',
|
||||
configName: 'custom-emulators',
|
||||
rootSchema: {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Config Path Located At: ", config.path);
|
||||
console.log("Custom Emulator Paths Located At: ", customEmulators.path);
|
||||
const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json'));
|
||||
console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath);
|
||||
export const jar = new CookieJar(fileCookieStore);
|
||||
await fs.mkdir(config.get('downloadPath'), { recursive: true });
|
||||
const sqlite = new Database(path.join(config.get('downloadPath'), 'db.sqlite'), { create: true, readwrite: true });
|
||||
export const db = drizzle(sqlite, { schema });
|
||||
migrate(db, { migrationsFolder: "./drizzle" });
|
||||
const emulatorsSqlite = new Database(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`, { readonly: true });
|
||||
export const emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema });
|
||||
export const taskQueue = new TaskQueue();
|
||||
config.onDidChange('rommAddress', v => client.setConfig({ baseUrl: v }));
|
||||
await login();
|
||||
export let activeGame: ActiveGame | undefined;
|
||||
export function setActiveGame (game: ActiveGame)
|
||||
{
|
||||
if (activeGame) throw new Error("Only one active game at a time");
|
||||
return activeGame = game;
|
||||
}
|
||||
export const events = new EventEmitter<AppEventMap>();
|
||||
events.addListener('activegameexit', () => activeGame = undefined);
|
||||
console.log("Logging In to Romm");
|
||||
|
||||
export async function cleanup ()
|
||||
{
|
||||
await taskQueue.close();
|
||||
sqlite.close();
|
||||
await logout();
|
||||
emulatorsSqlite.close();
|
||||
}
|
||||
|
||||
interface AppEventMap
|
||||
{
|
||||
activegameexit: [{ subprocess: Bun.Subprocess, exitCode: number | null, signalCode: number | null, error?: ErrorLike; }];
|
||||
exitapp: [];
|
||||
}
|
||||
97
src/bun/api/auth.ts
Normal file
97
src/bun/api/auth.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import Elysia, { status } from "elysia";
|
||||
import { config, db, jar } from "./app";
|
||||
import z from "zod";
|
||||
import { client } from "@clients/romm/client.gen";
|
||||
import { loginApiLoginPost } from "@clients/romm";
|
||||
import secrets from '../api/secrets';
|
||||
|
||||
export default new Elysia()
|
||||
.post('/login', async ({ body: { host, username, password } }) =>
|
||||
{
|
||||
if (config.has('rommAddress') && config.has('rommUser'))
|
||||
{
|
||||
await logout();
|
||||
const oldRommAddress = config.get('rommAddress');
|
||||
if (oldRommAddress)
|
||||
{
|
||||
const cookies = await jar.getCookies(oldRommAddress);
|
||||
cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key));
|
||||
}
|
||||
}
|
||||
|
||||
config.set('rommAddress', host);
|
||||
config.set('rommUser', username);
|
||||
|
||||
await secrets.set({ service: 'gameflow', name: 'romm', value: password });
|
||||
await login();
|
||||
|
||||
return status(200);
|
||||
}, { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) })
|
||||
.get('/login', async () =>
|
||||
{
|
||||
const credentials = await secrets.get({ service: 'gameflow', name: 'romm' });
|
||||
return { hasPassword: !!credentials };
|
||||
}, { response: z.object({ hasPassword: z.boolean() }) })
|
||||
.post('/logout', async () =>
|
||||
{
|
||||
await secrets.delete({ service: 'gameflow', name: 'romm' });
|
||||
await logout();
|
||||
const rommAddress = config.get('rommAddress');
|
||||
if (rommAddress)
|
||||
{
|
||||
const cookies = await jar.getCookies(rommAddress);
|
||||
cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key));
|
||||
}
|
||||
return status(200);
|
||||
}, { response: z.any() });
|
||||
|
||||
async function updateClient ()
|
||||
{
|
||||
client.setConfig({
|
||||
baseUrl: config.get('rommAddress'), headers: {
|
||||
cookie: await jar.getCookieString(config.get('rommAddress') ?? '')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function logout ()
|
||||
{
|
||||
if (!config.has('rommAddress'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const rommAddress = config.get('rommAddress');
|
||||
if (rommAddress)
|
||||
{
|
||||
console.log("Logging Out of ROMM");
|
||||
try
|
||||
{
|
||||
await loginApiLoginPost({
|
||||
baseUrl: rommAddress, headers: {
|
||||
'cookie': await jar.getCookieString(rommAddress)
|
||||
}
|
||||
});
|
||||
} catch (error)
|
||||
{
|
||||
console.error("Failed to logout of ROMM ", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function login ()
|
||||
{
|
||||
if (!config.has('rommAddress') || !config.has('rommUser'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const rommAddress = config.get('rommAddress');
|
||||
const rommUser = config.get('rommUser');
|
||||
if (rommAddress && rommUser)
|
||||
{
|
||||
console.log("Logging In to ROMM");
|
||||
const password = await secrets.get({ service: 'gameflow', name: "romm" });
|
||||
const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` });
|
||||
loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress));
|
||||
await updateClient();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +1,12 @@
|
|||
import z from "zod";
|
||||
import { config } from "./settings";
|
||||
import Elysia, { status } from "elysia";
|
||||
import keytar from '@hackolade/keytar';
|
||||
import { loginApiLoginPost } from "../../clients/romm";
|
||||
import { CookieJar } from 'tough-cookie';
|
||||
import FileCookieStore from 'tough-cookie-file-store';
|
||||
import path from 'node:path';
|
||||
import Elysia from "elysia";
|
||||
import { config, jar } from "./app";
|
||||
import games from "./games/games";
|
||||
import platforms from "./games/platforms";
|
||||
import auth from "./auth";
|
||||
|
||||
const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json'));
|
||||
console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath);
|
||||
const jar = new CookieJar(fileCookieStore);
|
||||
await login();
|
||||
|
||||
export async function logout ()
|
||||
{
|
||||
if (!config.has('rommAddress'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const rommAddress = config.get('rommAddress');
|
||||
if (rommAddress)
|
||||
{
|
||||
console.log("Logging Out of ROMM");
|
||||
try
|
||||
{
|
||||
await loginApiLoginPost({
|
||||
baseUrl: rommAddress, headers: {
|
||||
'cookie': await jar.getCookieString(rommAddress)
|
||||
}
|
||||
});
|
||||
} catch (error)
|
||||
{
|
||||
console.error("Failed to logout of ROMM ", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function login ()
|
||||
{
|
||||
if (!config.has('rommAddress') || !config.has('rommUser'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const rommAddress = config.get('rommAddress');
|
||||
const rommUser = config.get('rommUser');
|
||||
if (rommAddress && rommUser)
|
||||
{
|
||||
console.log("Logging In to ROMM");
|
||||
const password = await keytar.getPassword('romm', 'gameflow');
|
||||
const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` });
|
||||
loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress));
|
||||
}
|
||||
}
|
||||
|
||||
export const romm = new Elysia({ prefix: "/romm" })
|
||||
.post('/login', async ({ body: { host, username, password } }) =>
|
||||
{
|
||||
if (config.has('rommAddress') && config.has('rommUser'))
|
||||
{
|
||||
await logout();
|
||||
const oldRommAddress = config.get('rommAddress');
|
||||
if (oldRommAddress)
|
||||
{
|
||||
const cookies = await jar.getCookies(oldRommAddress);
|
||||
cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key));
|
||||
}
|
||||
}
|
||||
|
||||
config.set('rommAddress', host);
|
||||
config.set('rommUser', username);
|
||||
|
||||
await keytar.setPassword('romm', 'gameflow', password);
|
||||
await login();
|
||||
|
||||
return status(200);
|
||||
}, { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) })
|
||||
.get('/login', async () =>
|
||||
{
|
||||
const credentials = await keytar.getPassword('romm', 'gameflow');
|
||||
return { hasPassword: !!credentials };
|
||||
}, { response: z.object({ hasPassword: z.boolean() }) })
|
||||
.post('/logout', async () =>
|
||||
{
|
||||
await keytar.deletePassword('romm', 'gameflow');
|
||||
await logout();
|
||||
const rommAddress = config.get('rommAddress');
|
||||
if (rommAddress)
|
||||
{
|
||||
const cookies = await jar.getCookies(rommAddress);
|
||||
cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key));
|
||||
}
|
||||
return status(200);
|
||||
})
|
||||
export default new Elysia({ prefix: "/api/romm" })
|
||||
.use([games, platforms, auth])
|
||||
.all("/*", async ({ request, params, set }) =>
|
||||
{
|
||||
if (!config.has('rommAddress') && !config.get('rommAddress'))
|
||||
|
|
@ -119,19 +34,6 @@ export const romm = new Elysia({ prefix: "/romm" })
|
|||
redirect: 'manual', // avoid ROMM redirects
|
||||
});
|
||||
|
||||
/*
|
||||
if (rommResponse.status === 403 && config.has('rommUser'))
|
||||
{
|
||||
await login();
|
||||
headers.set('cookie', await jar.getCookieString(rommUrl.href));
|
||||
rommResponse = await fetch(url, {
|
||||
method: request.method,
|
||||
headers,
|
||||
body: await request.arrayBuffer(),
|
||||
redirect: 'manual', // avoid ROMM redirects
|
||||
});
|
||||
}*/
|
||||
|
||||
set.status = rommResponse.status;
|
||||
rommResponse.headers.forEach((value, key) =>
|
||||
{
|
||||
|
|
@ -139,4 +41,5 @@ export const romm = new Elysia({ prefix: "/romm" })
|
|||
});
|
||||
|
||||
return new Response(rommResponse.body, { status: rommResponse.status });
|
||||
}).on('stop', logout);
|
||||
}, { response: z.instanceof(Response) });
|
||||
|
||||
|
|
|
|||
245
src/bun/api/games/games.ts
Normal file
245
src/bun/api/games/games.ts
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
import Elysia, { status } from "elysia";
|
||||
import { activeGame, config, db, events, setActiveGame, taskQueue } from "../app";
|
||||
import { and, eq, getTableColumns } from "drizzle-orm";
|
||||
import z from "zod";
|
||||
import * as schema from "../schema/app";
|
||||
import fs from "node:fs/promises";
|
||||
import { FrontEndGameType, FrontEndGameTypeDetailed } from "@shared/constants";
|
||||
import { getRomApiRomsIdGet, getRomsApiRomsGet } from "@clients/romm";
|
||||
import { InstallJob } from "../jobs/install-job";
|
||||
import path from "node:path";
|
||||
import { calculateSize, checkInstalled, convertRomToFrontend, convertRomToFrontendDetailed, getLocalGameMatch } from "./services/utils";
|
||||
import buildStatusResponse, { getValidLaunchCommandsForGame } from "./services/statusService";
|
||||
import { errorToResponse } from "elysia/adapter/bun/handler";
|
||||
|
||||
export default new Elysia()
|
||||
.get('/game/local/:id/cover', async ({ params: { id }, set }) =>
|
||||
{
|
||||
const coverBlob = await db.query.games.findFirst({ columns: { cover: true, cover_type: true }, where: eq(schema.games.id, id) });
|
||||
if (!coverBlob || !coverBlob.cover)
|
||||
{
|
||||
return status(404);
|
||||
}
|
||||
if (coverBlob.cover_type)
|
||||
{
|
||||
set.headers["content-type"] = coverBlob.cover_type;
|
||||
}
|
||||
return status(200, coverBlob.cover);
|
||||
}, { response: { 200: z.instanceof(Buffer<ArrayBufferLike>), 404: z.any() }, params: z.object({ id: z.coerce.number() }) })
|
||||
.get('/screenshot/:id', async ({ params: { id }, set }) =>
|
||||
{
|
||||
const screenshot = await db.query.screenshots.findFirst({ where: eq(schema.screenshots.id, id), columns: { content: true, type: true } });
|
||||
if (screenshot)
|
||||
{
|
||||
if (screenshot.type)
|
||||
{
|
||||
set.headers["content-type"] = screenshot.type;
|
||||
}
|
||||
return screenshot.content;
|
||||
|
||||
}
|
||||
|
||||
return status(404);
|
||||
}, { params: z.object({ id: z.coerce.number() }) })
|
||||
.get("/game/local/:id/installed", async ({ params: { id } }) =>
|
||||
{
|
||||
const data = await db.query.games.findFirst({ where: eq(schema.games.id, id) });
|
||||
if (data && data.path_fs)
|
||||
{
|
||||
return { installed: await fs.exists(data.path_fs) };
|
||||
}
|
||||
|
||||
return { installed: false };
|
||||
}, {
|
||||
params: z.object({ id: z.number() }),
|
||||
response: z.object({ installed: z.boolean() })
|
||||
}).get('/games', async ({ query: { platform_id, collection_id } }) =>
|
||||
{
|
||||
const where: any[] = [];
|
||||
if (platform_id)
|
||||
{
|
||||
where.push(eq(schema.games.id, platform_id));
|
||||
}
|
||||
|
||||
const games: FrontEndGameType[] = [];
|
||||
|
||||
const localGames = await db.select({
|
||||
platform_display_name: schema.platforms.name,
|
||||
id: schema.games.id,
|
||||
last_played: schema.games.last_played,
|
||||
created_at: schema.games.created_at,
|
||||
platform_id: schema.games.platform_id,
|
||||
slug: schema.games.slug,
|
||||
name: schema.games.name,
|
||||
path_fs: schema.games.path_fs,
|
||||
source_id: schema.games.source_id,
|
||||
source: schema.games.source
|
||||
}).from(schema.games).leftJoin(schema.platforms, eq(schema.games.platform_id, schema.platforms.id)).where(and(...where));
|
||||
|
||||
const localGamesSet = new Set(localGames.map(g => g.source_id));
|
||||
games.push(...localGames.map(g =>
|
||||
{
|
||||
const game: FrontEndGameType = {
|
||||
...g,
|
||||
platform_display_name: g.platform_display_name ?? "Local",
|
||||
id: { id: g.id, source: 'local' },
|
||||
updated_at: g.created_at,
|
||||
path_cover: `/api/romm/game/local/${g.id}/cover`,
|
||||
source_id: g.source_id,
|
||||
source: g.source,
|
||||
path_platform_cover: `/api/romm/platform/local/${g.platform_id}/cover`
|
||||
};
|
||||
return game;
|
||||
}));
|
||||
|
||||
const rommGames = await getRomsApiRomsGet({ query: { platform_ids: platform_id ? [platform_id] : undefined, collection_id }, throwOnError: true });
|
||||
games.push(...rommGames.data.items.filter(g => !localGamesSet.has(g.id)).map(g =>
|
||||
{
|
||||
return convertRomToFrontend(g);
|
||||
}));
|
||||
|
||||
return { games };
|
||||
}, {
|
||||
query: z.object({ platform_id: z.coerce.number().optional(), collection_id: z.coerce.number().optional() }),
|
||||
})
|
||||
.get('/game/:source/:id', async ({ params: { source, id } }) =>
|
||||
{
|
||||
async function getLocalGameDetailed (match: any)
|
||||
{
|
||||
const localGames = await db.select({
|
||||
platform_display_name: schema.platforms.name,
|
||||
...getTableColumns(schema.games)
|
||||
}).from(schema.games).where(match).leftJoin(schema.platforms, eq(schema.games.platform_id, schema.platforms.id));
|
||||
if (localGames.length > 0)
|
||||
{
|
||||
const screenshots = await db.query.screenshots.findMany({ where: eq(schema.screenshots.game_id, localGames[0].id), columns: { id: true } });
|
||||
const exists = await checkInstalled(localGames[0].path_fs);
|
||||
const fileSize = await calculateSize(localGames[0].path_fs);
|
||||
const game: FrontEndGameTypeDetailed = {
|
||||
...localGames[0],
|
||||
path_cover: `/api/romm/game/local/${localGames[0].id}/cover`,
|
||||
updated_at: localGames[0].created_at,
|
||||
id: { id: localGames[0].id, source: 'local' },
|
||||
path_platform_cover: `/api/romm/platform/local/${localGames[0].platform_id}/cover`,
|
||||
fs_size_bytes: fileSize ?? null,
|
||||
paths_screenshots: screenshots.map(s => `/api/romm/screenshot/${s.id}`),
|
||||
local: true,
|
||||
missing: !exists
|
||||
};
|
||||
return game;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (source === 'local')
|
||||
{
|
||||
|
||||
const localGame = await getLocalGameDetailed(eq(schema.games.id, id));
|
||||
if (localGame) return localGame;
|
||||
return status('Not Found');
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
const localGame = await getLocalGameDetailed(getLocalGameMatch(id, source));
|
||||
if (localGame) return localGame;
|
||||
|
||||
const rom = await getRomApiRomsIdGet({ path: { id } });
|
||||
if (rom.data)
|
||||
{
|
||||
const romGame = convertRomToFrontendDetailed(rom.data);
|
||||
return romGame;
|
||||
}
|
||||
|
||||
return status("Not Found", rom.response);
|
||||
}
|
||||
|
||||
}, {
|
||||
params: z.object({ source: z.string(), id: z.coerce.number() })
|
||||
})
|
||||
.get('/status/:source/:id', async ({ params: { source, id }, set }) =>
|
||||
{
|
||||
set.headers["content-type"] = 'text/event-stream';
|
||||
set.headers["cache-control"] = 'no-cache';
|
||||
set.headers['connection'] = 'keep-alive';
|
||||
return buildStatusResponse(source, id);
|
||||
}, {
|
||||
response: z.any(),
|
||||
params: z.object({ id: z.coerce.number(), source: z.string() }),
|
||||
query: z.object({ isLocal: z.boolean().optional() })
|
||||
})
|
||||
.delete('/game/:source/:id', async ({ params: { source, id } }) =>
|
||||
{
|
||||
const deleted = await db.delete(schema.games).where(getLocalGameMatch(id, source)).returning({ path_fs: schema.games.path_fs });
|
||||
const downloadPath = config.get('downloadPath');
|
||||
await Promise.all(deleted.filter(d => !!d.path_fs).map(async d =>
|
||||
{
|
||||
await fs.rm(path.join(downloadPath, d.path_fs!), { recursive: true, force: true });
|
||||
}));
|
||||
|
||||
return status(deleted.length > 0 ? 'OK' : 'Not Modified');
|
||||
}, {
|
||||
params: z.object({ id: z.coerce.number(), source: z.string() }),
|
||||
})
|
||||
.post('/game/:source/:id/install', async ({ params: { id, source } }) =>
|
||||
{
|
||||
if (!taskQueue.hasActive())
|
||||
{
|
||||
taskQueue.enqueue(`install-rom-${source}-${id}`, new InstallJob(id));
|
||||
return status(200);
|
||||
} else
|
||||
{
|
||||
return status('Not Implemented');
|
||||
}
|
||||
}, {
|
||||
params: z.object({ id: z.coerce.number(), source: z.string() }),
|
||||
response: z.any()
|
||||
})
|
||||
.post('/game/:source/:id/play', async ({ params: { id, source }, set }) =>
|
||||
{
|
||||
const validCommand = await getValidLaunchCommandsForGame(source, id);
|
||||
if (validCommand)
|
||||
{
|
||||
if (validCommand instanceof Error)
|
||||
{
|
||||
return errorToResponse(validCommand, set);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (activeGame && activeGame.process.killed === false)
|
||||
{
|
||||
return status('Conflict', `${activeGame.name} currently running`);
|
||||
}
|
||||
|
||||
const localGame = await db.query.games.findFirst({
|
||||
where: eq(schema.games.id, validCommand.gameId), columns: {
|
||||
name: true
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const game = setActiveGame({
|
||||
process: Bun.spawn({
|
||||
cmd: validCommand.command.command.split(' '), onExit (subprocess, exitCode, signalCode, error)
|
||||
{
|
||||
events.emit('activegameexit', { subprocess, exitCode, signalCode, error });
|
||||
},
|
||||
}),
|
||||
name: localGame?.name ?? "Unknown",
|
||||
gameId: validCommand.gameId,
|
||||
command: validCommand.command.command
|
||||
});
|
||||
|
||||
await game.process.exited;
|
||||
if (game.process.exitCode && game.process.exitCode > 0)
|
||||
{
|
||||
return status('Internal Server Error');
|
||||
}
|
||||
return status('OK');
|
||||
}
|
||||
}
|
||||
}, {
|
||||
params: z.object({ id: z.coerce.number(), source: z.string() }),
|
||||
});
|
||||
86
src/bun/api/games/platforms.ts
Normal file
86
src/bun/api/games/platforms.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import Elysia, { status } from "elysia";
|
||||
import { getPlatformApiPlatformsIdGet, getPlatformsApiPlatformsGet } from "@clients/romm";
|
||||
import z from "zod";
|
||||
import { count, eq, getTableColumns, notInArray } from "drizzle-orm";
|
||||
import { db } from "../app";
|
||||
import { FrontEndPlatformType } from "@shared/constants";
|
||||
import * as schema from "../schema/app";
|
||||
|
||||
export default new Elysia()
|
||||
.get('/platforms', async () =>
|
||||
{
|
||||
const platforms: FrontEndPlatformType[] = [];
|
||||
let rommPlatformsSet: Set<string> | undefined;
|
||||
const { data: rommPlatforms } = await getPlatformsApiPlatformsGet();
|
||||
if (rommPlatforms)
|
||||
{
|
||||
const frontEndPlatforms = rommPlatforms.map(p =>
|
||||
{
|
||||
const platform: FrontEndPlatformType = {
|
||||
slug: p.slug,
|
||||
name: p.display_name,
|
||||
family_name: p.family_name,
|
||||
path_cover: `/api/romm/assets/platforms/${p.slug}.svg`,
|
||||
game_count: p.rom_count,
|
||||
updated_at: new Date(p.updated_at),
|
||||
id: { source: 'romm', id: p.id },
|
||||
source: null,
|
||||
source_id: null
|
||||
};
|
||||
|
||||
return platform;
|
||||
});
|
||||
rommPlatformsSet = new Set(rommPlatforms.map(p => p.slug));
|
||||
platforms.push(...frontEndPlatforms);
|
||||
}
|
||||
|
||||
const localPlatforms = await db.select({ ...getTableColumns(schema.platforms), game_count: count(schema.games.id) })
|
||||
.from(schema.platforms)
|
||||
.leftJoin(schema.games, eq(schema.games.platform_id, schema.platforms.id))
|
||||
.groupBy(schema.platforms.id)
|
||||
.where(notInArray(schema.platforms.slug, Array.from(rommPlatformsSet ?? [])));
|
||||
platforms.push(...localPlatforms.map(p =>
|
||||
{
|
||||
const platform: FrontEndPlatformType = {
|
||||
slug: p.slug,
|
||||
name: p.name,
|
||||
family_name: p.family_name,
|
||||
path_cover: `/api/romm/platform/local/${p.id}/cover`,
|
||||
game_count: p.game_count,
|
||||
updated_at: p.created_at,
|
||||
id: { source: 'local', id: p.id },
|
||||
source: null,
|
||||
source_id: null
|
||||
};
|
||||
|
||||
return platform;
|
||||
}));
|
||||
|
||||
return { platforms };
|
||||
}).get('/platforms/:source/:id', async ({ params: { source, id } }) =>
|
||||
{
|
||||
const rommPlatform = await getPlatformApiPlatformsIdGet({ path: { id } });
|
||||
if (rommPlatform.data)
|
||||
{
|
||||
return rommPlatform.data;
|
||||
}
|
||||
|
||||
return status("Not Found", rommPlatform.response);
|
||||
}, { params: z.object({ source: z.string(), id: z.coerce.number() }) }).get('/platform/local/:id/cover', async ({ params: { id }, set }) =>
|
||||
{
|
||||
const coverBlob = await db.query.platforms.findFirst({
|
||||
columns: {
|
||||
cover: true, cover_type: true
|
||||
|
||||
}, where: eq(schema.platforms.id, id)
|
||||
});
|
||||
if (!coverBlob || !coverBlob.cover)
|
||||
{
|
||||
return status(404);
|
||||
}
|
||||
if (coverBlob.cover_type)
|
||||
{
|
||||
set.headers["content-type"] = coverBlob.cover_type;
|
||||
}
|
||||
return status(200, coverBlob.cover);
|
||||
}, { response: { 200: z.instanceof(Buffer<ArrayBufferLike>), 404: z.any() }, params: z.object({ id: z.coerce.number() }) });
|
||||
219
src/bun/api/games/services/launchGameService.ts
Normal file
219
src/bun/api/games/services/launchGameService.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import path, { basename, dirname } from 'node:path';
|
||||
import { which } from 'bun';
|
||||
import fs from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import * as schema from '../../schema/emulators';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { config, emulatorsDb } from '../../app';
|
||||
import os from 'node:os';
|
||||
|
||||
export const varRegex = /%([^%]+)%/g;
|
||||
|
||||
interface CommandEntry
|
||||
{
|
||||
label?: string;
|
||||
command: string;
|
||||
valid: boolean;
|
||||
emulator?: string;
|
||||
}
|
||||
|
||||
export async function getValidLaunchCommands (data: {
|
||||
systemSlug: string;
|
||||
gamePath: string;
|
||||
customEmulatorConfig: {
|
||||
get: (id: string) => string | undefined,
|
||||
has: (id: string) => boolean,
|
||||
};
|
||||
}): Promise<CommandEntry[]>
|
||||
{
|
||||
|
||||
const system = await emulatorsDb.query.systems.findFirst({ with: { commands: true }, where: eq(schema.systems.name, data.systemSlug) });
|
||||
|
||||
if (!system)
|
||||
{
|
||||
throw new Error(`Could not find system '${data.systemSlug}'`);
|
||||
}
|
||||
|
||||
if (!system.extension || system.extension.length <= 0)
|
||||
{
|
||||
throw new Error(`No extensions listed for system '${data.systemSlug}'`);
|
||||
}
|
||||
|
||||
const downloadPath = config.get('downloadPath');
|
||||
const gamePath = path.join(downloadPath, data.gamePath);
|
||||
|
||||
const validFiles: string[] = [];
|
||||
if (!existsSync(gamePath))
|
||||
{
|
||||
throw new Error(`Provided rom path is missing: '${gamePath}'`);
|
||||
}
|
||||
|
||||
const gamePathStat = await fs.stat(gamePath);
|
||||
|
||||
const extensionList = system.extension.join(',');
|
||||
|
||||
if (gamePathStat.isDirectory())
|
||||
{
|
||||
for await (const file of fs.glob(path.join(gamePath, `/**/*.{${extensionList}}`)))
|
||||
{
|
||||
validFiles.push(file);
|
||||
}
|
||||
|
||||
if (validFiles.length <= 0)
|
||||
{
|
||||
throw new Error(`Could not find valid rom file. Must be a file that ends in '${extensionList}'`);
|
||||
}
|
||||
} else
|
||||
{
|
||||
if (system.extension.some(e => gamePath.toLocaleLowerCase().endsWith(e.toLocaleLowerCase())))
|
||||
{
|
||||
validFiles.push(gamePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Error(`Invalid Rom File. Must be a file that ends in '${extensionList}'`);
|
||||
}
|
||||
}
|
||||
|
||||
const formattedCommands = await Promise.all(system.commands.map(async command =>
|
||||
{
|
||||
const label = command.label;
|
||||
const cmd = command.command;
|
||||
|
||||
const matches = cmd.match(varRegex);
|
||||
if (matches)
|
||||
{
|
||||
let emulator: string | undefined = undefined;
|
||||
const varList = await Promise.all(matches.map(async (value) =>
|
||||
{
|
||||
if (value.startsWith("%EMULATOR_"))
|
||||
{
|
||||
const emulatorName = value.substring("%EMULATOR_".length, value.length - 1);
|
||||
let exec = await findExec(emulatorName);
|
||||
if (data.customEmulatorConfig.has(emulatorName))
|
||||
{
|
||||
exec = data.customEmulatorConfig.get(emulatorName);
|
||||
}
|
||||
|
||||
emulator = emulatorName;
|
||||
return [value, exec];
|
||||
}
|
||||
|
||||
const key = value.substring(1, value.length - 1);
|
||||
return [value, process.env[key]];
|
||||
}));
|
||||
const vars = Object.fromEntries(varList);
|
||||
vars['%ROM%'] = validFiles[0];
|
||||
vars['%ESPATH%'] = config.get('downloadPath');
|
||||
|
||||
// missing variable
|
||||
const invalid = Object.entries(vars).find(c => c[1] === undefined);
|
||||
|
||||
const command = cmd.replace(varRegex, (s) => vars[s] ?? '');
|
||||
return { label: label ?? undefined, command, valid: !invalid, emulator } satisfies CommandEntry;
|
||||
}
|
||||
}));
|
||||
|
||||
return formattedCommands.filter(c => !!c);
|
||||
}
|
||||
|
||||
export async function findExec (emulatorName: string)
|
||||
{
|
||||
const emulator = await emulatorsDb.query.emulators.findFirst({ where: eq(schema.emulators.name, emulatorName) });
|
||||
if (!emulator)
|
||||
{
|
||||
throw new Error(`Could not find emulator ${emulatorName}`);
|
||||
}
|
||||
if (os.platform() === 'win32')
|
||||
{
|
||||
const regValues = emulator.winregistrypath;
|
||||
if (regValues.length > 0)
|
||||
{
|
||||
for (const node of regValues)
|
||||
{
|
||||
const registryValue = await readRegistryValue(node);
|
||||
if (registryValue)
|
||||
{
|
||||
return registryValue;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const systempaths = emulator.systempath;
|
||||
if (systempaths.length > 0)
|
||||
{
|
||||
const systemPath = await resolveSystemPath(systempaths);
|
||||
if (systemPath)
|
||||
{
|
||||
return systemPath;
|
||||
}
|
||||
}
|
||||
|
||||
const staticPaths = emulator.staticpath;
|
||||
if (staticPaths.length > 0)
|
||||
{
|
||||
const staticPath = await resolveStaticPath(staticPaths);
|
||||
if (staticPath)
|
||||
{
|
||||
return staticPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readRegistryValue (text: string)
|
||||
{
|
||||
const params = text.split('|');
|
||||
const key = dirname(params[0]);
|
||||
const value = basename(params[0]);
|
||||
const bin = params.length > 1 ? params[1] : undefined;
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["reg", "QUERY", key, "/v", value],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = await new Response(proc.stdout).text();
|
||||
await proc.exited;
|
||||
|
||||
if (!output.includes(value)) return null;
|
||||
|
||||
const lines = output.split("\n");
|
||||
for (const line of lines)
|
||||
{
|
||||
if (line.includes(value))
|
||||
{
|
||||
const parts = line.trim().split(/\s{4,}/);
|
||||
return bin ? path.join(parts[2], bin) : parts[2]; // registry value
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolveStaticPath (entries: string[])
|
||||
{
|
||||
for (const entry of entries)
|
||||
{
|
||||
for await (const match of fs.glob(entry))
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolveSystemPath (entries: string[])
|
||||
{
|
||||
for (const entry of entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
const found = which(entry);
|
||||
return found;
|
||||
} catch { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
171
src/bun/api/games/services/statusService.ts
Normal file
171
src/bun/api/games/services/statusService.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { GameInstallProgress, GameStatusType, } from "@shared/constants";
|
||||
import { activeGame, customEmulators, db, events, taskQueue } from "../../app";
|
||||
import { getValidLaunchCommands } from "./launchGameService";
|
||||
import * as schema from '../../schema/app';
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getErrorMessage } from "@/bun/utils";
|
||||
import { getLocalGameMatch } from "./utils";
|
||||
|
||||
class CommandSearchError extends Error
|
||||
{
|
||||
constructor(status: GameStatusType, message: string)
|
||||
{
|
||||
super(message);
|
||||
this.name = status;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLocalGame (source: string, id: number)
|
||||
{
|
||||
const localGames = await db.select({ id: schema.games.id, path_fs: schema.games.path_fs, platform_slug: schema.platforms.es_slug })
|
||||
.from(schema.games)
|
||||
.where(getLocalGameMatch(id, source))
|
||||
.leftJoin(schema.platforms, eq(schema.games.platform_id, schema.platforms.id));
|
||||
|
||||
if (localGames.length > 0)
|
||||
{
|
||||
return localGames[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getValidLaunchCommandsForGame (source: string, id: number)
|
||||
{
|
||||
const localGame = await getLocalGame(source, id);
|
||||
if (localGame)
|
||||
{
|
||||
if (localGame.platform_slug)
|
||||
{
|
||||
if (localGame.path_fs)
|
||||
{
|
||||
try
|
||||
{
|
||||
const commands = await getValidLaunchCommands({ systemSlug: localGame.platform_slug, customEmulatorConfig: customEmulators, gamePath: localGame.path_fs });
|
||||
const validCommand = commands.find(c => c.valid);
|
||||
if (validCommand)
|
||||
{
|
||||
return { command: validCommand, gameId: localGame.id, source: source, sourceId: id };
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator).join(',')}`);
|
||||
}
|
||||
} catch (error)
|
||||
{
|
||||
console.error(error);
|
||||
return new CommandSearchError('error', getErrorMessage(error));
|
||||
}
|
||||
|
||||
} else
|
||||
{
|
||||
return new CommandSearchError('error', 'Missing Path');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new CommandSearchError('error', 'Missing Platform');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default async function buildStatusResponse (source: string, id: number)
|
||||
{
|
||||
let cleanup: (() => void) | undefined;
|
||||
return new Response(new ReadableStream({
|
||||
async start (controller)
|
||||
{
|
||||
function enqueue (data: GameInstallProgress, event?: 'error' | 'refresh')
|
||||
{
|
||||
const evntString = event ? `event: ${event}\n` : '';
|
||||
controller.enqueue(`${evntString}data: ${JSON.stringify(data)}\n\n`);
|
||||
}
|
||||
|
||||
const sourceId = `${source}-${id}`;
|
||||
|
||||
async function sendLatests ()
|
||||
{
|
||||
const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(id, source), columns: { id: true } });
|
||||
const activeTask = taskQueue.findJob(`install-rom-${source}-${id}`);
|
||||
if (activeTask)
|
||||
{
|
||||
enqueue({
|
||||
progress: activeTask.progress,
|
||||
status: activeTask.state as any
|
||||
});
|
||||
|
||||
} else if (activeGame && activeGame.gameId === localGame?.id)
|
||||
{
|
||||
enqueue({ status: 'playing' as GameStatusType, details: 'Playing' });
|
||||
}
|
||||
else
|
||||
{
|
||||
const validCommand = await getValidLaunchCommandsForGame(source, id);
|
||||
if (validCommand)
|
||||
{
|
||||
if (validCommand instanceof Error)
|
||||
{
|
||||
enqueue({ status: validCommand.name as GameStatusType, error: validCommand.message });
|
||||
}
|
||||
else
|
||||
{
|
||||
enqueue({ status: 'installed', details: validCommand.command.label });
|
||||
}
|
||||
|
||||
} else
|
||||
{
|
||||
enqueue({ status: 'install', details: 'Install' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await sendLatests();
|
||||
|
||||
const dispose: Function[] = [];
|
||||
const handleActiveExit = async () =>
|
||||
{
|
||||
await sendLatests();
|
||||
};
|
||||
events.on('activegameexit', handleActiveExit);
|
||||
dispose.push(() => events.off('activegameexit', handleActiveExit));
|
||||
dispose.push(taskQueue.on('progress', ({ id, progress, state }) =>
|
||||
{
|
||||
if (id.endsWith(sourceId))
|
||||
{
|
||||
enqueue({ progress, status: state as any });
|
||||
}
|
||||
}));
|
||||
dispose.push(taskQueue.on('completed', ({ id }) =>
|
||||
{
|
||||
if (id.endsWith(sourceId))
|
||||
{
|
||||
enqueue({}, 'refresh');
|
||||
}
|
||||
}));
|
||||
dispose.push(taskQueue.on('error', ({ id, error }) =>
|
||||
{
|
||||
if (id.endsWith(sourceId))
|
||||
{
|
||||
enqueue({
|
||||
status: 'error',
|
||||
error: error
|
||||
}, 'error');
|
||||
}
|
||||
}));
|
||||
|
||||
cleanup = () =>
|
||||
{
|
||||
dispose.forEach(f => f());
|
||||
};
|
||||
},
|
||||
cancel (reason)
|
||||
{
|
||||
cleanup?.();
|
||||
cleanup = undefined;
|
||||
},
|
||||
}));
|
||||
}
|
||||
65
src/bun/api/games/services/utils.ts
Normal file
65
src/bun/api/games/services/utils.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import getFolderSize from "get-folder-size";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { config } from "../../app";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import * as schema from "../../schema/app";
|
||||
import { FrontEndGameType, FrontEndGameTypeDetailed } from "@shared/constants";
|
||||
import { DetailedRomSchema, SimpleRomSchema } from "@clients/romm";
|
||||
|
||||
export async function calculateSize (installPath: string | null)
|
||||
{
|
||||
if (!installPath) return null;
|
||||
return (await getFolderSize(path.join(config.get('downloadPath'), installPath))).size;
|
||||
}
|
||||
|
||||
export async function checkInstalled (installPath: string | null)
|
||||
{
|
||||
if (!installPath) return false;
|
||||
return fs.exists(path.join(config.get('downloadPath'), installPath));
|
||||
}
|
||||
|
||||
export function getLocalGameMatch (id: number, source: string)
|
||||
{
|
||||
return source !== 'local' ? and(eq(schema.games.source_id, id), eq(schema.games.source, source)) : eq(schema.games.id, id);
|
||||
}
|
||||
|
||||
export function convertRomToFrontend (rom: SimpleRomSchema): FrontEndGameType
|
||||
{
|
||||
const game: FrontEndGameType = {
|
||||
id: { id: rom.id, source: 'romm' },
|
||||
path_cover: `/api/romm${rom.path_cover_large}`,
|
||||
last_played: rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null,
|
||||
updated_at: new Date(rom.updated_at),
|
||||
slug: rom.slug,
|
||||
platform_id: rom.platform_id,
|
||||
platform_display_name: rom.platform_display_name,
|
||||
name: rom.name,
|
||||
path_fs: null,
|
||||
path_platform_cover: `/api/romm/assets/platforms/${rom.platform_slug}.svg`,
|
||||
source: null,
|
||||
source_id: null
|
||||
};
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
export function convertRomToFrontendDetailed (rom: DetailedRomSchema)
|
||||
{
|
||||
const detailed: FrontEndGameTypeDetailed = {
|
||||
...convertRomToFrontend(rom),
|
||||
summary: rom.summary,
|
||||
fs_size_bytes: rom.fs_size_bytes,
|
||||
paths_screenshots: rom.merged_screenshots.map(s => `/api/romm${s}`),
|
||||
local: false,
|
||||
missing: rom.missing_from_fs
|
||||
};
|
||||
if (rom.merged_ra_metadata?.achievements)
|
||||
{
|
||||
detailed.achievements = {
|
||||
unlocked: rom.merged_ra_metadata.achievements?.map(a => a.num_awarded).length,
|
||||
total: rom.merged_ra_metadata.achievements.length
|
||||
};
|
||||
}
|
||||
return detailed;
|
||||
}
|
||||
180
src/bun/api/jobs/install-job.ts
Normal file
180
src/bun/api/jobs/install-job.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { IJob, JobContext } from "../task-queue";
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { eq, or } from 'drizzle-orm';
|
||||
import fs from 'node:fs/promises';
|
||||
import { DownloaderHelper } from 'node-downloader-helper';
|
||||
import StreamZip from 'node-stream-zip';
|
||||
import * as schema from "../schema/app";
|
||||
import * as emulatorSchema from "../schema/emulators";
|
||||
import path from 'node:path';
|
||||
import { getPlatformApiPlatformsIdGet, getRomApiRomsIdGet } from "@clients/romm";
|
||||
import { config, db, emulatorsDb, jar } from "../app";
|
||||
|
||||
interface JobConfig
|
||||
{
|
||||
dryRun?: boolean;
|
||||
dryDownload?: boolean;
|
||||
}
|
||||
|
||||
export class InstallJob implements IJob
|
||||
{
|
||||
public id: number;
|
||||
|
||||
public config?: JobConfig;
|
||||
|
||||
constructor(id: number, config?: JobConfig)
|
||||
{
|
||||
this.id = id;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public async start (cx: JobContext)
|
||||
{
|
||||
cx.setProgress(0, 'download');
|
||||
fs.mkdir(config.get('downloadPath'), { recursive: true });
|
||||
|
||||
if (this.config?.dryRun !== true)
|
||||
{
|
||||
const downloadPath = config.get('downloadPath');
|
||||
|
||||
if (this.config?.dryDownload !== true)
|
||||
{
|
||||
// download files for rom
|
||||
const downloadUrl = new URL(`${config.get('rommAddress')}/api/roms/download`);
|
||||
downloadUrl.searchParams.set('rom_ids', String(this.id));
|
||||
const downloader = new DownloaderHelper(downloadUrl.href, downloadPath, {
|
||||
headers: {
|
||||
cookie: await jar.getCookieString(config.get('rommAddress') ?? '')
|
||||
},
|
||||
fileName: `${this.id}.zip`,
|
||||
// Romm doesn't support resume download
|
||||
override: true
|
||||
});
|
||||
|
||||
cx.abortSignal.addEventListener('abort', downloader.stop);
|
||||
|
||||
downloader.on('progress.throttled', e =>
|
||||
{
|
||||
cx.setProgress(e.progress, 'download');
|
||||
});
|
||||
|
||||
downloader.on('error', (e) =>
|
||||
{
|
||||
cx.abort(e);
|
||||
});
|
||||
const finishPromise = new Promise<string>(resolve =>
|
||||
{
|
||||
downloader.on("end", ({ filePath }) => resolve(filePath));
|
||||
});
|
||||
|
||||
await downloader.start().catch(err => console.error(err));
|
||||
const zipFilePath = await finishPromise;
|
||||
|
||||
cx.setProgress(0, 'extract');
|
||||
|
||||
const zip = new StreamZip.async({ file: zipFilePath });
|
||||
const totalCount = await zip.entriesCount;
|
||||
let extractCount = 0;
|
||||
zip.on('extract', async (entry, file) =>
|
||||
{
|
||||
console.log(`Extracted ${entry.name} to ${file}`);
|
||||
cx.setProgress(extractCount / totalCount * 100, 'extract');
|
||||
extractCount++;
|
||||
});
|
||||
await zip.extract(null, downloadPath);
|
||||
await zip.close();
|
||||
|
||||
await fs.rm(zipFilePath);
|
||||
}
|
||||
|
||||
const rom = (await getRomApiRomsIdGet({ path: { id: this.id }, throwOnError: true })).data;
|
||||
const romPlatform = (await getPlatformApiPlatformsIdGet({ path: { id: rom.platform_id }, throwOnError: true })).data;
|
||||
|
||||
if (this.config?.dryDownload === true)
|
||||
{
|
||||
rom.files.length;
|
||||
await mkdir(path.join(downloadPath, rom.fs_path, rom.fs_name), { recursive: true });
|
||||
}
|
||||
|
||||
// pre-fetch screenshots
|
||||
const screenshots = await Promise.all(rom.merged_screenshots.map(s => fetch(`${config.get('rommAddress')}${s}`)));
|
||||
|
||||
const rommAddress = config.get('rommAddress');
|
||||
const coverResponse = await fetch(`${rommAddress}${rom.path_cover_large}`);
|
||||
|
||||
if (cx.abortSignal.aborted) return;
|
||||
|
||||
await db.transaction(async (tx) =>
|
||||
{
|
||||
// Search for existing platform
|
||||
const platformSearch = [];
|
||||
if (romPlatform.igdb_id) platformSearch.push(eq(schema.platforms.igdb_id, romPlatform.igdb_id));
|
||||
if (romPlatform.igdb_slug) platformSearch.push(eq(schema.platforms.igdb_slug, romPlatform.igdb_slug));
|
||||
if (romPlatform.ra_id) platformSearch.push(eq(schema.platforms.ra_id, romPlatform.ra_id));
|
||||
if (romPlatform.slug) platformSearch.push(eq(schema.platforms.slug, romPlatform.slug));
|
||||
if (romPlatform.moby_id) platformSearch.push(eq(schema.platforms.moby_id, romPlatform.moby_id));
|
||||
|
||||
const esPlatform = await emulatorsDb
|
||||
.select({ slug: emulatorSchema.systems.name, romm_slug: emulatorSchema.systemMappings.sourceSlug })
|
||||
.from(emulatorSchema.systems)
|
||||
.leftJoin(emulatorSchema.systemMappings, eq(emulatorSchema.systemMappings.source, 'romm'))
|
||||
.where(eq(emulatorSchema.systemMappings.sourceSlug, romPlatform.slug));
|
||||
|
||||
const existingPlatform = await tx.query.platforms.findFirst({ where: or(...platformSearch) });
|
||||
let platformId: number;
|
||||
if (!existingPlatform)
|
||||
{
|
||||
// Create new local platform
|
||||
const platformCover = await fetch(`${rommAddress}/assets/platforms/${romPlatform.slug.toLocaleLowerCase()}.svg`);
|
||||
const platform: typeof schema.platforms.$inferInsert = {
|
||||
slug: romPlatform.slug,
|
||||
igdb_id: romPlatform.igdb_id,
|
||||
igdb_slug: romPlatform.igdb_slug,
|
||||
ra_id: romPlatform.ra_id,
|
||||
cover: Buffer.from(await platformCover.arrayBuffer()),
|
||||
cover_type: platformCover.headers.get('content-type'),
|
||||
name: romPlatform.name,
|
||||
family_name: romPlatform.family_name,
|
||||
es_slug: esPlatform.length > 0 ? esPlatform[0].slug : undefined
|
||||
};
|
||||
// TODO: add ES slug once I have better way to query ES
|
||||
const [{ id }] = await tx.insert(schema.platforms).values(platform).returning({ id: schema.platforms.id });
|
||||
platformId = id;
|
||||
} else
|
||||
{
|
||||
platformId = existingPlatform.id;
|
||||
}
|
||||
|
||||
// create the rom
|
||||
const game: typeof schema.games.$inferInsert = {
|
||||
source_id: rom.id,
|
||||
source: 'romm',
|
||||
slug: rom.slug,
|
||||
path_fs: path.join(rom.fs_path, rom.fs_name),
|
||||
last_played: rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null,
|
||||
platform_id: platformId,
|
||||
igdb_id: rom.igdb_id,
|
||||
ra_id: rom.ra_id,
|
||||
summary: rom.summary,
|
||||
name: rom.name,
|
||||
cover: Buffer.from(await coverResponse.arrayBuffer()),
|
||||
cover_type: coverResponse.headers.get('content-type')
|
||||
};
|
||||
|
||||
// Save screenshots and update database
|
||||
const [{ id }] = await tx.insert(schema.games).values(game).returning({ id: schema.games.id });
|
||||
await tx.insert(schema.screenshots).values(await Promise.all(screenshots.map(async (response) =>
|
||||
{
|
||||
const screenshot: typeof schema.screenshots.$inferInsert = {
|
||||
game_id: id,
|
||||
content: Buffer.from(await response.arrayBuffer()),
|
||||
type: response.headers.get('content-type')
|
||||
};
|
||||
|
||||
return screenshot;
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import { RPC_PORT } from "../../shared/constants";
|
||||
import { settings } from "./settings";
|
||||
import { romm } from "./clients";
|
||||
import Elysia from "elysia";
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import Elysia from "elysia";
|
||||
import { RPC_PORT } from "../../shared/constants";
|
||||
import { host } from "../utils";
|
||||
import clients from "./clients";
|
||||
import { settings } from "./settings";
|
||||
import { system } from "./system";
|
||||
|
||||
const api = new Elysia({ prefix: "/api", serve: {} })
|
||||
.use(cors())
|
||||
.use(romm)
|
||||
.use(settings);
|
||||
const api = new Elysia({ serve: {} })
|
||||
.use([cors(), clients, settings, system]);
|
||||
|
||||
export type AppType = typeof api;
|
||||
export type RommAPIType = typeof clients;
|
||||
export type SettingsAPIType = typeof settings;
|
||||
export type SystemAPIType = typeof system;
|
||||
|
||||
export function RunAPIServer ()
|
||||
{
|
||||
|
|
@ -19,24 +20,11 @@ export function RunAPIServer ()
|
|||
apiServer: api.listen({
|
||||
port: RPC_PORT,
|
||||
hostname: host,
|
||||
development: process.env.NODE_ENV === 'development',
|
||||
fetch (req, server)
|
||||
{
|
||||
if (server.upgrade(req, {
|
||||
data: undefined
|
||||
}))
|
||||
{
|
||||
return;
|
||||
}
|
||||
return api.fetch(req);
|
||||
},
|
||||
websocket: {
|
||||
message (ws, message)
|
||||
{
|
||||
development: process.env.NODE_ENV === 'development'
|
||||
}),
|
||||
async cleanup ()
|
||||
{
|
||||
|
||||
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
54
src/bun/api/schema/app.ts
Normal file
54
src/bun/api/schema/app.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { sql } from "drizzle-orm";
|
||||
import { integer, text, sqliteTable, blob } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const games = sqliteTable('games', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
source_id: integer('source_id').unique(),
|
||||
source: text("source"),
|
||||
igdb_id: integer("igdb_id").unique(),
|
||||
name: text("name"),
|
||||
ra_id: integer('ra_id').unique(),
|
||||
path_fs: text("path_fs"),
|
||||
last_played: integer("last_played", { mode: 'timestamp' }),
|
||||
created_at: integer("created_at", { mode: 'timestamp' }).default(sql`(unixepoch())`).notNull(),
|
||||
metadata: text("metadata", { mode: 'json' }).default(sql`'{}'`),
|
||||
slug: text("slug").unique(),
|
||||
platform_id: integer("platform_id").references(() => platforms.id, { onUpdate: 'cascade' }).notNull(),
|
||||
cover: blob("cover", { mode: 'buffer' }),
|
||||
cover_type: text('type'),
|
||||
summary: text("summary")
|
||||
});
|
||||
|
||||
export const platforms = sqliteTable('platforms', {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
igdb_id: integer("igdb_id").unique(),
|
||||
igdb_slug: text("igdb_slug").unique(),
|
||||
moby_id: integer("moby_id").unique(),
|
||||
name: text("name").notNull(),
|
||||
es_slug: text('es_slug').unique(),
|
||||
ra_id: integer('ra_id').unique(),
|
||||
created_at: integer("created_at", { mode: 'timestamp' }).default(sql`(unixepoch())`).notNull(),
|
||||
slug: text("slug").unique().notNull(),
|
||||
metadata: text("metadata", { mode: 'json' }),
|
||||
cover: blob("cover", { mode: 'buffer' }),
|
||||
cover_type: text('type'),
|
||||
family_name: text("family_name")
|
||||
});
|
||||
|
||||
export const collections_games = sqliteTable('collections_games', {
|
||||
collection_id: integer('collection_id').notNull().references(() => collections.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
game_id: integer('game_id').notNull().references(() => games.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
created_at: integer("created_at", { mode: 'timestamp' }).default(sql`(unixepoch())`).notNull(),
|
||||
});
|
||||
|
||||
export const collections = sqliteTable('collections', {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text('name')
|
||||
});
|
||||
|
||||
export const screenshots = sqliteTable('screenshots', {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
game_id: integer('game_id').references(() => games.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
content: blob('content', { mode: 'buffer' }).notNull(),
|
||||
type: text('type')
|
||||
});
|
||||
43
src/bun/api/schema/emulators.ts
Normal file
43
src/bun/api/schema/emulators.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { relations, sql } from "drizzle-orm";
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const emulators = sqliteTable('emulators', {
|
||||
name: text().primaryKey().unique(),
|
||||
systempath: text({ mode: 'json' }).notNull().$type<string[]>().default(sql`(json_array())`),
|
||||
staticpath: text({ mode: 'json' }).notNull().$type<string[]>().default(sql`(json_array())`),
|
||||
corepath: text({ mode: 'json' }).notNull().$type<string[]>().default(sql`(json_array())`),
|
||||
androidpackage: text({ mode: 'json' }).notNull().$type<string[]>().default(sql`(json_array())`),
|
||||
winregistrypath: text({ mode: 'json' }).notNull().$type<string[]>().default(sql`(json_array())`),
|
||||
});
|
||||
|
||||
export const systems = sqliteTable('systems', {
|
||||
name: text().primaryKey().unique(),
|
||||
fullname: text(),
|
||||
path: text(),
|
||||
extension: text({ mode: 'json' }).notNull().$type<string[]>().default(sql`(json_array())`)
|
||||
});
|
||||
|
||||
export const systemsRelations = relations(systems, ({ many }) =>
|
||||
({
|
||||
commands: many(commands)
|
||||
}));
|
||||
|
||||
export const systemMappings = sqliteTable('systemMappings', {
|
||||
source: text(),
|
||||
sourceSlug: text(),
|
||||
sourceId: integer(),
|
||||
system: text().notNull().references(() => systems.name)
|
||||
});
|
||||
|
||||
export const commands = sqliteTable('commands', {
|
||||
system: text().references(() => systems.name, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
label: text(),
|
||||
command: text().notNull()
|
||||
});
|
||||
|
||||
export const commandsRelations = relations(commands, ({ one }) => ({
|
||||
author: one(systems, {
|
||||
fields: [commands.system],
|
||||
references: [systems.name],
|
||||
}),
|
||||
}));
|
||||
133
src/bun/api/secrets.ts
Normal file
133
src/bun/api/secrets.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import Conf from "conf";
|
||||
import projectPackage from '~/package.json';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
|
||||
let secrets: ISecrets;
|
||||
|
||||
interface ISecrets
|
||||
{
|
||||
set (data: { service: string, name: string, value: string; }): Promise<void>;
|
||||
get (data: { service: string, name: string; }): Promise<string | null>;
|
||||
delete (data: { service: string, name: string; }): Promise<boolean>;
|
||||
}
|
||||
|
||||
class BunSecrets implements ISecrets
|
||||
{
|
||||
public set (data: { service: string, name: string, value: string; })
|
||||
{
|
||||
return Bun.secrets.set(data);
|
||||
}
|
||||
|
||||
public get (data: { service: string, name: string; })
|
||||
{
|
||||
return Bun.secrets.get(data);
|
||||
}
|
||||
|
||||
public delete (data: { service: string, name: string; })
|
||||
{
|
||||
return Bun.secrets.delete(data);
|
||||
}
|
||||
}
|
||||
|
||||
class FallbackSecrets implements ISecrets
|
||||
{
|
||||
config: Conf<Record<string, string>>;
|
||||
machineKey?: Buffer<ArrayBufferLike>;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this.config = new Conf<Record<string, string>>({
|
||||
projectName: projectPackage.name,
|
||||
projectSuffix: 'bun',
|
||||
configFileMode: 0o600,
|
||||
configName: 'secrets'
|
||||
});
|
||||
console.log("Secrets Store Located at: ", this.config.path);
|
||||
}
|
||||
|
||||
async getMachineKey ()
|
||||
{
|
||||
if (!this.machineKey)
|
||||
{
|
||||
|
||||
let raw: string;
|
||||
try
|
||||
{
|
||||
raw = await fs.readFile("/etc/machine-id", 'utf-8');
|
||||
} catch (error)
|
||||
{
|
||||
raw = [
|
||||
os.homedir(),
|
||||
os.userInfo().username,
|
||||
os.platform(),
|
||||
os.arch(),
|
||||
os.cpus().map(c => c.model).join(','),
|
||||
String(os.totalmem())
|
||||
].filter(Boolean).join("|");
|
||||
}
|
||||
this.machineKey = crypto.createHash('sha256').update(raw.trim()).digest();
|
||||
}
|
||||
|
||||
return this.machineKey;
|
||||
}
|
||||
|
||||
public async set ({ service, name, value }: { service: string, name: string, value: string; })
|
||||
{
|
||||
const iv = crypto.randomBytes(16);
|
||||
const key = await this.getMachineKey();
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||
const encrypted = Buffer.concat([
|
||||
iv,
|
||||
cipher.update(value, "utf-8"),
|
||||
cipher.final()
|
||||
]);
|
||||
return this.config.set(`${service}-${name}`, encrypted.toString('base64'));
|
||||
}
|
||||
|
||||
public async get ({ service, name }: { service: string, name: string; })
|
||||
{
|
||||
const rawBase = this.config.get(`${service}-${name}`);
|
||||
if (!rawBase)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
const key = await this.getMachineKey();
|
||||
const raw = Buffer.from(rawBase, 'base64');
|
||||
|
||||
const iv = raw.subarray(0, 16);
|
||||
const ciphertext = raw.subarray(16);
|
||||
const cipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
||||
const data = Buffer.concat([cipher.update(ciphertext), cipher.final()]).toString("utf-8");
|
||||
|
||||
return data;
|
||||
} catch (error)
|
||||
{
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async delete ({ service, name }: { service: string, name: string; })
|
||||
{
|
||||
this.config.delete(`${service}-${name}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
await Bun.secrets.get({ service: 'test', name: 'test' });
|
||||
secrets = new BunSecrets();
|
||||
} catch
|
||||
{
|
||||
secrets = new FallbackSecrets();
|
||||
}*/
|
||||
|
||||
secrets = new FallbackSecrets();
|
||||
|
||||
export default secrets;
|
||||
|
|
@ -1,18 +1,95 @@
|
|||
import z from "zod";
|
||||
import { SettingsSchema, SettingsType } from "../../shared/constants";
|
||||
import Conf from "conf";
|
||||
import projectPackage from '../../../package.json';
|
||||
import { SettingsSchema } from "@shared/constants";
|
||||
import Elysia from "elysia";
|
||||
import { config, customEmulators, db, emulatorsDb } from "./app";
|
||||
import * as appSchema from './schema/app';
|
||||
import { findExec } from "./games/services/launchGameService";
|
||||
import * as emulatorSchema from "./schema/emulators";
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
export const config = new Conf<SettingsType>({
|
||||
projectName: projectPackage.name,
|
||||
projectSuffix: 'bun',
|
||||
schema: Object.fromEntries(Object.entries(SettingsSchema.shape).map(([key, schema]) => [key, schema.toJSONSchema() as any])) as any,
|
||||
defaults: SettingsSchema.parse({}),
|
||||
});
|
||||
console.log("Config Path Located At: ", config.path);
|
||||
export const settings = new Elysia({ prefix: '/api/settings' })
|
||||
.get('/emulators/automatic', async () =>
|
||||
{
|
||||
const localGames = await db.select({ es_slug: appSchema.platforms.es_slug, platform_id: appSchema.platforms.id })
|
||||
.from(appSchema.games)
|
||||
.leftJoin(appSchema.platforms, eq(appSchema.games.platform_id, appSchema.platforms.id))
|
||||
.groupBy(appSchema.platforms.es_slug);
|
||||
|
||||
export const settings = new Elysia({ prefix: '/settings' })
|
||||
const platformLookup = new Map(localGames.map(g => [g.es_slug, g.platform_id]));
|
||||
|
||||
const commands = await emulatorsDb
|
||||
.select({ command: emulatorSchema.commands.command, system_slug: emulatorSchema.systems.name })
|
||||
.from(emulatorSchema.commands).where(inArray(emulatorSchema.commands.system, Array.from(new Set(localGames.filter(g => g.es_slug).map(s => s.es_slug!)))))
|
||||
.leftJoin(emulatorSchema.systems, eq(emulatorSchema.systems.name, emulatorSchema.commands.system));
|
||||
|
||||
|
||||
const emulatorCounts: Record<string, number> = {};
|
||||
const emulators = commands
|
||||
.flatMap(command =>
|
||||
{
|
||||
const matches = command.command.match(/(?<=%EMULATOR_)[\w-]+(?=%)/);
|
||||
if (!matches)
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
|
||||
matches.forEach(m =>
|
||||
{
|
||||
emulatorCounts[m] = (emulatorCounts[m] ?? 0) + 1;
|
||||
});
|
||||
|
||||
return matches?.map(m => [m, command.system_slug] as [string, string]);
|
||||
}
|
||||
).filter(c => !!c);
|
||||
const uniqueEmulators = new Map(emulators);
|
||||
|
||||
return await Promise.all(Array.from(uniqueEmulators.entries()).map(async ([emulator, system_slug]) =>
|
||||
{
|
||||
let execPath: string | undefined;
|
||||
if (customEmulators.has(emulator))
|
||||
{
|
||||
execPath = customEmulators.get(emulator);
|
||||
} else
|
||||
{
|
||||
execPath = await findExec(emulator);
|
||||
}
|
||||
|
||||
let platform: number | null | undefined = null;
|
||||
if (emulatorCounts[emulator] <= 1)
|
||||
{
|
||||
platform = platformLookup.get(system_slug);
|
||||
}
|
||||
|
||||
return { emulator: emulator, path: execPath, exists: !!execPath && await fs.exists(execPath), path_cover: platform ? `/api/romm/platform/local/${platform}/cover` : null };
|
||||
}));
|
||||
}, {
|
||||
response: z.array(z.object({ emulator: z.string(), path: z.string().optional(), exists: z.boolean(), path_cover: z.string().nullable() }))
|
||||
})
|
||||
.put('/emulators/custom/:id', async ({ params: { id }, body: { value } }) =>
|
||||
{
|
||||
return customEmulators.set(id, value);
|
||||
},
|
||||
{
|
||||
body: z.object({ value: z.string() })
|
||||
})
|
||||
.delete('/emulators/custom/:id', async ({ params: { id } }) =>
|
||||
{
|
||||
return customEmulators.delete(id);
|
||||
})
|
||||
.get('/emulators/custom/:id', async ({ params: { id } }) =>
|
||||
{
|
||||
return customEmulators.get(id);
|
||||
},
|
||||
{
|
||||
response: z.string()
|
||||
})
|
||||
.get('/emulators/custom', async () =>
|
||||
{
|
||||
return Object.keys(customEmulators.store);
|
||||
}, {
|
||||
response: z.array(z.string())
|
||||
})
|
||||
.get("/:id", async ({ params: { id } }) =>
|
||||
{
|
||||
const value = config.get(id);
|
||||
|
|
@ -25,5 +102,6 @@ export const settings = new Elysia({ prefix: '/settings' })
|
|||
config.set(id, value);
|
||||
}, {
|
||||
params: z.object({ id: z.keyof(SettingsSchema) }),
|
||||
body: z.object({ value: z.any() })
|
||||
body: z.object({ value: z.any() }),
|
||||
});
|
||||
|
||||
|
|
|
|||
43
src/bun/api/system.ts
Normal file
43
src/bun/api/system.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import Elysia from "elysia";
|
||||
import open from 'open';
|
||||
import z from "zod";
|
||||
import os from 'node:os';
|
||||
import { events } from "./app";
|
||||
import { isSteamDeckGameMode } from "../utils";
|
||||
|
||||
// steam://open/keyboard?XPosition=%i&YPosition=%i&Width=%i&Height=%i&Mode=%d
|
||||
export const system = new Elysia({ prefix: '/api/system' })
|
||||
.post('/show_keyboard', async () =>
|
||||
{
|
||||
if (isSteamDeckGameMode())
|
||||
{
|
||||
open('steam://open/keyboard');
|
||||
}
|
||||
})
|
||||
.get('/info', () =>
|
||||
{
|
||||
return {
|
||||
homeDir: os.homedir(),
|
||||
user: os.userInfo().username,
|
||||
arch: os.arch(),
|
||||
platform: os.platform(),
|
||||
hostname: os.hostname(),
|
||||
steamDeck: process.env.SteamDeck,
|
||||
machine: os.machine()
|
||||
};
|
||||
})
|
||||
.post('/exit', () =>
|
||||
{
|
||||
if (process.env.PUBLIC_ACCESS)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
events.emit('exitapp');
|
||||
})
|
||||
.post('/open', async ({ query: { url } }) =>
|
||||
{
|
||||
open(url);
|
||||
}, {
|
||||
query: z.object({ url: z.url() })
|
||||
});
|
||||
207
src/bun/api/task-queue.ts
Normal file
207
src/bun/api/task-queue.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
|
||||
import EventEmitter from 'node:events';
|
||||
|
||||
export class TaskQueue
|
||||
{
|
||||
private activeQueue: { context: JobContext, promise?: Promise<void>; }[] = [];
|
||||
private queue?: { context: JobContext, promise?: Promise<void>; }[] = [];
|
||||
private events?: EventEmitter<EventsList> = new EventEmitter<EventsList>();
|
||||
|
||||
public enqueue (id: string, job: IJob): Promise<void>
|
||||
{
|
||||
this.disposeSafeguard();
|
||||
if (!this.queue || !this.events) throw new Error("Queue disposed");
|
||||
const context = new JobContext(id, this.events, job);
|
||||
this.queue.push({ context });
|
||||
return this.processQueue();
|
||||
}
|
||||
|
||||
private processQueue (): Promise<void>
|
||||
{
|
||||
if (!this.queue) return Promise.resolve();
|
||||
const top = this.queue.pop();
|
||||
if (top)
|
||||
{
|
||||
const promise = top.context.start();
|
||||
top.promise = promise;
|
||||
const index = this.queue.length;
|
||||
this.activeQueue.push(top);
|
||||
promise.finally(() =>
|
||||
{
|
||||
this.activeQueue.splice(index, 1);
|
||||
setTimeout(this.processQueue);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private disposeSafeguard ()
|
||||
{
|
||||
if (!this.queue) throw new Error("Queue disposed");
|
||||
}
|
||||
|
||||
public hasActive ()
|
||||
{
|
||||
return this.activeQueue.length > 0;
|
||||
}
|
||||
|
||||
public waitForJob (id: string): Promise<void>
|
||||
{
|
||||
return this.queue?.find(j => j.context.id === id)?.promise ?? Promise.resolve();
|
||||
}
|
||||
|
||||
public findJob (id: string): IPublicJob | undefined
|
||||
{
|
||||
return this.queue?.find(j => j.context.id === id)?.context;
|
||||
}
|
||||
|
||||
public on<E extends keyof EventsList> (event: E, listener: E extends keyof EventsList ? EventsList[E] extends unknown[] ? (...args: EventsList[E]) => void : never : never): () => void
|
||||
{
|
||||
this.events?.on(event, listener);
|
||||
return () => this.events?.removeListener(event, listener);
|
||||
}
|
||||
|
||||
public once<E extends keyof EventsList> (event: E, listener: E extends keyof EventsList ? EventsList[E] extends unknown[] ? (...args: EventsList[E]) => void : never : never)
|
||||
{
|
||||
this.events?.once(event, listener);
|
||||
}
|
||||
|
||||
public async close ()
|
||||
{
|
||||
this.queue = [];
|
||||
this.activeQueue.forEach(c => c.context.abort());
|
||||
return Promise.all(this.activeQueue.map(c => c.promise));
|
||||
}
|
||||
}
|
||||
|
||||
export interface EventsList
|
||||
{
|
||||
progress: [e: ProgressEvent];
|
||||
abort: [e: AbortEvent];
|
||||
completed: [e: CompletedEvent];
|
||||
error: [e: ErrorEvent];
|
||||
ended: [e: BaseEvent];
|
||||
}
|
||||
|
||||
interface BaseEvent
|
||||
{
|
||||
id: string;
|
||||
job: IJob;
|
||||
}
|
||||
|
||||
interface ErrorEvent extends BaseEvent
|
||||
{
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
interface AbortEvent extends BaseEvent
|
||||
{
|
||||
reason?: any;
|
||||
}
|
||||
|
||||
interface ProgressEvent extends BaseEvent
|
||||
{
|
||||
progress: number;
|
||||
state?: string;
|
||||
}
|
||||
|
||||
interface CompletedEvent extends BaseEvent
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export interface IJob
|
||||
{
|
||||
start (context: JobContext): Promise<any>;
|
||||
}
|
||||
|
||||
export type JobStatus = 'completed' | 'error' | 'running' | 'waiting' | 'aborted';
|
||||
|
||||
export interface IPublicJob
|
||||
{
|
||||
progress: number;
|
||||
state?: string;
|
||||
status: JobStatus;
|
||||
job: any;
|
||||
}
|
||||
|
||||
export class JobContext implements IPublicJob
|
||||
{
|
||||
private m_id: string;
|
||||
private m_progress: number = 0;
|
||||
private m_state?: string;
|
||||
private running: boolean = false;
|
||||
private aborted: boolean = false;
|
||||
private completed: boolean = false;
|
||||
private error?: any;
|
||||
private events: EventEmitter<EventsList>;
|
||||
private abortController: AbortController;
|
||||
private m_job: IJob;
|
||||
|
||||
constructor(id: string, events: EventEmitter<EventsList>, job: IJob)
|
||||
{
|
||||
this.m_id = id;
|
||||
this.m_job = job;
|
||||
this.abortController = new AbortController();
|
||||
this.abortController.signal.addEventListener('abort', () =>
|
||||
{
|
||||
this.aborted = true;
|
||||
this.events.emit('abort', { id: this.m_id, reason: this.abortController.signal.reason, job: this.m_job } satisfies AbortEvent);
|
||||
});
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
public async start (): Promise<void>
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.m_job.start(this);
|
||||
this.completed = true;
|
||||
this.events.emit('completed', { id: this.m_id, job: this.m_job });
|
||||
|
||||
} catch (error)
|
||||
{
|
||||
console.error(error);
|
||||
this.events.emit('error', { id: this.m_id, error });
|
||||
this.error = error;
|
||||
} finally
|
||||
{
|
||||
this.running = false;
|
||||
this.events.emit('ended', { id: this.m_id, job: this.m_job });
|
||||
}
|
||||
}
|
||||
|
||||
public get status (): JobStatus
|
||||
{
|
||||
if (this.completed) return 'completed';
|
||||
if (this.error) return 'error';
|
||||
if (this.aborted) return 'aborted';
|
||||
if (this.running) return 'running';
|
||||
return 'waiting';
|
||||
}
|
||||
|
||||
public get id () { return this.m_id; }
|
||||
|
||||
public get job () { return this.m_job; }
|
||||
|
||||
public get abortSignal () { return this.abortController.signal; }
|
||||
|
||||
public get progress () { return this.m_progress; }
|
||||
|
||||
public get state () { return this.m_state; }
|
||||
|
||||
public setProgress (progress: number, state?: string)
|
||||
{
|
||||
this.m_progress = progress;
|
||||
if (state)
|
||||
this.m_state = state;
|
||||
this.events.emit('progress', { id: this.m_id, progress, state: state ?? this.m_state, job: this.m_job });
|
||||
}
|
||||
|
||||
public abort (reason?: any)
|
||||
{
|
||||
this.error = reason;
|
||||
this.abortController.abort(reason);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ import { RunBunServer } from './server';
|
|||
import { RunAPIServer } from './api/rpc';
|
||||
import { spawnBrowser } from './utils/browser-spawner';
|
||||
import { BuildParams } from './utils/browser-params';
|
||||
import { cleanup as appCleanup, events } from './api/app';
|
||||
import os from 'node:os';
|
||||
|
||||
const api = RunAPIServer();
|
||||
let bunServer: { stop: () => void; url: URL; } | undefined;
|
||||
|
|
@ -13,43 +15,80 @@ if (!Bun.env.PUBLIC_ACCESS)
|
|||
|
||||
async function cleanup ()
|
||||
{
|
||||
await appCleanup();
|
||||
bunServer?.stop();
|
||||
await api.apiServer.stop();
|
||||
await api.cleanup();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try
|
||||
if (Bun.env.FORCE_BROWSER)
|
||||
{
|
||||
const webviewWorker = new Worker(process.env.IS_BINARY ? "./webview-worker.ts" : new URL("./webview-worker", import.meta.url).href, {
|
||||
await runBrowser();
|
||||
} else
|
||||
{
|
||||
try
|
||||
{
|
||||
await runWebview();
|
||||
} catch (error)
|
||||
{
|
||||
await runBrowser();
|
||||
}
|
||||
}
|
||||
|
||||
async function runWebview ()
|
||||
{
|
||||
const webviewWorker = new Worker(Bun.env.IS_BINARY ? `./webview/${os.platform()}.ts` : new URL(`./webview/${os.platform()}`, import.meta.url).href, {
|
||||
smol: true,
|
||||
});
|
||||
webviewWorker.addEventListener('error', console.error);
|
||||
await new Promise(resolve => webviewWorker.addEventListener('close', resolve));
|
||||
|
||||
await new Promise((resolve, reject) =>
|
||||
{
|
||||
webviewWorker.addEventListener('error', e =>
|
||||
{
|
||||
console.error(e.message);
|
||||
reject(e.error);
|
||||
});
|
||||
|
||||
webviewWorker.addEventListener('message', (e) =>
|
||||
{
|
||||
if (e.data === 'destroyed')
|
||||
{
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
events.on('exitapp', () =>
|
||||
{
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
await cleanup();
|
||||
}
|
||||
catch (error)
|
||||
|
||||
async function runBrowser ()
|
||||
{
|
||||
console.error(error);
|
||||
|
||||
const browserParams = await BuildParams();
|
||||
|
||||
if (!browserParams)
|
||||
{
|
||||
console.error("Could not find valid browser");
|
||||
process.exit();
|
||||
}
|
||||
await cleanup();
|
||||
} else
|
||||
{
|
||||
const browser = spawnBrowser({
|
||||
browser: browserParams.browser.type,
|
||||
args: browserParams.args,
|
||||
env: browserParams.env,
|
||||
detached: false,
|
||||
execPath: browserParams.browser.path,
|
||||
source: browserParams.browser.source,
|
||||
ipc (message)
|
||||
{
|
||||
console.log(message);
|
||||
},
|
||||
onExit: cleanup
|
||||
});
|
||||
|
||||
const browser = spawnBrowser({
|
||||
browser: browserParams.browser.type,
|
||||
args: browserParams.args,
|
||||
env: browserParams.env,
|
||||
detached: true,
|
||||
execPath: browserParams.browser.path,
|
||||
source: browserParams.browser.source,
|
||||
ipc (message)
|
||||
{
|
||||
console.log(message);
|
||||
},
|
||||
onExit: cleanup
|
||||
});
|
||||
events.on('exitapp', () => browser.kill(15));
|
||||
}
|
||||
}
|
||||
19
src/bun/types.d.ts
vendored
19
src/bun/types.d.ts
vendored
|
|
@ -1,19 +0,0 @@
|
|||
declare const IS_BINARY: string;
|
||||
|
||||
declare module 'download-chromium' {
|
||||
export default function download ({
|
||||
platform,
|
||||
revision = '499413',
|
||||
log = false,
|
||||
onProgress = undefined,
|
||||
installPath = '{__dirname}/.local-chromium' }: {
|
||||
platform?: 'linux' | 'mac' | 'win32' | 'win64',
|
||||
revision?: string,
|
||||
log?: boolean,
|
||||
installPath?: string,
|
||||
onProgress?: (percent: number, transferred: number, total: number) => void;
|
||||
}): Promise<string>
|
||||
{
|
||||
|
||||
};
|
||||
}
|
||||
8
src/bun/types/types.d.ts
vendored
Normal file
8
src/bun/types/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
declare const IS_BINARY: string;
|
||||
|
||||
export type ActiveGame = {
|
||||
process: Bun.Subprocess;
|
||||
gameId: number;
|
||||
name: string;
|
||||
command: string;
|
||||
};
|
||||
|
|
@ -17,3 +17,14 @@ export function checkRunning (pid: number)
|
|||
return error.code === 'EPERM';
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorMessage (error: unknown): string
|
||||
{
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
|
||||
export function isSteamDeckGameMode ()
|
||||
{
|
||||
return !!Bun.env.SteamDeck;
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@ import { SERVER_URL } from "../../shared/constants";
|
|||
import os from 'node:os';
|
||||
import path, { dirname } from 'node:path';
|
||||
import { getBrowserPath } from "./get-browser";
|
||||
import { config } from "../api/settings";
|
||||
import { host } from "../utils";
|
||||
import { host, isSteamDeckGameMode } from "../utils";
|
||||
import { config } from "../api/app";
|
||||
|
||||
export async function BuildParams ()
|
||||
{
|
||||
|
|
@ -42,7 +42,15 @@ export async function BuildParams ()
|
|||
args.push('--disable-component-update');
|
||||
args.push('--allow-insecure-localhost');
|
||||
args.push('--auto-accept-camera-and-microphone-capture');
|
||||
args.push(`--window-size=${config.get('windowSize.width')},${config.get('windowSize.height')}`);
|
||||
|
||||
if (isSteamDeckGameMode())
|
||||
{
|
||||
args.push('--kiosk');
|
||||
} else
|
||||
{
|
||||
args.push(`--window-size=${config.get('windowSize.width')},${config.get('windowSize.height')}`);
|
||||
}
|
||||
|
||||
args.push('--password-store=basic');
|
||||
args.push('--block-new-web-contents');
|
||||
args.push('--bwsi');
|
||||
|
|
@ -82,8 +90,8 @@ export async function BuildParams ()
|
|||
|
||||
if (os.platform() === 'linux')
|
||||
{
|
||||
args.push("--disable-web-security");
|
||||
args.push("--no-sandbox");
|
||||
//args.push("--disable-web-security");
|
||||
//args.push("--no-sandbox");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
17
src/bun/webview/base.ts
Normal file
17
src/bun/webview/base.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { SERVER_URL } from "@/shared/constants";
|
||||
import Webview from "@rcompat/webview";
|
||||
import { host } from "../utils";
|
||||
|
||||
export default function (webview: Webview)
|
||||
{
|
||||
self.addEventListener('message', (e) =>
|
||||
{
|
||||
console.log("Terminate");
|
||||
if (e.data === 'exit')
|
||||
{
|
||||
webview.destroy();
|
||||
}
|
||||
});
|
||||
webview.navigate(SERVER_URL(host));
|
||||
webview.run();
|
||||
}
|
||||
7
src/bun/webview/linux.ts
Normal file
7
src/bun/webview/linux.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Webview from "@rcompat/webview";
|
||||
import platform from "@rcompat/webview/linux-x64";
|
||||
import webviewWorkerBase from "./base";
|
||||
|
||||
console.log("Launching Webview");
|
||||
const webview = new Webview({ debug: import.meta.env.NODE_ENV === 'development', platform });
|
||||
webviewWorkerBase(webview);
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import Webview from "@rcompat/webview";
|
||||
import platform from "@rcompat/webview/windows-x64";
|
||||
import { SERVER_URL } from "../shared/constants";
|
||||
import { host } from "./utils";
|
||||
import webviewWorkerBase from "./base";
|
||||
|
||||
console.log("Launching Webview");
|
||||
const webview = new Webview({ debug: import.meta.env.NODE_ENV === 'development', platform });
|
||||
webview.navigate(SERVER_URL(host));
|
||||
webview.run();
|
||||
webviewWorkerBase(webview);
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { createContext, Ref, useContext, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, { createContext, JSX, Ref, useContext, useEffect, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { useSessionStorage } from 'usehooks-ts';
|
||||
|
||||
|
|
@ -13,15 +14,23 @@ export function AnimatedBackground (data: {
|
|||
animated?: boolean,
|
||||
})
|
||||
{
|
||||
const [lastBackgroundUrl, setLastBackgroundUrl] = data.backgroundUrl ? useSessionStorage<string | undefined>(
|
||||
const blurBackground = true;
|
||||
const animateBackground = true;
|
||||
|
||||
const [lastBackgroundUrl, setLastBackgroundUrl] = data.backgroundKey ? useSessionStorage<string | undefined>(
|
||||
`${data.backgroundKey!}-last`,
|
||||
data.backgroundUrl,
|
||||
) : useState<string | undefined>();
|
||||
|
||||
const [backgroundUrl, setBackgroundUrl] = data.backgroundUrl ? useSessionStorage<string | undefined>(
|
||||
const [backgroundUrl, setBackgroundUrl] = data.backgroundKey ? useSessionStorage<string | undefined>(
|
||||
data.backgroundKey!,
|
||||
data.backgroundUrl,
|
||||
) : useState(data.backgroundUrl);
|
||||
) : useState<string | undefined>();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setBackgroundUrl(data.backgroundUrl);
|
||||
}, [data.backgroundUrl]);
|
||||
|
||||
function handleSetBackground (url: string)
|
||||
{
|
||||
|
|
@ -36,6 +45,20 @@ export function AnimatedBackground (data: {
|
|||
color-mix(in srgb, var(--color-base-100) 80%, transparent)
|
||||
), url('${url}') center / cover`;
|
||||
|
||||
let backgroundElements: JSX.Element | undefined = undefined;
|
||||
if (true)
|
||||
{
|
||||
backgroundElements = <div id="container">
|
||||
<div id="container-inside">
|
||||
<div className={bgColor} id="circle-small"></div>
|
||||
<div className={bgColor} id="circle-medium"></div>
|
||||
<div className={bgColor} id="circle-large"></div>
|
||||
<div className={bgColor} id="circle-xlarge"></div>
|
||||
<div className={bgColor} id="circle-xxlarge"></div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatedBackgroundContext value={{ setBackground: handleSetBackground }}>
|
||||
<div ref={data.ref}
|
||||
|
|
@ -43,17 +66,9 @@ export function AnimatedBackground (data: {
|
|||
>
|
||||
{!!lastBackgroundUrl && <div className='absolute w-full h-full' style={{ background: backgroundStyle(lastBackgroundUrl), zIndex: -4 }}></div>}
|
||||
{!!backgroundUrl && <div key={backgroundUrl} className='absolute w-full h-full animate__animated animate__fadeIn' style={{ background: backgroundStyle(backgroundUrl), zIndex: -3 }}></div>}
|
||||
<div className="absolute w-full h-full backdrop-blur-3xl" style={{ zIndex: -2 }}></div>
|
||||
{data.animated && <div className="absolute overflow-hidden w-full h-full" style={{ zIndex: -1 }}>
|
||||
<div id="container">
|
||||
<div id="container-inside">
|
||||
<div className={bgColor} id="circle-small"></div>
|
||||
<div className={bgColor} id="circle-medium"></div>
|
||||
<div className={bgColor} id="circle-large"></div>
|
||||
<div className={bgColor} id="circle-xlarge"></div>
|
||||
<div className={bgColor} id="circle-xxlarge"></div>
|
||||
</div>
|
||||
</div>
|
||||
{blurBackground && <div className={"absolute w-full h-full backdrop-blur-3xl"} style={{ zIndex: -2 }}></div>}
|
||||
{data.animated && animateBackground && <div className="absolute overflow-hidden w-full h-full" style={{ zIndex: -1 }}>
|
||||
{backgroundElements}
|
||||
</div>}
|
||||
{data.children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,16 @@ import
|
|||
FocusContext,
|
||||
useFocusable,
|
||||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { GameMeta } from "../../shared/constants";
|
||||
import GameCard, { GameCardSkeleton } from "./GameCard";
|
||||
import { JSX, useEffect, useMemo, useState } from "react";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { useScrollSave } from "../scripts/utils";
|
||||
import { FrontEndId, GameMeta } from "../../shared/constants";
|
||||
import GameCard, { GameCardParams } from "./GameCard";
|
||||
import { JSX, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export interface GameMetaExtra extends GameMeta
|
||||
{
|
||||
preview?: JSX.Element;
|
||||
badge?: JSX.Element;
|
||||
preview?: GameCardParams['preview'];
|
||||
badges?: JSX.Element[];
|
||||
focusKey: string;
|
||||
}
|
||||
|
||||
|
|
@ -22,8 +21,9 @@ export function CardList (data: {
|
|||
type?: string;
|
||||
games: GameMetaExtra[];
|
||||
grid?: boolean;
|
||||
onSelectGame?: (id: number) => void;
|
||||
onGameFocus?: (id: number) => void;
|
||||
onSelectGame?: (id: string) => void;
|
||||
onGameFocus?: (id: string) => void;
|
||||
className?: string;
|
||||
})
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({
|
||||
|
|
@ -32,7 +32,7 @@ export function CardList (data: {
|
|||
|
||||
function BuildGame (g: GameMetaExtra, i: number)
|
||||
{
|
||||
let preview: JSX.Element | string | undefined = g.preview;
|
||||
let preview: GameCardParams['preview'] = g.preview;
|
||||
if (!preview && g.previewUrl)
|
||||
{
|
||||
preview = g.previewUrl;
|
||||
|
|
@ -48,11 +48,17 @@ export function CardList (data: {
|
|||
subtitle={g.subtitle ?? ""}
|
||||
onFocus={() =>
|
||||
{
|
||||
g.onFocus?.();
|
||||
data.onGameFocus?.(g.id);
|
||||
(document.querySelector(":root") as HTMLElement).style.setProperty('--selected-card-offset', `${i}s`);
|
||||
}}
|
||||
onAction={() =>
|
||||
{
|
||||
g.onSelect?.();
|
||||
data.onSelectGame?.(g.id);
|
||||
}}
|
||||
onAction={() => data.onSelectGame?.(g.id)}
|
||||
preview={preview}
|
||||
badge={g.badge}
|
||||
badges={g.badges}
|
||||
id={g.id}
|
||||
/>
|
||||
);
|
||||
|
|
@ -64,8 +70,9 @@ export function CardList (data: {
|
|||
id={`card-list-${data.id}`}
|
||||
ref={ref}
|
||||
save-child-focus="session"
|
||||
className={classNames("my-6 items-center justify-center-safe h-(--game-card-height) ",
|
||||
data.grid ? "card-grid h-fit gap-5" : 'card-list gap-6'
|
||||
className={twMerge("my-6 items-center justify-center-safe h-(--game-card-height) ",
|
||||
data.grid ? "card-grid h-fit gap-5" : 'card-list gap-6',
|
||||
data.className
|
||||
)}
|
||||
onKeyDown={(e) =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-naviga
|
|||
import { HeaderUI } from './Header';
|
||||
import { GameList, GameListFilter } from './GameList';
|
||||
import { Search, Settings2 } from 'lucide-react';
|
||||
import ShortcutPrompt from './ShortcutPrompt';
|
||||
import { selfFocusSmart } from '../scripts/utils';
|
||||
import { JSX, Suspense, useEffect, useState } from 'react';
|
||||
import { JSX, Suspense } from 'react';
|
||||
import Shortcuts from './Shortcuts';
|
||||
import { AutoFocus } from './AutoFocus';
|
||||
|
||||
|
|
@ -21,7 +19,7 @@ export interface CollectionsDetailParams
|
|||
|
||||
export function CollectionsDetail (data: CollectionsDetailParams)
|
||||
{
|
||||
const focusKey = `game-list-${data.id}-${data.filters.platformIds?.join()}-${data.filters.collectionId}`;
|
||||
const focusKey = `game-list-${data.id}-${data.filters.platformId}-${data.filters.collectionId}`;
|
||||
const { ref, focusSelf } = useFocusable({
|
||||
focusKey,
|
||||
preferredChildFocusKey: `${focusKey}-list`,
|
||||
|
|
@ -46,7 +44,7 @@ export function CollectionsDetail (data: CollectionsDetailParams)
|
|||
<div>
|
||||
{data.footer}
|
||||
</div>
|
||||
<Shortcuts />
|
||||
<Shortcuts shortcuts={[{ icon: 'steamdeck_button_b', label: 'Back' }]} />
|
||||
</footer>
|
||||
</AnimatedBackground>
|
||||
</FocusContext>
|
||||
|
|
|
|||
107
src/mainview/components/ContextDialog.tsx
Normal file
107
src/mainview/components/ContextDialog.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { FocusContext, FocusDetails, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
import { createContext, JSX, useContext, useEffect } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useEventListener } from "usehooks-ts";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
const ContextDialogContext = createContext({} as {
|
||||
close: () => void,
|
||||
id: string;
|
||||
});
|
||||
|
||||
export function ContextList (data: { options: DialogEntry[]; className?: string; showCloseButton?: boolean; })
|
||||
{
|
||||
const context = useContext(ContextDialogContext);
|
||||
return <ul className={twMerge("list max-h-[70vh] overflow-y-auto", data.className)}>
|
||||
{data.options.map(o => <OptionElement className="list-row" key={o.id} {...o} />)}
|
||||
{data.showCloseButton !== false && <OptionElement className="list-row" type='accent' icon={<X />} action={context.close} id="close" content="Close" />}
|
||||
</ul>;
|
||||
}
|
||||
|
||||
export function OptionElement (data: DialogEntry & { onFocus?: () => void; className?: string; })
|
||||
{
|
||||
const context = useContext(ContextDialogContext);
|
||||
const handleFocus = () =>
|
||||
{
|
||||
(ref.current as HTMLElement).scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
data.onFocus?.();
|
||||
};
|
||||
const handleAction = data.action ? () => data.action?.({ close: context.close, focus: focusSelf }) : undefined;
|
||||
const { ref, focused, focusSelf } = useFocusable({
|
||||
focusKey: `${context.id}-list-option-${data.id}`,
|
||||
onEnterPress: handleAction,
|
||||
onFocus: handleFocus
|
||||
});
|
||||
const colors = {
|
||||
primary: classNames("hover:bg-primary/40", { "bg-primary text-primary-content": focused }),
|
||||
secondary: classNames("hover:bg-secondary/40", { "bg-secondary text-secondary-content": focused }),
|
||||
accent: classNames("hover:bg-accent/40", { "bg-accent text-accent-content": focused }),
|
||||
info: classNames("hover:bg-info/40", { "bg-info text-info-content": focused }),
|
||||
warning: classNames("hover:bg-warning/40", { "bg-warning text-warning-content": focused }),
|
||||
error: classNames("hover:bg-error/40", { "bg-error text-error-content": focused })
|
||||
};
|
||||
return <li ref={ref}
|
||||
onClick={handleAction}
|
||||
className={
|
||||
twMerge("flex cursor-pointer")}>
|
||||
<p className={twMerge("flex w-full h-14 items-center px-5 rounded-2xl transition-all gap-2",
|
||||
colors[data.type],
|
||||
classNames({ "font-semibold": focused }),
|
||||
data.className)}>
|
||||
{data.icon}
|
||||
{data.content}
|
||||
</p>
|
||||
</li>;
|
||||
}
|
||||
|
||||
export interface DialogEntry
|
||||
{
|
||||
id: string,
|
||||
content: string | JSX.Element;
|
||||
icon?: string | JSX.Element;
|
||||
type: 'primary' | 'secondary' | 'accent' | 'info' | 'warning' | 'error';
|
||||
action?: (ctx: { close: () => void, focus: (focusDetails?: FocusDetails | undefined) => void; }) => void;
|
||||
}
|
||||
|
||||
export function ContextDialog (data: { id: string, children: any | any[], open: boolean, close: () => void; })
|
||||
{
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusable: data.open, focusKey: `${data.id}-context-dialog`, isFocusBoundary: true });
|
||||
useEffect(() =>
|
||||
{
|
||||
if (data.open)
|
||||
{
|
||||
focusSelf();
|
||||
}
|
||||
}, [data.open]);
|
||||
|
||||
useEventListener('cancel', (e) =>
|
||||
{
|
||||
if (data.open)
|
||||
{
|
||||
e.stopPropagation();
|
||||
data.close();
|
||||
}
|
||||
}, ref);
|
||||
|
||||
return <dialog ref={ref} open={data.open} closedby="any" className={
|
||||
twMerge("absolute modal cursor-pointer bg-base-300/80 backdrop-brightness-50 duration-300 ease-in-out transition-all text-base-content",
|
||||
classNames({ "opacity-0": !data.open }))
|
||||
}
|
||||
onClick={() =>
|
||||
{
|
||||
if (data.open) data.close();
|
||||
}}>
|
||||
<FocusContext value={focusKey}>
|
||||
<ContextDialogContext value={{ id: data.id, close: data.close }} >
|
||||
<div
|
||||
className={twMerge("bg-base-100/80 delay-200 rounded-4xl p-6 min-w-[30vw] cursor-auto", data.open ? "animate-scale-delayed" : "opacity-0")}
|
||||
style={{ backdropFilter: 'blur(24px)' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{data.children}
|
||||
</div>
|
||||
</ContextDialogContext>
|
||||
</FocusContext>
|
||||
</dialog>;
|
||||
}
|
||||
|
|
@ -27,10 +27,10 @@ function FilterCat (
|
|||
className={classNames(
|
||||
"flex px-4 h-12 items-center justify-center rounded-full transition-all",
|
||||
{
|
||||
"bg-primary text-primary-content drop-shadow-sm cursor-default":
|
||||
"bg-base-content px-3 text-base-300 drop-shadow cursor-default":
|
||||
focused || data.active,
|
||||
"ring-base-content ring-7": focused,
|
||||
"hover:bg-base-300 cursor-pointer": !focused,
|
||||
"ring-primary ring-7": focused,
|
||||
"hover:bg-base-content/40 cursor-pointer": !focused,
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,23 +16,30 @@ export function GameCardSkeleton ()
|
|||
);
|
||||
}
|
||||
|
||||
export default function GameCard (data: {
|
||||
export interface GameCardParams
|
||||
{
|
||||
title: string;
|
||||
type?: string;
|
||||
subtitle: string;
|
||||
preview?: string | JSX.Element;
|
||||
subtitle: string | JSX.Element;
|
||||
preview?: string | JSX.Element | ((p: { focused: boolean; }) => JSX.Element);
|
||||
focusKey: string;
|
||||
index: number;
|
||||
id: number;
|
||||
badge?: JSX.Element;
|
||||
onFocus?: (id: number) => void;
|
||||
id: string;
|
||||
badges?: JSX.Element[];
|
||||
className?: string;
|
||||
onFocus?: (id: string) => void;
|
||||
onBlur?: (id: string) => void;
|
||||
onAction?: () => void;
|
||||
})
|
||||
clickFocuses?: boolean;
|
||||
}
|
||||
|
||||
export default function GameCard (data: GameCardParams)
|
||||
{
|
||||
const { ref, focused, focusSelf } = useFocusable({
|
||||
focusKey: data.focusKey,
|
||||
onFocus: () => data.onFocus?.(data.id),
|
||||
onEnterPress: () => data.onAction?.(),
|
||||
onBlur: () => data.onBlur?.(data.id)
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
|
|
@ -59,33 +66,48 @@ export default function GameCard (data: {
|
|||
}}
|
||||
onFocus={focusSelf}
|
||||
onDoubleClick={data.onAction}
|
||||
onClick={focused ? data.onAction : focusSelf}
|
||||
onClick={() =>
|
||||
{
|
||||
focusSelf();
|
||||
data.onAction?.();
|
||||
}}
|
||||
className={twMerge(
|
||||
`game-card game-card-height flex flex-col justify-end`,
|
||||
`game-card game-card-height flex flex-col justify-end z-5`,
|
||||
'max-h-(--game-card-height) min-w-(--game-card-width) w-(--game-card-width)',
|
||||
"overflow-hidden transition-all duration-200 drop-shadow-lg cursor-pointer",
|
||||
focused ?
|
||||
`animate-wiggle ring-7 bg-base-content text-base-300 ring-primary drop-shadow-xl drop-shadow-base-300/60 scale-102 z-10` :
|
||||
"bg-base-300 text-base-content",
|
||||
`focused animate-wiggle ring-7 bg-base-content text-base-300 ring-primary drop-shadow-xl drop-shadow-black/30 scale-102 z-10` :
|
||||
"bg-base-300 hover:bg-base-100 hover:scale-102 text-base-content",
|
||||
classNames({
|
||||
"h-(--game-card-height)": typeof data.preview === "string"
|
||||
})
|
||||
}),
|
||||
data.className
|
||||
)}
|
||||
>
|
||||
<div className={twMerge("overflow-hidden bg-base-400 h-full rounded-t-xl rounded-b-md transition-all", focused ? "mt-2 mx-2" : "mt-2 mx-2")}>
|
||||
{typeof data.preview === "string" ? (
|
||||
<img src={data.preview}></img>
|
||||
<img width={5192} height={5192} className={classNames({ "animate-rotate-small": focused })} src={data.preview} ></img>
|
||||
) : (
|
||||
data.preview
|
||||
typeof data.preview === 'function' ? data.preview({ focused }) : data.preview
|
||||
)}</div>
|
||||
|
||||
<div className="h-0 flex pr-2 justify-end items-center">{data.badge}</div>
|
||||
<div className="h-0 flex pr-2 justify-end items-center">
|
||||
{data.badges?.map(b =>
|
||||
<div
|
||||
className={
|
||||
twMerge("bg-base-100 text-base-content drop-shadow-lg overflow-hidden rounded-full p-1 mr-4 transition-colors",
|
||||
classNames({ "bg-primary text-primary-content": focused }))}
|
||||
>
|
||||
{b}
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-col p-4">
|
||||
<div className="text-xl font-bold text-nowrap text-ellipsis overflow-hidden">
|
||||
{data.title}
|
||||
</div>
|
||||
<div className="text-s">{data.subtitle}</div>
|
||||
</div>
|
||||
</li>
|
||||
</li >
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { keepPreviousData, useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { getRomsApiRomsGetOptions } from "../../clients/romm/@tanstack/react-query.gen";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { GameMetaExtra, CardList } from "./CardList";
|
||||
import { DefaultRommStaleTime, RPC_URL } from "../../shared/constants";
|
||||
import { FrontEndId, RPC_URL } from "../../shared/constants";
|
||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { SaveSource } from "../scripts/spatialNavigation";
|
||||
import { gamesQueryOptions } from "../query-options";
|
||||
import { rommApi } from "../scripts/clientApi";
|
||||
import { HardDrive } from "lucide-react";
|
||||
import { JSX } from "react";
|
||||
|
||||
export interface GameListFilter
|
||||
{
|
||||
platformIds?: number[];
|
||||
platformId?: number;
|
||||
collectionId?: number;
|
||||
}
|
||||
|
||||
|
|
@ -19,30 +19,39 @@ export interface GameListParams
|
|||
filters?: GameListFilter,
|
||||
grid?: boolean,
|
||||
setBackground?: (url: string) => void;
|
||||
onGameSelect?: (id: number) => void;
|
||||
onGameSelect?: (id: FrontEndId) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function GameList (data: GameListParams)
|
||||
{
|
||||
const games = useSuspenseQuery(gamesQueryOptions(data.filters));
|
||||
const games = useSuspenseQuery({
|
||||
queryKey: ['games', data.filters ?? 'all'],
|
||||
queryFn: () => rommApi.api.romm.games.get({
|
||||
query: {
|
||||
platform_id: data.filters?.platformId,
|
||||
collection_id: data.filters?.collectionId
|
||||
}
|
||||
}).then(d => d.data)
|
||||
});
|
||||
const navigator = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const handleFocus = (id: number) =>
|
||||
const handleFocus = (id: FrontEndId) =>
|
||||
{
|
||||
const game = games.data?.items.find((g) => g.id === id);
|
||||
const game = games.data?.games.find((g) => g.id === id);
|
||||
if (game)
|
||||
{
|
||||
data.setBackground?.(
|
||||
`${RPC_URL(__HOST__)}/api/romm${game.path_cover_small}`,
|
||||
`${RPC_URL(__HOST__)}${game.path_cover}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function handleDefaultSelect (id: number)
|
||||
function handleDefaultSelect (id: FrontEndId, source: string | null, sourceId: number | null)
|
||||
{
|
||||
SaveSource('details', location.pathname);
|
||||
navigator({ to: '/game/$id', params: { id: String(id) }, viewTransition: { types: ['zoom-in'] } });
|
||||
SaveSource('details');
|
||||
navigator({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source }, viewTransition: { types: ['zoom-in'] } });
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -51,23 +60,34 @@ export function GameList (data: GameListParams)
|
|||
id={data.id}
|
||||
type="game"
|
||||
grid={data.grid}
|
||||
games={games.data.items.sort(
|
||||
(a, b) =>
|
||||
Date.parse(b.rom_user.last_played ?? b.updated_at) -
|
||||
Date.parse(a.rom_user.last_played ?? a.updated_at),
|
||||
)
|
||||
className={data.className}
|
||||
games={games.data?.games
|
||||
.map(
|
||||
(g) =>
|
||||
({
|
||||
id: g.id,
|
||||
{
|
||||
const badges: JSX.Element[] = [];
|
||||
if (g.id.source === 'local')
|
||||
{
|
||||
badges.push(<HardDrive className="size-8 m-1" />);
|
||||
}
|
||||
|
||||
return {
|
||||
id: `game-${g.id.source}-${g.id.id}`,
|
||||
focusKey: g.slug ?? `game-${g.id}`,
|
||||
title: g.name ?? "",
|
||||
subtitle: g.platform_display_name ?? "",
|
||||
previewUrl: `${RPC_URL(__HOST__)}/api/romm${g.path_cover_large}`,
|
||||
}) satisfies GameMetaExtra,
|
||||
)}
|
||||
onGameFocus={handleFocus}
|
||||
onSelectGame={id => data.onGameSelect ? data.onGameSelect(id) : handleDefaultSelect(id)}
|
||||
subtitle: (
|
||||
<div className="flex gap-1 items-center">
|
||||
{!!g.path_platform_cover && <img className="size-4" src={`${RPC_URL(__HOST__)}${g.path_platform_cover}`} />}
|
||||
<p className="opacity-80">{g.platform_display_name}</p>
|
||||
</div>
|
||||
),
|
||||
previewUrl: `${RPC_URL(__HOST__)}${g.path_cover}`,
|
||||
badges: badges,
|
||||
onSelect: () => data.onGameSelect ? data.onGameSelect(g.id) : handleDefaultSelect(g.id, g.source, g.source_id),
|
||||
onFocus: () => handleFocus(g.id)
|
||||
} satisfies GameMetaExtra;
|
||||
},
|
||||
) ?? []}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ function HeaderAvatar (data: {
|
|||
id={data.id}
|
||||
ref={ref}
|
||||
onClick={data.onSelect}
|
||||
style={{ viewTransitionName: data.id }}
|
||||
className={classNames(
|
||||
`avatar indicator ring-base-100 ring-offset-base-100 size-14 rounded-full flex items-center justify-center`,
|
||||
bgColors[data.type ?? "none"],
|
||||
|
|
@ -92,6 +91,7 @@ export interface HeaderButton
|
|||
id: string;
|
||||
icon: JSX.Element;
|
||||
external?: boolean;
|
||||
action?: () => void;
|
||||
}
|
||||
|
||||
export interface HeaderAccount
|
||||
|
|
@ -135,7 +135,7 @@ export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAcc
|
|||
],
|
||||
action: () =>
|
||||
{
|
||||
SaveSource('settings', location.pathname);
|
||||
SaveSource('settings');
|
||||
navigate({ to: '/settings/accounts', viewTransition: { types: ['zoom-in'] }, search: { focus: 'rommAddress' } });
|
||||
},
|
||||
status: user.data ? "status-success" : 'status-error',
|
||||
|
|
@ -182,6 +182,7 @@ export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAcc
|
|||
id={b.id}
|
||||
icon={b.icon}
|
||||
external={b.external}
|
||||
action={b.action}
|
||||
/>)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
72
src/mainview/components/PlatformsList.tsx
Normal file
72
src/mainview/components/PlatformsList.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { getPlatformsApiPlatformsGetOptions } from "../../clients/romm/@tanstack/react-query.gen";
|
||||
import { DefaultRommStaleTime, GameMeta, RPC_URL } from "../../shared/constants";
|
||||
import { CardList, GameMetaExtra } from "./CardList";
|
||||
import classNames from "classnames";
|
||||
import { rommApi } from "../scripts/clientApi";
|
||||
|
||||
export function PlatformsList (data: { id: string, setBackground: (url: string) => void; className?: string; })
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const { data: platforms } = useSuspenseQuery(
|
||||
{
|
||||
queryKey: ['platform', 'all'],
|
||||
queryFn: async () =>
|
||||
{
|
||||
const { data, error } = await rommApi.api.romm.platforms.get();
|
||||
if (error) throw error;
|
||||
return data.platforms;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: DefaultRommStaleTime,
|
||||
});
|
||||
|
||||
return (
|
||||
<CardList
|
||||
type="platform"
|
||||
id={data.id}
|
||||
className={data.className}
|
||||
games={platforms.sort((a, b) => a.updated_at.getTime() - b.updated_at.getTime())
|
||||
.map((g) => ({
|
||||
id: g.slug,
|
||||
focusKey: g.slug,
|
||||
title: g.name,
|
||||
subtitle: g.family_name ?? "",
|
||||
previewUrl: "",
|
||||
badges: [(<span className="text-lg font-bold p-2 rounded-full">
|
||||
{g.game_count}
|
||||
</span>)],
|
||||
onFocus: () => data.setBackground(
|
||||
`https://picsum.photos/id/${10 + g.slug.length}/1920/1080.webp`,
|
||||
),
|
||||
onSelect: () =>
|
||||
{
|
||||
navigate({ to: `/platform/${g.source ?? g.id.source}/${g.source_id ?? g.id.id}`, viewTransition: { types: ['zoom-in'] } });
|
||||
},
|
||||
preview:
|
||||
({ focused }) => <div
|
||||
className="flex h-60 p-6 bg-base-100 justify-center"
|
||||
style={{
|
||||
background: `linear-gradient(
|
||||
color-mix(in srgb, var(--color-base-content) 60%, transparent),
|
||||
color-mix(in srgb, var(--color-base-300) 60%, transparent)
|
||||
), url(https://picsum.photos/id/${8 + g.slug.length}/300/300.webp?blur=10) center / cover`,
|
||||
|
||||
backgroundBlendMode: "screen",
|
||||
boxShadow: 'inset 0 0 32px rgba(0,0,0,0.6)'
|
||||
}}
|
||||
>
|
||||
<img className={classNames("drop-shadow-2xl", { "animate-rotate": focused })}
|
||||
src={`${RPC_URL(__HOST__)}${g.path_cover}`}
|
||||
></img>
|
||||
</div>
|
||||
,
|
||||
} satisfies GameMetaExtra))}
|
||||
onSelectGame={(id) =>
|
||||
{
|
||||
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ export default function ShortcutPrompt (data: {
|
|||
<span
|
||||
onClick={data.onClick}
|
||||
className={twMerge(
|
||||
"flex md:gap-2 bg-base-100 text-base-content neutral-content md:pl-2 md:pr-3 md:py-1.5 rounded-full items-center md:text-lg drop-shadow-sm",
|
||||
"flex md:gap-2 bg-base-100 text-base-content neutral-content md:pl-2 md:pr-3 md:py-1.5 rounded-full items-center md:text-lg drop-shadow-sm ring-[1px] ring-base-content/10 drop-shadow-black/30",
|
||||
"sm:text-sm",
|
||||
data.className,
|
||||
classNames({
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import React from 'react';
|
||||
import ShortcutPrompt from './ShortcutPrompt';
|
||||
import { IconType } from './SvgIcon';
|
||||
|
||||
export default function Shortcuts ()
|
||||
export interface Shortcut
|
||||
{
|
||||
icon: IconType;
|
||||
label: string;
|
||||
action?: () => void;
|
||||
}
|
||||
|
||||
export default function Shortcuts (data: { shortcuts: Shortcut[]; })
|
||||
{
|
||||
return (
|
||||
<div style={{ viewTransitionName: 'shortcuts' }} className="flex gap-2">
|
||||
<ShortcutPrompt icon="steamdeck_button_a" label="Continue" />
|
||||
<ShortcutPrompt icon="steamdeck_button_b" label="Back" />
|
||||
<ShortcutPrompt icon="steamdeck_button_x" label="Close" />
|
||||
<ShortcutPrompt icon="steamdeck_button_y" label="Options" />
|
||||
{data.shortcuts.map((s, i) => <ShortcutPrompt key={i} onClick={s.action} icon={s.icon} label={s.label} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
29
src/mainview/components/backgrounds/dots.css
Normal file
29
src/mainview/components/backgrounds/dots.css
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
.ball {
|
||||
border-radius: 50%;
|
||||
animation: bounce 0.6s 32 alternate;
|
||||
}
|
||||
|
||||
.ball:nth-child(1) {
|
||||
background: var(--color-accent);
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.ball:nth-child(2) {
|
||||
background: var(--color-secondary);
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.ball:nth-child(3) {
|
||||
background: var(--color-primary);
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
}
|
||||
10
src/mainview/components/backgrounds/dots.tsx
Normal file
10
src/mainview/components/backgrounds/dots.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import './dots.css';
|
||||
|
||||
export default function DotsLoading ()
|
||||
{
|
||||
return <div className="flex gap-3 justify-center animation_alternate items-center pt-8">
|
||||
<div className="ball size-6"></div>
|
||||
<div className="ball size-6"></div>
|
||||
<div className="ball size-6"></div>
|
||||
</div>;
|
||||
}
|
||||
28
src/mainview/components/options/Button.tsx
Normal file
28
src/mainview/components/options/Button.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import
|
||||
{
|
||||
useFocusable,
|
||||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function Button (data: { id: string, children?: any, className?: string, disabled?: boolean, type: "reset" | "button" | "submit" | undefined; } & InteractParams & FocusParams)
|
||||
{
|
||||
const { ref, focused } = useFocusable({
|
||||
focusKey: data.id,
|
||||
onEnterPress: data.onAction,
|
||||
onFocus: data.onFocus,
|
||||
focusable: !data.disabled
|
||||
});
|
||||
return <button
|
||||
ref={ref}
|
||||
onClick={data.onAction}
|
||||
disabled={data.disabled}
|
||||
className={twMerge("btn rounded-full focus:bg-base-content focus:text-base-300 md:text-lg", classNames({
|
||||
"btn-accent": focused
|
||||
}, data.className))}
|
||||
type={data.type}
|
||||
>
|
||||
{data.children}
|
||||
</button>;
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import classNames from "classnames";
|
||||
import { ChangeEventHandler, FocusEventHandler, HTMLInputTypeAttribute, JSX, useRef } from "react";
|
||||
import { ChangeEventHandler, FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useOptionContext } from "./OptionSpace";
|
||||
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { systemApi } from "../../scripts/clientApi";
|
||||
|
||||
export function OptionInput (data: {
|
||||
name: string;
|
||||
|
|
@ -11,10 +13,18 @@ export function OptionInput (data: {
|
|||
icon?: JSX.Element;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
autocomplete?: HTMLInputAutoCompleteAttribute;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
})
|
||||
{
|
||||
const { ref, focused } = useFocusable({
|
||||
focusKey: data.name, onEnterPress: () =>
|
||||
{
|
||||
inputRef.current?.focus();
|
||||
systemApi.api.system.show_keyboard.post();
|
||||
}
|
||||
});
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const option = useOptionContext({
|
||||
onOptionEnterPress ()
|
||||
|
|
@ -24,10 +34,11 @@ export function OptionInput (data: {
|
|||
});
|
||||
|
||||
return (
|
||||
<label className="flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent">
|
||||
<span className={twMerge("text-base-content/80", classNames({
|
||||
<label ref={ref} className={twMerge("flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent",
|
||||
classNames({ "[&_input]:not-focus:ring-7 [&_input]:not-focus:ring-accent": focused }))}>
|
||||
{!!data.icon && <span className={twMerge("text-base-content/80", classNames({
|
||||
"text-primary-content": option.focused
|
||||
}))}>{data.icon}</span>
|
||||
}))}>{data.icon}</span>}
|
||||
<input
|
||||
ref={inputRef}
|
||||
id={data.name}
|
||||
|
|
@ -35,12 +46,13 @@ export function OptionInput (data: {
|
|||
value={data.value}
|
||||
defaultValue={data.defaultValue}
|
||||
type={data.type}
|
||||
autoComplete={data.autocomplete}
|
||||
onFocus={() => option.focus()}
|
||||
placeholder={data.placeholder}
|
||||
onChange={data.onChange}
|
||||
onBlur={data.onBlur}
|
||||
className={twMerge(
|
||||
"input grow rounded-full ring-primary-content focus:ring-3",
|
||||
"input grow rounded-full ring-primary-content focus:ring-7",
|
||||
data.className,
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ export function OptionSpace (data: {
|
|||
id?: string;
|
||||
className?: string;
|
||||
focusable?: boolean;
|
||||
children: JSX.Element;
|
||||
children?: any | any[];
|
||||
label?: string | JSX.Element;
|
||||
saveLastFocusedChild?: boolean;
|
||||
})
|
||||
{
|
||||
const eventTarget = useMemo(() => new EventTarget(), []);
|
||||
|
|
@ -51,6 +52,11 @@ export function OptionSpace (data: {
|
|||
focusKey: data.id,
|
||||
focusable: data.focusable !== false,
|
||||
trackChildren: true,
|
||||
saveLastFocusedChild: data.saveLastFocusedChild ?? false,
|
||||
onFocus ()
|
||||
{
|
||||
(ref.current as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
},
|
||||
onEnterPress ()
|
||||
{
|
||||
eventTarget.dispatchEvent(new CustomEvent("onEnterPress"));
|
||||
|
|
|
|||
72
src/mainview/components/options/SettingsOption.tsx
Normal file
72
src/mainview/components/options/SettingsOption.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { HTMLInputTypeAttribute, JSX, useCallback, useState } from "react";
|
||||
import { SettingsType } from "../../../shared/constants";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { OptionSpace } from "./OptionSpace";
|
||||
import { OptionInput } from "./OptionInput";
|
||||
import { settingsApi } from "../../scripts/clientApi";
|
||||
|
||||
type KeysWithValueAssignableTo<T, Value> = {
|
||||
[K in keyof T]: Exclude<T[K], undefined> extends Value ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export function SettingsOption (data: {
|
||||
label: string;
|
||||
id: KeysWithValueAssignableTo<SettingsType, string>;
|
||||
type: HTMLInputTypeAttribute;
|
||||
placeholder?: string;
|
||||
icon?: JSX.Element;
|
||||
})
|
||||
{
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [localValue, setLocalValue] = useState<string | undefined>();
|
||||
useQuery({
|
||||
enabled: !!data.id,
|
||||
queryKey: ["setting", data.id],
|
||||
queryFn: async () =>
|
||||
{
|
||||
const { data: value, error } = await settingsApi.api.settings({ id: data.id! }).get();
|
||||
if (error) throw error;
|
||||
if (!dirty)
|
||||
{
|
||||
setLocalValue(String(value.value));
|
||||
}
|
||||
return value.value;
|
||||
},
|
||||
});
|
||||
const setSettingMutation = useMutation({
|
||||
mutationKey: ["setting", data.id],
|
||||
mutationFn: async (value: any) =>
|
||||
{
|
||||
const response = await settingsApi.api.settings({ id: data.id! }).post({ value });
|
||||
if (response.error) throw response.error;
|
||||
return response.data;
|
||||
}
|
||||
});
|
||||
|
||||
const handleSave = useCallback(() =>
|
||||
{
|
||||
if (dirty)
|
||||
{
|
||||
setDirty(false);
|
||||
setSettingMutation.mutate(localValue);
|
||||
}
|
||||
}, [dirty, setDirty, localValue]);
|
||||
|
||||
return (
|
||||
<OptionSpace label={data.label}>
|
||||
<OptionInput
|
||||
icon={data.icon}
|
||||
name={data.id ?? ""}
|
||||
type={data.type}
|
||||
placeholder={data.placeholder}
|
||||
onBlur={handleSave}
|
||||
onChange={(e) =>
|
||||
{
|
||||
setLocalValue(e.currentTarget.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
value={localValue}
|
||||
/>
|
||||
</OptionSpace>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import classNames from "classnames";
|
||||
import { CircleX, Cross, X } from "lucide-react";
|
||||
import { createContext, JSX, useContext, useEffect, useState } from "react";
|
||||
import toast, { ToastBar, Toaster } from "react-hot-toast";
|
||||
|
||||
let toasterGlobalId = 0;
|
||||
|
||||
const ToastersContext = createContext(
|
||||
{} as {
|
||||
showToaster: (data: Toast) => void;
|
||||
},
|
||||
);
|
||||
|
||||
export function useToasters ()
|
||||
{
|
||||
const toasters = useContext(ToastersContext);
|
||||
return { ...toasters };
|
||||
}
|
||||
|
||||
interface Toast
|
||||
{
|
||||
message: string | JSX.Element;
|
||||
type: "success" | "info" | "error" | "warning";
|
||||
duration?: number;
|
||||
icon?: JSX.Element;
|
||||
}
|
||||
|
||||
interface ToastExtra extends Toast
|
||||
{
|
||||
timeout?: NodeJS.Timeout;
|
||||
id: number;
|
||||
}
|
||||
|
||||
function ToastComponent (data: { toast: Toast; })
|
||||
{
|
||||
return (
|
||||
<div className={classNames(`alert alert-${data.toast.type} `)}>
|
||||
<span>
|
||||
{data.toast.icon}
|
||||
{data.toast.message}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Toasters ()
|
||||
{
|
||||
const [visibleToasters, setVisible] = useState<ToastExtra[]>([]);
|
||||
useEffect(() =>
|
||||
{
|
||||
return () =>
|
||||
{
|
||||
visibleToasters.filter((t) => t.timeout).forEach((t) => clearTimeout(t.timeout));
|
||||
};
|
||||
}, [setVisible]);
|
||||
|
||||
return (
|
||||
<Toaster toastOptions={{
|
||||
className: "bg-base-300 text-base-content", success: {
|
||||
className: 'bg-success'
|
||||
}
|
||||
}}>
|
||||
{(t) => <ToastBar toast={t} >
|
||||
{({ icon, message }) => (
|
||||
<>
|
||||
{icon}
|
||||
{message}
|
||||
{t.type !== 'loading' && (
|
||||
<button className="size-6 p-0 rounded-full cursor-pointer text-base-100" onClick={() => toast.dismiss(t.id)}><CircleX className="size-5" /></button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ToastBar>}
|
||||
</Toaster>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,10 +11,13 @@
|
|||
import { Route as rootRouteImport } from './../routes/__root'
|
||||
import { Route as SettingsRouteRouteImport } from './../routes/settings/route'
|
||||
import { Route as IndexRouteImport } from './../routes/index'
|
||||
import { Route as SettingsDirectoriesRouteImport } from './../routes/settings/directories'
|
||||
import { Route as SettingsAccountsRouteImport } from './../routes/settings/accounts'
|
||||
import { Route as PlatformIdRouteImport } from './../routes/platform/$id'
|
||||
import { Route as GameIdRouteImport } from './../routes/game/$id'
|
||||
import { Route as CollectionIdRouteImport } from './../routes/collection/$id'
|
||||
import { Route as SettingsAboutRouteImport } from './../routes/settings/about'
|
||||
import { Route as CollectionIdRouteImport } from './../routes/collection.$id'
|
||||
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'
|
||||
|
||||
const SettingsRouteRoute = SettingsRouteRouteImport.update({
|
||||
id: '/settings',
|
||||
|
|
@ -26,51 +29,75 @@ const IndexRoute = IndexRouteImport.update({
|
|||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const SettingsDirectoriesRoute = SettingsDirectoriesRouteImport.update({
|
||||
id: '/directories',
|
||||
path: '/directories',
|
||||
getParentRoute: () => SettingsRouteRoute,
|
||||
} as any)
|
||||
const SettingsAccountsRoute = SettingsAccountsRouteImport.update({
|
||||
id: '/accounts',
|
||||
path: '/accounts',
|
||||
getParentRoute: () => SettingsRouteRoute,
|
||||
} as any)
|
||||
const PlatformIdRoute = PlatformIdRouteImport.update({
|
||||
id: '/platform/$id',
|
||||
path: '/platform/$id',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const GameIdRoute = GameIdRouteImport.update({
|
||||
id: '/game/$id',
|
||||
path: '/game/$id',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
const SettingsAboutRoute = SettingsAboutRouteImport.update({
|
||||
id: '/about',
|
||||
path: '/about',
|
||||
getParentRoute: () => SettingsRouteRoute,
|
||||
} as any)
|
||||
const CollectionIdRoute = CollectionIdRouteImport.update({
|
||||
id: '/collection/$id',
|
||||
path: '/collection/$id',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PlatformSourceIdRoute = PlatformSourceIdRouteImport.update({
|
||||
id: '/platform/$source/$id',
|
||||
path: '/platform/$source/$id',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const LauncherSourceIdRoute = LauncherSourceIdRouteImport.update({
|
||||
id: '/launcher/$source/$id',
|
||||
path: '/launcher/$source/$id',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const GameSourceIdRoute = GameSourceIdRouteImport.update({
|
||||
id: '/game/$source/$id',
|
||||
path: '/game/$source/$id',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/settings': typeof SettingsRouteRouteWithChildren
|
||||
'/collection/$id': typeof CollectionIdRoute
|
||||
'/game/$id': typeof GameIdRoute
|
||||
'/platform/$id': typeof PlatformIdRoute
|
||||
'/settings/about': typeof SettingsAboutRoute
|
||||
'/settings/accounts': typeof SettingsAccountsRoute
|
||||
'/settings/directories': typeof SettingsDirectoriesRoute
|
||||
'/game/$source/$id': typeof GameSourceIdRoute
|
||||
'/launcher/$source/$id': typeof LauncherSourceIdRoute
|
||||
'/platform/$source/$id': typeof PlatformSourceIdRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/settings': typeof SettingsRouteRouteWithChildren
|
||||
'/collection/$id': typeof CollectionIdRoute
|
||||
'/game/$id': typeof GameIdRoute
|
||||
'/platform/$id': typeof PlatformIdRoute
|
||||
'/settings/about': typeof SettingsAboutRoute
|
||||
'/settings/accounts': typeof SettingsAccountsRoute
|
||||
'/settings/directories': typeof SettingsDirectoriesRoute
|
||||
'/game/$source/$id': typeof GameSourceIdRoute
|
||||
'/launcher/$source/$id': typeof LauncherSourceIdRoute
|
||||
'/platform/$source/$id': typeof PlatformSourceIdRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/settings': typeof SettingsRouteRouteWithChildren
|
||||
'/collection/$id': typeof CollectionIdRoute
|
||||
'/game/$id': typeof GameIdRoute
|
||||
'/platform/$id': typeof PlatformIdRoute
|
||||
'/settings/about': typeof SettingsAboutRoute
|
||||
'/settings/accounts': typeof SettingsAccountsRoute
|
||||
'/settings/directories': typeof SettingsDirectoriesRoute
|
||||
'/game/$source/$id': typeof GameSourceIdRoute
|
||||
'/launcher/$source/$id': typeof LauncherSourceIdRoute
|
||||
'/platform/$source/$id': typeof PlatformSourceIdRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
|
|
@ -78,33 +105,43 @@ export interface FileRouteTypes {
|
|||
| '/'
|
||||
| '/settings'
|
||||
| '/collection/$id'
|
||||
| '/game/$id'
|
||||
| '/platform/$id'
|
||||
| '/settings/about'
|
||||
| '/settings/accounts'
|
||||
| '/settings/directories'
|
||||
| '/game/$source/$id'
|
||||
| '/launcher/$source/$id'
|
||||
| '/platform/$source/$id'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/settings'
|
||||
| '/collection/$id'
|
||||
| '/game/$id'
|
||||
| '/platform/$id'
|
||||
| '/settings/about'
|
||||
| '/settings/accounts'
|
||||
| '/settings/directories'
|
||||
| '/game/$source/$id'
|
||||
| '/launcher/$source/$id'
|
||||
| '/platform/$source/$id'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/settings'
|
||||
| '/collection/$id'
|
||||
| '/game/$id'
|
||||
| '/platform/$id'
|
||||
| '/settings/about'
|
||||
| '/settings/accounts'
|
||||
| '/settings/directories'
|
||||
| '/game/$source/$id'
|
||||
| '/launcher/$source/$id'
|
||||
| '/platform/$source/$id'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
SettingsRouteRoute: typeof SettingsRouteRouteWithChildren
|
||||
CollectionIdRoute: typeof CollectionIdRoute
|
||||
GameIdRoute: typeof GameIdRoute
|
||||
PlatformIdRoute: typeof PlatformIdRoute
|
||||
GameSourceIdRoute: typeof GameSourceIdRoute
|
||||
LauncherSourceIdRoute: typeof LauncherSourceIdRoute
|
||||
PlatformSourceIdRoute: typeof PlatformSourceIdRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
|
|
@ -123,6 +160,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/settings/directories': {
|
||||
id: '/settings/directories'
|
||||
path: '/directories'
|
||||
fullPath: '/settings/directories'
|
||||
preLoaderRoute: typeof SettingsDirectoriesRouteImport
|
||||
parentRoute: typeof SettingsRouteRoute
|
||||
}
|
||||
'/settings/accounts': {
|
||||
id: '/settings/accounts'
|
||||
path: '/accounts'
|
||||
|
|
@ -130,19 +174,12 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof SettingsAccountsRouteImport
|
||||
parentRoute: typeof SettingsRouteRoute
|
||||
}
|
||||
'/platform/$id': {
|
||||
id: '/platform/$id'
|
||||
path: '/platform/$id'
|
||||
fullPath: '/platform/$id'
|
||||
preLoaderRoute: typeof PlatformIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/game/$id': {
|
||||
id: '/game/$id'
|
||||
path: '/game/$id'
|
||||
fullPath: '/game/$id'
|
||||
preLoaderRoute: typeof GameIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
'/settings/about': {
|
||||
id: '/settings/about'
|
||||
path: '/about'
|
||||
fullPath: '/settings/about'
|
||||
preLoaderRoute: typeof SettingsAboutRouteImport
|
||||
parentRoute: typeof SettingsRouteRoute
|
||||
}
|
||||
'/collection/$id': {
|
||||
id: '/collection/$id'
|
||||
|
|
@ -151,15 +188,40 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof CollectionIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/platform/$source/$id': {
|
||||
id: '/platform/$source/$id'
|
||||
path: '/platform/$source/$id'
|
||||
fullPath: '/platform/$source/$id'
|
||||
preLoaderRoute: typeof PlatformSourceIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/launcher/$source/$id': {
|
||||
id: '/launcher/$source/$id'
|
||||
path: '/launcher/$source/$id'
|
||||
fullPath: '/launcher/$source/$id'
|
||||
preLoaderRoute: typeof LauncherSourceIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/game/$source/$id': {
|
||||
id: '/game/$source/$id'
|
||||
path: '/game/$source/$id'
|
||||
fullPath: '/game/$source/$id'
|
||||
preLoaderRoute: typeof GameSourceIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SettingsRouteRouteChildren {
|
||||
SettingsAboutRoute: typeof SettingsAboutRoute
|
||||
SettingsAccountsRoute: typeof SettingsAccountsRoute
|
||||
SettingsDirectoriesRoute: typeof SettingsDirectoriesRoute
|
||||
}
|
||||
|
||||
const SettingsRouteRouteChildren: SettingsRouteRouteChildren = {
|
||||
SettingsAboutRoute: SettingsAboutRoute,
|
||||
SettingsAccountsRoute: SettingsAccountsRoute,
|
||||
SettingsDirectoriesRoute: SettingsDirectoriesRoute,
|
||||
}
|
||||
|
||||
const SettingsRouteRouteWithChildren = SettingsRouteRoute._addFileChildren(
|
||||
|
|
@ -170,8 +232,9 @@ const rootRouteChildren: RootRouteChildren = {
|
|||
IndexRoute: IndexRoute,
|
||||
SettingsRouteRoute: SettingsRouteRouteWithChildren,
|
||||
CollectionIdRoute: CollectionIdRoute,
|
||||
GameIdRoute: GameIdRoute,
|
||||
PlatformIdRoute: PlatformIdRoute,
|
||||
GameSourceIdRoute: GameSourceIdRoute,
|
||||
LauncherSourceIdRoute: LauncherSourceIdRoute,
|
||||
PlatformSourceIdRoute: PlatformSourceIdRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
|
|
|||
|
|
@ -3,17 +3,96 @@
|
|||
@plugin "daisyui";
|
||||
|
||||
@theme {
|
||||
--color-dark: #333333;
|
||||
--color-light: #464646;
|
||||
--color-light: #828282;
|
||||
--color-light2: #bcbcbc;
|
||||
--color-primary: #E5FF00;
|
||||
--color-alt: #4656E6;
|
||||
--color-alert: #E60012;
|
||||
--game-card-height: calc(var(--spacing) * 100);
|
||||
--game-card-width: calc(var(--spacing) * 64);
|
||||
|
||||
--animate-wiggle: wiggle 0.3s ease-in-out 1;
|
||||
--animate-rotate: rotate 0.3s ease-in-out 1 0.2s;
|
||||
--animate-rotate-small: rotate-small 0.3s ease-in-out 1 0.2s;
|
||||
--animate-scale: scale 0.3s ease-in-out 1;
|
||||
--animate-slide-up: slide-up 0.2s ease-in-out 1;
|
||||
--animate-scale-delayed: scale 0.3s ease-in-out 1 100ms;
|
||||
--animate-scale-small: scale-small 0.3s ease-in-out 1;
|
||||
--animate-fade-out: fade-out 0.3s ease-out 1;
|
||||
|
||||
@keyframes slide-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(0, 10px)
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-small {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(100%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(102%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(100%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(105%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-small {
|
||||
|
||||
0%,
|
||||
50%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(0.2deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate(-0.2deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
|
||||
0%,
|
||||
50%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(1deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate(-1deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
|
||||
|
|
@ -39,11 +118,27 @@ html {
|
|||
}
|
||||
|
||||
@layer components {
|
||||
|
||||
.background {
|
||||
-webkit-backface-visibility: hidden;
|
||||
-webkit-perspective: 1000;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
-webkit-transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000;
|
||||
transform: translate3d(0, 0, 0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.game-card {
|
||||
@apply rounded-2xl;
|
||||
}
|
||||
|
||||
.menu-icon svg {
|
||||
@apply sm:size-7 md:size-9 transition-all;
|
||||
}
|
||||
|
||||
.menu-icon.focus svg {
|
||||
@apply sm:size-8 md:size-10;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,28 +10,15 @@ import
|
|||
} from "@tanstack/react-router";
|
||||
import { routeTree } from "./gen/routeTree.gen";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { AppType } from "../bun/api/rpc";
|
||||
import { RPC_URL } from "../shared/constants";
|
||||
import "./scripts/gamepads";
|
||||
import "./scripts/windowEvents";
|
||||
import { Toasters } from "./contexts/ToasterContext";
|
||||
import { client as rommClient } from "../clients/romm/client.gen";
|
||||
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
|
||||
import "./scripts/spatialNavigation";
|
||||
import
|
||||
{
|
||||
treaty
|
||||
} from '@elysiajs/eden';
|
||||
|
||||
const hashHistory = createHashHistory({});
|
||||
|
||||
export const client = treaty<AppType>(RPC_URL(__HOST__), {
|
||||
keepDomain: true,
|
||||
fetch: {
|
||||
credentials: 'include',
|
||||
}
|
||||
});
|
||||
|
||||
rommClient.setConfig({
|
||||
baseUrl: `${RPC_URL(__HOST__)}/api/romm`,
|
||||
credentials: "include",
|
||||
|
|
@ -51,8 +38,7 @@ export const Router = createRouter({
|
|||
history: hashHistory,
|
||||
defaultPreload: "intent",
|
||||
context: { queryClient },
|
||||
scrollRestoration: true,
|
||||
scrollToTopSelectors: ["[save-scroll]"],
|
||||
scrollRestoration: false,
|
||||
defaultNotFoundComponent: () =>
|
||||
{
|
||||
return (
|
||||
|
|
@ -86,7 +72,6 @@ if (!rootElement.innerHTML)
|
|||
root.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={Router} />
|
||||
<Toasters />
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { useEventListener, useSessionStorage } from 'usehooks-ts';
|
||||
import { CollectionsDetail } from '../../components/CollectionsDetail';
|
||||
import { getRomsApiRomsGetOptions } from '../../../clients/romm/@tanstack/react-query.gen';
|
||||
import { DefaultRommStaleTime } from '../../../shared/constants';
|
||||
import { CollectionsDetail } from '../components/CollectionsDetail';
|
||||
import { getRomsApiRomsGetOptions } from '../../clients/romm/@tanstack/react-query.gen';
|
||||
import { DefaultRommStaleTime } from '../../shared/constants';
|
||||
|
||||
export const Route = createFileRoute('/collection/$id')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -20,7 +20,7 @@ function RouteComponent ()
|
|||
undefined,
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
useEventListener("cancel", () => navigate({ to: "/", viewTransition: { types: ['zoom-out'] } }));
|
||||
useEventListener("cancel", () => navigate({ to: "/", viewTransition: { types: ["zoom-out"] } }));
|
||||
|
||||
return (
|
||||
<CollectionsDetail setBackground={setBackground} filters={{ collectionId: Number(id) }} />
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
import { createFileRoute, useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { getRomApiRomsIdGetOptions } from "../../../clients/romm/@tanstack/react-query.gen";
|
||||
import { DefaultRommStaleTime, RPC_URL } from "../../../shared/constants";
|
||||
import { twJoin, twMerge } from "tailwind-merge";
|
||||
import { JSX, Ref, RefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { FocusContext, getCurrentFocusKey, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
import { Clock, HardDrive, Image, Play, Settings, Trophy } from "lucide-react";
|
||||
import ShortcutPrompt from "../../components/ShortcutPrompt";
|
||||
import { HeaderUI } from "../../components/Header";
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { DetailedRomSchema } from "../../../clients/romm";
|
||||
import { useEventListener } from "usehooks-ts";
|
||||
import { PopSource } from "../../scripts/spatialNavigation";
|
||||
import { gameQueryOptions } from "../../query-options";
|
||||
import { AnimatedBackground } from "../../components/AnimatedBackground";
|
||||
|
||||
export const Route = createFileRoute("/game/$id")({
|
||||
loader: ({ params, context }) => context.queryClient.fetchQuery(gameQueryOptions(Number(params.id))),
|
||||
component: GameDetailsUI,
|
||||
pendingComponent: GameDetailsUIPending
|
||||
});
|
||||
|
||||
function GameDetailsUIPending ()
|
||||
{
|
||||
return <AnimatedBackground>
|
||||
<div className="flex flex-col p-2 px-3 w-full h-full">
|
||||
<HeaderUI />
|
||||
<div className="flex flex-col justify-center items-center grow">
|
||||
<span className="loading loading-dots loading-xl"></span>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedBackground>;
|
||||
}
|
||||
|
||||
export function GameDetailsUI ()
|
||||
{
|
||||
// In a component!
|
||||
const { id } = Route.useParams();
|
||||
const data = Route.useLoaderData();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details" });
|
||||
const backgroundImage = `${RPC_URL(__HOST__)}/api/romm${data.path_cover_small}`;
|
||||
const mainAreaRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
focusSelf();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AnimatedBackground backgroundUrl={backgroundImage}>
|
||||
<div className="z-0">
|
||||
<FocusContext value={focusKey}>
|
||||
<div className="px-3 py-2" ref={mainAreaRef}>
|
||||
<HeaderUI />
|
||||
<Details mainAreaRef={mainAreaRef} game={data} />
|
||||
</div>
|
||||
<div className="divider"><Image className="size-16" />Screenshots</div>
|
||||
<Screenshots screenshots={data.merged_screenshots} />
|
||||
<footer className="absolute left-0 bottom-0 w-full p-2 flex items-center justify-between z-10">
|
||||
<div className="flex gap-2 text-sm">
|
||||
</div>
|
||||
<Shortcuts />
|
||||
</footer>
|
||||
</FocusContext>
|
||||
</div>
|
||||
</AnimatedBackground>
|
||||
);
|
||||
}
|
||||
|
||||
function Details (data: { mainAreaRef: RefObject<HTMLDivElement | null>, game: DetailedRomSchema; })
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: 'main-details', onFocus: () =>
|
||||
{
|
||||
data.mainAreaRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
},
|
||||
preferredChildFocusKey: "play-btn",
|
||||
saveLastFocusedChild: false
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const platformCoverImg = `${RPC_URL(__HOST__)}/api/romm/assets/platforms/${data.game.platform_slug}.svg`;
|
||||
const gameCoverImg = `${RPC_URL(__HOST__)}/api/romm${data.game.path_cover_large}`;
|
||||
useEventListener("cancel", () =>
|
||||
{
|
||||
navigate({ to: PopSource('details') ?? '/', viewTransition: { types: ['zoom-out'] } });
|
||||
});
|
||||
|
||||
return <main ref={ref} className="flex p-3 flex-col h-[75vh]">
|
||||
<FocusContext value={focusKey}>
|
||||
<section className="flex my-4 p-12 pt-4 gap-12 h-full rounded-4xl z-0">
|
||||
<div className="flex flex-col gap-6">
|
||||
<img className="h-full w-auto rounded-3xl drop-shadow-2xl drop-shadow-base-300/40 object-contain" src={gameCoverImg}></img>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col gap-6 pt-16">
|
||||
<div className="flex gap-6">
|
||||
<Detail icon={<Clock />} >{data.game.rom_user.last_played ? new Date(data.game.rom_user.last_played).toDateString() : "Never"}</Detail>
|
||||
<Detail icon={<HardDrive />} >{prettyBytes(data.game.fs_size_bytes)}</Detail>
|
||||
<Detail icon={<img className="size-6" src={platformCoverImg}></img>} >{data.game.platform_display_name}</Detail>
|
||||
</div>
|
||||
<p className="text-base-content/80 leading-relaxed grow text-wrap whitespace-break-spaces text-ellipsis overflow-hidden">
|
||||
{data.game.summary}
|
||||
</p>
|
||||
<ActionButtons game={data.game} />
|
||||
</div>
|
||||
</section>
|
||||
</FocusContext>
|
||||
</main>;
|
||||
}
|
||||
|
||||
function Screenshot (data: { url: string; index: number; setFocused?: (index: number) => void; })
|
||||
{
|
||||
const { ref, focused, focusSelf } = useFocusable({
|
||||
focusKey: `screenshot-${data.index}`,
|
||||
onFocus: () =>
|
||||
{
|
||||
(ref.current as HTMLElement).scrollIntoView({ inline: 'center', behavior: 'smooth' });
|
||||
data.setFocused?.(data.index);
|
||||
}
|
||||
});
|
||||
return <img onClick={focusSelf} ref={ref} className={twJoin("h-[60vh] rounded-3xl", classNames({
|
||||
"ring-7 ring-primary": focused,
|
||||
"cursor-pointer": !focused
|
||||
}))} src={`${RPC_URL(__HOST__)}/api/romm${data.url}`}></img>;
|
||||
}
|
||||
|
||||
function Screenshots (data: { screenshots: string[]; })
|
||||
{
|
||||
const [focusedScreenshot, setFocusedScreenshot] = useState(-1);
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: 'screenshot-list',
|
||||
onFocus: () => (ref.current as HTMLElement).scrollIntoView({ block: 'center', behavior: 'smooth' }),
|
||||
onBlur: () => setFocusedScreenshot(-1)
|
||||
});
|
||||
|
||||
return <div ref={ref} className="flex flex-col p-16 pt-2 w-full z-0">
|
||||
<FocusContext value={focusKey}>
|
||||
<div className="flex gap-6 px-16 py-2 overflow-hidden">
|
||||
{data.screenshots.map((s, i) => <Screenshot setFocused={setFocusedScreenshot} index={i} url={s} />)}
|
||||
</div>
|
||||
<div className="flex gap-2 py-6 justify-center items-center h-3">{data.screenshots.map((s, i) =>
|
||||
{
|
||||
const focused = i === focusedScreenshot;
|
||||
return <button onClick={() => setFocus(`screenshot-${i}`)} className={twMerge("cursor-pointer rounded-full size-2 bg-base-content/40 transition-all", classNames({
|
||||
"size-3 bg-base-content drop-shadow-lg drop-shadow-base-300/40": focused
|
||||
}))}></button>;
|
||||
})}</div>
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function PlayButton ()
|
||||
{
|
||||
const { focused, ref } = useFocusable({
|
||||
focusKey: "play-btn"
|
||||
});
|
||||
return (
|
||||
<div ref={ref} className="flex gap-3 items-center font-semibold">
|
||||
<button className={twMerge("bg-primary p-6 rounded-full cursor-pointer",
|
||||
"hover:bg-base-content hover:text-base-200 hover:ring-7 hover:ring-primary",
|
||||
classNames({
|
||||
"bg-base-content text-base-200 ring-7 ring-primary": focused
|
||||
}))}><Play className="size-8" /></button>
|
||||
<p className="text-4xl">Play</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
//<PlayButton />
|
||||
|
||||
function ActionButtons (data: { game: DetailedRomSchema; })
|
||||
{
|
||||
const [hoverText, setHoverText] = useState<string | undefined>(undefined);
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'actions', onBlur: () => setHoverText(undefined) });
|
||||
|
||||
return <div ref={ref} className="flex gap-4 items-center">
|
||||
<FocusContext value={focusKey}>
|
||||
<ActionButton onFocus={() => setHoverText("")} type='primary' id="play"><Play /></ActionButton>
|
||||
<div className="divider divider-horizontal m-0"></div>
|
||||
{!!data.game.merged_ra_metadata?.achievements && <ActionButton onFocus={() => setHoverText("Achievements")} type="base" id="achievements" >
|
||||
<div className="flex flex-col gap-2 items-center text-2xl">
|
||||
<div className="flex flex-row">
|
||||
<Trophy />
|
||||
{`${data.game.merged_ra_metadata.achievements.filter(a => a.type).length}/${data.game.merged_ra_metadata.achievements.length}`}
|
||||
</div>
|
||||
<progress className="progress progress-secondary w-full" value={50} max="100"></progress>
|
||||
</div>
|
||||
</ActionButton>}
|
||||
<ActionButton onFocus={() => setHoverText("Settings")} type="base" id="settings" icon={<Settings />} />
|
||||
{!!hoverText && <p className="py-2 px-4 bg-accent text-accent-content rounded-full">{hoverText}</p>}
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Shortcuts ()
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({ focusKey: "action-buttons" });
|
||||
return <div ref={ref} className="flex gap-2" style={{ viewTransitionName: 'shortcuts' }}>
|
||||
<FocusContext value={focusKey}>
|
||||
<ShortcutPrompt icon="steamdeck_button_a" label="Continue" />
|
||||
<ShortcutPrompt icon="steamdeck_button_b" label="Back" />
|
||||
<ShortcutPrompt icon="steamdeck_button_x" label="Close" />
|
||||
<ShortcutPrompt icon="steamdeck_button_y" label="Options" />
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Detail (data: { icon: JSX.Element; children?: any | any[]; })
|
||||
{
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{data.icon}
|
||||
{data.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButton (data: { id: string, icon?: JSX.Element, children?: any | any[]; className?: string; type: "primary" | 'base' | "accent"; onFocus?: () => void; })
|
||||
{
|
||||
const { ref, focused } = useFocusable({ focusKey: data.id, onFocus: data.onFocus });
|
||||
const styles = {
|
||||
primary: twMerge("bg-primary text-primary-content rounded-full size-21 hover:bg-base-content hover:text-base-300 hover:ring-7 hover:ring-primary",
|
||||
classNames({
|
||||
"bg-base-content text-base-300 ring-7 ring-primary": focused
|
||||
})),
|
||||
base: twMerge(" text-base-content border-dashed border-base-content/20 border-2", classNames({
|
||||
"bg-base-content text-base-300 ring-7 ring-primary": focused
|
||||
})),
|
||||
accent: twMerge("bg-primary text-primary-content ", classNames({
|
||||
"bg-base-content text-base-300 ring-7 ring-primary": focused
|
||||
}))
|
||||
};
|
||||
return (
|
||||
<button ref={ref} className={twMerge("header-icon flex flex-col gap-2 px-5 py-4 rounded-3xl text-2xl justify-center items-center cursor-pointer",
|
||||
"hover:ring-7 hover:ring-primary", styles[data.type], data.className)}>
|
||||
{data.icon}
|
||||
{data.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
490
src/mainview/routes/game/$source.$id.tsx
Normal file
490
src/mainview/routes/game/$source.$id.tsx
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { FrontEndGameTypeDetailed, GameInstallProgress, GameStatusType, RPC_URL } from "@shared/constants";
|
||||
import { twJoin, twMerge } from "tailwind-merge";
|
||||
import { JSX, RefObject, useEffect, useRef, useState } from "react";
|
||||
import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
import { Clock, CloudDownload, Download, Folder, HardDrive, Image, PackageOpen, Play, Settings, Store, Trash, TriangleAlert, Trophy } from "lucide-react";
|
||||
import { HeaderUI } from "../../components/Header";
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { useEventListener } from "usehooks-ts";
|
||||
import { PopSource, SaveSource, useFocusEventListener } from "../../scripts/spatialNavigation";
|
||||
import { AnimatedBackground } from "../../components/AnimatedBackground";
|
||||
import { rommApi } from "../../scripts/clientApi";
|
||||
import toast from "react-hot-toast";
|
||||
import { queryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Router } from "../..";
|
||||
import { ContextDialog, ContextList, DialogEntry } from "../../components/ContextDialog";
|
||||
import Shortcuts from "../../components/Shortcuts";
|
||||
|
||||
const placeholderText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam eleifend ante magna, id euismod quam tempus sit amet. Maecenas sem lectus, euismod imperdiet volutpat ac, posuere in turpis. Vestibulum commodo lacinia lectus sit amet ultricies. Integer euismod consequat elit, sit amet dapibus libero fermentum nec. Aliquam accumsan placerat dui a maximus. Nunc lectus urna, scelerisque a magna non, imperdiet lobortis turpis. Aliquam magna dui, porttitor in nisl vitae, pretium fringilla sem. ";
|
||||
|
||||
const gameQuery = (source: string, id: number) => queryOptions({
|
||||
queryKey: ['game', source, id],
|
||||
queryFn: async () =>
|
||||
{
|
||||
const { data, error } = await rommApi.api.romm.game({ source })({ id }).get();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/game/$source/$id")({
|
||||
loader: ({ params, context }) =>
|
||||
{
|
||||
context.queryClient.prefetchQuery(gameQuery(params.source, Number(params.id)));
|
||||
},
|
||||
component: GameDetailsUI,
|
||||
pendingComponent: GameDetailsUIPending,
|
||||
});
|
||||
|
||||
function GameDetailsUIPending ()
|
||||
{
|
||||
return <AnimatedBackground>
|
||||
<div className="flex flex-col p-2 px-3 w-full h-full">
|
||||
<HeaderUI />
|
||||
<div className="flex flex-col justify-center items-center grow">
|
||||
<span className="loading loading-dots loading-xl"></span>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedBackground>;
|
||||
}
|
||||
|
||||
export function GameDetailsUI ()
|
||||
{
|
||||
const { source, id } = Route.useParams();
|
||||
const { data, isSuccess } = useQuery(gameQuery(source, Number(id)));
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details" });
|
||||
const backgroundImage = data?.path_cover ? `${RPC_URL(__HOST__)}${data?.path_cover}` : undefined;
|
||||
const mainAreaRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEventListener("cancel", (e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
HandleGoBack();
|
||||
}, ref);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (isSuccess)
|
||||
{
|
||||
focusSelf();
|
||||
}
|
||||
|
||||
}, [isSuccess]);
|
||||
|
||||
return (
|
||||
<AnimatedBackground ref={ref} backgroundKey="game-details" backgroundUrl={backgroundImage}>
|
||||
<div className="z-0 overflow-y-scroll">
|
||||
<FocusContext value={focusKey}>
|
||||
<div className="px-3 py-2" ref={mainAreaRef}>
|
||||
<HeaderUI />
|
||||
<Details mainAreaRef={mainAreaRef} game={data} />
|
||||
</div>
|
||||
<div className="divider"><div className="flex items-center gap-3 opacity-60"><Image className="size-6" />Screenshots</div></div>
|
||||
{!!data && <Screenshots screenshots={data.paths_screenshots} />}
|
||||
<footer className="absolute left-0 bottom-0 w-full p-2 flex items-center justify-between z-10">
|
||||
<div className="flex gap-2 text-sm">
|
||||
</div>
|
||||
<Shortcuts shortcuts={[{ icon: 'steamdeck_button_a', label: "Play" }]} />
|
||||
</footer>
|
||||
</FocusContext>
|
||||
</div>
|
||||
</AnimatedBackground>
|
||||
);
|
||||
}
|
||||
|
||||
function HandleGoBack ()
|
||||
{
|
||||
Router.navigate({ to: PopSource('details') ?? '/', viewTransition: { types: ['zoom-out'] } });
|
||||
}
|
||||
|
||||
function Details (data: { mainAreaRef: RefObject<HTMLDivElement | null>, game?: FrontEndGameTypeDetailed; })
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: 'main-details', onFocus: () =>
|
||||
{
|
||||
data.mainAreaRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
},
|
||||
preferredChildFocusKey: "play-btn",
|
||||
saveLastFocusedChild: false
|
||||
});
|
||||
|
||||
const platformCoverImg = `${RPC_URL(__HOST__)}${data.game?.path_platform_cover}`;
|
||||
const gameCoverImg = data.game?.path_cover ? `${RPC_URL(__HOST__)}${data.game?.path_cover}` : undefined;
|
||||
|
||||
let fileSizeIcon: JSX.Element | undefined;
|
||||
if (!data.game)
|
||||
{
|
||||
fileSizeIcon = <span className="loading loading-spinner loading-lg"></span>;
|
||||
} else if (data.game.missing)
|
||||
{
|
||||
fileSizeIcon = <TriangleAlert />;
|
||||
} else if (data.game.local)
|
||||
{
|
||||
fileSizeIcon = <HardDrive />;
|
||||
} else
|
||||
{
|
||||
fileSizeIcon = <CloudDownload />;
|
||||
}
|
||||
|
||||
return <main ref={ref} className="flex p-3 flex-col h-[75vh]">
|
||||
<FocusContext value={focusKey}>
|
||||
<section className="flex my-4 p-12 pt-4 gap-12 h-full rounded-4xl z-0">
|
||||
<div className="flex gap-6 overflow-hidden bg-base-300 justify-end h-full rounded-3xl aspect-3/4">
|
||||
{gameCoverImg ?
|
||||
<img className="drop-shadow-2xl drop-shadow-base-300/40 h-full" src={gameCoverImg}></img> :
|
||||
<div className="skeleton w-full h-full"></div>
|
||||
}
|
||||
</div>
|
||||
<div className="flex-2 flex flex-col gap-6 pt-16">
|
||||
<div className="flex gap-6">
|
||||
<Detail icon={<Clock />} >{data.game?.last_played ? new Date(data.game.last_played).toDateString() : "Never"}</Detail>
|
||||
{!!data.game && (data.game.fs_size_bytes !== null || data.game.missing) &&
|
||||
<div className={classNames({ "text-error": data.game.missing })}>
|
||||
<div className="tooltip" data-tip={data.game.path_fs}>
|
||||
<Detail icon={fileSizeIcon} >{data.game.missing ? 'Missing' : prettyBytes(data.game.fs_size_bytes!)}</Detail>
|
||||
</div>
|
||||
</div>}
|
||||
<Detail icon={<img className="size-6" src={platformCoverImg}></img>} >{data.game?.platform_display_name ?? <div className="skeleton h-4 w-32"></div>}</Detail>
|
||||
<Detail icon={
|
||||
<Store />
|
||||
} >
|
||||
{data.game?.source ?? data.game?.id.source}
|
||||
{data.game?.local && <small className="text-base-content/60 font-semibold">local</small>}</Detail>
|
||||
</div>
|
||||
<p className="text-base-content/80 leading-relaxed grow text-wrap whitespace-break-spaces text-ellipsis overflow-hidden">
|
||||
{data.game?.summary ?? <div className="flex flex-col gap-4 w-full">
|
||||
<div className="skeleton h-4 w-[30%]"></div>
|
||||
<div className="skeleton h-4 w-[80%]"></div>
|
||||
<div className="skeleton h-4 w-full"></div>
|
||||
<div className="skeleton h-4 w-[60%]"></div>
|
||||
<div className="skeleton h-4 w-full"></div>
|
||||
<div className="skeleton h-4 w-[80%]"></div>
|
||||
</div>}
|
||||
</p>
|
||||
{!!data.game && <ActionButtons key="actions" game={data.game} />}
|
||||
</div>
|
||||
</section>
|
||||
</FocusContext>
|
||||
</main>;
|
||||
}
|
||||
|
||||
function Screenshot (data: { path: string; index: number; setFocused?: (index: number) => void; })
|
||||
{
|
||||
const { ref, focused, focusSelf } = useFocusable({
|
||||
focusKey: `screenshot-${data.index}`,
|
||||
onFocus: () =>
|
||||
{
|
||||
(ref.current as HTMLElement).scrollIntoView({ inline: 'center', behavior: 'smooth' });
|
||||
data.setFocused?.(data.index);
|
||||
}
|
||||
}); 4096;
|
||||
return <img className={twJoin("h-[60vh] rounded-3xl", classNames({
|
||||
"ring-7 ring-primary": focused,
|
||||
"cursor-pointer": !focused
|
||||
}))} onClick={focusSelf} ref={ref} src={`${RPC_URL(__HOST__)}${data.path}`} loading="lazy" />;
|
||||
}
|
||||
|
||||
function Screenshots (data: { screenshots: string[]; })
|
||||
{
|
||||
const scrollRef = useRef(null);
|
||||
const [focusedScreenshot, setFocusedScreenshot] = useState(-1);
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: 'screenshot-list',
|
||||
onFocus: () => (ref.current as HTMLElement).scrollIntoView({ block: 'center', behavior: 'smooth' }),
|
||||
onBlur: () => setFocusedScreenshot(-1)
|
||||
});
|
||||
|
||||
return <div ref={ref} className="flex flex-col p-16 pt-2 w-full z-0">
|
||||
<FocusContext value={focusKey}>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex gap-6 px-16 py-2 overflow-hidden justify-center-safe"
|
||||
>
|
||||
{data.screenshots.map((s, i) => <Screenshot key={s} setFocused={setFocusedScreenshot} index={i} path={s} />)}
|
||||
</div>
|
||||
<div className="flex gap-2 py-6 justify-center items-center h-3">{data.screenshots.map((s, i) =>
|
||||
{
|
||||
const focused = i === focusedScreenshot;
|
||||
return <button key={i} onClick={() => setFocus(`screenshot-${i}`)} className={twMerge("cursor-pointer rounded-full size-2 bg-base-content/40 transition-all", classNames({
|
||||
"size-3 bg-base-content drop-shadow-lg drop-shadow-base-300/40": focused
|
||||
}))}></button>;
|
||||
})}</div>
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function AchievementsInfo (data: { game: FrontEndGameTypeDetailed; })
|
||||
{
|
||||
if (!data.game.achievements)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return <ActionButton key="achievements" square tooltip="Achievements" type="base" id="achievements" >
|
||||
<div className="flex flex-col gap-2 items-center text-2xl">
|
||||
<div className="flex flex-row">
|
||||
<Trophy />
|
||||
{`${data.game.achievements.unlocked}/${data.game.achievements.total}`}
|
||||
</div>
|
||||
<progress className="progress progress-secondary w-full" value={50} max="100"></progress>
|
||||
</div>
|
||||
</ActionButton>;
|
||||
}
|
||||
|
||||
function MainActions (data: { game: FrontEndGameTypeDetailed; })
|
||||
{
|
||||
const { source, id } = Route.useParams();
|
||||
const installMutation = useMutation({
|
||||
mutationKey: ['install'],
|
||||
mutationFn: async () =>
|
||||
{
|
||||
const { error } = await rommApi.api.romm.game({ source: data.game.id.source })({ id: data.game.id.id }).install.post();
|
||||
if (error) throw error;
|
||||
}
|
||||
});
|
||||
const playMutation = useMutation({
|
||||
mutationKey: ['play'],
|
||||
mutationFn: async () =>
|
||||
{
|
||||
const { error } = await rommApi.api.romm.game({ source: data.game.id.source })({ id: data.game.id.id }).play.post();
|
||||
if (error) throw error;
|
||||
}
|
||||
});
|
||||
const [progress, setProgress] = useState<number | undefined>(undefined);
|
||||
const [status, setStatus] = useState<GameStatusType | undefined>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const [details, setDetails] = useState<string | undefined>(undefined);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const es = new EventSource(`${RPC_URL(__HOST__)}/api/romm/status/${data.game.id.source}/${data.game.id.id}`);
|
||||
|
||||
es.onmessage = ({ data }) =>
|
||||
{
|
||||
const stats = JSON.parse(data) as GameInstallProgress;
|
||||
setProgress(stats.progress);
|
||||
setStatus(stats.status);
|
||||
setDetails(stats.details);
|
||||
setError(stats.error);
|
||||
};
|
||||
|
||||
es.addEventListener('refresh', () =>
|
||||
{
|
||||
queryClient.invalidateQueries({ queryKey: ['game', data.game.id] });
|
||||
location.reload();
|
||||
});
|
||||
|
||||
es.onerror = (event) =>
|
||||
{
|
||||
const error = (event as any).data?.error;
|
||||
if (error)
|
||||
{
|
||||
toast.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return () => es.close();
|
||||
}, [data.game.id]);
|
||||
|
||||
let progressIcon: JSX.Element | undefined = undefined;
|
||||
switch (status)
|
||||
{
|
||||
case 'download':
|
||||
progressIcon = <Download />;
|
||||
break;
|
||||
case 'extract':
|
||||
progressIcon = <PackageOpen />;
|
||||
break;
|
||||
}
|
||||
|
||||
let mainButton: JSX.Element | undefined = undefined;
|
||||
if (status === 'installed')
|
||||
{
|
||||
mainButton = <ActionButton onAction={() =>
|
||||
{
|
||||
playMutation.mutate();
|
||||
SaveSource('launch');
|
||||
Router.navigate({ to: '/launcher/$source/$id', viewTransition: { types: ['zoom-in'] }, params: { source, id } });
|
||||
}} tooltip={details} key="primary" type='primary' id="mainAction"><Play /></ActionButton>;
|
||||
}
|
||||
else if (error)
|
||||
{
|
||||
mainButton = <ActionButton
|
||||
key="error"
|
||||
tooltip={error}
|
||||
tooltip_type="error"
|
||||
type='error'
|
||||
onAction={() =>
|
||||
{
|
||||
if (status === 'missing-emulator')
|
||||
{
|
||||
SaveSource('settings');
|
||||
Router.navigate({ to: '/settings/directories', viewTransition: { types: ['zoom-in'] } });
|
||||
}
|
||||
}}
|
||||
id="mainAction">
|
||||
<TriangleAlert />
|
||||
</ActionButton>;
|
||||
}
|
||||
else
|
||||
{
|
||||
mainButton = <ActionButton
|
||||
key={status ?? 'unknown'}
|
||||
disabled={installMutation.isPending}
|
||||
onAction={() =>
|
||||
{
|
||||
if (status === 'install')
|
||||
{
|
||||
installMutation.mutate();
|
||||
}
|
||||
}}
|
||||
tooltip={details ?? status}
|
||||
type='primary'
|
||||
id="mainAction">
|
||||
{status === 'install' ? <Download /> : <span className="loading loading-spinner loading-lg"></span>}
|
||||
</ActionButton>;
|
||||
}
|
||||
|
||||
return <div className="flex gap-2">
|
||||
{mainButton}
|
||||
<div className="divider divider-horizontal m-0"></div>
|
||||
{progress !== null && !!progressIcon && <ActionButton key="progress" square tooltip={details} type="base" id="progress" >
|
||||
<div key={`install-${status}`} data-tooltip={details ?? status} className="flex flex-col gap-2 w-16 items-center text-2xl">
|
||||
<div className="flex flex-row">
|
||||
{progressIcon}
|
||||
</div>
|
||||
<progress className="progress progress-secondary w-full" value={progress} max="100"></progress>
|
||||
</div>
|
||||
</ActionButton>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ActionButtons (data: { game: FrontEndGameTypeDetailed; })
|
||||
{
|
||||
const [hoverText, setHoverText] = useState<string | undefined>(undefined);
|
||||
const [hoverTextType, setHoverTextType] = useState<string>('accent');
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'actions', onBlur: () => setHoverText(undefined) });
|
||||
const [open, setOpen] = useState(false);
|
||||
const deleteMutation = useMutation({
|
||||
mutationKey: ['delete', data.game.id],
|
||||
mutationFn: () => rommApi.api.romm.game({ source: data.game.id.source })({ id: data.game.id.id }).delete(),
|
||||
onSuccess: () =>
|
||||
{
|
||||
location.reload();
|
||||
console.log("Deleted");
|
||||
}
|
||||
});
|
||||
|
||||
const contextOptions: DialogEntry[] = [];
|
||||
if (data.game.local)
|
||||
{
|
||||
contextOptions.push({
|
||||
id: 'delete',
|
||||
action: () =>
|
||||
{
|
||||
deleteMutation.mutate();
|
||||
},
|
||||
icon: <Trash />,
|
||||
content: "Delete",
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
const handleTooltipSet = (e: HTMLElement) =>
|
||||
{
|
||||
const dataTooltip = e.getAttribute('data-tooltip');
|
||||
setHoverText(dataTooltip ?? undefined);
|
||||
setHoverTextType(e.getAttribute('data-tooltip_type') ?? 'accent');
|
||||
};
|
||||
|
||||
useFocusEventListener('focuschanged', (e) =>
|
||||
{
|
||||
if (e.target instanceof HTMLElement)
|
||||
{
|
||||
handleTooltipSet(e.target);
|
||||
}
|
||||
|
||||
}, ref);
|
||||
|
||||
const tooltipStyles = {
|
||||
base: 'bg-base-100 text-base-content',
|
||||
accent: 'bg-accent text-accent-content',
|
||||
error: 'bg-error text-error-content'
|
||||
};
|
||||
|
||||
return <div ref={ref} className="flex overflow-hidden p-2 gap-4 h-32 items-center">
|
||||
<FocusContext value={focusKey}>
|
||||
<MainActions game={data.game} />
|
||||
<AchievementsInfo game={data.game} />
|
||||
<ActionButton tooltip="Settings" onAction={() => setOpen(true)} type="base" id="settings" icon={<Settings />} >
|
||||
|
||||
</ActionButton >
|
||||
<ContextDialog id="settings-context" open={open} close={() =>
|
||||
{
|
||||
setOpen(false);
|
||||
setFocus("settings");
|
||||
}}>
|
||||
<ContextList options={contextOptions} />
|
||||
</ContextDialog>
|
||||
{!!hoverText && <p className={twMerge("flex py-2 px-4 rounded-4xl text-wrap wrap-anywhere", (tooltipStyles as any)[hoverTextType])}>{hoverText}</p>}
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Detail (data: { icon: JSX.Element; children?: any | any[]; })
|
||||
{
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{data.icon}
|
||||
{data.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButton (data: {
|
||||
id: string,
|
||||
icon?: JSX.Element,
|
||||
children?: any | any[];
|
||||
className?: string;
|
||||
type: "primary" | 'base' | "accent" | 'error';
|
||||
square?: boolean,
|
||||
onFocus?: () => void;
|
||||
tooltip?: string,
|
||||
tooltip_type?: 'accent' | 'error';
|
||||
onAction?: () => void;
|
||||
disabled?: boolean;
|
||||
})
|
||||
{
|
||||
const { ref, focused } = useFocusable({ focusKey: data.id, onFocus: data.onFocus, onEnterPress: data.onAction, focusable: data.disabled !== true });
|
||||
const styles = {
|
||||
primary: twMerge("bg-primary text-primary-content",
|
||||
classNames({
|
||||
"bg-base-content text-base-300 ring-7 ring-primary": focused
|
||||
})),
|
||||
base: twMerge(" text-base-content border-dashed border-base-content/20 border-2", classNames({
|
||||
"bg-base-content text-base-300 ring-7 ring-primary": focused
|
||||
})),
|
||||
accent: twMerge("bg-primary text-primary-content ", classNames({
|
||||
"bg-base-content text-base-300 ring-7 ring-primary": focused
|
||||
})),
|
||||
error: twMerge("bg-error text-error-content ", classNames({
|
||||
"bg-error text-error-content ring-7 ring-primary": focused
|
||||
})),
|
||||
};
|
||||
return (
|
||||
<button
|
||||
disabled={data.disabled}
|
||||
ref={ref}
|
||||
onClick={data.onAction}
|
||||
data-tooltip={data.tooltip}
|
||||
data-tooltip_type={data.tooltip_type}
|
||||
className={twMerge("header-icon flex flex-col gap-2 px-5 py-4 rounded-3xl text-2xl justify-center items-center cursor-pointer disabled:opacity-30",
|
||||
"hover:ring-7 hover:ring-primary", styles[data.type], classNames({ "rounded-full size-21 hover:bg-base-content hover:text-base-300 hover:ring-7 hover:ring-primary": !data.square }), data.className)}>
|
||||
{data.icon}
|
||||
{data.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { JSX, Suspense, useContext } from "react";
|
||||
import { JSX, Suspense, useContext, useState } from "react";
|
||||
import
|
||||
{
|
||||
Gamepad2,
|
||||
|
|
@ -16,7 +16,7 @@ import
|
|||
useLocation,
|
||||
useNavigate,
|
||||
} from "@tanstack/react-router";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import
|
||||
{
|
||||
FocusContext,
|
||||
|
|
@ -24,13 +24,12 @@ import
|
|||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
import { DefaultRommStaleTime, RPC_URL } from "../../shared/constants";
|
||||
import { useLocalStorage, useSessionStorage } from "usehooks-ts";
|
||||
import { useEventListener, useLocalStorage } from "usehooks-ts";
|
||||
import
|
||||
{
|
||||
getCollectionsApiCollectionsGetOptions,
|
||||
getPlatformsApiPlatformsGetOptions,
|
||||
} from "../../clients/romm/@tanstack/react-query.gen";
|
||||
import { CardList } from "../components/CardList";
|
||||
import { CardList, GameMetaExtra } from "../components/CardList";
|
||||
import { HeaderUI } from "../components/Header";
|
||||
import { FilterUI } from "../components/Filters";
|
||||
import { AnimatedBackground, AnimatedBackgroundContext } from "../components/AnimatedBackground";
|
||||
|
|
@ -42,6 +41,8 @@ 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 { systemApi } from "../scripts/clientApi";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: ConsoleHomeUI,
|
||||
|
|
@ -60,64 +61,7 @@ const filters = {
|
|||
},
|
||||
};
|
||||
|
||||
function PlatformList (data: { id: string, setBackground: (url: string) => void; })
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const { data: platforms } = useSuspenseQuery({
|
||||
...getPlatformsApiPlatformsGetOptions(),
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: DefaultRommStaleTime,
|
||||
});
|
||||
|
||||
return (
|
||||
<CardList
|
||||
type="platform"
|
||||
id={data.id}
|
||||
games={platforms.sort((a, b) => Date.parse(a.updated_at) - Date.parse(b.updated_at))
|
||||
.map((g) => ({
|
||||
id: g.id,
|
||||
focusKey: g.slug,
|
||||
title: g.display_name,
|
||||
subtitle: g.family_name ?? "",
|
||||
previewUrl: g.url_logo ?? "",
|
||||
badge: (
|
||||
<span className="text-lg font-bold badge bg-base-100 shadow-md shadow-base-300 h-8 rounded-full mr-2">
|
||||
{g.rom_count}
|
||||
</span>
|
||||
),
|
||||
preview: (
|
||||
<div
|
||||
className="flex h-60 p-6 bg-base-100 justify-center items-center"
|
||||
style={{
|
||||
background: `linear-gradient(
|
||||
color-mix(in srgb, var(--color-base-content) 60%, transparent),
|
||||
color-mix(in srgb, var(--color-base-300) 60%, transparent)
|
||||
), url(https://picsum.photos/id/${10 + g.id}/300/300.webp?blur=10) center / cover`,
|
||||
|
||||
backgroundBlendMode: "screen",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`${RPC_URL(__HOST__)}/api/romm/assets/platforms/${g.slug.toLocaleLowerCase()}.svg`}
|
||||
></img>
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
onSelectGame={(id) =>
|
||||
{
|
||||
navigate({ to: `/platform/${id}`, viewTransition: { types: ['zoom-in'] } });
|
||||
}}
|
||||
onGameFocus={(id) =>
|
||||
{
|
||||
data.setBackground(
|
||||
`https://picsum.photos/id/${10 + (id ?? 0)}/1920/1080.webp`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CollectionList (data: { id: string, setBackground: (url: string) => void; })
|
||||
function CollectionList (data: { id: string, setBackground: (url: string) => void; className?: string; })
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const { data: collections } = useSuspenseQuery({
|
||||
|
|
@ -130,19 +74,20 @@ function CollectionList (data: { id: string, setBackground: (url: string) => voi
|
|||
<CardList
|
||||
type="collection"
|
||||
id={data.id}
|
||||
className={data.className}
|
||||
games={collections.sort((a, b) => Date.parse(a.updated_at) - Date.parse(b.updated_at))
|
||||
.map((g) => ({
|
||||
id: g.id,
|
||||
id: String(g.id),
|
||||
title: g.name,
|
||||
focusKey: `collection-${g.id}`,
|
||||
subtitle: g.user__username,
|
||||
previewUrl: `${RPC_URL(__HOST__)}/api/romm/${g.path_covers_large[0]}`,
|
||||
badge: (
|
||||
badges: [
|
||||
<span className="text-lg font-bold badge bg-base-100 shadow-md shadow-base-300 h-8 rounded-full mr-2">
|
||||
{g.rom_count}
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
],
|
||||
} satisfies GameMetaExtra))}
|
||||
onSelectGame={(id) =>
|
||||
{
|
||||
navigate({ to: `/collection/${id}`, viewTransition: { types: ['zoom-in'] } });
|
||||
|
|
@ -171,21 +116,45 @@ function HomeList (data: {
|
|||
})
|
||||
{
|
||||
const bg = useContext(AnimatedBackgroundContext);
|
||||
|
||||
const { ref, focused, focusKey, focusSelf } = useFocusable({
|
||||
focusKey: "home-list",
|
||||
preferredChildFocusKey: `${data.selectedFilter}-list`
|
||||
});
|
||||
|
||||
const lists = {
|
||||
consoles: <PlatformList id={"consoles-list"} setBackground={bg.setBackground} />,
|
||||
games: <GameList id="games-list" setBackground={bg.setBackground} />,
|
||||
collections: <CollectionList id={"collections-list"} setBackground={bg.setBackground} />,
|
||||
consoles: <PlatformsList className="animate-slide-up" key="consoles-list" id="consoles-list" setBackground={bg.setBackground} />,
|
||||
games: <GameList className="animate-slide-up" key="games-list" id="games-list" setBackground={bg.setBackground} />,
|
||||
collections: <CollectionList className="animate-slide-up" key="collections-list" id="collections-list" setBackground={bg.setBackground} />,
|
||||
};
|
||||
|
||||
useEventListener('wheel', e =>
|
||||
{
|
||||
const deltaY = e.deltaY;
|
||||
const deltaYSign = Math.sign(e.deltaY);
|
||||
|
||||
if (deltaYSign == -1)
|
||||
{
|
||||
(ref.current as HTMLElement)?.scrollBy({
|
||||
top: 0,
|
||||
left: deltaY,
|
||||
behavior: 'auto'
|
||||
});
|
||||
|
||||
} else
|
||||
{
|
||||
(ref.current as HTMLElement)?.scrollBy({
|
||||
top: 0,
|
||||
left: deltaY,
|
||||
behavior: 'auto'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<FocusContext value={focusKey}>
|
||||
<div ref={ref} className="flex overflow-x-scroll no-scrollbar pb-3 mb-1 justify-center-safe">
|
||||
<div ref={ref} className="flex overflow-x-scroll no-scrollbar pb-3 mb-1 justify-center-safe" style={{
|
||||
mask: `linear-gradient(to right, rgba(0,0,0,0.8) 0%, black 10%, black 90%, rgba(0,0,0,0.8) 100%)`
|
||||
}}>
|
||||
<div className="flex px-16">
|
||||
<ErrorBoundary fallback={<HomeListError focused={focused} />}>
|
||||
<Suspense key={data.selectedFilter} fallback={<LoadingCardList placeholderCount={8} />}>
|
||||
|
|
@ -206,6 +175,14 @@ export default function ConsoleHomeUI ()
|
|||
keyof typeof filters
|
||||
>("home-filter-selected", "games");
|
||||
|
||||
const closeMutation = useMutation({
|
||||
mutationKey: ['close'], mutationFn: async () =>
|
||||
{
|
||||
const { error } = await systemApi.api.system.exit.post();
|
||||
if (error) throw error;
|
||||
}
|
||||
});
|
||||
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
forceFocus: true,
|
||||
autoRestoreFocus: false,
|
||||
|
|
@ -220,7 +197,7 @@ export default function ConsoleHomeUI ()
|
|||
<div className="px-3 w-full pt-2">
|
||||
<HeaderUI buttons={[
|
||||
{ id: "search", icon: <Search /> },
|
||||
{ id: "power-button", icon: <Power />, external: true }
|
||||
{ id: "power-button", icon: <Power />, external: true, action: () => closeMutation.mutate() }
|
||||
]} />
|
||||
</div>
|
||||
<div className="flex w-full flex-col grow justify-evenly">
|
||||
|
|
@ -243,7 +220,7 @@ export default function ConsoleHomeUI ()
|
|||
<footer className="px-2 pb-2 flex items-center justify-between">
|
||||
<div className="flex gap-2 text-sm">
|
||||
</div>
|
||||
<Shortcuts />
|
||||
<Shortcuts shortcuts={[{ icon: 'steamdeck_button_a', label: 'Select' }]} />
|
||||
</footer>
|
||||
</FocusContext.Provider>
|
||||
</AnimatedBackground>
|
||||
|
|
@ -282,7 +259,7 @@ function MainMenu (data: {})
|
|||
<CircleIcon
|
||||
action={() =>
|
||||
{
|
||||
SaveSource('settings', location.pathname);
|
||||
SaveSource('settings');
|
||||
navigate({ to: "/settings/accounts", viewTransition: { types: ['zoom-in'] } });
|
||||
}}
|
||||
icon={<Settings />}
|
||||
|
|
@ -319,7 +296,7 @@ function CircleIcon (data: {
|
|||
'sm:w-14 sm:h-14',
|
||||
typeClasses[data.type ?? "none"], classNames(
|
||||
{
|
||||
"ring-7 ring-primary drop-shadow-2xl": focused,
|
||||
"focus ring-7 ring-primary drop-shadow-2xl animate-scale": focused,
|
||||
"hover:ring-7 hover:ring-primary": true,
|
||||
})
|
||||
)}
|
||||
|
|
|
|||
58
src/mainview/routes/launcher.$source.$id.tsx
Normal file
58
src/mainview/routes/launcher.$source.$id.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { GameInstallProgress, RPC_URL } from '@/shared/constants';
|
||||
import DotsLoading from '../components/backgrounds/dots';
|
||||
import { useEventListener } from 'usehooks-ts';
|
||||
import { Router } from '..';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { rommApi } from '../scripts/clientApi';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export const Route = createFileRoute('/launcher/$source/$id')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
function HandleGoBack ()
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id } });
|
||||
}
|
||||
|
||||
const { source, id } = Route.useParams();
|
||||
const { data } = useQuery({ queryKey: ['romm', 'game'], queryFn: () => rommApi.api.romm.game({ source })({ id }).get() });
|
||||
|
||||
useEventListener("cancel", (e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
HandleGoBack();
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const es = new EventSource(`${RPC_URL(__HOST__)}/api/romm/status/${source}/${id}`);
|
||||
|
||||
es.onmessage = ({ data }) =>
|
||||
{
|
||||
const stats = JSON.parse(data) as GameInstallProgress;
|
||||
if (stats.status !== 'playing')
|
||||
{
|
||||
HandleGoBack();
|
||||
}
|
||||
};
|
||||
|
||||
es.addEventListener('refresh', HandleGoBack);
|
||||
|
||||
es.onerror = HandleGoBack;
|
||||
|
||||
return () => es.close();
|
||||
}, []);
|
||||
|
||||
|
||||
return <AnimatedBackground backgroundKey='game-details'>
|
||||
<div className='flex shadow-2xs shadow-black flex-col absolute w-screen h-screen overflow-hidden justify-center items-center gap-4'>
|
||||
<DotsLoading />
|
||||
<h1 className='font-semibold'>Launching {data?.data?.name} ...</h1>
|
||||
</div>
|
||||
</AnimatedBackground>;
|
||||
}
|
||||
54
src/mainview/routes/platform.$source.$id.tsx
Normal file
54
src/mainview/routes/platform.$source.$id.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useEventListener, useSessionStorage } from "usehooks-ts";
|
||||
import { CollectionsDetail } from "../components/CollectionsDetail";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { DefaultRommStaleTime, RPC_URL } from "../../shared/constants";
|
||||
import { Suspense } from "react";
|
||||
import { rommApi } from "../scripts/clientApi";
|
||||
|
||||
export const Route = createFileRoute("/platform/$source/$id")({
|
||||
component: RouteComponent
|
||||
});
|
||||
|
||||
function PlatformTitle ()
|
||||
{
|
||||
const { source, id } = Route.useParams();
|
||||
const { data: platform } = useSuspenseQuery({
|
||||
queryKey: ['platform', source, id], queryFn: async () =>
|
||||
{
|
||||
const { data, error } = await rommApi.api.romm.platforms({ source })({ id }).get();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}, staleTime: DefaultRommStaleTime
|
||||
});
|
||||
|
||||
return <div className="flex flex-col gap-2 pl-2 text-2xl font-semibold text-base-content justify-center drop-shadow">
|
||||
|
||||
<div className="divider mb-6 mt-0">
|
||||
<img className="size-14 rounded-full p-2" src={`${RPC_URL(__HOST__)}/api/romm/assets/platforms/${platform.slug.toLocaleLowerCase()}.svg`} ></img>
|
||||
{platform.display_name}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { id } = Route.useParams();
|
||||
|
||||
const [, setBackground] = useSessionStorage<string | undefined>(
|
||||
"home-background",
|
||||
undefined,
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
useEventListener("cancel", () => navigate({ to: "/", viewTransition: { types: ['zoom-out'] } }));
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<CollectionsDetail
|
||||
title={<Suspense><PlatformTitle /></Suspense>}
|
||||
setBackground={setBackground}
|
||||
filters={{ platformId: Number(id) }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import
|
||||
{
|
||||
getPlatformApiPlatformsIdGetOptions,
|
||||
getRomsApiRomsGetOptions,
|
||||
} from "../../../clients/romm/@tanstack/react-query.gen";
|
||||
import { useEventListener, useSessionStorage } from "usehooks-ts";
|
||||
import { CollectionsDetail } from "../../components/CollectionsDetail";
|
||||
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { DefaultRommStaleTime, RPC_PORT, RPC_URL } from "../../../shared/constants";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export const Route = createFileRoute("/platform/$id")({
|
||||
component: RouteComponent
|
||||
});
|
||||
|
||||
function PlatformSlug ()
|
||||
{
|
||||
const { id } = Route.useParams();
|
||||
const { data: platform } = useSuspenseQuery({ ...getPlatformApiPlatformsIdGetOptions({ path: { id: Number(id) } }), staleTime: DefaultRommStaleTime });
|
||||
|
||||
return <div className="flex gap-2 pr-4 pl-2 text-2xl font-semibold text-base-content items-center justify-center drop-shadow drop-shadow-base-300/10 ">
|
||||
<img className="size-10 rounded-full bg-base-100 p-2" src={`${RPC_URL(__HOST__)}/api/romm/assets/platforms/${platform.slug.toLocaleLowerCase()}.svg`} ></img>
|
||||
{platform.display_name}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function PlatformTitle ()
|
||||
{
|
||||
const { id } = Route.useParams();
|
||||
const { data: platform } = useSuspenseQuery({ ...getPlatformApiPlatformsIdGetOptions({ path: { id: Number(id) } }), staleTime: DefaultRommStaleTime });
|
||||
|
||||
return <div className="flex flex-col gap-2 pl-2 text-2xl font-semibold text-base-content justify-center drop-shadow">
|
||||
|
||||
<div className="divider mb-6 mt-0">
|
||||
<img className="size-14 rounded-full p-2" src={`${RPC_URL(__HOST__)}/api/romm/assets/platforms/${platform.slug.toLocaleLowerCase()}.svg`} ></img>
|
||||
{platform.display_name}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { id } = Route.useParams();
|
||||
|
||||
const [, setBackground] = useSessionStorage<string | undefined>(
|
||||
"home-background",
|
||||
undefined,
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
useEventListener("cancel", () => navigate({ to: "/", viewTransition: { types: ['zoom-out'] } }));
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<CollectionsDetail
|
||||
title={<Suspense><PlatformTitle /></Suspense>}
|
||||
setBackground={setBackground}
|
||||
filters={{ platformIds: [Number(id)] }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
src/mainview/routes/settings/about.tsx
Normal file
60
src/mainview/routes/settings/about.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { rommApi, systemApi } from '@/mainview/scripts/clientApi';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/settings/about')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { data: systemInfo } = useQuery({ queryKey: ['system-info'], queryFn: () => systemApi.api.system.info.get() });
|
||||
return <div className="overflow-x-auto">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Agent</th>
|
||||
<td>{navigator.userAgent}</td>
|
||||
</tr>
|
||||
{/* row 2 */}
|
||||
<tr>
|
||||
<th>Platform</th>
|
||||
<td>{navigator.platform}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Resolution</th>
|
||||
<td>{screen.width}x{screen.height}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Window</th>
|
||||
<td>{window.innerWidth}x{window.innerHeight}</td>
|
||||
</tr>
|
||||
{/* row 3 */}
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<td>{systemInfo?.data?.user}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Architecture</th>
|
||||
<td>{systemInfo?.data?.arch}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>System</th>
|
||||
<td>{systemInfo?.data?.platform}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<td>{systemInfo?.data?.hostname}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Machine</th>
|
||||
<td>{systemInfo?.data?.machine}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Steam Deck</th>
|
||||
<td>{systemInfo?.data?.steamDeck ?? 'false'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -3,119 +3,31 @@ import
|
|||
FocusContext,
|
||||
useFocusable,
|
||||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { useIsMutating, useMutation, useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||
import { useIsMutating, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import classNames from "classnames";
|
||||
import { Cross, Delete, Key, Link, Lock, Save, Trash, User, X } from "lucide-react";
|
||||
import { Key, Link, Lock, Save, Trash, User, X } from "lucide-react";
|
||||
import
|
||||
{
|
||||
HTMLInputTypeAttribute,
|
||||
JSX,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { client } from "../..";
|
||||
import { RPC_URL, SettingsType } from "../../../shared/constants";
|
||||
import { RPC_URL } from "../../../shared/constants";
|
||||
import
|
||||
{
|
||||
getCurrentUserApiUsersMeGetOptions,
|
||||
statsApiStatsGetOptions,
|
||||
} from "../../../clients/romm/@tanstack/react-query.gen";
|
||||
import { UserSchema } from "../../../clients/romm";
|
||||
import toast from "react-hot-toast";
|
||||
import z from "zod";
|
||||
import { OptionSpace } from "../../components/options/OptionSpace";
|
||||
import { OptionInput } from "../../components/options/OptionInput";
|
||||
import { useSettingsForm, useSettingsFormContext } from "../../components/options/SettingsAppForm";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { rommApi, settingsApi } from "../../scripts/clientApi";
|
||||
import { Button } from "../../components/options/Button";
|
||||
|
||||
export const Route = createFileRoute("/settings/accounts")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
type KeysWithValueAssignableTo<T, Value> = {
|
||||
[K in keyof T]: Exclude<T[K], undefined> extends Value ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
function Option (data: {
|
||||
label: string;
|
||||
id: KeysWithValueAssignableTo<SettingsType, string>;
|
||||
type: HTMLInputTypeAttribute;
|
||||
placeholder?: string;
|
||||
icon?: JSX.Element;
|
||||
})
|
||||
{
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [localValue, setLocalValue] = useState<string | undefined>();
|
||||
useQuery({
|
||||
enabled: !!data.id,
|
||||
queryKey: ["setting", data.id],
|
||||
queryFn: async () =>
|
||||
{
|
||||
const value = await client.api.settings({ id: data.id! }).get().then(d => d.data?.value);
|
||||
if (!dirty)
|
||||
{
|
||||
setLocalValue(String(value));
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
const setSettingMultation = useMutation({
|
||||
mutationKey: ["setting", data.id],
|
||||
mutationFn: (value: any) =>
|
||||
client.api.settings({ id: data.id! }).post({ value }).then(d => d.status)
|
||||
});
|
||||
|
||||
const handleSave = useCallback(() =>
|
||||
{
|
||||
if (dirty)
|
||||
{
|
||||
setDirty(false);
|
||||
setSettingMultation.mutate(localValue);
|
||||
}
|
||||
}, [dirty, setDirty, localValue]);
|
||||
|
||||
return (
|
||||
<OptionSpace label={data.label}>
|
||||
<OptionInput
|
||||
icon={data.icon}
|
||||
name={data.id ?? ""}
|
||||
type={data.type}
|
||||
placeholder={data.placeholder}
|
||||
onBlur={handleSave}
|
||||
onChange={(e) =>
|
||||
{
|
||||
setLocalValue(e.currentTarget.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
value={localValue}
|
||||
/>
|
||||
</OptionSpace>
|
||||
);
|
||||
}
|
||||
|
||||
function Button (data: { children?: any, className?: string, disabled?: boolean, type: "reset" | "button" | "submit" | undefined; } & InteractParams & FocusParams)
|
||||
{
|
||||
const { ref, focused } = useFocusable({
|
||||
focusKey: data.type,
|
||||
onEnterPress: data.onAction,
|
||||
onFocus: data.onFocus,
|
||||
focusable: !data.disabled
|
||||
});
|
||||
return <button
|
||||
ref={ref}
|
||||
onClick={data.onAction}
|
||||
disabled={data.disabled}
|
||||
className={twMerge("btn rounded-full focus:bg-base-content focus:text-base-300 md:text-lg", classNames({
|
||||
"btn-accent": focused
|
||||
}, data.className))}
|
||||
type={data.type}
|
||||
>
|
||||
{data.children}
|
||||
</button>;
|
||||
}
|
||||
|
||||
function LoginControls (data: { hasPassword: boolean; })
|
||||
{
|
||||
const user = useQuery({
|
||||
|
|
@ -128,7 +40,7 @@ function LoginControls (data: { hasPassword: boolean; })
|
|||
context.state.canSubmit;
|
||||
const isMutatingRomm = useIsMutating({ mutationKey: ["romm", "auth"] }) > 0;
|
||||
const logoutMutation = useMutation({
|
||||
mutationKey: ["romm", "auth", "logout"], mutationFn: () => client.api.romm.logout.post(),
|
||||
mutationKey: ["romm", "auth", "logout"], mutationFn: () => rommApi.api.romm.logout.post(),
|
||||
onSuccess: async (d, v, r, c) =>
|
||||
{
|
||||
c.client.invalidateQueries({ queryKey: ["romm", "auth"] });
|
||||
|
|
@ -167,10 +79,9 @@ function RouteComponent ()
|
|||
preferredChildFocusKey: focus
|
||||
});
|
||||
|
||||
const { data: hasPassword } = useQuery({ queryKey: ['romm', 'auth', 'passLength'], queryFn: () => client.api.romm.login.get().then(d => d.data?.hasPassword as boolean) });
|
||||
const { data: hostname } = useQuery({ queryKey: ['romm', 'auth', 'hostname'], queryFn: () => client.api.settings({ id: 'rommAddress' }).get().then(d => d.data?.value as string) });
|
||||
const { data: username } = useQuery({ queryKey: ['romm', 'auth', 'username'], queryFn: () => client.api.settings({ id: 'rommUser' }).get().then(d => d.data?.value as string) });
|
||||
|
||||
const { data: hasPassword } = useQuery({ queryKey: ['romm', 'auth', 'passLength'], queryFn: () => rommApi.api.romm.login.get().then(d => d.data?.hasPassword as boolean) });
|
||||
const { data: hostname } = useQuery({ queryKey: ['romm', 'auth', 'hostname'], queryFn: () => settingsApi.api.settings({ id: 'rommAddress' }).get().then(d => d.data?.value as string) });
|
||||
const { data: username } = useQuery({ queryKey: ['romm', 'auth', 'username'], queryFn: () => settingsApi.api.settings({ id: 'rommUser' }).get().then(d => d.data?.value as string) });
|
||||
|
||||
const loginForm = useSettingsForm({
|
||||
defaultValues: {
|
||||
|
|
@ -210,7 +121,7 @@ function RouteComponent ()
|
|||
mutationKey: ["romm", "login"],
|
||||
mutationFn: (data: z.infer<typeof dataSchema>) =>
|
||||
{
|
||||
return client.api.romm.login.post({ username: data.username, password: data.password, host: data.hostname });
|
||||
return rommApi.api.romm.login.post({ username: data.username, password: data.password, host: data.hostname });
|
||||
},
|
||||
onSuccess: (d, v, r, c) =>
|
||||
{
|
||||
|
|
|
|||
242
src/mainview/routes/settings/directories.tsx
Normal file
242
src/mainview/routes/settings/directories.tsx
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { SettingsOption } from '../../components/options/SettingsOption';
|
||||
import { OptionSpace } from '../../components/options/OptionSpace';
|
||||
import { OptionInput } from '../../components/options/OptionInput';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { settingsApi } from '../../scripts/clientApi';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Button } from '../../components/options/Button';
|
||||
import { Check, ChevronDown, SearchAlert, 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 emulators from '@emulators';
|
||||
|
||||
export const Route = createFileRoute('/settings/directories')({
|
||||
component: RouteComponent,
|
||||
pendingComponent: EmulatorsPending,
|
||||
});
|
||||
|
||||
function EmulatorsPending ()
|
||||
{
|
||||
return <div className="flex flex-col p-2 px-3 w-full h-full">
|
||||
<div className="flex flex-col justify-center items-center grow">
|
||||
<span className="loading loading-dots loading-xl"></span>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function EmulatorListCat (data: { selected: string, set: (c: string) => void; })
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'categories' });
|
||||
return <ul className='flex gap-1' ref={ref}>
|
||||
<FocusContext value={focusKey}>
|
||||
{[..."ABCDEFGHIJKLMNOPQRSTVWXYZ"].map(c =>
|
||||
<OptionElement key={c} className={classNames('p-2 justify-center size-8 text-base-content bg-base-300 text-lg', { "ring-4 ring-primary": data.selected === c })} onFocus={() => data.set(c)} content={c} id={c} action={(ctx) => ctx.focus()} type="primary" />
|
||||
)}
|
||||
</FocusContext>
|
||||
</ul>;
|
||||
}
|
||||
|
||||
function EmulatorListType (data: { category: string, action: (e: string) => void, })
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'list-section' });
|
||||
return <div ref={ref} className='grow'>
|
||||
<FocusContext value={focusKey}>
|
||||
<ContextList className='h-[60vh]' options={Object.keys(emulators).filter(e => e.startsWith(data.category)).map(e => ({
|
||||
id: e,
|
||||
action: (ctx) =>
|
||||
{
|
||||
data.action(e);
|
||||
ctx.close();
|
||||
},
|
||||
type: 'primary',
|
||||
content: e
|
||||
} satisfies DialogEntry))} />
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function NewEmulatorPath (data: {})
|
||||
{
|
||||
const [newEmulatorTypeOpen, setNewEmulatorTypeOpen] = useState(false);
|
||||
const [newEmulatorContextCat, setNewEmulatorContextCat] = useState('A');
|
||||
const handleCloseContext = () =>
|
||||
{
|
||||
setNewEmulatorTypeOpen(false);
|
||||
setFocus('emulator');
|
||||
};
|
||||
const addOverrideMutation = useMutation({
|
||||
mutationKey: ['emulator', 'custom', 'add'],
|
||||
mutationFn: async (id: string) =>
|
||||
{
|
||||
const { data, error } = await settingsApi.api.settings.emulators.custom({ id }).put({ value: '' });
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
onSuccess: (d, v, r, ctx) => ctx.client.invalidateQueries({ queryKey: ['custom-emulators'] })
|
||||
});
|
||||
|
||||
return <OptionSpace label={"Custom Emulator Path"}>
|
||||
<Button disabled={addOverrideMutation.isPending} id='emulator' type='button' onAction={() => setNewEmulatorTypeOpen(true)} >
|
||||
Emulator
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
<ContextDialog open={newEmulatorTypeOpen} id='new-emulator-type-context' close={handleCloseContext}>
|
||||
<div className='flex flex-col'>
|
||||
<EmulatorListCat selected={newEmulatorContextCat} set={setNewEmulatorContextCat} />
|
||||
<div className="divider mb-1 mt-2"></div>
|
||||
<EmulatorListType category={newEmulatorContextCat} action={e =>
|
||||
{
|
||||
addOverrideMutation.mutate(e);
|
||||
}} />
|
||||
</div>
|
||||
</ContextDialog>
|
||||
</OptionSpace>;
|
||||
}
|
||||
|
||||
function EmulatorPath (data: { id: string; })
|
||||
{
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [localValue, setLocalValue] = useState<string | undefined>();
|
||||
const { data: remoteValue } = useQuery({
|
||||
enabled: !!data.id,
|
||||
queryKey: ["emulator", data.id],
|
||||
queryFn: async () =>
|
||||
{
|
||||
const { data: value, error } = await settingsApi.api.settings.emulators.custom({ id: data.id }).get();
|
||||
if (error) throw error;
|
||||
return value;
|
||||
},
|
||||
});
|
||||
const setSettingMutation = useMutation({
|
||||
mutationKey: ["emulator", data.id, 'set'],
|
||||
mutationFn: async (value: string) => settingsApi.api.settings.emulators.custom({ id: data.id }).put({ value }),
|
||||
onSuccess: (d, v, r, ctx) =>
|
||||
{
|
||||
ctx.client.invalidateQueries({ queryKey: ["emulator", data.id] });
|
||||
ctx.client.invalidateQueries({ queryKey: ["auto-emulators"] });
|
||||
}
|
||||
});
|
||||
const deleteMutation = useMutation({
|
||||
mutationKey: ["emulator", data.id, 'delete'],
|
||||
mutationFn: async () =>
|
||||
{
|
||||
const { error } = await settingsApi.api.settings.emulators.custom({ id: data.id }).delete();
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: (d, v, r, ctx) =>
|
||||
{
|
||||
ctx.client.invalidateQueries({ queryKey: ['custom-emulators'] });
|
||||
ctx.client.invalidateQueries({ queryKey: ["auto-emulators"] });
|
||||
}
|
||||
});
|
||||
|
||||
const handleSave = useCallback(() =>
|
||||
{
|
||||
if (dirty)
|
||||
{
|
||||
setDirty(false);
|
||||
setSettingMutation.mutate(localValue ?? '');
|
||||
}
|
||||
}, [dirty, setDirty, localValue]);
|
||||
|
||||
return (
|
||||
<OptionSpace label={<><p className='font-semibold'>{data.id}</p><small className='text-base-content/40'>{emulators[data.id]}</small></>}>
|
||||
<div className='flex gap-2'>
|
||||
<OptionInput
|
||||
name={data.id ?? ""}
|
||||
type="text"
|
||||
onBlur={handleSave}
|
||||
autocomplete="off"
|
||||
defaultValue={remoteValue}
|
||||
onChange={(e) =>
|
||||
{
|
||||
setLocalValue(e.currentTarget.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
value={localValue}
|
||||
/>
|
||||
<Button id={`delete-${data.id}`} className='p-2' onAction={() => deleteMutation.mutate()} type='button' >
|
||||
<Trash />
|
||||
</Button>
|
||||
</div>
|
||||
</OptionSpace>
|
||||
);
|
||||
}
|
||||
|
||||
function EmulatorBadge (data: { path?: string, exists: boolean, emulator: string; pathCover?: string; })
|
||||
{
|
||||
const { ref, focused } = useFocusable({
|
||||
focusKey: `badge-${data.emulator}`, onFocus: () =>
|
||||
{
|
||||
(ref.current as HTMLElement).scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
return <div className={classNames("tooltip tooltip-primary", { "tooltip-open": focused })} data-tip={`${emulators[data.emulator]}`}>
|
||||
<div ref={ref} className={
|
||||
twMerge('flex flex-col rounded-3xl bg-base-300 w-64 h-16 justify-center items-center p-4 overflow-hidden',
|
||||
classNames({
|
||||
"bg-base-200/50": !data.path,
|
||||
"border-dashed border-base-content/40 border-2": focused
|
||||
|
||||
}))
|
||||
}>
|
||||
<p className='flex gap-2 font-semibold'>
|
||||
{data.path ? data.exists ? <Check /> : <TriangleAlert className='text-error' /> : <SearchAlert className='text-warning' />}
|
||||
{!!data.pathCover && <img className='size-6 drop-shadow drop-shadow-black/20' src={`${RPC_URL(__HOST__)}${data.pathCover}`}></img>}
|
||||
{data.emulator}
|
||||
</p>
|
||||
{data.path ? <small className={classNames('opacity-60 max-w-full overflow-clip text-nowrap text-ellipsis', { 'text-error': !data.exists })}>{data.path}</small> : ""}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function EmulatorBadges (data: { path?: string; })
|
||||
{
|
||||
const { data: autoEmulators } = useQuery({ queryKey: ['auto-emulators'], queryFn: async () => settingsApi.api.settings.emulators.automatic.get() });
|
||||
const { ref, focusKey } = useFocusable({ focusKey: `emulator-badges`, focusable: !!autoEmulators?.data && autoEmulators.data.length > 0 });
|
||||
return <div ref={ref} className='flex flex-wrap gap-2 justify-center-safe'>
|
||||
<FocusContext value={focusKey}>
|
||||
{autoEmulators?.data?.map(e => <EmulatorBadge pathCover={e.path_cover ?? undefined} path={e.path} exists={e.exists} emulator={e.emulator} />)}
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { focus } = Route.useSearch();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
preferredChildFocusKey: focus
|
||||
});
|
||||
const { data: customEmulators } = useQuery({
|
||||
queryKey: ['custom-emulators'], queryFn: async () =>
|
||||
{
|
||||
const { data, error } = await settingsApi.api.settings.emulators.custom.get();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
return <FocusContext value={focusKey}>
|
||||
<ul ref={ref} className="list rounded-box gap-2">
|
||||
<div className="divider text-2xl mt-0 md:mt-4">
|
||||
<div className="flex flex-col">
|
||||
<h3>Romm</h3>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsOption label="Download Path" id="downloadPath" type="text" />
|
||||
<div className="divider text-2xl mt-0 md:mt-4">
|
||||
<div className="flex flex-col">
|
||||
<h3>Emulatos</h3>
|
||||
</div>
|
||||
</div>
|
||||
<EmulatorBadges />
|
||||
<div className="divider text-base-content/40">Overrides</div>
|
||||
<NewEmulatorPath />
|
||||
{!!customEmulators && customEmulators.map((key) => <EmulatorPath key={key} id={key} />)}
|
||||
</ul>
|
||||
</FocusContext>;
|
||||
}
|
||||
|
|
@ -17,16 +17,18 @@ import
|
|||
{
|
||||
ArrowBigLeft,
|
||||
FingerprintPattern,
|
||||
HardDrive,
|
||||
Info,
|
||||
MonitorCog,
|
||||
} from "lucide-react";
|
||||
import { JSX, useEffect } from "react";
|
||||
import { JSX, useEffect, useRef } from "react";
|
||||
import { useEventListener } from "usehooks-ts";
|
||||
import ShortcutPrompt from "../../components/ShortcutPrompt";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import z from "zod";
|
||||
import { SettingsSchema } from "../../../shared/constants";
|
||||
import { PopSource } from "../../scripts/spatialNavigation";
|
||||
import { Router } from "../..";
|
||||
|
||||
export const Route = createFileRoute("/settings")({
|
||||
component: SettingsUI,
|
||||
|
|
@ -78,8 +80,9 @@ function MenuItem (data: {
|
|||
className={twMerge(
|
||||
"group rounded-full p-3 pl-5 text-base-content/80",
|
||||
classNames({
|
||||
"bg-primary/40 text-primary-content": !focused && acitve,
|
||||
"bg-primary text-primary-content font-semibold": focused,
|
||||
"bg-primary text-primary-content": acitve,
|
||||
"font-semibold ring-7 ring-primary-content": focused,
|
||||
"bg-secondary text-secondary-content ring-primary": data.return && focused,
|
||||
}),
|
||||
data.linkClassName,
|
||||
)}
|
||||
|
|
@ -100,7 +103,7 @@ function SettingsMenu (data: {})
|
|||
const { ref, focusKey } = useFocusable({
|
||||
focusable: true,
|
||||
focusKey: 'settings-menu',
|
||||
preferredChildFocusKey: "/settings/accounts"
|
||||
preferredChildFocusKey: location.hash.replace("#", '')
|
||||
});
|
||||
|
||||
return <ul
|
||||
|
|
@ -120,6 +123,12 @@ function SettingsMenu (data: {})
|
|||
label="Visual"
|
||||
icon={<MonitorCog />}
|
||||
/>
|
||||
<MenuItem
|
||||
focusSelect
|
||||
route="/settings/directories"
|
||||
label="Directories"
|
||||
icon={<HardDrive />}
|
||||
/>
|
||||
<MenuItem
|
||||
focusSelect
|
||||
route="/settings/about"
|
||||
|
|
@ -138,15 +147,32 @@ function SettingsMenu (data: {})
|
|||
</ul>;
|
||||
}
|
||||
|
||||
function HandleGoBack ()
|
||||
{
|
||||
|
||||
if (document.activeElement && document.activeElement !== document.body && document.activeElement instanceof HTMLElement)
|
||||
{
|
||||
document.activeElement.blur();
|
||||
} else
|
||||
{
|
||||
const source = PopSource('settings');
|
||||
if (source)
|
||||
{
|
||||
console.log("Found source ", source, " to go back to");
|
||||
}
|
||||
Router.navigate({ to: source ?? "/", viewTransition: { types: ['zoom-out'] } });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function SettingsUI ()
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
focusKey: "settings-page-layout",
|
||||
preferredChildFocusKey: 'settings-menu'
|
||||
});
|
||||
|
||||
useEventListener("cancel", () => navigate({ to: PopSource('settings') ?? "/", viewTransition: { types: ['zoom-out'] } }));
|
||||
useEventListener("cancel", HandleGoBack, ref);
|
||||
useEffect(() =>
|
||||
{
|
||||
focusSelf();
|
||||
|
|
@ -166,7 +192,7 @@ export function SettingsUI ()
|
|||
</div>
|
||||
<div className="divider divider-end">
|
||||
<ShortcutPrompt
|
||||
onClick={() => navigate({ to: "/" })}
|
||||
onClick={HandleGoBack}
|
||||
icon="steamdeck_button_b"
|
||||
label="Back"
|
||||
/>
|
||||
|
|
|
|||
22
src/mainview/scripts/clientApi.ts
Normal file
22
src/mainview/scripts/clientApi.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { treaty } from "@elysiajs/eden";
|
||||
import { RommAPIType, SettingsAPIType, SystemAPIType } from "../../bun/api/rpc";
|
||||
import { RPC_URL } from "../../shared/constants";
|
||||
|
||||
export const rommApi = treaty<RommAPIType>(RPC_URL(__HOST__), {
|
||||
keepDomain: true,
|
||||
fetch: {
|
||||
credentials: 'include',
|
||||
}
|
||||
});
|
||||
export const settingsApi = treaty<SettingsAPIType>(RPC_URL(__HOST__), {
|
||||
keepDomain: true,
|
||||
fetch: {
|
||||
credentials: 'include',
|
||||
}
|
||||
});
|
||||
export const systemApi = treaty<SystemAPIType>(RPC_URL(__HOST__), {
|
||||
keepDomain: true,
|
||||
fetch: {
|
||||
credentials: 'include',
|
||||
}
|
||||
});
|
||||
|
|
@ -1,14 +1,18 @@
|
|||
import { navigateByDirection } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { getCurrentFocusKey, navigateByDirection, SpatialNavigation } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { dispatchFocusedEvent, GetFocusedElement } from "./spatialNavigation";
|
||||
|
||||
let loopStarted = false;
|
||||
|
||||
window.addEventListener("gamepadconnected", (evt) => {
|
||||
if (!loopStarted) {
|
||||
requestAnimationFrame(updateStatus);
|
||||
loopStarted = true;
|
||||
}
|
||||
window.addEventListener("gamepadconnected", (evt) =>
|
||||
{
|
||||
if (!loopStarted)
|
||||
{
|
||||
requestAnimationFrame(updateStatus);
|
||||
loopStarted = true;
|
||||
}
|
||||
});
|
||||
window.addEventListener("gamepaddisconnected", (evt) => {
|
||||
window.addEventListener("gamepaddisconnected", (evt) =>
|
||||
{
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -21,10 +25,10 @@ function throttleNav (key: string, dir: string, event: Event)
|
|||
const currentDate = new Date();
|
||||
const lastTime = throttleMap.get(key);
|
||||
const acceleration = throttleAcceleration.get(key) ?? 0;
|
||||
const speed = Math.max(maxSpeed - (maxSpeed - minSpeed) * (acceleration / 6),minSpeed);
|
||||
const speed = Math.max(maxSpeed - (maxSpeed - minSpeed) * (acceleration / 6), minSpeed);
|
||||
if ((currentDate.getTime() - (lastTime ?? 0) > speed))
|
||||
{
|
||||
navigateByDirection(dir, { event })
|
||||
navigateByDirection(dir, { event });
|
||||
throttleMap.set(key, currentDate.getTime());
|
||||
throttleAcceleration.set(key, acceleration + 1);
|
||||
}
|
||||
|
|
@ -34,11 +38,17 @@ window.addEventListener('keydown', e =>
|
|||
{
|
||||
if (e.key === 'Escape')
|
||||
{
|
||||
window.dispatchEvent(new Event('cancel'));
|
||||
const focusedElement = GetFocusedElement(getCurrentFocusKey());
|
||||
const finalTarget = focusedElement ?? window;
|
||||
const evn = new Event('cancel', { bubbles: true, cancelable: true });
|
||||
finalTarget.dispatchEvent(evn);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function updateStatus () {
|
||||
|
||||
|
||||
function updateStatus ()
|
||||
{
|
||||
for (const gamepad of navigator.getGamepads().filter(g => !!g))
|
||||
{
|
||||
const gamepadEvent = new GamepadEvent('gamepad-navigation', { gamepad, });
|
||||
|
|
@ -47,14 +57,14 @@ function updateStatus () {
|
|||
{
|
||||
if (!throttleMap.has('enter'))
|
||||
{
|
||||
window.dispatchEvent(new KeyboardEvent('keydown',{key: 'Enter', code: 'Enter', charCode: 13, keyCode: 13, view: window, bubbles: true}));
|
||||
dispatchFocusedEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', charCode: 13, keyCode: 13, view: window, bubbles: true }), window);
|
||||
throttleMap.set('enter', 0);
|
||||
}
|
||||
} else
|
||||
{
|
||||
if (throttleMap.delete('enter'))
|
||||
{
|
||||
window.dispatchEvent(new KeyboardEvent('keyup', {key: 'Enter'}));
|
||||
dispatchFocusedEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +72,8 @@ function updateStatus () {
|
|||
{
|
||||
if (!throttleMap.has('cancel'))
|
||||
{
|
||||
window.dispatchEvent(new Event('cancel'));
|
||||
const evn = new Event('cancel', { bubbles: true, cancelable: true });
|
||||
dispatchFocusedEvent(evn);
|
||||
throttleMap.set('cancel', 0);
|
||||
}
|
||||
} else
|
||||
|
|
@ -70,79 +81,87 @@ function updateStatus () {
|
|||
throttleMap.delete('cancel');
|
||||
}
|
||||
|
||||
if (gamepad.buttons[12].pressed)
|
||||
const activeFocus = GetFocusedElement(getCurrentFocusKey());
|
||||
if (activeFocus instanceof HTMLInputElement)
|
||||
{
|
||||
throttleNav('gp-up', "up", gamepadEvent);
|
||||
} else
|
||||
{
|
||||
throttleAcceleration.delete('gp-up');
|
||||
throttleMap.delete('gp-up');
|
||||
}
|
||||
if (gamepad.buttons[13].pressed)
|
||||
{
|
||||
throttleNav('gp-down', "down", gamepadEvent);
|
||||
} else
|
||||
{
|
||||
throttleAcceleration.delete('gp-down');
|
||||
throttleMap.delete('gp-down');
|
||||
}
|
||||
if (gamepad.buttons[14].pressed)
|
||||
{
|
||||
throttleNav('gp-left', "left", gamepadEvent);
|
||||
} else
|
||||
{
|
||||
throttleAcceleration.delete('gp-left');
|
||||
throttleMap.delete('gp-left');
|
||||
}
|
||||
if (gamepad.buttons[15].pressed)
|
||||
{
|
||||
throttleNav('gp-right', "right", gamepadEvent);
|
||||
} else
|
||||
{
|
||||
throttleAcceleration.delete('gp-right');
|
||||
throttleMap.delete('gp-right');
|
||||
}
|
||||
|
||||
const deadzone = 0.5;
|
||||
const cancelDeadzone = 0.3;
|
||||
|
||||
function AxisControls ()
|
||||
} else
|
||||
{
|
||||
if (gamepad.axes[0] > deadzone)
|
||||
if (gamepad.buttons[12].pressed)
|
||||
{
|
||||
throttleNav('gpa-right', "right", gamepadEvent);
|
||||
return;
|
||||
}
|
||||
else if (gamepad.axes[0] < -deadzone)
|
||||
{
|
||||
throttleNav('gpa-left', "left", gamepadEvent);
|
||||
return;
|
||||
}
|
||||
else if ((throttleMap.has('gpa-left') || throttleMap.has('gpa-left')) && gamepad.axes[0] < cancelDeadzone && gamepad.axes[0] > -cancelDeadzone)
|
||||
{
|
||||
throttleAcceleration.delete('gpa-right');
|
||||
throttleAcceleration.delete('gpa-left');
|
||||
throttleMap.delete('gpa-left');
|
||||
throttleMap.delete('gpa-left');
|
||||
}
|
||||
|
||||
if (gamepad.axes[1] > deadzone)
|
||||
{
|
||||
throttleNav('gpa-down', "down", gamepadEvent);
|
||||
}
|
||||
else if (gamepad.axes[1] < -deadzone)
|
||||
{
|
||||
throttleNav('gpa-up', "up", gamepadEvent);
|
||||
throttleNav('gp-up', "up", gamepadEvent);
|
||||
} else
|
||||
{
|
||||
throttleAcceleration.delete('gpa-up');
|
||||
throttleAcceleration.delete('gpa-down');
|
||||
throttleMap.delete('gpa-up');
|
||||
throttleMap.delete('gpa-down');
|
||||
throttleAcceleration.delete('gp-up');
|
||||
throttleMap.delete('gp-up');
|
||||
}
|
||||
if (gamepad.buttons[13].pressed)
|
||||
{
|
||||
throttleNav('gp-down', "down", gamepadEvent);
|
||||
} else
|
||||
{
|
||||
throttleAcceleration.delete('gp-down');
|
||||
throttleMap.delete('gp-down');
|
||||
}
|
||||
if (gamepad.buttons[14].pressed)
|
||||
{
|
||||
throttleNav('gp-left', "left", gamepadEvent);
|
||||
} else
|
||||
{
|
||||
throttleAcceleration.delete('gp-left');
|
||||
throttleMap.delete('gp-left');
|
||||
}
|
||||
if (gamepad.buttons[15].pressed)
|
||||
{
|
||||
throttleNav('gp-right', "right", gamepadEvent);
|
||||
} else
|
||||
{
|
||||
throttleAcceleration.delete('gp-right');
|
||||
throttleMap.delete('gp-right');
|
||||
}
|
||||
|
||||
const deadzone = 0.5;
|
||||
const cancelDeadzone = 0.3;
|
||||
|
||||
function AxisControls ()
|
||||
{
|
||||
if (gamepad.axes[0] > deadzone)
|
||||
{
|
||||
throttleNav('gpa-right', "right", gamepadEvent);
|
||||
return;
|
||||
}
|
||||
else if (gamepad.axes[0] < -deadzone)
|
||||
{
|
||||
throttleNav('gpa-left', "left", gamepadEvent);
|
||||
return;
|
||||
}
|
||||
else if ((throttleMap.has('gpa-left') || throttleMap.has('gpa-left')) && gamepad.axes[0] < cancelDeadzone && gamepad.axes[0] > -cancelDeadzone)
|
||||
{
|
||||
throttleAcceleration.delete('gpa-right');
|
||||
throttleAcceleration.delete('gpa-left');
|
||||
throttleMap.delete('gpa-left');
|
||||
throttleMap.delete('gpa-left');
|
||||
}
|
||||
|
||||
if (gamepad.axes[1] > deadzone)
|
||||
{
|
||||
throttleNav('gpa-down', "down", gamepadEvent);
|
||||
}
|
||||
else if (gamepad.axes[1] < -deadzone)
|
||||
{
|
||||
throttleNav('gpa-up', "up", gamepadEvent);
|
||||
} else
|
||||
{
|
||||
throttleAcceleration.delete('gpa-up');
|
||||
throttleAcceleration.delete('gpa-down');
|
||||
throttleMap.delete('gpa-up');
|
||||
throttleMap.delete('gpa-down');
|
||||
}
|
||||
}
|
||||
|
||||
AxisControls();
|
||||
}
|
||||
|
||||
AxisControls();
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateStatus);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,28 @@
|
|||
import
|
||||
{
|
||||
getCurrentFocusKey,
|
||||
init,
|
||||
SpatialNavigation,
|
||||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { RefObject, useEffect } from "react";
|
||||
|
||||
init({
|
||||
shouldFocusDOMNode: false,
|
||||
throttle: 200,
|
||||
throttle: 200
|
||||
});
|
||||
|
||||
let addFocusable = SpatialNavigation.addFocusable.bind(SpatialNavigation);
|
||||
let removeFocusable = SpatialNavigation.removeFocusable.bind(SpatialNavigation);
|
||||
let setCurrentFocusedKey = SpatialNavigation.setCurrentFocusedKey.bind(SpatialNavigation);
|
||||
|
||||
type SaveFocusType = "session" | "local";
|
||||
|
||||
type HistorySourceType = "settings" | 'details';
|
||||
type HistorySourceType = "settings" | 'details' | 'launch';
|
||||
const historySourceMap = new Map<string, string>();
|
||||
|
||||
export function SaveSource (id: HistorySourceType, url: string)
|
||||
export function SaveSource (id: HistorySourceType, url?: string)
|
||||
{
|
||||
historySourceMap.set(id, url);
|
||||
historySourceMap.set(id, url ?? location.hash.replace("#", ''));
|
||||
}
|
||||
|
||||
export function HasSource (id: HistorySourceType)
|
||||
|
|
@ -29,11 +32,49 @@ export function HasSource (id: HistorySourceType)
|
|||
|
||||
export function PopSource (id: HistorySourceType)
|
||||
{
|
||||
if (!historySourceMap.has(id))
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
const source = historySourceMap.get(id);
|
||||
historySourceMap.delete(id);
|
||||
return source;
|
||||
}
|
||||
|
||||
export function GetFocusedElement (focusKey: string)
|
||||
{
|
||||
return (SpatialNavigation as any).focusableComponents[focusKey]?.node as HTMLElement;
|
||||
}
|
||||
|
||||
export function dispatchFocusedEvent (event: Event, override?: Element | Window)
|
||||
{
|
||||
const focusedElement = GetFocusedElement(getCurrentFocusKey());
|
||||
const finalTarget = override ?? focusedElement ?? window;
|
||||
return finalTarget.dispatchEvent(event);
|
||||
}
|
||||
|
||||
export interface FocusEventMap
|
||||
{
|
||||
'focuschanged': Event;
|
||||
}
|
||||
|
||||
export function useFocusEventListener<K extends keyof FocusEventMap, O extends HTMLElement> (eventName: K, handler: (event: FocusEventMap[K]) => void, element?: RefObject<O | null | undefined>): void
|
||||
{
|
||||
useEffect(() =>
|
||||
{
|
||||
const finalElement = element ? element.current : window;
|
||||
finalElement?.addEventListener(eventName, handler);
|
||||
|
||||
return () => finalElement?.removeEventListener(eventName, handler);
|
||||
}, [eventName, handler, element?.current]);
|
||||
}
|
||||
|
||||
SpatialNavigation.setCurrentFocusedKey = (newFocusKey, focusDetails) =>
|
||||
{
|
||||
setCurrentFocusedKey(newFocusKey, focusDetails);
|
||||
dispatchFocusedEvent(new Event('focuschanged', { bubbles: true }));
|
||||
};
|
||||
|
||||
SpatialNavigation.addFocusable = (toAdd) =>
|
||||
{
|
||||
addFocusable(toAdd);
|
||||
|
|
|
|||
|
|
@ -56,3 +56,8 @@ export function useScrollSave (data: ScrollSaveParams)
|
|||
|
||||
return { ref: data.ref };
|
||||
}
|
||||
|
||||
export function serverOp ()
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { client } from "../index";
|
||||
import { settingsApi } from "./clientApi";
|
||||
|
||||
window.addEventListener("resize", () =>
|
||||
{
|
||||
client.api.settings({ id: 'windowSize' }).post({ value: { width: window.innerWidth, height: window.innerHeight } });
|
||||
settingsApi.api.settings({ id: 'windowSize' }).post({ value: { width: window.innerWidth, height: window.innerHeight } });
|
||||
});
|
||||
|
||||
let lastWindowPosX: number = window.screenX;
|
||||
|
|
@ -11,7 +11,7 @@ var screenPositionInternal: NodeJS.Timeout = setInterval(() =>
|
|||
{
|
||||
if (lastWindowPosX != window.screenX || lastWindowPosY != window.screenY)
|
||||
{
|
||||
client.api.settings({ 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;
|
||||
|
|
|
|||
5
src/mainview/types.d.ts
vendored
5
src/mainview/types.d.ts
vendored
|
|
@ -1,4 +1,9 @@
|
|||
declare const __HOST__: string;
|
||||
declare const __EMULATORS__: Record<string, string>;
|
||||
declare module "@emulators" {
|
||||
const data: Record<string, string>;
|
||||
export default data;
|
||||
}
|
||||
|
||||
global
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
import { JSX } from 'react';
|
||||
import * as z from 'zod';
|
||||
|
||||
export const SERVER_PORT = 5173;
|
||||
|
|
@ -9,14 +10,15 @@ export const RPC_URL = (host: string) => `http://${host}:${RPC_PORT}`;
|
|||
export const SOCKETS_URL = (host: string) => `ws://${host}:${RPC_PORT}`;
|
||||
|
||||
export const DefaultRommStaleTime = 60 * 1000; // A minute
|
||||
export const GameMetaSchema = z.object({
|
||||
id: z.number(),
|
||||
title: z.string(),
|
||||
subtitle: z.string(),
|
||||
previewUrl: z.url().optional()
|
||||
});
|
||||
|
||||
export type GameMeta = z.infer<typeof GameMetaSchema>;
|
||||
export interface GameMeta
|
||||
{
|
||||
id: string,
|
||||
onSelect?: () => void,
|
||||
onFocus?: () => void,
|
||||
title: string,
|
||||
subtitle: string | JSX.Element,
|
||||
previewUrl?: string;
|
||||
};
|
||||
|
||||
export const SettingsSchema = z.object({
|
||||
rommAddress: z.url().optional(),
|
||||
|
|
@ -24,6 +26,78 @@ export const SettingsSchema = z.object({
|
|||
disableBlur: z.boolean().default(false),
|
||||
windowSize: z.object({ width: z.number(), height: z.number() }).default({ width: 1280, height: 800 }),
|
||||
windowPosition: z.object({ x: z.number(), y: z.number() }).optional(),
|
||||
downloadPath: z.string().default('./downloads')
|
||||
});
|
||||
|
||||
export const CustomEmulatorSchema = z.record(z.string(), z.string());
|
||||
|
||||
export interface FrontEndId
|
||||
{
|
||||
id: number;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface FrontEndPlatformType
|
||||
{
|
||||
id: FrontEndId;
|
||||
source: string | null;
|
||||
source_id: number | null;
|
||||
slug: string;
|
||||
name: string;
|
||||
family_name?: string | null;
|
||||
path_cover: string | null;
|
||||
game_count: number;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface FrontEndGameType
|
||||
{
|
||||
platform_display_name: string | null,
|
||||
path_platform_cover: string | null;
|
||||
id: FrontEndId,
|
||||
source: string | null,
|
||||
source_id: number | 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,
|
||||
};
|
||||
|
||||
export interface FrontEndGameTypeDetailed extends FrontEndGameType
|
||||
{
|
||||
summary: string | null;
|
||||
fs_size_bytes: number | null;
|
||||
missing: boolean;
|
||||
local: boolean;
|
||||
paths_screenshots: string[];
|
||||
achievements?: {
|
||||
unlocked: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type SettingsType = z.infer<typeof SettingsSchema>;
|
||||
export interface GameInstallProgress
|
||||
{
|
||||
progress?: number;
|
||||
status?: GameStatusType;
|
||||
details?: string;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
export type GameInstallProgressEvent = 'refresh';
|
||||
|
||||
export const PlatformSchema = z.object({ slug: z.string() });
|
||||
export const GameLaunchSchema = z.object({ platform: PlatformSchema, id: z.number(), slug: z.string(), directory: z.string() });
|
||||
|
||||
export const GameflowPluginSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
getSupportedPlatform: z.function({ output: z.array(PlatformSchema) }),
|
||||
launchGame: z.function({ input: [GameLaunchSchema] })
|
||||
});
|
||||
export interface GameflowPlugin extends z.infer<typeof GameflowPluginSchema> { }
|
||||
export type GameStatusType = 'installed' | 'missing-emulator' | 'error' | 'install' | 'download' | 'extract' | 'playing';
|
||||
3
src/shared/public-types.ts
Normal file
3
src/shared/public-types.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { GameflowPlugin } from "./constants";
|
||||
|
||||
export type GameflowPluginType = GameflowPlugin;
|
||||
19
src/tests/game-launching.test.ts
Normal file
19
src/tests/game-launching.test.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { expect, test, mock } from 'bun:test';
|
||||
|
||||
test("uses custom emulator", async () =>
|
||||
{
|
||||
const { getValidLaunchCommands: getLaunchCommands } = await import('../bun/api/games/services/launchGameService');
|
||||
const commands = await getLaunchCommands({
|
||||
systemSlug: 'ps2',
|
||||
gamePath: './src/tests/mock-roms/mock-rom.iso',
|
||||
customEmulatorConfig: new Map([['PCSX2', "./src/tests/mock-roms/pcsx2.exe"]])
|
||||
});
|
||||
|
||||
expect(commands)
|
||||
.toSatisfy((d) =>
|
||||
!!d?.find(c =>
|
||||
c?.command.includes("./src/tests/mock-roms/mock-rom.iso") &&
|
||||
c.command.includes("./src/tests/mock-roms/pcsx2.exe")
|
||||
)
|
||||
);
|
||||
});
|
||||
0
src/tests/mock-roms/mock-rom.iso
Normal file
0
src/tests/mock-roms/mock-rom.iso
Normal file
2
src/tests/preload.ts
Normal file
2
src/tests/preload.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { mock } from 'bun:test';
|
||||
|
||||
|
|
@ -2,7 +2,11 @@
|
|||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
|
|
@ -14,7 +18,25 @@
|
|||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"~/*": [
|
||||
"./*"
|
||||
],
|
||||
"@shared/*": [
|
||||
"./src/shared/*"
|
||||
],
|
||||
"@clients/*": [
|
||||
"./src/clients/*"
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": ["src", "vite.config.ts", "vite-env-override.d.ts"]
|
||||
"include": [
|
||||
"src",
|
||||
"vite.config.ts",
|
||||
"vite-env-override.d.ts"
|
||||
]
|
||||
}
|
||||
4
vendors/es-de/README.md
vendored
Normal file
4
vendors/es-de/README.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# ES-DE
|
||||
Resources used from https://gitlab.com/es-de/emulationstation-de
|
||||
|
||||
Configuration is compiled into a sqlite tables for backend use along with json emulator lists for the frontend
|
||||
76
vendors/es-de/emulators.darwin.x64.json
vendored
Normal file
76
vendors/es-de/emulators.darwin.x64.json
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"OS-SHELL": "Operating system shell",
|
||||
"RETROARCH": "RETROARCH",
|
||||
"3DSEN": "Nintendo NES and Famicom emulator 3dSen",
|
||||
"ACE-DL": "Amstrad CPC emulator ACE-DL",
|
||||
"AETHERSX2": "Sony PlayStation 2 emulator AetherSX2",
|
||||
"AMIBERRY": "Commodore Amiga emulator Amiberry",
|
||||
"ARES": "Multi-system emulator ares",
|
||||
"ATARI800": "Atari 8-bit computer emulator Atari800",
|
||||
"AZAHAR": "Nintendo 3DS emulator Azahar",
|
||||
"BASILISKII": "Macintosh 68k emulator Basilisk II",
|
||||
"CEMU": "Nintendo Wii U emulator Cemu",
|
||||
"CITRA": "Nintendo 3DS emulator Citra",
|
||||
"CPCEMU": "Amstrad CPC emulator CPCemu",
|
||||
"DOLPHIN": "Nintendo GameCube and Wii emulator Dolphin",
|
||||
"DOSBOX-PURE-UNLEASHED": "DOS emulator DOSBox Pure Unleashed",
|
||||
"DOSBOX-STAGING": "DOS emulator DOSBox Staging",
|
||||
"DOSBOX-X": "DOS emulator DOSBox-X",
|
||||
"DREAMM": "LucasArts game engine DREAMM",
|
||||
"DUCKSTATION": "Sony PlayStation 1 emulator DuckStation",
|
||||
"EASYRPG": "EasyRPG game engine",
|
||||
"EKA2L1": "Symbian and Nokia N-Gage emulator EKA2L1",
|
||||
"FLYCAST": "Sega Dreamcast emulator Flycast",
|
||||
"FLYCAST-DOJO": "Sega Dreamcast emulator Flycast Dojo",
|
||||
"FS-UAE": "Commodore Amiga emulator FS-UAE",
|
||||
"FUSE": "Sinclair ZX Spectrum emulator Fuse",
|
||||
"GARGOYLE": "Interactive fiction player Gargoyle",
|
||||
"HATARI": "Atari ST/STE/TT/Falcon emulator Hatari",
|
||||
"HYPSEUS-SINGE": "Hypseus Singe, fork of arcade LaserDisc emulator Daphne",
|
||||
"KEGS": "Apple IIGS emulator KEGS",
|
||||
"LIME3DS": "Nintendo 3DS emulator Lime3DS",
|
||||
"MAME": "Arcade emulator MAME",
|
||||
"MANDARINE": "Nintendo 3DS emulator Mandarine",
|
||||
"MEDNAFEN": "Multi-system emulator Mednafen",
|
||||
"MELONDS": "Nintendo DS and DSi emulator melonDS",
|
||||
"MGBA": "Nintendo Game Boy Advance emulator mGBA",
|
||||
"MUPEN64PLUS": "Nintendo 64 emulator Mupen64Plus",
|
||||
"OPENMSX": "MSX home computer emulator openMSX",
|
||||
"ORICUTRON": "Tangerine Computer Systems emulator Oricutron",
|
||||
"PANDA3DS": "Nintendo 3DS emulator Panda3DS",
|
||||
"PARALLEL-LAUNCHER": "Nintendo 64 emulator Parallel Launcher",
|
||||
"PCSX2": "Sony PlayStation 2 emulator PCSX2",
|
||||
"PICO-8": "PICO-8 Fantasy Console (game engine)",
|
||||
"PLAY!": "Sony PlayStation 2 emulator Play!",
|
||||
"PPSSPP": "Sony PlayStation Portable emulator PPSSPP",
|
||||
"PRBOOM-PLUS": "Doom engine source port PrBoom+",
|
||||
"QUASI88": "NEC PC-8800 series emulator QUASI88",
|
||||
"REDREAM": "Sega Dreamcast emulator Redream",
|
||||
"RPCS3": "Sony PlayStation 3 emulator RPCS3",
|
||||
"RUFFLE": "Adobe Flash player Ruffle",
|
||||
"RYUJINX": "Nintendo Switch emulator Ryujinx",
|
||||
"SAMEBOY": "Nintendo Game Boy and Game Boy Color emulator SameBoy",
|
||||
"SIMCOUPE": "SAM Coupé emulator SimCoupé",
|
||||
"SCUMMVM": "ScummVM game engine",
|
||||
"SHADPS4": "Sony PlayStation 4 emulator shadPS4",
|
||||
"SHADPS4-GUI": "Sony PlayStation 4 emulator shadPS4 (GUI)",
|
||||
"SHEEPSHAVER": "Apple Macintosh PowerPC emulator SheepShaver",
|
||||
"SIXTYFORCE": "Nintendo 64 emulator sixtyforce",
|
||||
"SKYEMU": "Nintendo Game Boy, Game Boy Color, Game Boy Advance and DS emulator SkyEmu",
|
||||
"SNES9X": "Super Nintendo emulator Snes9x",
|
||||
"SOLARUS": "Solarus game engine",
|
||||
"STEAM": "Valve Steam",
|
||||
"STELLA": "Atari 2600 emulator Stella",
|
||||
"SUPERMODEL": "Sega Model 3 emulator Supermodel",
|
||||
"TIC-80": "TIC-80 Fantasy Computer (game engine)",
|
||||
"VBA-M": "Nintendo Game Boy Advance emulator VBA-M",
|
||||
"VICE-X64SC": "Commodore 8-bit computer emulator VICE (x64sc accurate)",
|
||||
"VICE-XPLUS4": "Commodore 8-bit computer emulator VICE (xplus4)",
|
||||
"VICE-XVIC": "Commodore 8-bit computer emulator VICE (xvic)",
|
||||
"VISUAL-PINBALL": "Pinball simulator Visual Pinball",
|
||||
"VITA3K": "Sony PlayStation Vita emulator Vita3K",
|
||||
"XEMU": "Microsoft Xbox emulator xemu",
|
||||
"XROAR": "Dragon and Tandy 8-bit computer emulator XRoar",
|
||||
"YMIR": "Sega Saturn emulator Ymir",
|
||||
"ZESARUX": "Sinclair ZX Spectrum emulator ZEsarUX"
|
||||
}
|
||||
BIN
vendors/es-de/emulators.darwin.x64.sqlite
vendored
Normal file
BIN
vendors/es-de/emulators.darwin.x64.sqlite
vendored
Normal file
Binary file not shown.
18
vendors/es-de/emulators.haiku.x64.json
vendored
Normal file
18
vendors/es-de/emulators.haiku.x64.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"OS-SHELL": "Operating system shell",
|
||||
"RETROARCH": "RETROARCH",
|
||||
"ADVANCEMAME": "Arcade emulator AdvanceMAME",
|
||||
"DOSBOX-X": "DOS emulator DOSBox-X",
|
||||
"FINALBURN-NEO": "Arcade emulator FinalBurn Neo",
|
||||
"FS-UAE": "Commodore Amiga emulator FS-UAE",
|
||||
"FUSE": "Sinclair ZX Spectrum emulator Fuse",
|
||||
"MAME": "Arcade emulator MAME",
|
||||
"MEDNAFEN": "Multi-system emulator Mednafen",
|
||||
"MELONDS": "Nintendo DS and DSi emulator melonDS",
|
||||
"MGBA": "Nintendo Game Boy Advance emulator mGBA",
|
||||
"MUPEN64PLUS": "Nintendo 64 emulator Mupen64Plus",
|
||||
"PPSSPP": "Sony PlayStation Portable emulator PPSSPP",
|
||||
"SCUMMVM": "ScummVM game engine",
|
||||
"SOLARUS": "Solarus game engine",
|
||||
"ZESARUX": "Sinclair ZX Spectrum emulator ZEsarUX"
|
||||
}
|
||||
BIN
vendors/es-de/emulators.haiku.x64.sqlite
vendored
Normal file
BIN
vendors/es-de/emulators.haiku.x64.sqlite
vendored
Normal file
Binary file not shown.
55
vendors/es-de/emulators.linux.arm.json
vendored
Normal file
55
vendors/es-de/emulators.linux.arm.json
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"OS-SHELL": "Operating system shell",
|
||||
"RETROARCH": "RETROARCH",
|
||||
"ACE-DL": "Amstrad CPC emulator ACE-DL",
|
||||
"AMIBERRY": "Commodore Amiga emulator Amiberry",
|
||||
"ARES": "Multi-system emulator ares",
|
||||
"ATARI800": "Atari 8-bit computer emulator Atari800",
|
||||
"AZAHAR": "Nintendo 3DS emulator Azahar",
|
||||
"BIGPEMU": "Atari Jaguar emulator BigPEmu",
|
||||
"BSNES": "Super Nintendo/Super Famicom emulator bsnes",
|
||||
"CPCEMU": "Amstrad CPC emulator CPCemu",
|
||||
"DESMUME": "Nintendo DS emulator DeSmuME",
|
||||
"DOLPHIN": "Nintendo GameCube and Wii emulator Dolphin",
|
||||
"DOSBOX-PURE-UNLEASHED": "DOS emulator DOSBox Pure Unleashed",
|
||||
"DOSBOX-STAGING": "DOS emulator DOSBox Staging",
|
||||
"DOSBOX-X": "DOS emulator DOSBox-X",
|
||||
"DREAMM": "LucasArts game engine DREAMM",
|
||||
"DUCKSTATION": "Sony PlayStation 1 emulator DuckStation",
|
||||
"EASYRPG": "EasyRPG game engine",
|
||||
"FLYCAST": "Sega Dreamcast emulator Flycast",
|
||||
"FS-UAE": "Commodore Amiga emulator FS-UAE",
|
||||
"FUSE": "Sinclair ZX Spectrum emulator Fuse",
|
||||
"GARGOYLE": "Interactive fiction player Gargoyle",
|
||||
"GOPHER64": "Nintendo 64 emulator Gopher64",
|
||||
"HATARI": "Atari ST/STE/TT/Falcon emulator Hatari",
|
||||
"MAME": "Arcade emulator MAME",
|
||||
"MEDNAFEN": "Multi-system emulator Mednafen",
|
||||
"MELONDS": "Nintendo DS and DSi emulator melonDS",
|
||||
"MESEN": "Multi-system emulator Mesen",
|
||||
"MGBA": "Nintendo Game Boy Advance emulator mGBA",
|
||||
"MUPEN64PLUS": "Nintendo 64 emulator Mupen64Plus",
|
||||
"NESTOPIA-UE": "Nintendo NES and Famicom emulator Nestopia UE",
|
||||
"OPENMSX": "MSX home computer emulator openMSX",
|
||||
"PICO-8": "PICO-8 Fantasy Console (game engine)",
|
||||
"PPSSPP": "Sony PlayStation Portable emulator PPSSPP",
|
||||
"PRBOOM-PLUS": "Doom engine source port PrBoom+",
|
||||
"PRIMEHACK": "PrimeHack, fork of Nintendo GameCube and Wii emulator Dolphin",
|
||||
"ROSALIES-MUPEN-GUI": "Nintendo 64 emulator Rosalie's Mupen GUI",
|
||||
"RPCS3": "Sony PlayStation 3 emulator RPCS3",
|
||||
"RUFFLE": "Adobe Flash player Ruffle",
|
||||
"RYUJINX": "Nintendo Switch emulator Ryujinx",
|
||||
"SAMEBOY": "Nintendo Game Boy and Game Boy Color emulator SameBoy",
|
||||
"SCUMMVM": "ScummVM game engine",
|
||||
"SNES9X": "Super Nintendo emulator Snes9x",
|
||||
"STEAM": "Valve Steam",
|
||||
"STELLA": "Atari 2600 emulator Stella",
|
||||
"SUPERMODEL": "Sega Model 3 emulator Supermodel",
|
||||
"TIC-80": "TIC-80 Fantasy Computer (game engine)",
|
||||
"VBA-M": "Nintendo Game Boy Advance emulator VBA-M",
|
||||
"VICE-X64SC": "Commodore 8-bit computer emulator VICE (x64sc accurate)",
|
||||
"VICE-XPLUS4": "Commodore 8-bit computer emulator VICE (xplus4)",
|
||||
"VICE-XVIC": "Commodore 8-bit computer emulator VICE (xvic)",
|
||||
"XEMU": "Microsoft Xbox emulator xemu",
|
||||
"YMIR": "Sega Saturn emulator Ymir"
|
||||
}
|
||||
BIN
vendors/es-de/emulators.linux.arm.sqlite
vendored
Normal file
BIN
vendors/es-de/emulators.linux.arm.sqlite
vendored
Normal file
Binary file not shown.
119
vendors/es-de/emulators.linux.x64.json
vendored
Normal file
119
vendors/es-de/emulators.linux.x64.json
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
{
|
||||
"OS-SHELL": "Operating system shell",
|
||||
"RETROARCH": "RETROARCH",
|
||||
"3DSEN": "Nintendo NES and Famicom emulator 3dSen",
|
||||
"3DSEN-WINDOWS": "Nintendo NES and Famicom emulator 3dSen",
|
||||
"A7800": "Atari 7800 emulator A7800",
|
||||
"ACE-DL": "Amstrad CPC emulator ACE-DL",
|
||||
"ADVANCEMAME": "Arcade emulator AdvanceMAME",
|
||||
"AMIBERRY": "Commodore Amiga emulator Amiberry",
|
||||
"ARES": "Multi-system emulator ares",
|
||||
"ATARI800": "Atari 8-bit computer emulator Atari800",
|
||||
"AZAHAR": "Nintendo 3DS emulator Azahar",
|
||||
"AZAHARPLUS": "Nintendo 3DS emulator AzaharPlus",
|
||||
"BASILISKII": "Macintosh 68k emulator Basilisk II",
|
||||
"BIGPEMU": "Atari Jaguar emulator BigPEmu",
|
||||
"BIGPEMU-WINDOWS": "Atari Jaguar emulator BigPEmu",
|
||||
"BLASTEM": "Sega Mega Drive/Genesis emulator BlastEm",
|
||||
"BSNES": "Super Nintendo/Super Famicom emulator bsnes",
|
||||
"CEMU": "Nintendo Wii U emulator Cemu",
|
||||
"CITRA": "Nintendo 3DS emulator Citra",
|
||||
"COLEM": "Coleco ColecoVision and Adam emulator ColEm",
|
||||
"CPCEMU": "Amstrad CPC emulator CPCemu",
|
||||
"CSPECT": "Sinclair ZX Spectrum Next emulator #CSpect",
|
||||
"DEMUL-WINDOWS": "Sega Dreamcast emulator Demul",
|
||||
"DESMUME": "Nintendo DS emulator DeSmuME",
|
||||
"DOLPHIN": "Nintendo GameCube and Wii emulator Dolphin",
|
||||
"DOSBOX-PURE-UNLEASHED": "DOS emulator DOSBox Pure Unleashed",
|
||||
"DOSBOX-STAGING": "DOS emulator DOSBox Staging",
|
||||
"DOSBOX-X": "DOS emulator DOSBox-X",
|
||||
"DREAMM": "LucasArts game engine DREAMM",
|
||||
"DUCKSTATION": "Sony PlayStation 1 emulator DuckStation",
|
||||
"EASYRPG": "EasyRPG game engine",
|
||||
"EKA2L1": "Symbian and Nokia N-Gage emulator EKA2L1",
|
||||
"EKA2L1-WINDOWS": "Symbian and Nokia N-Gage emulator EKA2L1",
|
||||
"FINALBURN-NEO": "Arcade emulator FinalBurn Neo",
|
||||
"FLYCAST": "Sega Dreamcast emulator Flycast",
|
||||
"FLYCAST-DOJO": "Sega Dreamcast emulator Flycast Dojo",
|
||||
"FS-UAE": "Commodore Amiga emulator FS-UAE",
|
||||
"FUSE": "Sinclair ZX Spectrum emulator Fuse",
|
||||
"GARGOYLE": "Interactive fiction player Gargoyle",
|
||||
"GEARBOY": "Nintendo Game Boy and Game Boy Color emulator Gearboy",
|
||||
"GOPHER2600": "Atari 2600 emulator Gopher2600",
|
||||
"GOPHER64": "Nintendo 64 emulator Gopher64",
|
||||
"HATARI": "Atari ST/STE/TT/Falcon emulator Hatari",
|
||||
"HYPSEUS-SINGE": "Hypseus Singe, fork of arcade LaserDisc emulator Daphne",
|
||||
"IZAPPLE2": "Apple II emulator izapple2",
|
||||
"JGENESIS": "Multi-system emulator jgenesis",
|
||||
"KEGS": "Apple IIGS emulator KEGS",
|
||||
"KRONOS": "Sega Saturn emulator Kronos",
|
||||
"LIGHTSPARK": "Adobe Flash player Lightspark",
|
||||
"LIME3DS": "Nintendo 3DS emulator Lime3DS",
|
||||
"LINAPPLE": "Apple II emulator LinApple",
|
||||
"LINDBERGH-LOADER": "Sega Lindbergh emulator Lindbergh Loader",
|
||||
"M2EMULATOR-WINDOWS": "Sega Model 2 Emulator",
|
||||
"MAME": "Arcade emulator MAME",
|
||||
"MANDARINE": "Nintendo 3DS emulator Mandarine",
|
||||
"MEDNAFEN": "Multi-system emulator Mednafen",
|
||||
"MELONDS": "Nintendo DS and DSi emulator melonDS",
|
||||
"MESEN": "Multi-system emulator Mesen",
|
||||
"MFME-WINDOWS": "Fruit machine emulator MFME",
|
||||
"MGBA": "Nintendo Game Boy Advance emulator mGBA",
|
||||
"MUPEN64PLUS": "Nintendo 64 emulator Mupen64Plus",
|
||||
"NESTOPIA-UE": "Nintendo NES and Famicom emulator Nestopia UE",
|
||||
"NOODS": "Nintendo DS and Game Boy Advance emulator NooDS",
|
||||
"OPENMSX": "MSX home computer emulator openMSX",
|
||||
"ORICUTRON": "Tangerine Computer Systems emulator Oricutron",
|
||||
"PANDA3DS": "Nintendo 3DS emulator Panda3DS",
|
||||
"PARALLEL-LAUNCHER": "Nintendo 64 emulator Parallel Launcher",
|
||||
"PCSX2": "Sony PlayStation 2 emulator PCSX2",
|
||||
"PCSX2-LEGACY": "Sony PlayStation 2 emulator PCSX2 (legacy)",
|
||||
"PICO-8": "PICO-8 Fantasy Console (game engine)",
|
||||
"PLASTIC": "Nintendo NES and Famicom emulator Plastic",
|
||||
"PLAY!": "Sony PlayStation 2 emulator Play!",
|
||||
"PPSSPP": "Sony PlayStation Portable emulator PPSSPP",
|
||||
"PRBOOM-PLUS": "Doom engine source port PrBoom+",
|
||||
"PRIMEHACK": "PrimeHack, fork of Nintendo GameCube and Wii emulator Dolphin",
|
||||
"PROTON": "Microsoft Windows compatibility layer Proton",
|
||||
"PUNES": "Nintendo NES and Famicom emulator puNES",
|
||||
"QUASI88": "NEC PC-8800 series emulator QUASI88",
|
||||
"REDREAM": "Sega Dreamcast emulator Redream",
|
||||
"ROSALIES-MUPEN-GUI": "Nintendo 64 emulator Rosalie's Mupen GUI",
|
||||
"RPCS3": "Sony PlayStation 3 emulator RPCS3",
|
||||
"RUFFLE": "Adobe Flash player Ruffle",
|
||||
"RYUJINX": "Nintendo Switch emulator Ryujinx",
|
||||
"SAMEBOY": "Nintendo Game Boy and Game Boy Color emulator SameBoy",
|
||||
"SCUMMVM": "ScummVM game engine",
|
||||
"SDL2TRS": "Tandy TRS-80 emulator sdl2trs",
|
||||
"SHADPS4": "Sony PlayStation 4 emulator shadPS4",
|
||||
"SHADPS4-GUI": "Sony PlayStation 4 emulator shadPS4 (GUI)",
|
||||
"SHEEPSHAVER": "Apple Macintosh PowerPC emulator SheepShaver",
|
||||
"SIMCOUPE": "SAM Coupé emulator SimCoupé",
|
||||
"SIMPLE64": "Nintendo 64 emulator simple64",
|
||||
"SKYEMU": "Nintendo Game Boy, Game Boy Color, Game Boy Advance and DS emulator SkyEmu",
|
||||
"SNES9X": "Super Nintendo emulator Snes9x",
|
||||
"SOLARUS": "Solarus game engine",
|
||||
"STEAM": "Valve Steam",
|
||||
"STELLA": "Atari 2600 emulator Stella",
|
||||
"SUPERMODEL": "Sega Model 3 emulator Supermodel",
|
||||
"TIC-80": "TIC-80 Fantasy Computer (game engine)",
|
||||
"TRIFORCE": "Triforce, fork of Nintendo GameCube and Wii emulator Dolphin",
|
||||
"TSUGARU": "Fujitsu FM Towns emulator Tsugaru",
|
||||
"VBA-M": "Nintendo Game Boy Advance emulator VBA-M",
|
||||
"VICE-X64SC": "Commodore 8-bit computer emulator VICE (x64sc accurate)",
|
||||
"VICE-XPLUS4": "Commodore 8-bit computer emulator VICE (xplus4)",
|
||||
"VICE-XVIC": "Commodore 8-bit computer emulator VICE (xvic)",
|
||||
"VIRCON32": "Vircon32 Virtual Console",
|
||||
"VISUAL-PINBALL": "Pinball simulator Visual Pinball",
|
||||
"VITA3K": "Sony PlayStation Vita emulator Vita3K",
|
||||
"WINE": "Microsoft Windows compatibility layer Wine",
|
||||
"XEMU": "Microsoft Xbox emulator xemu",
|
||||
"XENIA": "Microsoft Xbox 360 emulator xenia",
|
||||
"XENIA-EDGE": "Microsoft Xbox 360 emulator Xenia Edge",
|
||||
"XENIA-WINDOWS": "Microsoft Xbox 360 emulator xenia",
|
||||
"XM6PRO68K-WINDOWS": "Sharp X68000 emulator XM6 Pro-68k",
|
||||
"XM6TYPEG-WINDOWS": "Sharp X68000 emulator XM6 TypeG",
|
||||
"XROAR": "Dragon and Tandy 8-bit computer emulator XRoar",
|
||||
"YMIR": "Sega Saturn emulator Ymir",
|
||||
"ZESARUX": "Sinclair ZX Spectrum emulator ZEsarUX"
|
||||
}
|
||||
BIN
vendors/es-de/emulators.linux.x64.sqlite
vendored
Normal file
BIN
vendors/es-de/emulators.linux.x64.sqlite
vendored
Normal file
Binary file not shown.
119
vendors/es-de/emulators.win32.x64.json
vendored
Normal file
119
vendors/es-de/emulators.win32.x64.json
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
{
|
||||
"OS-SHELL": "Operating system shell",
|
||||
"RETROARCH": "RETROARCH",
|
||||
"3DSEN": "Nintendo NES and Famicom emulator 3dSen",
|
||||
"A7800": "Atari 7800 emulator A7800",
|
||||
"ACE-DL": "Amstrad CPC emulator ACE-DL",
|
||||
"ADVANCEMAME": "Arcade emulator AdvanceMAME",
|
||||
"ALTIRRA": "Atari 8-bit computer emulator Altirra",
|
||||
"APPLEWIN": "Apple II emulator AppleWin",
|
||||
"ARCADEFLASHWEB": "Adobe Flash player ArcadeFlashWeb",
|
||||
"ARES": "Multi-system emulator ares",
|
||||
"ATARI800": "Atari 8-bit computer emulator Atari800",
|
||||
"AZAHAR": "Nintendo 3DS emulator Azahar",
|
||||
"AZAHARPLUS": "Nintendo 3DS emulator AzaharPlus",
|
||||
"BASILISKII": "Macintosh 68k emulator Basilisk II",
|
||||
"BEEBEM": "Acorn Computers BBC Micro emulator BeebEm",
|
||||
"BIGPEMU": "Atari Jaguar emulator BigPEmu",
|
||||
"BLASTEM": "Sega Mega Drive/Genesis emulator BlastEm",
|
||||
"BSNES": "Super Nintendo/Super Famicom emulator bsnes",
|
||||
"CEMU": "Nintendo Wii U emulator Cemu",
|
||||
"CITRA": "Nintendo 3DS emulator Citra",
|
||||
"COLEM": "Coleco ColecoVision and Adam emulator ColEm",
|
||||
"CPCEMU": "Amstrad CPC emulator CPCemu",
|
||||
"CSPECT": "Sinclair ZX Spectrum Next emulator #CSpect",
|
||||
"CXBX-RELOADED": "Microsoft Xbox emulator Cxbx-Reloaded",
|
||||
"DEMUL": "Sega Dreamcast emulator Demul",
|
||||
"DOLPHIN": "Nintendo GameCube and Wii emulator Dolphin",
|
||||
"DOSBOX-PURE-UNLEASHED": "DOS emulator DOSBox Pure Unleashed",
|
||||
"DOSBOX-STAGING": "DOS emulator DOSBox Staging",
|
||||
"DOSBOX-X": "DOS emulator DOSBox-X",
|
||||
"DREAMM": "LucasArts game engine DREAMM",
|
||||
"DUCKSTATION": "Sony PlayStation 1 emulator DuckStation",
|
||||
"EASYRPG": "EasyRPG game engine",
|
||||
"EKA2L1": "Symbian and Nokia N-Gage emulator EKA2L1",
|
||||
"FINALBURN-NEO": "Arcade emulator FinalBurn Neo",
|
||||
"FLYCAST": "Sega Dreamcast emulator Flycast",
|
||||
"FLYCAST-DOJO": "Sega Dreamcast emulator Flycast Dojo",
|
||||
"FS-UAE": "Commodore Amiga emulator FS-UAE",
|
||||
"FUSE": "Sinclair ZX Spectrum emulator Fuse",
|
||||
"FUTURE-PINBALL": "Pinball simulator Future Pinball",
|
||||
"FUTURE-PINBALL-FPLOADER": "Pinball simulator Future Pinball (FPLoader)",
|
||||
"GARGOYLE": "Interactive fiction player Gargoyle",
|
||||
"GEARBOY": "Nintendo Game Boy and Game Boy Color emulator Gearboy",
|
||||
"GOPHER2600": "Atari 2600 emulator Gopher2600",
|
||||
"GOPHER64": "Nintendo 64 emulator Gopher64",
|
||||
"HATARI": "Atari ST/STE/TT/Falcon emulator Hatari",
|
||||
"HYPSEUS-SINGE": "Hypseus Singe, fork of arcade LaserDisc emulator Daphne",
|
||||
"IZAPPLE2": "Apple II emulator izapple2",
|
||||
"JGENESIS": "Multi-system emulator jgenesis",
|
||||
"KEGS": "Apple IIGS emulator KEGS",
|
||||
"KEMULATOR": "Java 2 Micro Edition emulator KEmulator",
|
||||
"KRONOS": "Sega Saturn emulator Kronos",
|
||||
"LIME3DS": "Nintendo 3DS emulator Lime3DS",
|
||||
"M2EMULATOR": "Sega Model 2 Emulator",
|
||||
"MAME": "Arcade emulator MAME",
|
||||
"MANDARINE": "Nintendo 3DS emulator Mandarine",
|
||||
"MEDNAFEN": "Multi-system emulator Mednafen",
|
||||
"MELONDS": "Nintendo DS and DSi emulator melonDS",
|
||||
"MESEN": "Multi-system emulator Mesen",
|
||||
"MFME": "Fruit machine emulator MFME",
|
||||
"MGBA": "Nintendo Game Boy Advance emulator mGBA",
|
||||
"MUPEN64PLUS": "Nintendo 64 emulator Mupen64Plus",
|
||||
"NOODS": "Nintendo DS and Game Boy Advance emulator NooDS",
|
||||
"OPENMSX": "MSX home computer emulator openMSX",
|
||||
"ORICUTRON": "Tangerine Computer Systems emulator Oricutron",
|
||||
"PANDA3DS": "Nintendo 3DS emulator Panda3DS",
|
||||
"PARALLEL-LAUNCHER": "Nintendo 64 emulator Parallel Launcher",
|
||||
"PCSX2": "Sony PlayStation 2 emulator PCSX2",
|
||||
"PCSX2-LEGACY": "Sony PlayStation 2 emulator PCSX2 (legacy)",
|
||||
"PICO-8": "PICO-8 Fantasy Console (game engine)",
|
||||
"PLAY!": "Sony PlayStation 2 emulator Play!",
|
||||
"PPSSPP": "Sony PlayStation Portable emulator PPSSPP",
|
||||
"PRBOOM-PLUS": "Doom engine source port PrBoom+",
|
||||
"PRIMEHACK": "PrimeHack, fork of Nintendo GameCube and Wii emulator Dolphin",
|
||||
"PROJECT64": "Nintendo 64 emulator Project64",
|
||||
"PUNES": "Nintendo NES and Famicom emulator puNES",
|
||||
"QUASI88": "NEC PC-8800 series emulator QUASI88",
|
||||
"REDREAM": "Sega Dreamcast emulator Redream",
|
||||
"ROSALIES-MUPEN-GUI": "Nintendo 64 emulator Rosalie's Mupen GUI",
|
||||
"RPCS3": "Sony PlayStation 3 emulator RPCS3",
|
||||
"RUFFLE": "Adobe Flash player Ruffle",
|
||||
"RYUJINX": "Nintendo Switch emulator Ryujinx",
|
||||
"SAMEBOY": "Nintendo Game Boy and Game Boy Color emulator SameBoy",
|
||||
"SCUMMVM": "ScummVM game engine",
|
||||
"SDL2TRS": "Tandy TRS-80 emulator sdl2trs",
|
||||
"SHADPS4": "Sony PlayStation 4 emulator shadPS4",
|
||||
"SHADPS4-GUI": "Sony PlayStation 4 emulator shadPS4 (GUI)",
|
||||
"SHEEPSHAVER": "Apple Macintosh PowerPC emulator SheepShaver",
|
||||
"SIMCOUPE": "SAM Coupé emulator SimCoupé",
|
||||
"SIMPLE64": "Nintendo 64 emulator simple64",
|
||||
"SKYEMU": "Nintendo Game Boy, Game Boy Color, Game Boy Advance and DS emulator SkyEmu",
|
||||
"SNES9X": "Super Nintendo emulator Snes9x",
|
||||
"SOLARUS": "Solarus game engine",
|
||||
"SSF": "Sega Saturn emulator SSF",
|
||||
"STEAM": "Valve Steam",
|
||||
"STELLA": "Atari 2600 emulator Stella",
|
||||
"SUPERMODEL": "Sega Model 3 emulator Supermodel",
|
||||
"SUPERMODEL-DOJO": "Sega Model 3 emulator Supermodel Dojo",
|
||||
"TIC-80": "TIC-80 Fantasy Computer (game engine)",
|
||||
"TRIFORCE": "Triforce, fork of Nintendo GameCube and Wii emulator Dolphin",
|
||||
"TSUGARU": "Fujitsu FM Towns emulator Tsugaru",
|
||||
"VBA-M": "Nintendo Game Boy Advance emulator VBA-M",
|
||||
"VICE-X64SC": "Commodore 8-bit computer emulator VICE (x64sc accurate)",
|
||||
"VICE-XPLUS4": "Commodore 8-bit computer emulator VICE (xplus4)",
|
||||
"VICE-XVIC": "Commodore 8-bit computer emulator VICE (xvic)",
|
||||
"VIRCON32": "Vircon32 Virtual Console",
|
||||
"VISUAL-PINBALL": "Pinball simulator Visual Pinball",
|
||||
"VITA3K": "Sony PlayStation Vita emulator Vita3K",
|
||||
"WINARCADIA": "Emerson Arcadia 2001 emulator WinArcadia",
|
||||
"XEMU": "Microsoft Xbox emulator xemu",
|
||||
"XENIA": "Microsoft Xbox 360 emulator xenia",
|
||||
"XENIA-EDGE": "Microsoft Xbox 360 emulator Xenia Edge",
|
||||
"XM6PRO68K": "Sharp X68000 emulator XM6 Pro-68k",
|
||||
"XM6TYPEG": "Sharp X68000 emulator XM6 TypeG",
|
||||
"XROAR": "Dragon and Tandy 8-bit computer emulator XRoar",
|
||||
"YABASANSHIRO-2": "Sega Saturn emulator Yaba Sanshiro 2",
|
||||
"YMIR": "Sega Saturn emulator Ymir",
|
||||
"ZESARUX": "Sinclair ZX Spectrum emulator ZEsarUX"
|
||||
}
|
||||
BIN
vendors/es-de/emulators.win32.x64.sqlite
vendored
Normal file
BIN
vendors/es-de/emulators.win32.x64.sqlite
vendored
Normal file
Binary file not shown.
523
vendors/es-de/systems/android/es_find_rules.xml
vendored
Normal file
523
vendors/es-de/systems/android/es_find_rules.xml
vendored
Normal file
|
|
@ -0,0 +1,523 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- This is the ES-DE Frontend find rules configuration file for Android -->
|
||||
<ruleList>
|
||||
<emulator name="RETROARCH">
|
||||
<rule type="androidpackage">
|
||||
<!-- 64-bit and 32-bit releases from the retroarch.com website -->
|
||||
<entry>com.retroarch.aarch64/com.retroarch.browser.retroactivity.RetroActivityFuture</entry>
|
||||
<entry>com.retroarch.ra32/com.retroarch.browser.retroactivity.RetroActivityFuture</entry>
|
||||
<!-- All other releases -->
|
||||
<entry>com.retroarch/com.retroarch.browser.retroactivity.RetroActivityFuture</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="2600-EMU">
|
||||
<!-- Atari 2600 emulator 2600.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.A2600Emu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="AETHERSX2">
|
||||
<!-- Sony PlayStation 2 emulator AetherSX2 -->
|
||||
<rule type="androidpackage">
|
||||
<entry>xyz.aethersx2.android/.EmulationActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="APS3E">
|
||||
<!-- Sony PlayStation 3 emulator aPS3e -->
|
||||
<rule type="androidpackage">
|
||||
<entry>aenu.aps3e.premium/aenu.aps3e.EmulatorActivity</entry>
|
||||
<entry>aenu.aps3e/aenu.aps3e.EmulatorActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="ARMSX2">
|
||||
<!-- Sony PlayStation 2 emulator ARMSX2 -->
|
||||
<rule type="androidpackage">
|
||||
<entry>come.nanodata.armsx2/kr.co.iefriends.pcsx2.MainActivity</entry>
|
||||
<entry>come.nanodata.armsx2.debug/kr.co.iefriends.pcsx2.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="AZAHAR">
|
||||
<!-- Nintendo 3DS emulator Azahar -->
|
||||
<rule type="androidpackage">
|
||||
<entry>io.github.lime3ds.android/org.citra.citra_emu.activities.EmulationActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="AZAHARPLUS">
|
||||
<!-- Nintendo 3DS emulator AzaharPlus -->
|
||||
<rule type="androidpackage">
|
||||
<entry>io.github.azaharplus.android/org.citra.citra_emu.activities.EmulationActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="C64-EMU">
|
||||
<!-- Commodore 8-bit computer emulator C64.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.C64Emu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="CEMU">
|
||||
<!-- Nintendo Wii U emulator Cemu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>info.cemu.cemu/info.cemu.cemu.emulation.EmulationActivity</entry>
|
||||
<entry>info.cemu.Cemu/info.cemu.Cemu.emulation.EmulationActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="CITRA">
|
||||
<!-- Nintendo 3DS emulator Citra -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.citra.citra_emu/.activities.EmulationActivity</entry>
|
||||
<entry>org.citra.citra_emu/.ui.main.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="CITRA-CANARY">
|
||||
<!-- Nintendo 3DS emulator Citra Canary -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.citra.citra_emu.canary/org.citra.citra_emu.activities.EmulationActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="CITRA-MMJ">
|
||||
<!-- Nintendo 3DS emulator Citra MMJ -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.citra.emu/.ui.EmulationActivity</entry>
|
||||
<entry>com.antutu.ABenchMark/org.citra.emu.ui.EmulationActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="COLEM">
|
||||
<!-- Coleco ColecoVision and Adam emulator ColEm -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.fms.colem.deluxe/com.fms.emulib.TVActivity</entry>
|
||||
<entry>com.fms.colem/com.fms.emulib.TVActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="DOLPHIN">
|
||||
<!-- Nintendo GameCube and Wii emulator Dolphin -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.dolphinemu.dolphinemu/.ui.main.TvMainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="DOLPHIN-MMJR">
|
||||
<!-- Dolphin MMJR, fork of Nintendo GameCube and Wii emulator Dolphin -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.mm.jr/org.dolphinemu.dolphinemu.ui.main.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="DOLPHIN-MMJR2">
|
||||
<!-- Dolphin MMJR2, fork of Nintendo GameCube and Wii emulator Dolphin -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.dolphinemu.mmjr/org.dolphinemu.dolphinemu.ui.main.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="DROIDARCADIA">
|
||||
<!-- Emerson Arcadia 2001 emulator DroidArcadia -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.amigan.droidarcadia/.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="DRASTIC">
|
||||
<!-- Nintendo DS emulator DraStic -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.dsemu.drastic/.DraSticActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="DUCKSTATION">
|
||||
<!-- Sony PlayStation 1 emulator DuckStation -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.github.stenzek.duckstation/.EmulationActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="EKA2L1">
|
||||
<!-- Symbian and Nokia N-Gage emulator EKA2L1 -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.github.eka2l1/.emu.EmulatorActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="EPSXE">
|
||||
<!-- Sony PlayStation 1 emulator ePSXe -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.epsxe.ePSXe/.ePSXe</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="FLYCAST">
|
||||
<!-- Sega Dreamcast emulator Flycast -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.flycast.emulator/com.flycast.emulator.MainActivity</entry>
|
||||
<entry>com.flycast.emulator/com.reicast.emulator.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="FMSX">
|
||||
<!-- MSX home computer emulator fMSX -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.fms.fmsx.deluxe/com.fms.emulib.TVActivity</entry>
|
||||
<entry>com.fms.fmsx/com.fms.emulib.TVActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="FPSE">
|
||||
<!-- Sony PlayStation 1 emulator FPse -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.emulator.fpse/.Main</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="FPSE-NG">
|
||||
<!-- Sony PlayStation 1 emulator FPseNG -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.emulator.fpse64/.Main</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="GAMEHUB-LITE">
|
||||
<!-- Steam client GameHub Lite -->
|
||||
<rule type="androidpackage">
|
||||
<entry>emuready.gamehub.lite/com.xj.landscape.launcher.ui.gamedetail.GameDetailActivity</entry>
|
||||
<entry>gamehub.lite/com.xj.landscape.launcher.ui.gamedetail.GameDetailActivity</entry>
|
||||
<entry>com.antutu.ABenchMark/com.xj.landscape.launcher.ui.gamedetail.GameDetailActivity</entry>
|
||||
<entry>com.tencent.ig/com.xj.landscape.launcher.ui.gamedetail.GameDetailActivity</entry>
|
||||
<entry>com.ludashi.aibench/com.xj.landscape.launcher.ui.gamedetail.GameDetailActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="GAMENATIVE">
|
||||
<!-- Steam client GameNative -->
|
||||
<rule type="androidpackage">
|
||||
<entry>app.gamenative/.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="GBA-EMU">
|
||||
<!-- Nintendo Game Boy Advance emulator GBA.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.GbaEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="GBC-EMU">
|
||||
<!-- Nintendo Game Boy and Game Boy Color emulator GBC.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.GbcEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="INES">
|
||||
<!-- Nintendo NES and Famicom emulator iNES -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.fms.ines.free/com.fms.emulib.TVActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="INFINITY">
|
||||
<!-- PICO-8 Fantasy Console game engine Infinity -->
|
||||
<rule type="androidpackage">
|
||||
<entry>me.dt2dev.infinity/.SchemeActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="IRATAJAGUAR">
|
||||
<!-- Atari Jaguar emulator IrataJaguar -->
|
||||
<rule type="androidpackage">
|
||||
<entry>ru.vastness.altmer.iratajaguar/.EmulatorActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="J2ME-LOADER">
|
||||
<!-- Java 2 Micro Edition emulator J2ME Loader -->
|
||||
<rule type="androidpackage">
|
||||
<entry>ru.playsoftware.j2meloader/.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="JL-MOD">
|
||||
<!-- Java 2 Micro Edition emulator JL-Mod -->
|
||||
<rule type="androidpackage">
|
||||
<entry>ru.woesss.j2meloader/ru.playsoftware.j2meloader.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="KENJI-NX">
|
||||
<!-- Nintendo Switch emulator Kenji-NX -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.kenjinx.android/.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="LIME3DS">
|
||||
<!-- Nintendo 3DS emulator Lime3DS -->
|
||||
<rule type="androidpackage">
|
||||
<entry>io.github.lime3ds.android/.activities.EmulationActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="LINKBOY">
|
||||
<!-- Nintendo Game Boy, Game Boy Color and Game Boy Advance emulator Linkboy -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.pixelrespawn.linkboy/.EmulatorActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="LYNX-EMU">
|
||||
<!-- Atari Lynx emulator Lynx.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.LynxEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="M64PLUS-FZ">
|
||||
<!-- Nintendo 64 emulator M64Plus FZ -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.mupen64plusae.v3.fzurita.amazon/paulscode.android.mupen64plusae.SplashActivity</entry>
|
||||
<entry>org.mupen64plusae.v3.fzurita.pro/paulscode.android.mupen64plusae.SplashActivity</entry>
|
||||
<entry>org.mupen64plusae.v3.fzurita/paulscode.android.mupen64plusae.SplashActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MAME4DROID-CURRENT">
|
||||
<!-- Arcade emulator MAME4droid Current -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.seleuco.mame4d2024/com.seleuco.mame4droid.MAME4droid</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MAME4DROID">
|
||||
<!-- Arcade emulator MAME4droid -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.seleuco.mame4droid/.MAME4droid</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MAME4DROID-2024">
|
||||
<!-- Arcade emulator MAME4droid 2024 -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.seleuco.mame4d2024/com.seleuco.mame4droid.MAME4droid</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MANDARINE">
|
||||
<!-- Nintendo 3DS emulator Mandarine -->
|
||||
<rule type="androidpackage">
|
||||
<entry>io.github.mandarine3ds.mandarine/.activities.EmulationActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MASTERGEAR">
|
||||
<!-- Sega Game Gear, Master System and SG-1000 emulator MasterGear -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.fms.mg/com.fms.emulib.TVActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MD-EMU">
|
||||
<!-- Sega Mega Drive/Genesis emulator MD.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.MdEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MELONDS">
|
||||
<!-- Nintendo DS and DSi emulator melonDS -->
|
||||
<rule type="androidpackage">
|
||||
<entry>me.magnum.melonds/.ui.emulator.EmulatorActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MELONDS-NIGHTLY">
|
||||
<!-- Nintendo DS and DSi emulator melonDS (Nightly build) -->
|
||||
<rule type="androidpackage">
|
||||
<entry>me.magnum.melonds.nightly/me.magnum.melonds.ui.emulator.EmulatorActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MSX-EMU">
|
||||
<!-- MSX home computer and ColecoVision emulator MSX.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.MsxEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MUPEN64PLUS-AE">
|
||||
<!-- Nintendo 64 emulator Mupen64Plus AE -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.mupen64plusae.v3.alpha/paulscode.android.mupen64plusae.SplashActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MY-BOY">
|
||||
<!-- Nintendo Game Boy Advance emulator My Boy! -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.fastemulator.gba/.EmulatorActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MY-OLDBOY">
|
||||
<!-- Nintendo Game Boy and Game Boy Color emulator My OldBoy! -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.fastemulator.gbc/.EmulatorActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="NEO-EMU">
|
||||
<!-- SNK Neo Geo emulator NEO.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.NeoEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="NES-EMU">
|
||||
<!-- Nintendo NES and Famicom emulator NES.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.NesEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="NESOID">
|
||||
<!-- Nintendo NES and Famicom emulator Nesoid -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.androidemu.nes/.EmulatorActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="NGP-EMU">
|
||||
<!-- SNK Neo Geo Pocket emulator NGP.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.NgpEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="NOODS">
|
||||
<!-- Nintendo DS and Game Boy Advance emulator NooDS -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.hydra.noods/.FileBrowser</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="OPENBOR">
|
||||
<!-- OpenBOR game engine -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.openbor.engine/.GameActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="PANDA3DS">
|
||||
<!-- Nintendo 3DS emulator Panda3DS -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.panda3ds.pandroid/.app.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="PCE-EMU">
|
||||
<!-- NEC PC Engine/TurboGrafx-16 emulator PCE.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.PceEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="PIZZA-BOY-GBA">
|
||||
<!-- Nintendo Game Boy Advance emulator Pizza Boy GBA -->
|
||||
<rule type="androidpackage">
|
||||
<entry>it.dbtecno.pizzaboygbapro/it.dbtecno.pizzaboygbapro.MainActivity</entry>
|
||||
<entry>it.dbtecno.pizzaboygba/it.dbtecno.pizzaboygba.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="PIZZA-BOY-GBC">
|
||||
<!-- Nintendo Game Boy and Game Boy Color emulator Pizza Boy GBC -->
|
||||
<rule type="androidpackage">
|
||||
<entry>it.dbtecno.pizzaboypro/it.dbtecno.pizzaboypro.MainActivity</entry>
|
||||
<entry>it.dbtecno.pizzaboy/it.dbtecno.pizzaboy.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="PIZZA-BOY-SC">
|
||||
<!-- Sega multi-system emulator Pizza Boy SC -->
|
||||
<rule type="androidpackage">
|
||||
<entry>it.dbtecno.pizzaboyscpro/.MainActivity</entry>
|
||||
<entry>it.dbtecno.pizzaboyscbasic/.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="PLAY!">
|
||||
<!-- Sony PlayStation 2 emulator Play! -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.virtualapplications.play/.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="PPSSPP">
|
||||
<!-- Sony PlayStation Portable emulator PPSSPP -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.ppsspp.ppssppgold/org.ppsspp.ppsspp.PpssppActivity</entry>
|
||||
<entry>org.ppsspp.ppsspp/.PpssppActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="REAL3DOPLAYER">
|
||||
<!-- 3DO Interactive Multiplayer emulator Real3DOPlayer -->
|
||||
<rule type="androidpackage">
|
||||
<entry>ru.vastness.altmer.real3doplayer/.EmulatorActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="REDREAM">
|
||||
<!-- Sega Dreamcast emulator Redream -->
|
||||
<rule type="androidpackage">
|
||||
<entry>io.recompiled.redream/.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="RUFFLE">
|
||||
<!-- Adobe Flash player Ruffle -->
|
||||
<rule type="androidpackage">
|
||||
<entry>rs.ruffle/.PlayerActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="SATURN-EMU">
|
||||
<!-- Sega Saturn emulator Saturn.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.SaturnEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="SCUMMVM">
|
||||
<!-- ScummVM game engine -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.scummvm.scummvm.debug/org.scummvm.scummvm.SplashActivity</entry>
|
||||
<entry>org.scummvm.scummvm/org.scummvm.scummvm.SplashActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="SKYEMU">
|
||||
<!-- Nintendo Game Boy, Game Boy Color, Game Boy Advance and DS emulator SkyEmu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.sky.SkyEmu/.EnhancedNativeActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="SKYLINE">
|
||||
<!-- Nintendo Switch emulator Skyline -->
|
||||
<rule type="androidpackage">
|
||||
<entry>skyline.emu/emu.skyline.EmulationActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="SNES9X-EXPLUS">
|
||||
<!-- Super Nintendo emulator Snes9x EX+ -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.Snes9xPlus/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="SPECCY">
|
||||
<!-- Sinclair ZX Spectrum emulator Speccy -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.fms.speccy.deluxe/com.fms.emulib.TVActivity</entry>
|
||||
<entry>com.fms.speccy/com.fms.emulib.TVActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="SWAN-EMU">
|
||||
<!-- Bandai WonderSwan emulator Swan.emu -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.explusalpha.SwanEmu/com.imagine.BaseActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="SWF-PLAYER">
|
||||
<!-- Adobe Flash player SWF Player -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.issess.flashplayerpro/com.issess.flashplayer.player.FlashPlayerActivity</entry>
|
||||
<entry>com.issess.flashplayer/.player.FlashPlayerActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="VIRTUAL-VIRTUAL-BOY">
|
||||
<!-- Nintendo Virtual Boy emulator Virtual Virtual Boy -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.simongellis.vvb/.MainActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="VISUAL-PINBALL">
|
||||
<!-- Pinball simulator Visual Pinball -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.vpinball.vpinball_bgfx/org.vpinball.app.VPinballActivity</entry>
|
||||
<entry>org.vpinball.app/.VpxLauncherActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="VITA3K">
|
||||
<!-- Sony PlayStation Vita emulator Vita3K -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.vita3k.emulator/.Emulator</entry>
|
||||
<entry>org.vita3k.emulator.ikhoeyZX/org.vita3k.emulator.Emulator</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="WINLATOR-CMOD">
|
||||
<!-- Microsoft Windows emulator Winlator Cmod -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.winlator.cmod/.XServerDisplayActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="WINLATOR-GLIBC">
|
||||
<!-- Microsoft Windows emulator Winlator Cmod Glibc -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.winlator/.XServerDisplayActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="WINLATOR-PROOT">
|
||||
<!-- Microsoft Windows emulator Winlator Cmod PRoot -->
|
||||
<rule type="androidpackage">
|
||||
<entry>com.cmodded.winlator/com.winlator.XServerDisplayActivity</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="YABASANSHIRO-2">
|
||||
<!-- Sega Saturn emulator Yaba Sanshiro 2 -->
|
||||
<rule type="androidpackage">
|
||||
<entry>org.devmiyax.yabasanshioro2.pro/org.uoyabause.android.Yabause</entry>
|
||||
<entry>org.devmiyax.yabasanshioro2/org.uoyabause.android.Yabause</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
</ruleList>
|
||||
29
vendors/es-de/systems/android/es_import_rules.xml
vendored
Normal file
29
vendors/es-de/systems/android/es_import_rules.xml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- This is the ES-DE import rules configuration file for Android -->
|
||||
<ruleList>
|
||||
<system name="androidapps">
|
||||
<rule name="Android Package" type="androidpackage">
|
||||
<extension>.app</extension>
|
||||
</rule>
|
||||
</system>
|
||||
<system name="androidgames">
|
||||
<rule name="Android Package" type="androidpackage">
|
||||
<extension>.app</extension>
|
||||
</rule>
|
||||
</system>
|
||||
<system name="emulators">
|
||||
<rule name="Android Package" type="androidpackage">
|
||||
<extension>.app</extension>
|
||||
</rule>
|
||||
</system>
|
||||
<system name="epic">
|
||||
<rule name="Android Package" type="androidpackage">
|
||||
<extension>.app</extension>
|
||||
</rule>
|
||||
</system>
|
||||
<system name="n64">
|
||||
<rule name="Android Package" type="androidpackage">
|
||||
<extension>.app</extension>
|
||||
</rule>
|
||||
</system>
|
||||
</ruleList>
|
||||
2184
vendors/es-de/systems/android/es_systems.xml
vendored
Normal file
2184
vendors/es-de/systems/android/es_systems.xml
vendored
Normal file
File diff suppressed because it is too large
Load diff
138
vendors/es-de/systems/haiku/es_find_rules.xml
vendored
Normal file
138
vendors/es-de/systems/haiku/es_find_rules.xml
vendored
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- This is the ES-DE find rules configuration file for Haiku -->
|
||||
<ruleList>
|
||||
<emulator name="OS-SHELL">
|
||||
<!-- Operating system shell -->
|
||||
<rule type="systempath">
|
||||
<entry>bash</entry>
|
||||
<entry>sh</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="RETROARCH">
|
||||
<rule type="systempath">
|
||||
<entry>retroarch</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/boot/system/apps/retroarch</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<core name="RETROARCH">
|
||||
<rule type="corepath">
|
||||
<entry>/boot/system/add-ons/libretro</entry>
|
||||
</rule>
|
||||
</core>
|
||||
<emulator name="ADVANCEMAME">
|
||||
<!-- Arcade emulator AdvanceMAME -->
|
||||
<rule type="systempath">
|
||||
<entry>advmame</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/bin/advmame</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="DOSBOX-X">
|
||||
<!-- DOS emulator DOSBox-X -->
|
||||
<rule type="systempath">
|
||||
<entry>DOSBox-X</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/boot/system/apps/DOSBox-X</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="FINALBURN-NEO">
|
||||
<!-- Arcade emulator FinalBurn Neo -->
|
||||
<rule type="systempath">
|
||||
<entry>fbneo</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/boot/system/apps/fbneo</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="FS-UAE">
|
||||
<!-- Commodore Amiga emulator FS-UAE -->
|
||||
<rule type="systempath">
|
||||
<entry>fs-uae-launcher</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/bin/fs-uae-launcher</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="FUSE">
|
||||
<!-- Sinclair ZX Spectrum emulator Fuse -->
|
||||
<rule type="staticpath">
|
||||
<entry>/boot/system/apps/Fuse/Fuse</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MAME">
|
||||
<!-- Arcade emulator MAME -->
|
||||
<rule type="systempath">
|
||||
<entry>mame</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/bin/mame</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MEDNAFEN">
|
||||
<!-- Multi-system emulator Mednafen -->
|
||||
<rule type="systempath">
|
||||
<entry>mednafen</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/bin/mednafen</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MELONDS">
|
||||
<!-- Nintendo DS and DSi emulator melonDS -->
|
||||
<rule type="systempath">
|
||||
<entry>melonDS</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/boot/system/apps/melonDS</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MGBA">
|
||||
<!-- Nintendo Game Boy Advance emulator mGBA -->
|
||||
<rule type="systempath">
|
||||
<entry>mgba</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/bin/mgba</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="MUPEN64PLUS">
|
||||
<!-- Nintendo 64 emulator Mupen64Plus -->
|
||||
<rule type="staticpath">
|
||||
<entry>/boot/system/apps/Mupen64Plus/bin/Mupen64Plus</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="PPSSPP">
|
||||
<!-- Sony PlayStation Portable emulator PPSSPP -->
|
||||
<rule type="staticpath">
|
||||
<entry>/boot/system/apps/PPSSPP/PPSSPP</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="SCUMMVM">
|
||||
<!-- ScummVM game engine -->
|
||||
<rule type="systempath">
|
||||
<entry>ScummVM</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/boot/system/apps/ScummVM</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="SOLARUS">
|
||||
<!-- Solarus game engine -->
|
||||
<rule type="systempath">
|
||||
<entry>solarus-run</entry>
|
||||
</rule>
|
||||
<rule type="staticpath">
|
||||
<entry>/bin/solarus-run</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
<emulator name="ZESARUX">
|
||||
<!-- Sinclair ZX Spectrum emulator ZEsarUX -->
|
||||
<rule type="staticpath">
|
||||
<entry>/boot/system/apps/ZEsarUX/ZEsarUX</entry>
|
||||
</rule>
|
||||
</emulator>
|
||||
</ruleList>
|
||||
4
vendors/es-de/systems/haiku/es_import_rules.xml
vendored
Normal file
4
vendors/es-de/systems/haiku/es_import_rules.xml
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- This is the ES-DE import rules configuration file for Haiku -->
|
||||
<ruleList>
|
||||
</ruleList>
|
||||
1952
vendors/es-de/systems/haiku/es_systems.xml
vendored
Normal file
1952
vendors/es-de/systems/haiku/es_systems.xml
vendored
Normal file
File diff suppressed because it is too large
Load diff
1492
vendors/es-de/systems/linux/es_find_rules.xml
vendored
Normal file
1492
vendors/es-de/systems/linux/es_find_rules.xml
vendored
Normal file
File diff suppressed because it is too large
Load diff
47
vendors/es-de/systems/linux/es_import_rules.xml
vendored
Normal file
47
vendors/es-de/systems/linux/es_import_rules.xml
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- This is the ES-DE import rules configuration file for Linux -->
|
||||
<ruleList>
|
||||
<system name="desktop">
|
||||
<rule name="Desktop Shortcut" type="desktopshortcut">
|
||||
<directory>/usr/share/applications</directory>
|
||||
<directory>/usr/local/share/applications</directory>
|
||||
<directory>~/.local/share/applications</directory>
|
||||
<directory>/var/lib/flatpak/exports/share/applications</directory>
|
||||
<directory>/var/lib/snapd/desktop/applications</directory>
|
||||
<directory>~/Desktop</directory>
|
||||
</rule>
|
||||
</system>
|
||||
<system name="emulators">
|
||||
<rule name="Desktop Shortcut" type="desktopshortcut">
|
||||
<directory>/usr/share/applications</directory>
|
||||
<directory>/usr/local/share/applications</directory>
|
||||
<directory>~/.local/share/applications</directory>
|
||||
<directory>/var/lib/flatpak/exports/share/applications</directory>
|
||||
<directory>/var/lib/snapd/desktop/applications</directory>
|
||||
</rule>
|
||||
</system>
|
||||
<system name="epic">
|
||||
<rule name="Desktop Shortcut" type="desktopshortcut">
|
||||
<directory execFilter="heroic://launch">~/.local/share/applications</directory>
|
||||
<directory execFilter="heroic://launch">~/Desktop</directory>
|
||||
</rule>
|
||||
</system>
|
||||
<system name="lutris">
|
||||
<rule name="Desktop Shortcut" type="desktopshortcut">
|
||||
<directory execFilter="lutris:rungameid">~/.local/share/applications</directory>
|
||||
<directory execFilter="lutris:rungameid">~/Desktop</directory>
|
||||
</rule>
|
||||
</system>
|
||||
<system name="ps3">
|
||||
<rule name="Desktop Shortcut" type="desktopshortcut">
|
||||
<directory execFilter="RPCS3_GAMEID">~/.local/share/applications/RPCS3</directory>
|
||||
<directory execFilter="RPCS3_GAMEID">~/Desktop</directory>
|
||||
</rule>
|
||||
</system>
|
||||
<system name="steam">
|
||||
<rule name="Desktop Shortcut" type="desktopshortcut">
|
||||
<directory execFilter="steam://rungameid">~/.local/share/applications</directory>
|
||||
<directory execFilter="steam://rungameid">~/Desktop</directory>
|
||||
</rule>
|
||||
</system>
|
||||
</ruleList>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue