feat: implemented a basic store and emulatorjs
This commit is contained in:
parent
2f32cbc730
commit
7286541822
121 changed files with 5900 additions and 1092 deletions
|
|
@ -4,6 +4,8 @@ on:
|
||||||
tags:
|
tags:
|
||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
env:
|
||||||
|
TWITCH_CLIENT_ID: ${{ env.TWITCH_CLIENT_ID }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -54,6 +56,8 @@ jobs:
|
||||||
|
|
||||||
- name: Build Canary
|
- name: Build Canary
|
||||||
run: bun run package:Linux
|
run: bun run package:Linux
|
||||||
|
env:
|
||||||
|
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
|
|
||||||
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
|
|
@ -59,6 +59,7 @@ jobs:
|
||||||
run: bun run package:${{ runner.os }}
|
run: bun run package:${{ runner.os }}
|
||||||
env:
|
env:
|
||||||
BUILD_DIR: ./build/${{ runner.os }}
|
BUILD_DIR: ./build/${{ runner.os }}
|
||||||
|
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Install 7zip (minimal)
|
- name: Install 7zip (minimal)
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -26,3 +26,4 @@ trace
|
||||||
downloads
|
downloads
|
||||||
.flatpak-builder
|
.flatpak-builder
|
||||||
gameflow-deck.code-workspace
|
gameflow-deck.code-workspace
|
||||||
|
.env.local
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -9,7 +9,7 @@
|
||||||
"internalConsoleOptions": "neverOpen",
|
"internalConsoleOptions": "neverOpen",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"name": "Attach Bun",
|
"name": "Attach Bun",
|
||||||
"url": "ws://127.0.0.1:9229/fixed-session",
|
"url": "ws://127.0.0.1:9228/fixed-session",
|
||||||
"localRoot": "${workspaceFolder}",
|
"localRoot": "${workspaceFolder}",
|
||||||
"stopOnEntry": false,
|
"stopOnEntry": false,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
5
.vscode/tasks.json
vendored
5
.vscode/tasks.json
vendored
|
|
@ -38,6 +38,11 @@
|
||||||
"label": "Start Dev (Hot Reload)",
|
"label": "Start Dev (Hot Reload)",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "bun run dev:hmr",
|
"command": "bun run dev:hmr",
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
"FORCE_BROWSER": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"presentation": {
|
"presentation": {
|
||||||
|
|
|
||||||
327
bun.lock
327
bun.lock
|
|
@ -9,6 +9,7 @@
|
||||||
"@elysiajs/cors": "^1.4.1",
|
"@elysiajs/cors": "^1.4.1",
|
||||||
"@elysiajs/eden": "^1.4.6",
|
"@elysiajs/eden": "^1.4.6",
|
||||||
"@elysiajs/static": "^1.4.7",
|
"@elysiajs/static": "^1.4.7",
|
||||||
|
"@jimp/wasm-webp": "^1.6.0",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"conf": "^15.0.2",
|
"conf": "^15.0.2",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
"systeminformation": "^5.31.1",
|
"systeminformation": "^5.31.1",
|
||||||
"tough-cookie": "^6.0.0",
|
"tough-cookie": "^6.0.0",
|
||||||
"tough-cookie-file-store": "^3.3.0",
|
"tough-cookie-file-store": "^3.3.0",
|
||||||
|
"ts-igdb-client": "^0.4.2",
|
||||||
"unzip-stream": "^0.3.4",
|
"unzip-stream": "^0.3.4",
|
||||||
"webview-bun": "^2.4.0",
|
"webview-bun": "^2.4.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
|
|
@ -31,6 +33,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ap0nia/eden": "^1.0.0-next.22",
|
"@ap0nia/eden": "^1.0.0-next.22",
|
||||||
"@ap0nia/eden-tanstack-query": "^1.0.0-next.22",
|
"@ap0nia/eden-tanstack-query": "^1.0.0-next.22",
|
||||||
|
"@emulatorjs/emulatorjs": "^4.2.3",
|
||||||
"@hey-api/openapi-ts": "^0.91.0",
|
"@hey-api/openapi-ts": "^0.91.0",
|
||||||
"@noriginmedia/norigin-spatial-navigation": "^2.3.0",
|
"@noriginmedia/norigin-spatial-navigation": "^2.3.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
|
@ -140,6 +143,106 @@
|
||||||
|
|
||||||
"@elysiajs/static": ["@elysiajs/static@1.4.7", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Go4kIXZ0G3iWfkAld07HmLglqIDMVXdyRKBQK/sVEjtpDdjHNb+rUIje73aDTWpZYg4PEVHUpi9v4AlNEwrQug=="],
|
"@elysiajs/static": ["@elysiajs/static@1.4.7", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Go4kIXZ0G3iWfkAld07HmLglqIDMVXdyRKBQK/sVEjtpDdjHNb+rUIje73aDTWpZYg4PEVHUpi9v4AlNEwrQug=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-81": ["@emulatorjs/core-81@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-oPQEqjpR3z7Yedte4u3sOXDZ4NXAykNcbENjYcB+x3QshF8I+3MQCo8kINOT2lsqqgx91WR4kmEaYQqU39YsDA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-a5200": ["@emulatorjs/core-a5200@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-/9yS0/MKHp/wO9iuxWfWTGUwiVNKykEOb7fEN5UM9BfIVQ1SAqep4Ji+TigmYW4weH/mASvYzON9ett3dmD6oQ=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-beetle_vb": ["@emulatorjs/core-beetle_vb@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-/e6qkA/spYw27qDmzxMiMW5ic36N5W1AVU3Qki8BTZGWKXDseZtNuV/zlPts9rwtTuH8k4+fsQLF+dHp905+/Q=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-cap32": ["@emulatorjs/core-cap32@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-RkEpGVsOo3S0ZEdDzhfZ5qjeYWYEeK6rGjyvJhYVUnmpPGSQFXvPu0POtIKFyTk9XQrlicllep8ZHzEC7Nngsg=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-crocods": ["@emulatorjs/core-crocods@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-p536fgB5ROd2CjOeDxGxB8YVKrZ1sGS9X0lYBahCmk6unkz4FL8bU20/TQk4XdKMIsSkhiyonvBg8JZZbs5mWg=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-desmume": ["@emulatorjs/core-desmume@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-3WgGPBWzjZJWPDKVOaK/Jmg34as0nMY9ClfmrGEiMFUfa3tocpQvL9unJ8Oh4ofWEDTf+45bak/PZ7wp2xwekA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-desmume2015": ["@emulatorjs/core-desmume2015@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-GpSuu7/q4yIEbBGrZUlJV3CHoH/b+cNs9Xn0PnC2WTkIuQgEcD5FxUFLup+v208tEKB3bPxXeaLX72i13bkspQ=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-dosbox_pure": ["@emulatorjs/core-dosbox_pure@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-48CT0ztvnh/M+NRLtHS+pSysdnvH+p+6tgMLJU3+jvfPXdX1dlksiq8PvHPwtpuEF3d9mt2yECCBY9Vq6nkgdw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-fbalpha2012_cps1": ["@emulatorjs/core-fbalpha2012_cps1@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-GeAEX/mJcWskcMnEZ1OjjefftL2vznmE+Lms19XyRRSwfJ23/wc8p1mt7P82QCR/aB0925gksjJqOX2WBvKV6A=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-fbalpha2012_cps2": ["@emulatorjs/core-fbalpha2012_cps2@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-RqmLd0J/Gh8dkyjnpGN7YGIRS1fJjtCKqBs8r54Uz35KmHxw4Qs/ghCc836DRXP/IrIYX871jMxVvsOIOdCB/g=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-fbneo": ["@emulatorjs/core-fbneo@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-54Yx8Kcq8DH42caTakws3pMUZc6UkBNqtZgg7Q04lNmWWc/W0wPGN0gHxG90xmlOFGHHtxp9QnicBb1NXMjGWg=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-fceumm": ["@emulatorjs/core-fceumm@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-XX9Vv2N/hzp0TstNMCTSppEs+sg+1lpJpPdSDuRqIO/cwdt7dUcF+WjNX1yQJLRbP5+XwcNHZ6K4BKy8CJpndQ=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-fuse": ["@emulatorjs/core-fuse@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-LIu2Zc9ALYWoLN5iSanA0LJWsDx9w/OeIu6oueVEpKpej064vf2OKIYYlQz9v6CwwmhsnmY0bURbBPS70gkgCQ=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-gambatte": ["@emulatorjs/core-gambatte@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-Oq3AJL9SKnMsNAmtAP2eN934TQts3NHeaAe0z1BO9Lx/L3xDZKLpxL+XwgcSXIrl4Sx1oXxYEUwD7JjPEQ0DGg=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-gearcoleco": ["@emulatorjs/core-gearcoleco@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-todwg9FhUzIBe1xkut+HOKmXvwIHgLSNKwERJm2yfJMY7gx/S1MHITHWWUHL3qxSmApacEucr9nZfpuTqVcjpA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-genesis_plus_gx": ["@emulatorjs/core-genesis_plus_gx@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-D1e2XA2CPRAjfr0JurpXJgR9dePxl/xHtaE8v1T9BqD9A3DLfobIFWsWU9jCFS4NfUxCvcLdTYdY/We3QzvcTw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-handy": ["@emulatorjs/core-handy@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-miv2nSSVIIHFcGeEdeO7BpYKsljL1j+0an4BO7xw44s9iUp2PZnHiY1mHWUIOzf4o22VuiXd/TvHOKUGaEYMNw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-mame2003": ["@emulatorjs/core-mame2003@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-ZfcNooiHvkY45ZDgBkWlIFUY2RRQ1U+aesTvmsWUdDJBWMkyIdK0HUQLWUY7kKdz52sjL97cBqG0dL8xM+MAig=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-mame2003_plus": ["@emulatorjs/core-mame2003_plus@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-z37RxbT4R6dJe4r+/+GGN0gDW7d3K+DACJPowqJDSYO8TQK9RQBWPl0nsgp1aiLYkRL7yspbJn2tu2inBMWZow=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-mednafen_ngp": ["@emulatorjs/core-mednafen_ngp@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-q/BhrhIk1+yGQ8zXPHaRXMm7Bqgu+7RkRTwbnUSlH3E/ybj8npA3SOP8tvA8TK2ceZU2F5pWdDAwp3dzR3iikg=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-mednafen_pce": ["@emulatorjs/core-mednafen_pce@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-PwnQAKKxWT8m8dUscH4VX8jcClvgxgR/NkE6FxNE8qiRDyBH3Jze7+tF9+UdWLoJRBwssR5knd7o/Es9xIlU6A=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-mednafen_pcfx": ["@emulatorjs/core-mednafen_pcfx@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-o7AfEktQ7xjH8Dj6Np9J4EyGfXgHCcsKqc32vCL6Wts1luCsSApMR+ZcGX5n0u231GtZMU3UJaesNKmSocFXLw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-mednafen_psx_hw": ["@emulatorjs/core-mednafen_psx_hw@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-SzNrmaqMqBknf/0hTvPg3pIACQeLb6PVSfaJKjYI4i6u0mpAhgsIW4PJiLOHn5RfLtTpy+eIWQJvC1DQGApfeQ=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-mednafen_wswan": ["@emulatorjs/core-mednafen_wswan@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-ePj0OoSTErXS171PCUsiZl3OrfPJVwupvRYFiMSzu6K2UaQjK3ESThWf8Rh1niJt1F1OQyjQENAeElSm0IcojQ=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-melonds": ["@emulatorjs/core-melonds@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-IjsEBNLPPbqU2GSeWEoEkOgAonK3JEyzvoYR3ucgOXu6InzDNdfuA5kP/mMh3d6DwCUbOwfnqGZuZXMTyNE2wA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-mgba": ["@emulatorjs/core-mgba@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-daiHzZQKEr+P9fra7j5YoEAXiyYUEtBhFQ8EAV/SeCtrkvqtayU7GQ9LYgoSgzkKSwsbNSskApqGuA9EGARYPA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-mupen64plus_next": ["@emulatorjs/core-mupen64plus_next@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-HnnEXbOEpYxD7f2wUJQLxWUpEB0bT2kC56u5s7PLyj/xNI5ckQKka5+ay1QhU2fOv2BuPRwjjhlzmVJmk5e3hw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-nestopia": ["@emulatorjs/core-nestopia@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-Flz0+Sebpn5sgjJvghhQydZ5OM+Jf4jwa+E6tG1TuZGQ4+5sYeJFB3FA5zHAIXZuFBS91cWnIXRbpOUennf8Fw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-opera": ["@emulatorjs/core-opera@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-cJSZXUAtHF2DLznDEFfo5Owuc9YQh5pg1qCb1TDhtkDZ4Q4za1dTIM0WZYbARd7IN83OX1UB2wd03BSDmCQAFQ=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-parallel_n64": ["@emulatorjs/core-parallel_n64@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-leRDMzJ36HdLyJ7gpvocaT2gA8SHiPALhEVlR8tHOpiV2vph9hPpNCGmiJL2CbP2eI5DW6A1PY1zFuIXbJnEJw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-pcsx_rearmed": ["@emulatorjs/core-pcsx_rearmed@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-mMbl/NszCryFI15X5QniOqLNL7YGZ6tLP24gr5y1q2ekFOIbtc39BcVTMDEwRndNGovh2nPoCcMjB+W3fgQCog=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-picodrive": ["@emulatorjs/core-picodrive@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-FUxeHeJIIKi4XG+UdOoCARDjH7hV11XLb6LFUO644F7Ri08veGqMQkOJAoCP8uXGxa/JdgS7SXqc0a40+vLv2w=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-ppsspp": ["@emulatorjs/core-ppsspp@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-jSCvK+74PYFwpqbEWzAkuDalK1TQXYogVXUxs24wn0SJcMhykQuziNggn4QQgM7+4wy60Jkh0Xb00PM9fLlWvA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-prboom": ["@emulatorjs/core-prboom@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-raqb5bZE1xrhnZkwY+NoiGyd9MG9pOlXsl2x0+2499NrEqow1946BB8xxzz1HHkbAkZ9fGxu+Xif2cbm9toVlw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-prosystem": ["@emulatorjs/core-prosystem@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-Dd+gkjtXzzO4vNRKh1AtwUL8iKveQgz3Oy+ENx7uUvpUYAoQc/UT5aDfKP5vwgJJOg6ErwDH1ShC0WW100YvZQ=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-puae": ["@emulatorjs/core-puae@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-at/pvoBtpVsMlUyG3LCFOX6iGP4wQzbSaiATrJe8ydiuJVTH/2Eta6veMX6VUsEKd6ClU8SnTMOjqXn70qIqPg=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-same_cdi": ["@emulatorjs/core-same_cdi@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-Rh0TgLmpr8MnTyJtIsUYjj8CtAO3hitkZ/iDuvgnCDHSuVZrUo0oA1lnWgbzIPg16ThahB+X1t8kxIOmZapiiw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-smsplus": ["@emulatorjs/core-smsplus@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-93G2pY9HJVl5pImoIQbDhtswVkX9aIgrrihdGTFHqurhisYHl74XCpzmCoMzdB5UMx09nFW12S+n+bLTIuiLrA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-snes9x": ["@emulatorjs/core-snes9x@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-JTe9Rv9eOuCkgkG4ILwuCZUqNcVI3m9ju1NHTTjridUlFgJSqL4DUqF/vLKN9olCpsqlJFfcCayAOoikJxefwA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-stella2014": ["@emulatorjs/core-stella2014@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-bs5az26pMrM6jswhZ5YxBXLpIwclwfdnWVbaEpip7yjJFQHactL7KYH7hVLts0JWDnfEK2lNxh4zlTfI6HAHmQ=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-vice_x128": ["@emulatorjs/core-vice_x128@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-3wmM8O764qdrWHzpHIcihYEYFH6n/BhzB+6Cng8epa298KObF4uklW3jPG/HJyDjWWJsdTlSgo52h6NQC3j/rw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-vice_x64": ["@emulatorjs/core-vice_x64@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-y9ea8mPNS9rWBIBYwBuG/rzDApndW9JOL2Bs3qrBRhUeRPFEkQlGPaXKgOCyaV2Iuhy3Zrb3SmtoG0+hn67vOw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-vice_x64sc": ["@emulatorjs/core-vice_x64sc@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-gNnVRa/aXmzMXp1+7WXdlW7nIx21O2lS2j4eFt5dun8U6XnSKsqSpsyguJiotlKxzEGtzuE0BZRdSnh/YJQdFA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-vice_xpet": ["@emulatorjs/core-vice_xpet@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-jCGjMuU7CXAlxbGHYgVSGLpBFgCaERrnqYi2krDwX8jkubZjpkgE3gdO6wqQrMm9de6icnyk7fkwsRFTMfjwug=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-vice_xplus4": ["@emulatorjs/core-vice_xplus4@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-9ZeW5adQpqHbp28Z8dPkM7ouPiSX+qGe3mfyXBj6FhBdNrUPrBvbl+A3tLJVYiPX03d9ZLYcxeVn3i60a0WsgQ=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-vice_xvic": ["@emulatorjs/core-vice_xvic@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-7YFTY1Wa4cjNTsZl+oHecq+OAHskVPEHd29MEKbYQ9jruMmcEXlchuLjjjiEbUs0YLsOr8Gbg3RPBf/wxVi+fA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-virtualjaguar": ["@emulatorjs/core-virtualjaguar@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-aZ5oWVrLrXwyVByK11jgcrcdqKM4MfhDucucuBk4duO2blHB8TS8f1XYUDbz0/N44Wl5+sphc5U5IaiXgvdFQw=="],
|
||||||
|
|
||||||
|
"@emulatorjs/core-yabause": ["@emulatorjs/core-yabause@4.2.3", "", { "dependencies": { "@emulatorjs/emulatorjs": "latest" } }, "sha512-12JwbgwoS1l4+KbsQ0jcIU71jVhn5XgXN80/1Fc8aZE4i4sEQatYKU3dZSMBlIJZaWgRN76PEjQymASMJA9/4w=="],
|
||||||
|
|
||||||
|
"@emulatorjs/cores": ["@emulatorjs/cores@4.2.3", "", { "dependencies": { "@emulatorjs/core-81": "latest", "@emulatorjs/core-a5200": "latest", "@emulatorjs/core-beetle_vb": "latest", "@emulatorjs/core-cap32": "latest", "@emulatorjs/core-crocods": "latest", "@emulatorjs/core-desmume": "latest", "@emulatorjs/core-desmume2015": "latest", "@emulatorjs/core-dosbox_pure": "latest", "@emulatorjs/core-fbalpha2012_cps1": "latest", "@emulatorjs/core-fbalpha2012_cps2": "latest", "@emulatorjs/core-fbneo": "latest", "@emulatorjs/core-fceumm": "latest", "@emulatorjs/core-fuse": "latest", "@emulatorjs/core-gambatte": "latest", "@emulatorjs/core-gearcoleco": "latest", "@emulatorjs/core-genesis_plus_gx": "latest", "@emulatorjs/core-handy": "latest", "@emulatorjs/core-mame2003": "latest", "@emulatorjs/core-mame2003_plus": "latest", "@emulatorjs/core-mednafen_ngp": "latest", "@emulatorjs/core-mednafen_pce": "latest", "@emulatorjs/core-mednafen_pcfx": "latest", "@emulatorjs/core-mednafen_psx_hw": "latest", "@emulatorjs/core-mednafen_wswan": "latest", "@emulatorjs/core-melonds": "latest", "@emulatorjs/core-mgba": "latest", "@emulatorjs/core-mupen64plus_next": "latest", "@emulatorjs/core-nestopia": "latest", "@emulatorjs/core-opera": "latest", "@emulatorjs/core-parallel_n64": "latest", "@emulatorjs/core-pcsx_rearmed": "latest", "@emulatorjs/core-picodrive": "latest", "@emulatorjs/core-ppsspp": "latest", "@emulatorjs/core-prboom": "latest", "@emulatorjs/core-prosystem": "latest", "@emulatorjs/core-puae": "latest", "@emulatorjs/core-same_cdi": "latest", "@emulatorjs/core-smsplus": "latest", "@emulatorjs/core-snes9x": "latest", "@emulatorjs/core-stella2014": "latest", "@emulatorjs/core-vice_x128": "latest", "@emulatorjs/core-vice_x64": "latest", "@emulatorjs/core-vice_x64sc": "latest", "@emulatorjs/core-vice_xpet": "latest", "@emulatorjs/core-vice_xplus4": "latest", "@emulatorjs/core-vice_xvic": "latest", "@emulatorjs/core-virtualjaguar": "latest", "@emulatorjs/core-yabause": "latest", "@emulatorjs/emulatorjs": "latest" } }, "sha512-1JxUjVMvEAM7ijUErkzgRSvWjibpjavaUtLlbqWicac4m6NQjKgi+Oc3Po4dcVH+ZPkS1Jt4LHb1WGpGmSIjwA=="],
|
||||||
|
|
||||||
|
"@emulatorjs/emulatorjs": ["@emulatorjs/emulatorjs@4.2.3", "", { "dependencies": { "@node-minify/clean-css": "^9.0.1", "@node-minify/core": "^9.0.2", "@node-minify/terser": "^9.0.1", "http-server": "^14.1.1", "nipplejs": "^0.10.2", "node-7z": "^3.0.0", "node-fetch": "^3.3.2", "socket.io": "^4.8.1" }, "optionalDependencies": { "@emulatorjs/cores": "latest" } }, "sha512-7z3qaA4LwyurhuGvdMUDF9xJpEbxC3SNy9+E9tSaOsRo8FCS2QXam/0k/lc9kqHWRFIlLKWahNjPAStyL0rFnw=="],
|
||||||
|
|
||||||
"@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="],
|
"@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="],
|
||||||
|
|
||||||
"@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-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=="],
|
||||||
|
|
@ -214,6 +317,8 @@
|
||||||
|
|
||||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||||
|
|
||||||
"@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="],
|
"@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="],
|
||||||
|
|
||||||
"@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="],
|
"@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="],
|
||||||
|
|
@ -270,6 +375,8 @@
|
||||||
|
|
||||||
"@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="],
|
"@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="],
|
||||||
|
|
||||||
|
"@jimp/wasm-webp": ["@jimp/wasm-webp@1.6.0", "", { "dependencies": { "@jsquash/webp": "^1.4.0", "zod": "^3.23.8" } }, "sha512-P0zUpK6n2XIAn8bt0F6rhSn1+FgteBTrL+TBb6Oqw8v5qEDJoNYkd6LlfZYN8YwtRBTBdZ8GFnWsg2Sar+qOkA=="],
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
@ -284,6 +391,16 @@
|
||||||
|
|
||||||
"@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="],
|
"@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="],
|
||||||
|
|
||||||
|
"@jsquash/webp": ["@jsquash/webp@1.5.0", "", { "dependencies": { "wasm-feature-detect": "^1.2.11" } }, "sha512-KggLoj2MnRSfIqTeKe1EmbljTX2vuV7mh79k89PCL1pyqiDULcPM1L47twxXt0hkb68F70bXiL31MxsuoZtKFw=="],
|
||||||
|
|
||||||
|
"@node-minify/clean-css": ["@node-minify/clean-css@9.0.1", "", { "dependencies": { "@node-minify/utils": "9.0.1", "clean-css": "5.3.3" } }, "sha512-GHTMmjGloRvNzqdG7foI0iZeS2QmuYCQvdASJP9sCKjkpH45bygODpXPYKnlzUEpQgYvPK9Q3GxqYnVY9SdoqA=="],
|
||||||
|
|
||||||
|
"@node-minify/core": ["@node-minify/core@9.0.2", "", { "dependencies": { "@node-minify/utils": "9.0.1", "glob": "10.3.3", "mkdirp": "3.0.1" } }, "sha512-FNhv29Wom6wKrrFKaeAfmZqz7TX5A1E6P+bpd0VIc+DYWMLUIhAViS8riaZg3A1oD0s06s+5BG2Fg7RqMKiKHw=="],
|
||||||
|
|
||||||
|
"@node-minify/terser": ["@node-minify/terser@9.0.1", "", { "dependencies": { "@node-minify/utils": "9.0.1", "terser": "5.36.0" } }, "sha512-WF78Ex+/xNZZDYvzwB7+sLUYQbJzyyS36ZjMhVqhORejHOsoOcTjQ9TdOgKZoY7wlfxjnLxeMKkO8R/R1KL9aQ=="],
|
||||||
|
|
||||||
|
"@node-minify/utils": ["@node-minify/utils@9.0.1", "", { "dependencies": { "gzip-size": "6.0.0" } }, "sha512-aC1+mhKTP3IMa2VcuGl3ui92LO/7CPQWldNGzu3BVGKiMNJ70AKJW/R6huuYCSuQyHDGM9oFwiVClsZnFxn67g=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
|
|
@ -322,6 +439,8 @@
|
||||||
|
|
||||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
||||||
|
|
||||||
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="],
|
||||||
|
|
@ -376,6 +495,8 @@
|
||||||
|
|
||||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
|
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
|
||||||
|
|
||||||
|
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
||||||
|
|
@ -468,6 +589,8 @@
|
||||||
|
|
||||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||||
|
|
||||||
|
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
"@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="],
|
"@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="],
|
||||||
|
|
@ -494,6 +617,8 @@
|
||||||
|
|
||||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||||
|
|
||||||
|
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
"add-stream": ["add-stream@1.0.0", "", {}, "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ=="],
|
"add-stream": ["add-stream@1.0.0", "", {}, "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ=="],
|
||||||
|
|
@ -526,10 +651,16 @@
|
||||||
|
|
||||||
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||||
|
|
||||||
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
"atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="],
|
"atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="],
|
||||||
|
|
||||||
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
|
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
|
||||||
|
|
||||||
|
"axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
|
||||||
|
|
||||||
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
||||||
|
|
||||||
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
|
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
|
||||||
|
|
@ -538,8 +669,12 @@
|
||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
|
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ=="],
|
||||||
|
|
||||||
|
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||||
|
|
||||||
"binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="],
|
"binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="],
|
||||||
|
|
||||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
@ -566,6 +701,10 @@
|
||||||
|
|
||||||
"c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
|
"c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
|
||||||
|
|
||||||
"camelcase-keys": ["camelcase-keys@6.2.2", "", { "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", "quick-lru": "^4.0.1" } }, "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg=="],
|
"camelcase-keys": ["camelcase-keys@6.2.2", "", { "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", "quick-lru": "^4.0.1" } }, "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg=="],
|
||||||
|
|
@ -586,6 +725,8 @@
|
||||||
|
|
||||||
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
||||||
|
|
||||||
|
"clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="],
|
||||||
|
|
||||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
@ -598,6 +739,8 @@
|
||||||
|
|
||||||
"colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="],
|
"colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
|
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
|
||||||
|
|
||||||
"compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],
|
"compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],
|
||||||
|
|
@ -656,6 +799,10 @@
|
||||||
|
|
||||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||||
|
|
||||||
|
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||||
|
|
||||||
|
"corser": ["corser@2.0.1", "", {}, "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ=="],
|
||||||
|
|
||||||
"cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="],
|
"cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
@ -674,6 +821,8 @@
|
||||||
|
|
||||||
"dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="],
|
"dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="],
|
||||||
|
|
||||||
|
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||||
|
|
||||||
"dateformat": ["dateformat@3.0.3", "", {}, "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q=="],
|
"dateformat": ["dateformat@3.0.3", "", {}, "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q=="],
|
||||||
|
|
||||||
"debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="],
|
"debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="],
|
||||||
|
|
@ -692,6 +841,8 @@
|
||||||
|
|
||||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||||
|
|
||||||
"detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
|
"detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
|
||||||
|
|
@ -722,6 +873,12 @@
|
||||||
|
|
||||||
"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=="],
|
"dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
|
||||||
|
|
||||||
|
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.277", "", {}, "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw=="],
|
||||||
|
|
@ -732,6 +889,10 @@
|
||||||
|
|
||||||
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
|
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
|
||||||
|
|
||||||
|
"engine.io": ["engine.io@6.6.5", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3" } }, "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A=="],
|
||||||
|
|
||||||
|
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
"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=="],
|
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||||
|
|
@ -740,6 +901,14 @@
|
||||||
|
|
||||||
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
|
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
"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": ["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=="],
|
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||||
|
|
@ -752,6 +921,8 @@
|
||||||
|
|
||||||
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||||
|
|
||||||
|
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||||
|
|
||||||
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||||
|
|
||||||
"exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="],
|
"exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="],
|
||||||
|
|
@ -772,6 +943,8 @@
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||||
|
|
||||||
"figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="],
|
"figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="],
|
||||||
|
|
||||||
"file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
|
"file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
|
||||||
|
|
@ -780,6 +953,14 @@
|
||||||
|
|
||||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||||
|
|
||||||
|
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||||
|
|
||||||
|
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||||
|
|
||||||
"fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
|
"fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
@ -792,8 +973,12 @@
|
||||||
|
|
||||||
"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-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-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
"get-pkg-repo": ["get-pkg-repo@4.2.1", "", { "dependencies": { "@hutson/parse-repository-url": "^3.0.0", "hosted-git-info": "^4.0.0", "through2": "^2.0.0", "yargs": "^16.2.0" }, "bin": { "get-pkg-repo": "src/cli.js" } }, "sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA=="],
|
"get-pkg-repo": ["get-pkg-repo@4.2.1", "", { "dependencies": { "@hutson/parse-repository-url": "^3.0.0", "hosted-git-info": "^4.0.0", "through2": "^2.0.0", "yargs": "^16.2.0" }, "bin": { "get-pkg-repo": "src/cli.js" } }, "sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||||
|
|
||||||
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
||||||
|
|
@ -808,28 +993,44 @@
|
||||||
|
|
||||||
"gitconfiglocal": ["gitconfiglocal@1.0.0", "", { "dependencies": { "ini": "^1.3.2" } }, "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ=="],
|
"gitconfiglocal": ["gitconfiglocal@1.0.0", "", { "dependencies": { "ini": "^1.3.2" } }, "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ=="],
|
||||||
|
|
||||||
|
"glob": ["glob@10.3.3", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.0.3", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { "glob": "dist/cjs/src/bin.js" } }, "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw=="],
|
||||||
|
|
||||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"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=="],
|
"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=="],
|
"goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
|
||||||
|
|
||||||
"handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="],
|
"handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="],
|
||||||
|
|
||||||
"hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="],
|
"hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
|
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
|
||||||
|
|
||||||
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
||||||
|
|
||||||
|
"html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="],
|
||||||
|
|
||||||
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
||||||
|
|
||||||
|
"http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="],
|
||||||
|
|
||||||
|
"http-server": ["http-server@14.1.1", "", { "dependencies": { "basic-auth": "^2.0.1", "chalk": "^4.1.2", "corser": "^2.0.1", "he": "^1.2.0", "html-encoding-sniffer": "^3.0.0", "http-proxy": "^1.18.1", "mime": "^1.6.0", "minimist": "^1.2.6", "opener": "^1.5.1", "portfinder": "^1.0.28", "secure-compare": "3.0.1", "union": "~0.5.0", "url-join": "^4.0.1" }, "bin": { "http-server": "bin/http-server" } }, "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A=="],
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
"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=="],
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
@ -878,6 +1079,8 @@
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"jackspeak": ["jackspeak@2.3.6", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ=="],
|
||||||
|
|
||||||
"jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="],
|
"jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="],
|
||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
@ -944,8 +1147,18 @@
|
||||||
|
|
||||||
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
|
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
|
||||||
|
|
||||||
|
"lodash.defaultsdeep": ["lodash.defaultsdeep@4.6.1", "", {}, "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA=="],
|
||||||
|
|
||||||
|
"lodash.defaultto": ["lodash.defaultto@4.14.0", "", {}, "sha512-G6tizqH6rg4P5j32Wy4Z3ZIip7OfG8YWWlPFzUFGcYStH1Ld0l1tWs6NevEQNEDnO1M3NZYjuHuraaFSN5WqeQ=="],
|
||||||
|
|
||||||
|
"lodash.flattendeep": ["lodash.flattendeep@4.4.0", "", {}, "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ=="],
|
||||||
|
|
||||||
|
"lodash.isempty": ["lodash.isempty@4.4.0", "", {}, "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg=="],
|
||||||
|
|
||||||
"lodash.ismatch": ["lodash.ismatch@4.4.0", "", {}, "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g=="],
|
"lodash.ismatch": ["lodash.ismatch@4.4.0", "", {}, "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g=="],
|
||||||
|
|
||||||
|
"lodash.negate": ["lodash.negate@3.0.2", "", {}, "sha512-JGJYYVslKYC0tRMm/7igfdHulCjoXjoganRNWM8AgS+RXfOvFnPkOveDhPI65F9aAypCX9QEEQoBqWf7Q6uAeA=="],
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
@ -956,6 +1169,8 @@
|
||||||
|
|
||||||
"map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="],
|
"map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
"mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
|
"mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
|
||||||
|
|
||||||
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||||
|
|
@ -966,7 +1181,11 @@
|
||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
"mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||||
|
|
||||||
|
|
@ -978,6 +1197,8 @@
|
||||||
|
|
||||||
"minimist-options": ["minimist-options@4.1.0", "", { "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" } }, "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A=="],
|
"minimist-options": ["minimist-options@4.1.0", "", { "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" } }, "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A=="],
|
||||||
|
|
||||||
|
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||||
|
|
||||||
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||||
|
|
||||||
"modify-values": ["modify-values@1.0.1", "", {}, "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw=="],
|
"modify-values": ["modify-values@1.0.1", "", {}, "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw=="],
|
||||||
|
|
@ -986,14 +1207,24 @@
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
||||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||||
|
|
||||||
|
"nipplejs": ["nipplejs@0.10.2", "", {}, "sha512-XGxFY8C2DOtobf1fK+MXINTzkkXJLjZDDpfQhOUZf4TSytbc9s4bmA0lB9eKKM8iDivdr9NQkO7DpIQfsST+9g=="],
|
||||||
|
|
||||||
|
"node-7z": ["node-7z@3.0.0", "", { "dependencies": { "debug": "^4.3.2", "lodash.defaultsdeep": "^4.6.1", "lodash.defaultto": "^4.14.0", "lodash.flattendeep": "^4.4.0", "lodash.isempty": "^4.4.0", "lodash.negate": "^3.0.2", "normalize-path": "^3.0.0" } }, "sha512-KIznWSxIkOYO/vOgKQfJEaXd7rgoFYKZbaurainCEdMhYc7V7mRHX+qdf2HgbpQFcdJL/Q6/XOPrDLoBeTfuZA=="],
|
||||||
|
|
||||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||||
|
|
||||||
"node-disk-info": ["node-disk-info@1.3.0", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-NEx858vJZ0AoBtmD/ChBIHLjFTF28xCsDIgmFl4jtGKsvlUx9DU/OrMDjvj3qp/E4hzLN0HvTg7eJx5XFQvbeg=="],
|
"node-disk-info": ["node-disk-info@1.3.0", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-NEx858vJZ0AoBtmD/ChBIHLjFTF28xCsDIgmFl4jtGKsvlUx9DU/OrMDjvj3qp/E4hzLN0HvTg7eJx5XFQvbeg=="],
|
||||||
|
|
||||||
|
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||||
|
|
||||||
"node-downloader-helper": ["node-downloader-helper@2.1.10", "", { "bin": { "ndh": "bin/ndh" } }, "sha512-8LdieUd4Bqw/CzfZLf30h+1xSAq3riWSDfWKsPJYz8EULoWxjS1vw6BGLYFZDxQgXjDR7UmC9UpQ0oV93U98Fg=="],
|
"node-downloader-helper": ["node-downloader-helper@2.1.10", "", { "bin": { "ndh": "bin/ndh" } }, "sha512-8LdieUd4Bqw/CzfZLf30h+1xSAq3riWSDfWKsPJYz8EULoWxjS1vw6BGLYFZDxQgXjDR7UmC9UpQ0oV93U98Fg=="],
|
||||||
|
|
||||||
|
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||||
|
|
||||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
"node-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-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="],
|
||||||
|
|
@ -1014,6 +1245,8 @@
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||||
|
|
||||||
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
||||||
|
|
@ -1022,6 +1255,8 @@
|
||||||
|
|
||||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||||
|
|
||||||
|
"opener": ["opener@1.5.2", "", { "bin": { "opener": "bin/opener-bin.js" } }, "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="],
|
||||||
|
|
||||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||||
|
|
||||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
@ -1050,6 +1285,8 @@
|
||||||
|
|
||||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||||
|
|
||||||
|
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||||
|
|
||||||
"path-type": ["path-type@3.0.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg=="],
|
"path-type": ["path-type@3.0.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg=="],
|
||||||
|
|
||||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
@ -1070,6 +1307,8 @@
|
||||||
|
|
||||||
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
||||||
|
|
||||||
|
"portfinder": ["portfinder@1.0.38", "", { "dependencies": { "async": "^3.2.6", "debug": "^4.3.6" } }, "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||||
|
|
@ -1090,10 +1329,14 @@
|
||||||
|
|
||||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||||
|
|
||||||
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
"q": ["q@1.5.1", "", {}, "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="],
|
"q": ["q@1.5.1", "", {}, "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="],
|
||||||
|
|
||||||
"qr.js": ["qr.js@0.0.0", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="],
|
"qr.js": ["qr.js@0.0.0", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="],
|
||||||
|
|
||||||
|
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
"quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="],
|
"quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="],
|
||||||
|
|
@ -1132,6 +1375,8 @@
|
||||||
|
|
||||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
|
"requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
|
||||||
|
|
||||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
@ -1146,7 +1391,7 @@
|
||||||
|
|
||||||
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||||
|
|
||||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
|
@ -1194,6 +1439,8 @@
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
|
"secure-compare": ["secure-compare@3.0.1", "", {}, "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
"seroval": ["seroval@1.4.2", "", {}, "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ=="],
|
"seroval": ["seroval@1.4.2", "", {}, "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ=="],
|
||||||
|
|
@ -1206,8 +1453,24 @@
|
||||||
|
|
||||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
"simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="],
|
"simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="],
|
||||||
|
|
||||||
|
"socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="],
|
||||||
|
|
||||||
|
"socket.io-adapter": ["socket.io-adapter@2.5.6", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" } }, "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ=="],
|
||||||
|
|
||||||
|
"socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
@ -1230,12 +1493,16 @@
|
||||||
|
|
||||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||||
|
|
||||||
"stringify-package": ["stringify-package@1.0.1", "", {}, "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg=="],
|
"stringify-package": ["stringify-package@1.0.1", "", {}, "sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||||
|
|
||||||
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
||||||
|
|
@ -1304,6 +1571,10 @@
|
||||||
|
|
||||||
"trim-newlines": ["trim-newlines@3.0.1", "", {}, "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw=="],
|
"trim-newlines": ["trim-newlines@3.0.1", "", {}, "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw=="],
|
||||||
|
|
||||||
|
"ts-apicalypse": ["ts-apicalypse@0.4.2", "", { "dependencies": { "axios": "^1.4.0" } }, "sha512-A02KFDFZHYTft0fTkxr5AERLb//XK/qjvGxrX8uNCwZAvFpLI/4TrOVxbq6kV2caZXWAn8eZZfRzVn1xd/cSMw=="],
|
||||||
|
|
||||||
|
"ts-igdb-client": ["ts-igdb-client@0.4.2", "", { "dependencies": { "axios": "^1.4.0", "ts-apicalypse": "^0.4.2" } }, "sha512-VGQbIyy75GbHW0WGGHBXsdJzuj8wOW/jiWURmQWltpkFjCTMSqqx9gTPSUUcJ0W2kdlWcyMU4TDDHo4N3Fij7A=="],
|
||||||
|
|
||||||
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
"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=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
@ -1324,6 +1595,8 @@
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
|
"union": ["union@0.5.0", "", { "dependencies": { "qs": "^6.4.0" } }, "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA=="],
|
||||||
|
|
||||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||||
|
|
||||||
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
||||||
|
|
@ -1332,6 +1605,8 @@
|
||||||
|
|
||||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
|
"url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="],
|
||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
@ -1344,6 +1619,8 @@
|
||||||
|
|
||||||
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
|
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
|
||||||
|
|
||||||
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"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": ["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-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=="],
|
||||||
|
|
@ -1352,6 +1629,10 @@
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"wasm-feature-detect": ["wasm-feature-detect@1.8.0", "", {}, "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ=="],
|
||||||
|
|
||||||
|
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||||
|
|
||||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||||
|
|
||||||
"webview-bun": ["webview-bun@2.4.0", "", {}, "sha512-0+ugnQlcUHmuW+iLeb+Lzb8rGUJh7WEdXvNsuvaVEXT3EagK380XdD7heVJu0Ek/mNxMY3G2JM142YRQ1hDUGQ=="],
|
"webview-bun": ["webview-bun@2.4.0", "", {}, "sha512-0+ugnQlcUHmuW+iLeb+Lzb8rGUJh7WEdXvNsuvaVEXT3EagK380XdD7heVJu0Ek/mNxMY3G2JM142YRQ1hDUGQ=="],
|
||||||
|
|
@ -1368,6 +1649,10 @@
|
||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||||
|
|
||||||
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
|
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
|
||||||
|
|
@ -1396,8 +1681,16 @@
|
||||||
|
|
||||||
"@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=="],
|
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||||
|
|
||||||
"@jimp/core/file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
|
"@jimp/core/file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
|
||||||
|
|
||||||
|
"@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||||
|
|
||||||
"@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
@ -1430,6 +1723,12 @@
|
||||||
|
|
||||||
"@jimp/types/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@jimp/types/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
|
"@jimp/wasm-webp/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
|
"@node-minify/core/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||||
|
|
||||||
|
"@node-minify/terser/terser": ["terser@5.36.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.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/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=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
|
|
@ -1456,6 +1755,8 @@
|
||||||
|
|
||||||
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
|
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"compare-func/dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
|
"compare-func/dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
|
||||||
|
|
||||||
"conventional-changelog-writer/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"conventional-changelog-writer/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
@ -1470,16 +1771,22 @@
|
||||||
|
|
||||||
"elysia/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
"elysia/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||||
|
|
||||||
|
"engine.io/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
"get-pkg-repo/through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="],
|
"get-pkg-repo/through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="],
|
||||||
|
|
||||||
"get-pkg-repo/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
"get-pkg-repo/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
||||||
|
|
||||||
"git-semver-tags/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"git-semver-tags/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||||
|
|
||||||
"handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
"hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||||
|
|
||||||
|
"html-encoding-sniffer/whatwg-encoding": ["whatwg-encoding@2.0.0", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg=="],
|
||||||
|
|
||||||
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
"image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="],
|
"image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="],
|
||||||
|
|
@ -1498,6 +1805,8 @@
|
||||||
|
|
||||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||||
|
|
||||||
|
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"path-type/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="],
|
"path-type/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="],
|
||||||
|
|
||||||
"pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
"pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
||||||
|
|
@ -1520,6 +1829,8 @@
|
||||||
|
|
||||||
"standard-version/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
"standard-version/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
||||||
|
|
||||||
|
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
"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=="],
|
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
@ -1574,10 +1885,18 @@
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
|
|
||||||
|
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
"@jimp/core/file-type/strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
|
"@jimp/core/file-type/strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
|
||||||
|
|
||||||
"@jimp/core/file-type/token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="],
|
"@jimp/core/file-type/token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="],
|
||||||
|
|
||||||
|
"@node-minify/terser/terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
||||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||||
|
|
||||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||||
|
|
@ -1590,6 +1909,8 @@
|
||||||
|
|
||||||
"get-pkg-repo/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
"get-pkg-repo/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
||||||
|
|
||||||
|
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
"hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
"hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||||
|
|
||||||
"meow/read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
"meow/read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||||
|
|
@ -1722,8 +2043,6 @@
|
||||||
|
|
||||||
"dotgitignore/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
"dotgitignore/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||||
|
|
||||||
"get-pkg-repo/through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
|
||||||
|
|
||||||
"get-pkg-repo/through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
"get-pkg-repo/through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
|
||||||
"meow/read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
"meow/read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||||
|
|
|
||||||
27
drizzle/0001_outstanding_silk_fever.sql
Normal file
27
drizzle/0001_outstanding_silk_fever.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_games` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`source_id` text,
|
||||||
|
`source` text,
|
||||||
|
`igdb_id` integer,
|
||||||
|
`name` text,
|
||||||
|
`ra_id` integer,
|
||||||
|
`path_fs` text,
|
||||||
|
`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
|
||||||
|
INSERT INTO `__new_games`("id", "source_id", "source", "igdb_id", "name", "ra_id", "path_fs", "last_played", "created_at", "metadata", "slug", "platform_id", "cover", "type", "summary") SELECT "id", "source_id", "source", "igdb_id", "name", "ra_id", "path_fs", "last_played", "created_at", "metadata", "slug", "platform_id", "cover", "type", "summary" FROM `games`;--> statement-breakpoint
|
||||||
|
DROP TABLE `games`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_games` RENAME TO `games`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `games_igdb_id_unique` ON `games` (`igdb_id`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `games_ra_id_unique` ON `games` (`ra_id`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `games_slug_unique` ON `games` (`slug`);
|
||||||
451
drizzle/meta/0001_snapshot.json
Normal file
451
drizzle/meta/0001_snapshot.json
Normal file
|
|
@ -0,0 +1,451 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "6dc00b41-64d7-4cb3-ad90-f9e8e35af643",
|
||||||
|
"prevId": "673fe5dc-58a5-495b-8fb1-104e7945e90b",
|
||||||
|
"tables": {
|
||||||
|
"collections": {
|
||||||
|
"name": "collections",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"collections_games": {
|
||||||
|
"name": "collections_games",
|
||||||
|
"columns": {
|
||||||
|
"collection_id": {
|
||||||
|
"name": "collection_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"game_id": {
|
||||||
|
"name": "game_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"collections_games_collection_id_collections_id_fk": {
|
||||||
|
"name": "collections_games_collection_id_collections_id_fk",
|
||||||
|
"tableFrom": "collections_games",
|
||||||
|
"tableTo": "collections",
|
||||||
|
"columnsFrom": [
|
||||||
|
"collection_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
},
|
||||||
|
"collections_games_game_id_games_id_fk": {
|
||||||
|
"name": "collections_games_game_id_games_id_fk",
|
||||||
|
"tableFrom": "collections_games",
|
||||||
|
"tableTo": "games",
|
||||||
|
"columnsFrom": [
|
||||||
|
"game_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"games": {
|
||||||
|
"name": "games",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"source_id": {
|
||||||
|
"name": "source_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"igdb_id": {
|
||||||
|
"name": "igdb_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ra_id": {
|
||||||
|
"name": "ra_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"path_fs": {
|
||||||
|
"name": "path_fs",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"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_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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,13 @@
|
||||||
"when": 1771508990238,
|
"when": 1771508990238,
|
||||||
"tag": "0000_pretty_harry_osborn",
|
"tag": "0000_pretty_harry_osborn",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1772998956867,
|
||||||
|
"tag": "0001_outstanding_silk_fever",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
"packageManager": "bun@1.3.9",
|
"packageManager": "bun@1.3.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development bun run build:vite && WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS='--remote-debugging-port=9222' bun run ./scripts/dev.ts",
|
"dev": "NODE_ENV=development bun run build:vite && bun run ./scripts/dev.ts",
|
||||||
"dev:hmr": "PUBLIC_ACCESS=true conc -k 'bun run hmr' 'bun run dev'",
|
"dev:hmr": "PUBLIC_ACCESS=true conc -k 'bun run hmr' 'bun run ./scripts/dev.ts'",
|
||||||
"build:vite": "vite build",
|
"build:vite": "vite build",
|
||||||
"build:prod:vite": "NODE_ENV=production bun run build:vite",
|
"build:prod:vite": "NODE_ENV=production bun run build:vite",
|
||||||
"build:dev:vite": "NODE_ENV=development bun run build:vite",
|
"build:dev:vite": "NODE_ENV=development bun run build:vite",
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
"@elysiajs/cors": "^1.4.1",
|
"@elysiajs/cors": "^1.4.1",
|
||||||
"@elysiajs/eden": "^1.4.6",
|
"@elysiajs/eden": "^1.4.6",
|
||||||
"@elysiajs/static": "^1.4.7",
|
"@elysiajs/static": "^1.4.7",
|
||||||
|
"@jimp/wasm-webp": "^1.6.0",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"conf": "^15.0.2",
|
"conf": "^15.0.2",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
|
@ -59,6 +60,7 @@
|
||||||
"systeminformation": "^5.31.1",
|
"systeminformation": "^5.31.1",
|
||||||
"tough-cookie": "^6.0.0",
|
"tough-cookie": "^6.0.0",
|
||||||
"tough-cookie-file-store": "^3.3.0",
|
"tough-cookie-file-store": "^3.3.0",
|
||||||
|
"ts-igdb-client": "^0.4.2",
|
||||||
"unzip-stream": "^0.3.4",
|
"unzip-stream": "^0.3.4",
|
||||||
"webview-bun": "^2.4.0",
|
"webview-bun": "^2.4.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|
@ -66,6 +68,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ap0nia/eden": "^1.0.0-next.22",
|
"@ap0nia/eden": "^1.0.0-next.22",
|
||||||
"@ap0nia/eden-tanstack-query": "^1.0.0-next.22",
|
"@ap0nia/eden-tanstack-query": "^1.0.0-next.22",
|
||||||
|
"@emulatorjs/emulatorjs": "^4.2.3",
|
||||||
"@hey-api/openapi-ts": "^0.91.0",
|
"@hey-api/openapi-ts": "^0.91.0",
|
||||||
"@noriginmedia/norigin-spatial-navigation": "^2.3.0",
|
"@noriginmedia/norigin-spatial-navigation": "^2.3.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,28 @@
|
||||||
// watcher.ts - run this instead of --watch
|
// watcher.ts - run this instead of --watch
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { watch } from "fs";
|
|
||||||
import browser from '../src/bun/browser';
|
import browser from '../src/bun/browser';
|
||||||
|
import { tmpdir } from "os";
|
||||||
|
import path from "path";
|
||||||
const events = new EventEmitter();
|
const events = new EventEmitter();
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
process.env.WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = "--remote-debugging-port=9222";
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
|
||||||
|
let retries = 0;
|
||||||
|
|
||||||
function spawnServer ()
|
function spawnServer ()
|
||||||
{
|
{
|
||||||
return Bun.spawn(["bun", "run", "--inspect=127.0.0.1:9229/fixed-session", '--watch', "./src/bun/index.ts"], {
|
return Bun.spawn(["bun", "run", '--watch', "--inspect=127.0.0.1:9228/fixed-session", "./src/bun/index.ts"], {
|
||||||
env: {
|
env: {
|
||||||
...Bun.env,
|
...Bun.env,
|
||||||
HEADLESS: "true",
|
HEADLESS: "true",
|
||||||
},
|
},
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
stderr: "inherit",
|
stderr: "inherit",
|
||||||
stdin: "inherit",
|
stdin: "pipe",
|
||||||
|
signal: abortController.signal,
|
||||||
|
killSignal: 'SIGUSR1',
|
||||||
ipc (message, subprocess, handle)
|
ipc (message, subprocess, handle)
|
||||||
{
|
{
|
||||||
if (message.type === 'exitapp')
|
if (message.type === 'exitapp')
|
||||||
|
|
@ -22,9 +31,17 @@ function spawnServer ()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onExit (subprocess, exitCode, signalCode)
|
onExit (subprocess, exitCode, signalCode)
|
||||||
|
{
|
||||||
|
if (exitCode === 1 && retries <= 3)
|
||||||
|
{
|
||||||
|
server = spawnServer();
|
||||||
|
retries++;
|
||||||
|
} else
|
||||||
{
|
{
|
||||||
process.exit();
|
process.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,12 +49,18 @@ function spawnBrowser ()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return browser(events, !!Bun.env.FORCE_BROWSER);
|
|
||||||
|
return browser(events, Bun.env.FORCE_BROWSER === "true", { configPath: path.join(tmpdir(), 'gameflow') });
|
||||||
} catch (error)
|
} catch (error)
|
||||||
{
|
{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = spawnServer();
|
let server = spawnServer();
|
||||||
spawnBrowser()?.then(e => server.send({ type: 'exitapp' }));
|
spawnBrowser()?.then(async e =>
|
||||||
|
{
|
||||||
|
console.log("Sending exit Signal to server");
|
||||||
|
await server.stdin.write('shutdown\n');
|
||||||
|
await server.stdin.flush();
|
||||||
|
});
|
||||||
|
|
@ -6,6 +6,8 @@ import { Database } from "bun:sqlite";
|
||||||
import * as schema from '../src/bun/api/schema/emulators';
|
import * as schema from '../src/bun/api/schema/emulators';
|
||||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
|
import path from 'node:path';
|
||||||
|
import { ensureDir } from 'fs-extra';
|
||||||
|
|
||||||
/** get all latest supported romm platforms */
|
/** get all latest supported romm platforms */
|
||||||
const rommPlatforms = await getSupportedPlatformsEndpointApiPlatformsSupportedGet({ baseUrl: "https://demo.romm.app" });
|
const rommPlatforms = await getSupportedPlatformsEndpointApiPlatformsSupportedGet({ baseUrl: "https://demo.romm.app" });
|
||||||
|
|
@ -41,10 +43,10 @@ await Promise.all(platforms.map(async ([platform, arch]) =>
|
||||||
|
|
||||||
await Promise.all(platforms.map(async ([platform, arch]) =>
|
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 systemsXml = await Bun.file(`./vendors/es-de/systems/${mapSystem(platform, arch)}/es_systems.xml`).arrayBuffer();
|
||||||
const $s = cheerio.load(Buffer.from(systems));
|
const $s = cheerio.load(Buffer.from(systemsXml));
|
||||||
const rules = await Bun.file(`./vendors/es-de/systems/${mapSystem(platform, arch)}/es_find_rules.xml`).arrayBuffer();
|
const rulesXml = await Bun.file(`./vendors/es-de/systems/${mapSystem(platform, arch)}/es_find_rules.xml`).arrayBuffer();
|
||||||
const $r = cheerio.load(Buffer.from(rules));
|
const $r = cheerio.load(Buffer.from(rulesXml));
|
||||||
|
|
||||||
const sqlitePath = `./vendors/es-de/emulators.${platform}.${arch}.sqlite`;
|
const sqlitePath = `./vendors/es-de/emulators.${platform}.${arch}.sqlite`;
|
||||||
const sqlite = new Database(sqlitePath, { create: true, readwrite: true });
|
const sqlite = new Database(sqlitePath, { create: true, readwrite: true });
|
||||||
|
|
@ -52,7 +54,7 @@ await Promise.all(platforms.map(async ([platform, arch]) =>
|
||||||
migrate(db, { migrationsFolder: "./scripts/drizzle/es-de" });
|
migrate(db, { migrationsFolder: "./scripts/drizzle/es-de" });
|
||||||
|
|
||||||
/** Save the ruleset for emulators */
|
/** Save the ruleset for emulators */
|
||||||
await db.insert(schema.emulators).values($r('ruleList emulator').toArray().map(s =>
|
const emulators = $r('ruleList emulator').toArray().map(s =>
|
||||||
{
|
{
|
||||||
const $emulator = $r(s);
|
const $emulator = $r(s);
|
||||||
const $systempath = $emulator.find('rule[type=systempath] entry');
|
const $systempath = $emulator.find('rule[type=systempath] entry');
|
||||||
|
|
@ -71,13 +73,27 @@ await Promise.all(platforms.map(async ([platform, arch]) =>
|
||||||
winregistrypath: $winregistrypath.toArray().map(p => $r(p).text()),
|
winregistrypath: $winregistrypath.toArray().map(p => $r(p).text()),
|
||||||
};
|
};
|
||||||
return emulator;
|
return emulator;
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
await db.insert(schema.emulators).values(emulators);
|
||||||
|
|
||||||
/** Save the systems like ps2 or psp */
|
/** Save the systems like ps2 or psp */
|
||||||
await Promise.all($s(`systemList system`).toArray().map(async s =>
|
const systems = await Promise.all($s(`systemList system`).toArray().map(async s =>
|
||||||
{
|
{
|
||||||
const name = $s(s).find("name").text();
|
const name = $s(s).find("name").text();
|
||||||
const fullname = $s(s).find("fullname").text();
|
const fullname = $s(s).find("fullname").text();
|
||||||
|
|
||||||
|
const commands = $s(s).find("command").toArray().map(c =>
|
||||||
|
{
|
||||||
|
const command: typeof schema.commands.$inferInsert = {
|
||||||
|
label: $s(c).attr('label'),
|
||||||
|
command: $s(c).text(),
|
||||||
|
system: name
|
||||||
|
};
|
||||||
|
|
||||||
|
return command;
|
||||||
|
});
|
||||||
|
|
||||||
const rommMapping = rommPlatforms.data?.find(p =>
|
const rommMapping = rommPlatforms.data?.find(p =>
|
||||||
p.slug === (customMappings as any)[name] ||
|
p.slug === (customMappings as any)[name] ||
|
||||||
p.slug === name ||
|
p.slug === name ||
|
||||||
|
|
@ -87,17 +103,13 @@ await Promise.all(platforms.map(async ([platform, arch]) =>
|
||||||
p.display_name === fullname
|
p.display_name === fullname
|
||||||
);
|
);
|
||||||
|
|
||||||
const system: typeof schema.systems.$inferInsert = {
|
const mappings: {
|
||||||
name,
|
source: string;
|
||||||
fullname,
|
sourceId: number | null;
|
||||||
path: $s(s).find("path").text(),
|
sourceSlug: string | null;
|
||||||
extension: $s(s).find("extension").text().replaceAll('.', '').split(' ')
|
system: string;
|
||||||
};
|
}[] = [];
|
||||||
|
|
||||||
/** Store mappings to all other sources for easy reference */
|
|
||||||
db.transaction(async (tx) =>
|
|
||||||
{
|
|
||||||
await tx.insert(schema.systems).values(system);
|
|
||||||
if (rommMapping)
|
if (rommMapping)
|
||||||
{
|
{
|
||||||
const sources: [string, keyof typeof rommMapping | null, keyof typeof rommMapping | null][] = [
|
const sources: [string, keyof typeof rommMapping | null, keyof typeof rommMapping | null][] = [
|
||||||
|
|
@ -114,27 +126,40 @@ await Promise.all(platforms.map(async ([platform, arch]) =>
|
||||||
['igdb', 'igdb_id', 'igdb_slug']
|
['igdb', 'igdb_id', 'igdb_slug']
|
||||||
];
|
];
|
||||||
|
|
||||||
await tx.insert(schema.systemMappings)
|
mappings.push(...sources.map(([source, sourceId, sourceSlug]) => ({
|
||||||
.values(sources.map(([source, sourceId, sourceSlug]) => ({
|
|
||||||
source,
|
source,
|
||||||
sourceId: sourceId ? rommMapping[sourceId] as number : null,
|
sourceId: sourceId ? rommMapping[sourceId] as number : null,
|
||||||
sourceSlug: sourceSlug ? rommMapping[sourceSlug] as string : null,
|
sourceSlug: sourceSlug ? rommMapping[sourceSlug] as string : null,
|
||||||
system: system.name
|
system: name
|
||||||
} satisfies typeof schema.systemMappings.$inferInsert))
|
}))
|
||||||
.filter(m => m.sourceId !== null || m.sourceSlug !== null));
|
.filter(m => m.sourceId !== null || m.sourceSlug !== null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const system = {
|
||||||
|
name,
|
||||||
|
fullname,
|
||||||
|
path: $s(s).find("path").text(),
|
||||||
|
extension: $s(s).find("extension").text().replaceAll('.', '').split(' '),
|
||||||
|
commands,
|
||||||
|
mappings
|
||||||
|
};
|
||||||
|
return system;
|
||||||
|
}));
|
||||||
|
|
||||||
|
await Promise.all(systems.map(async system =>
|
||||||
|
{
|
||||||
|
/** Store mappings to all other sources for easy reference */
|
||||||
|
await db.transaction(async (tx) =>
|
||||||
|
{
|
||||||
|
await tx.insert(schema.systems).values(system);
|
||||||
|
if (system.mappings.length > 0)
|
||||||
|
{
|
||||||
|
await tx.insert(schema.systemMappings)
|
||||||
|
.values(system.mappings);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.insert(schema.commands).values($s(s).find("command").toArray().map(c =>
|
await db.insert(schema.commands).values(system.commands);
|
||||||
{
|
|
||||||
const command: typeof schema.commands.$inferInsert = {
|
|
||||||
label: $s(c).attr('label'),
|
|
||||||
command: $s(c).text(),
|
|
||||||
system: system.name
|
|
||||||
};
|
|
||||||
|
|
||||||
return command;
|
|
||||||
}));
|
|
||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ import Conf from "conf";
|
||||||
import projectPackage from '~/package.json';
|
import projectPackage from '~/package.json';
|
||||||
import { Notification, SettingsSchema, SettingsType } from "@shared/constants";
|
import { Notification, SettingsSchema, SettingsType } from "@shared/constants";
|
||||||
import { client } from "@clients/romm/client.gen";
|
import { client } from "@clients/romm/client.gen";
|
||||||
import * as schema from "./schema/app";
|
import * as schema from "@schema/app";
|
||||||
import * as emulatorSchema from "./schema/emulators";
|
import cacheSchema from "@schema/cache";
|
||||||
|
import * as emulatorSchema from "@schema/emulators";
|
||||||
import { login, logout } from "./auth";
|
import { login, logout } from "./auth";
|
||||||
import fs from 'node:fs/promises';
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { ActiveGame } from "../types/types";
|
import { ActiveGame } from "../types/types";
|
||||||
import EventEmitter from "node:events";
|
import EventEmitter from "node:events";
|
||||||
|
|
@ -21,6 +21,7 @@ import { ErrorLike } from "bun";
|
||||||
import { appPath, getErrorMessage } from "../utils";
|
import { appPath, getErrorMessage } from "../utils";
|
||||||
import { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
|
import { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
|
||||||
import { ensureDir } from "fs-extra";
|
import { ensureDir } from "fs-extra";
|
||||||
|
import UpdateStoreJob from "./jobs/update-store";
|
||||||
|
|
||||||
export const config = new Conf<SettingsType>({
|
export const config = new Conf<SettingsType>({
|
||||||
projectName: projectPackage.name,
|
projectName: projectPackage.name,
|
||||||
|
|
@ -50,7 +51,10 @@ const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path),
|
||||||
console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath);
|
console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath);
|
||||||
export const jar = new CookieJar(fileCookieStore);
|
export const jar = new CookieJar(fileCookieStore);
|
||||||
let sqlite: Database;
|
let sqlite: Database;
|
||||||
|
export const cachePath = path.join(os.tmpdir(), 'gameflow', 'cache.sqlite');
|
||||||
|
let cacheSqlite: Database;
|
||||||
export let db: DrizzleSqliteDODatabase<typeof schema>;
|
export let db: DrizzleSqliteDODatabase<typeof schema>;
|
||||||
|
export let cache: DrizzleSqliteDODatabase<typeof cacheSchema>;
|
||||||
await reloadDatabase();
|
await reloadDatabase();
|
||||||
const emulatorsSqlite = new Database(appPath(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`), { readonly: true });
|
const emulatorsSqlite = new Database(appPath(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`), { readonly: true });
|
||||||
export const emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema });
|
export const emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema });
|
||||||
|
|
@ -73,6 +77,7 @@ events.addListener('activegameexit', ({ error }) =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
config.onDidChange('downloadPath', () => reloadDatabase());
|
config.onDidChange('downloadPath', () => reloadDatabase());
|
||||||
|
taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob());
|
||||||
|
|
||||||
export async function cleanup ()
|
export async function cleanup ()
|
||||||
{
|
{
|
||||||
|
|
@ -86,13 +91,25 @@ export async function reloadDatabase ()
|
||||||
{
|
{
|
||||||
await ensureDir(config.get('downloadPath'));
|
await ensureDir(config.get('downloadPath'));
|
||||||
sqlite = new Database(path.join(config.get('downloadPath'), 'db.sqlite'), { create: true, readwrite: true });
|
sqlite = new Database(path.join(config.get('downloadPath'), 'db.sqlite'), { create: true, readwrite: true });
|
||||||
|
await ensureDir(path.join(os.tmpdir(), 'gameflow'));
|
||||||
|
console.log("Loaded Cache from: ", cachePath);
|
||||||
|
cacheSqlite = new Database(cachePath, { create: true, readwrite: true });
|
||||||
db = drizzle(sqlite, { schema });
|
db = drizzle(sqlite, { schema });
|
||||||
|
cache = drizzle(cacheSqlite, { schema: cacheSchema });
|
||||||
migrate(db!, { migrationsFolder: appPath("./drizzle") });
|
migrate(db!, { migrationsFolder: appPath("./drizzle") });
|
||||||
|
cache.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS item_cache (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
expire_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppEventMap
|
interface AppEventMap
|
||||||
{
|
{
|
||||||
activegameexit: [{ source: string, id: number, subprocess?: Bun.Subprocess, exitCode: number | null, signalCode: number | null, error?: ErrorLike; }];
|
activegameexit: [{ source: string, id: string, subprocess?: Bun.Subprocess, exitCode: number | null, signalCode: number | null, error?: ErrorLike; }];
|
||||||
exitapp: [];
|
exitapp: [];
|
||||||
notification: [Notification];
|
notification: [Notification];
|
||||||
}
|
}
|
||||||
|
|
@ -1,45 +1,117 @@
|
||||||
import Elysia, { sse, status } from "elysia";
|
import Elysia, { sse, status } from "elysia";
|
||||||
import { config, jar, taskQueue } from "./app";
|
import { config, events, jar, taskQueue } from "./app";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { client } from "@clients/romm/client.gen";
|
import { client } from "@clients/romm/client.gen";
|
||||||
import { loginApiLoginPost, logoutApiLogoutPost } from "@clients/romm";
|
import { loginApiLoginPost, logoutApiLogoutPost } from "@clients/romm";
|
||||||
import secrets from '../api/secrets';
|
import secrets from '../api/secrets';
|
||||||
import { LoginJob } from "./jobs/login-job";
|
import { LoginJob } from "./jobs/login-job";
|
||||||
|
import TwitchLoginJob from "./jobs/twitch-login-job";
|
||||||
|
|
||||||
export default new Elysia()
|
export default new Elysia()
|
||||||
.post('/login/remote/start', async () =>
|
.post('/login/twitch', async ({ body: { openInBrowser } }) =>
|
||||||
|
{
|
||||||
|
if (taskQueue.hasActiveOfType(TwitchLoginJob))
|
||||||
|
{
|
||||||
|
return status("Conflict", `Twitch Authentication already in progress`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.TWITCH_CLIENT_ID)
|
||||||
|
{
|
||||||
|
return status("Not Found", "Twitch Client ID not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskQueue.enqueue(TwitchLoginJob.id, new TwitchLoginJob(process.env.TWITCH_CLIENT_ID, openInBrowser ?? false));
|
||||||
|
},
|
||||||
|
{ body: z.object({ openInBrowser: z.boolean().optional() }) })
|
||||||
|
.post('/logout/twitch', async () =>
|
||||||
|
{
|
||||||
|
if (!process.env.TWITCH_CLIENT_ID)
|
||||||
|
{
|
||||||
|
return status("Not Found", "Twitch Client ID not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch('https://id.twitch.tv/oauth2/revoke', {
|
||||||
|
method: "POST", headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: process.env.TWITCH_CLIENT_ID
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
await secrets.delete({ service: 'gamflow_twitch', name: 'access_token' });
|
||||||
|
await secrets.delete({ service: 'gamflow_twitch', name: 'refresh_token' });
|
||||||
|
await secrets.delete({ service: 'gamflow_twitch', name: 'expires_in' });
|
||||||
|
|
||||||
|
return status(res.status, res.statusText);
|
||||||
|
})
|
||||||
|
.get('/login/twitch', async () =>
|
||||||
|
{
|
||||||
|
const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' });
|
||||||
|
if (!access_token)
|
||||||
|
{
|
||||||
|
return status('Not Found', "Not Logged In");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${access_token}` } });
|
||||||
|
if (res.ok)
|
||||||
|
{
|
||||||
|
return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.TWITCH_CLIENT_ID)
|
||||||
|
{
|
||||||
|
return status("Not Found", "Twitch Client ID not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const refresh_token = await secrets.get({ service: 'gamflow_twitch', name: "refresh_token" });
|
||||||
|
if (!refresh_token)
|
||||||
|
{
|
||||||
|
return status("Not Found", "Refresh Token Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh token
|
||||||
|
const refreshResponse = await fetch('https://id.twitch.tv/oauth2/token', {
|
||||||
|
method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({
|
||||||
|
client_id: process.env.TWITCH_CLIENT_ID,
|
||||||
|
access_token,
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (refreshResponse.ok)
|
||||||
|
{
|
||||||
|
const data: {
|
||||||
|
access_token: string,
|
||||||
|
refresh_token: string,
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
} = await refreshResponse.json();
|
||||||
|
|
||||||
|
await secrets.set({ service: 'gamflow_twitch', name: 'access_token', value: data.access_token });
|
||||||
|
await secrets.set({ service: 'gamflow_twitch', name: 'refresh_token', value: data.refresh_token });
|
||||||
|
await secrets.set({ service: 'gamflow_twitch', name: 'expires_in', value: new Date(new Date().getTime() + data.expires_in).toString() });
|
||||||
|
|
||||||
|
events.emit('notification', { message: "Twitch Refresh Successful", type: 'success' });
|
||||||
|
|
||||||
|
const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${data.access_token}` } });
|
||||||
|
if (res.ok)
|
||||||
|
{
|
||||||
|
return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status(400, res.statusText);
|
||||||
|
})
|
||||||
|
.post('/login/romm', async () =>
|
||||||
{
|
{
|
||||||
if (taskQueue.hasActiveOfType(LoginJob))
|
if (taskQueue.hasActiveOfType(LoginJob))
|
||||||
{
|
{
|
||||||
return status("Conflict", "Login Already Active");
|
return status("Conflict", "Login Already Active");
|
||||||
}
|
}
|
||||||
|
|
||||||
const job = new LoginJob();
|
return taskQueue.enqueue(LoginJob.id, new LoginJob());
|
||||||
taskQueue.enqueue("login", job);
|
|
||||||
return status("OK");
|
|
||||||
})
|
|
||||||
.get('/login/remote/status', async function* ()
|
|
||||||
{
|
|
||||||
const job = taskQueue.findJob("login");
|
|
||||||
if (job)
|
|
||||||
{
|
|
||||||
const loginJob = job.job as LoginJob;
|
|
||||||
yield sse({ data: { endsAt: loginJob.endsAt, url: loginJob.url } });
|
|
||||||
await taskQueue.waitForJob('login');
|
|
||||||
yield sse({ data: {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
yield sse({ data: {} });
|
|
||||||
})
|
|
||||||
.post('/login/remote/cancel', async () =>
|
|
||||||
{
|
|
||||||
const job = taskQueue.findJob("login");
|
|
||||||
if (job)
|
|
||||||
{
|
|
||||||
job.abort("cancel");
|
|
||||||
await taskQueue.waitForJob('login');
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
})
|
})
|
||||||
.post('/login', async ({ body }) => tryLoginAndSave(body), { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) })
|
.post('/login', async ({ body }) => tryLoginAndSave(body), { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) })
|
||||||
.get('/login', async () =>
|
.get('/login', async () =>
|
||||||
|
|
|
||||||
34
src/bun/api/cache.ts
Normal file
34
src/bun/api/cache.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { cache } from "./app";
|
||||||
|
import cacheSchema from "@schema/cache";
|
||||||
|
|
||||||
|
export const CACHE_KEYS = {
|
||||||
|
ROM_PLATFORMS: 'rom-platforms',
|
||||||
|
STORE_GAME: (path: string) => `store-game-${path}`,
|
||||||
|
STORE_GAME_MANIFEST: 'store-game-manifest'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export async function getOrCached<T> (key: string, getter: () => Promise<T>, options?: { expireMs?: number; }): Promise<T>
|
||||||
|
{
|
||||||
|
const cached = await cache.query.item_cache.findFirst({ where: eq(cacheSchema.item_cache.key, key) });
|
||||||
|
const updated_at = new Date();
|
||||||
|
|
||||||
|
if (cached && cached.expire_at > updated_at)
|
||||||
|
{
|
||||||
|
return cached.data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getter();
|
||||||
|
|
||||||
|
const expire_at = options?.expireMs ? new Date(updated_at.getTime() + options.expireMs) : new Date(updated_at.getTime() + 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
await cache.insert(cacheSchema.item_cache)
|
||||||
|
.values({ key, data, updated_at, expire_at })
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: cacheSchema.item_cache.key,
|
||||||
|
set: { data, updated_at, expire_at }
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
46
src/bun/api/emulatorjs/emulatorjs.ts
Normal file
46
src/bun/api/emulatorjs/emulatorjs.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
// ES-DE to emulator JS mapping
|
||||||
|
// TODO: use the retroarch cores based on ES-DE
|
||||||
|
export const cores: Record<string, string> = {
|
||||||
|
"atari5200": "atari5200",
|
||||||
|
"virtualboy": "vb",
|
||||||
|
"nds": "nds",
|
||||||
|
"arcade": "arcade",
|
||||||
|
"nes": "nes",
|
||||||
|
"gb": "gb",
|
||||||
|
"gbc": "gb",
|
||||||
|
"colecovision": "coleco",
|
||||||
|
"mastersystem": "segaMS",
|
||||||
|
"megadrive": "segaMD",
|
||||||
|
"gamegear": "segaGG",
|
||||||
|
"segacd": "segaCD",
|
||||||
|
"sega32x": "sega32x",
|
||||||
|
"genesis": "sega",
|
||||||
|
"mark3": "sega",
|
||||||
|
"megacd": "sega",
|
||||||
|
"megacdjp": "sega",
|
||||||
|
"megadrivejp": "sega",
|
||||||
|
"sg-1000": "sega",
|
||||||
|
"atarilynx": "lynx",
|
||||||
|
"mame": "mame",
|
||||||
|
"ngp": "ngp",
|
||||||
|
"supergrafx": "pce",
|
||||||
|
"pcfx": "pcfx",
|
||||||
|
"psx": "psx",
|
||||||
|
"wonderswan": "ws",
|
||||||
|
"gba": "gba",
|
||||||
|
"n64": "n64",
|
||||||
|
"3do": "3do",
|
||||||
|
"psp": "psp",
|
||||||
|
"atari7800": "atari7800",
|
||||||
|
"snes": "snes",
|
||||||
|
"atari2600": "atari2600",
|
||||||
|
"atarijaguar": "jaguar",
|
||||||
|
"saturn": "segaSaturn",
|
||||||
|
"amiga": "amiga",
|
||||||
|
"c64": "c64",
|
||||||
|
"c128": "c128",
|
||||||
|
"pet": "pet",
|
||||||
|
"plus4": "plus4",
|
||||||
|
"vic20": "vic20",
|
||||||
|
"dos": "dos"
|
||||||
|
};
|
||||||
|
|
@ -1,23 +1,32 @@
|
||||||
import Elysia, { status } from "elysia";
|
import Elysia, { status } from "elysia";
|
||||||
import { config, db, taskQueue } from "../app";
|
import { activeGame, config, db, events, taskQueue } from "../app";
|
||||||
import { and, eq, getTableColumns, sql } from "drizzle-orm";
|
import { and, eq, getTableColumns, sql } from "drizzle-orm";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import * as schema from "../schema/app";
|
import * as schema from "@schema/app";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { FrontEndGameType, FrontEndGameTypeDetailed, GameListFilterSchema } from "@shared/constants";
|
import { FrontEndGameType, FrontEndGameTypeDetailed, GameListFilterSchema } from "@shared/constants";
|
||||||
import { getRomApiRomsIdGet, getRomsApiRomsGet } from "@clients/romm";
|
import { getRomApiRomsIdGet, getRomsApiRomsGet } from "@clients/romm";
|
||||||
import { InstallJob } from "../jobs/install-job";
|
import { InstallJob } from "../jobs/install-job";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { calculateSize, checkInstalled, convertRomToFrontend, convertRomToFrontendDetailed, getLocalGameMatch } from "./services/utils";
|
import { calculateSize, checkInstalled, convertLocalToFrontend, convertRomToFrontend, convertRomToFrontendDetailed, convertStoreToFrontend, convertStoreToFrontendDetailed, getLocalGameMatch } from "./services/utils";
|
||||||
import buildStatusResponse, { getValidLaunchCommandsForGame } from "./services/statusService";
|
import buildStatusResponse, { getValidLaunchCommandsForGame } from "./services/statusService";
|
||||||
import { errorToResponse } from "elysia/adapter/bun/handler";
|
import { errorToResponse } from "elysia/adapter/bun/handler";
|
||||||
import { launchCommand } from "./services/launchGameService";
|
import { launchCommand } from "./services/launchGameService";
|
||||||
import { getErrorMessage } from "@/bun/utils";
|
import { getErrorMessage } from "@/bun/utils";
|
||||||
import { Jimp } from 'jimp';
|
import { defaultFormats, defaultPlugins } from 'jimp';
|
||||||
|
import { createJimp } from "@jimp/core";
|
||||||
|
import webp from "@jimp/wasm-webp";
|
||||||
|
import { extractStoreGameSourceId, getStoreGame, getStoreGameFromPath, getStoreGameManifest } from "../store/services/gamesService";
|
||||||
|
|
||||||
async function processImage (img: string | Buffer | ArrayBuffer, { blur, width, height }: { blur?: number, width?: number, height?: number; })
|
// A custom jimp that supports webp
|
||||||
|
const Jimp = createJimp({
|
||||||
|
formats: [...defaultFormats, webp],
|
||||||
|
plugins: defaultPlugins,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function processImage (img: string | Buffer | ArrayBuffer, { blur, width, height, noBlur }: { blur?: number, width?: number, height?: number; noBlur?: boolean; })
|
||||||
{
|
{
|
||||||
if (blur)
|
if (blur && !noBlur)
|
||||||
{
|
{
|
||||||
const jimp = await Jimp.read(img);
|
const jimp = await Jimp.read(img);
|
||||||
if (width)
|
if (width)
|
||||||
|
|
@ -48,6 +57,8 @@ async function processImage (img: string | Buffer | ArrayBuffer, { blur, width,
|
||||||
export default new Elysia()
|
export default new Elysia()
|
||||||
.get('/game/local/:id/cover', async ({ params: { id }, query, set }) =>
|
.get('/game/local/:id/cover', async ({ params: { id }, query, set }) =>
|
||||||
{
|
{
|
||||||
|
set.headers["cross-origin-resource-policy"] = 'cross-origin';
|
||||||
|
|
||||||
const coverBlob = await db.query.games.findFirst({ columns: { cover: true, cover_type: true }, where: eq(schema.games.id, id) });
|
const coverBlob = await db.query.games.findFirst({ columns: { cover: true, cover_type: true }, where: eq(schema.games.id, id) });
|
||||||
if (!coverBlob || !coverBlob.cover)
|
if (!coverBlob || !coverBlob.cover)
|
||||||
{
|
{
|
||||||
|
|
@ -71,7 +82,7 @@ export default new Elysia()
|
||||||
return processImage(`${rommAdress}/${path}`, query);
|
return processImage(`${rommAdress}/${path}`, query);
|
||||||
}
|
}
|
||||||
return status('Not Found');
|
return status('Not Found');
|
||||||
}, { query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() }) })
|
}, { query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional(), noBlur: z.coerce.boolean().optional() }) })
|
||||||
.get('/image', async ({ query }) =>
|
.get('/image', async ({ query }) =>
|
||||||
{
|
{
|
||||||
return processImage(query.url, query);
|
return processImage(query.url, query);
|
||||||
|
|
@ -106,18 +117,24 @@ export default new Elysia()
|
||||||
}, {
|
}, {
|
||||||
params: z.object({ id: z.number() }),
|
params: z.object({ id: z.number() }),
|
||||||
response: z.object({ installed: z.boolean() })
|
response: z.object({ installed: z.boolean() })
|
||||||
}).get('/games', async ({ query: { platform_source, platform_slug, platform_id, collection_id } }) =>
|
})
|
||||||
|
.get('/games', async ({ query, set }) =>
|
||||||
{
|
{
|
||||||
const where: any[] = [];
|
const where: any[] = [];
|
||||||
if (platform_slug)
|
if (query.platform_slug)
|
||||||
{
|
{
|
||||||
where.push(eq(schema.platforms.slug, platform_slug));
|
where.push(eq(schema.platforms.slug, query.platform_slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.source)
|
||||||
|
{
|
||||||
|
where.push(eq(schema.games.source, query.source));
|
||||||
}
|
}
|
||||||
|
|
||||||
const games: FrontEndGameType[] = [];
|
const games: FrontEndGameType[] = [];
|
||||||
let localGamesSet: Set<number> | undefined;
|
let localGamesSet: Set<string> | undefined;
|
||||||
|
|
||||||
if (!collection_id)
|
if (!query.collection_id)
|
||||||
{
|
{
|
||||||
const localGames = await db.select({
|
const localGames = await db.select({
|
||||||
...getTableColumns(schema.games),
|
...getTableColumns(schema.games),
|
||||||
|
|
@ -128,45 +145,87 @@ export default new Elysia()
|
||||||
.leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id))
|
.leftJoin(schema.platforms, eq(schema.platforms.id, schema.games.platform_id))
|
||||||
.leftJoin(schema.screenshots, eq(schema.screenshots.game_id, schema.games.id))
|
.leftJoin(schema.screenshots, eq(schema.screenshots.game_id, schema.games.id))
|
||||||
.groupBy(schema.games.id)
|
.groupBy(schema.games.id)
|
||||||
|
.offset(query.offset ?? 0)
|
||||||
|
.limit(query.limit ?? 50)
|
||||||
.where(and(...where));
|
.where(and(...where));
|
||||||
|
|
||||||
localGamesSet = new Set(localGames.filter(g => !!g.source_id).map(g => g.source_id!));
|
localGamesSet = new Set(localGames.filter(g => !!g.source_id && !!g.source).map(g => `${g.source}@${g.source_id}`));
|
||||||
games.push(...localGames.map(g =>
|
games.push(...localGames.map(g =>
|
||||||
{
|
{
|
||||||
const game: FrontEndGameType = {
|
return convertLocalToFrontend(g);
|
||||||
platform_display_name: g.platform?.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`,
|
|
||||||
paths_screenshots: g.screenshotIds?.map(s => `/api/romm/screenshot/${s}`) ?? [],
|
|
||||||
path_fs: g.path_fs,
|
|
||||||
last_played: g.last_played,
|
|
||||||
slug: g.slug,
|
|
||||||
name: g.name,
|
|
||||||
platform_id: g.platform_id
|
|
||||||
};
|
|
||||||
return game;
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((!platform_source || platform_source === 'romm') || !!collection_id)
|
if (((!query.platform_source || query.platform_source === 'romm') || !!query.collection_id) && (!query.source || query.source === 'romm'))
|
||||||
{
|
{
|
||||||
const rommGames = await getRomsApiRomsGet({ query: { platform_ids: platform_id ? [platform_id] : undefined, collection_id }, throwOnError: true });
|
const rommGames = await getRomsApiRomsGet({
|
||||||
games.push(...rommGames.data.items.filter(g => !localGamesSet?.has(g.id)).map(g =>
|
query: {
|
||||||
|
platform_ids: query.platform_id ? [query.platform_id] : undefined,
|
||||||
|
collection_id: query.collection_id,
|
||||||
|
limit: query.limit,
|
||||||
|
offset: query.offset
|
||||||
|
}, throwOnError: true
|
||||||
|
});
|
||||||
|
games.push(...rommGames.data.items.filter(g => !localGamesSet?.has(`romm@${g.id}`)).map(g =>
|
||||||
{
|
{
|
||||||
return convertRomToFrontend(g);
|
return convertRomToFrontend(g);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.source === 'store')
|
||||||
|
{
|
||||||
|
const gamesManifest = await getStoreGameManifest();
|
||||||
|
set.headers['x-max-items'] = gamesManifest.filter(g => g.type === 'blob').length;
|
||||||
|
|
||||||
|
const storeGames = await Promise.all(gamesManifest
|
||||||
|
.slice(query.offset ?? 0, Math.min((query.offset ?? 0) + (query.limit ?? 50), gamesManifest.length))
|
||||||
|
.map(async (e) =>
|
||||||
|
{
|
||||||
|
const system = path.dirname(e.path);
|
||||||
|
const id = path.basename(e.path, path.extname(e.path));
|
||||||
|
|
||||||
|
const localGame = await db.query.games.findFirst({ columns: { id: true }, where: and(eq(schema.games.source, 'store'), eq(schema.games.source_id, `${system}@${id}`)) });
|
||||||
|
|
||||||
|
if (localGame)
|
||||||
|
{
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeGame = await getStoreGameFromPath(e.path);
|
||||||
|
|
||||||
|
return convertStoreToFrontend(system, id, storeGame);
|
||||||
|
}));
|
||||||
|
games.push(...storeGames.filter(g => g !== undefined));
|
||||||
|
}
|
||||||
|
|
||||||
return { games };
|
return { games };
|
||||||
}, {
|
}, {
|
||||||
query: GameListFilterSchema,
|
query: GameListFilterSchema,
|
||||||
})
|
})
|
||||||
|
.get('/rom/:source/:id', async ({ params: { id, source } }) =>
|
||||||
|
{
|
||||||
|
const localGame = await db.query.games.findFirst({
|
||||||
|
where: getLocalGameMatch(id, source),
|
||||||
|
columns: { path_fs: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!localGame?.path_fs)
|
||||||
|
{
|
||||||
|
return status("Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadPath = config.get('downloadPath');
|
||||||
|
const path_fs = path.join(downloadPath, localGame.path_fs);
|
||||||
|
const stats = await fs.stat(path_fs);
|
||||||
|
if (stats.isDirectory())
|
||||||
|
{
|
||||||
|
return status("Not Found", "Rom is a folder");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bun.file(path_fs);
|
||||||
|
}, {
|
||||||
|
params: z.object({ source: z.string(), id: z.string() })
|
||||||
|
})
|
||||||
.get('/game/:source/:id', async ({ params: { source, id } }) =>
|
.get('/game/:source/:id', async ({ params: { source, id } }) =>
|
||||||
{
|
{
|
||||||
async function getLocalGameDetailed (match: any)
|
async function getLocalGameDetailed (match: any)
|
||||||
|
|
@ -175,7 +234,7 @@ export default new Elysia()
|
||||||
where: match,
|
where: match,
|
||||||
with: {
|
with: {
|
||||||
screenshots: { columns: { id: true } },
|
screenshots: { columns: { id: true } },
|
||||||
platform: { columns: { name: true } }
|
platform: { columns: { name: true, slug: true } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (localGame)
|
if (localGame)
|
||||||
|
|
@ -185,7 +244,7 @@ export default new Elysia()
|
||||||
const game: FrontEndGameTypeDetailed = {
|
const game: FrontEndGameTypeDetailed = {
|
||||||
path_cover: `/api/romm/game/local/${localGame.id}/cover`,
|
path_cover: `/api/romm/game/local/${localGame.id}/cover`,
|
||||||
updated_at: localGame.created_at,
|
updated_at: localGame.created_at,
|
||||||
id: { id: localGame.id, source: 'local' },
|
id: { id: String(localGame.id), source: 'local' },
|
||||||
path_platform_cover: `/api/romm/platform/local/${localGame.platform_id}/cover`,
|
path_platform_cover: `/api/romm/platform/local/${localGame.platform_id}/cover`,
|
||||||
fs_size_bytes: fileSize ?? null,
|
fs_size_bytes: fileSize ?? null,
|
||||||
paths_screenshots: localGame.screenshots.map(s => `/api/romm/screenshot/${s.id}`),
|
paths_screenshots: localGame.screenshots.map(s => `/api/romm/screenshot/${s.id}`),
|
||||||
|
|
@ -199,7 +258,8 @@ export default new Elysia()
|
||||||
last_played: localGame.last_played,
|
last_played: localGame.last_played,
|
||||||
slug: localGame.slug,
|
slug: localGame.slug,
|
||||||
name: localGame.name,
|
name: localGame.name,
|
||||||
platform_id: localGame.platform_id
|
platform_id: localGame.platform_id,
|
||||||
|
platform_slug: localGame.platform.slug
|
||||||
};
|
};
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +269,7 @@ export default new Elysia()
|
||||||
|
|
||||||
if (source === 'local')
|
if (source === 'local')
|
||||||
{
|
{
|
||||||
const localGame = await getLocalGameDetailed(eq(schema.games.id, id));
|
const localGame = await getLocalGameDetailed(eq(schema.games.id, Number(id)));
|
||||||
if (localGame) return localGame;
|
if (localGame) return localGame;
|
||||||
return status('Not Found');
|
return status('Not Found');
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +278,9 @@ export default new Elysia()
|
||||||
const localGame = await getLocalGameDetailed(getLocalGameMatch(id, source));
|
const localGame = await getLocalGameDetailed(getLocalGameMatch(id, source));
|
||||||
if (localGame) return localGame;
|
if (localGame) return localGame;
|
||||||
|
|
||||||
const rom = await getRomApiRomsIdGet({ path: { id } });
|
if (source === 'romm')
|
||||||
|
{
|
||||||
|
const rom = await getRomApiRomsIdGet({ path: { id: Number(id) } });
|
||||||
if (rom.data)
|
if (rom.data)
|
||||||
{
|
{
|
||||||
const romGame = convertRomToFrontendDetailed(rom.data);
|
const romGame = convertRomToFrontendDetailed(rom.data);
|
||||||
|
|
@ -227,9 +289,19 @@ export default new Elysia()
|
||||||
|
|
||||||
return status("Not Found", rom.response);
|
return status("Not Found", rom.response);
|
||||||
}
|
}
|
||||||
|
else if (source === 'store')
|
||||||
|
{
|
||||||
|
const gameId = extractStoreGameSourceId(id);
|
||||||
|
const storeGame = await getStoreGame(gameId.system, gameId.id);
|
||||||
|
if (!storeGame) return status("Not Found");
|
||||||
|
return convertStoreToFrontendDetailed(gameId.system, gameId.id, storeGame);
|
||||||
|
}
|
||||||
|
|
||||||
|
return status("Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
}, {
|
}, {
|
||||||
params: z.object({ source: z.string(), id: z.coerce.number() })
|
params: z.object({ source: z.string(), id: z.string() })
|
||||||
})
|
})
|
||||||
.get('/status/:source/:id', async ({ params: { source, id }, set }) =>
|
.get('/status/:source/:id', async ({ params: { source, id }, set }) =>
|
||||||
{
|
{
|
||||||
|
|
@ -239,7 +311,7 @@ export default new Elysia()
|
||||||
return buildStatusResponse(source, id);
|
return buildStatusResponse(source, id);
|
||||||
}, {
|
}, {
|
||||||
response: z.any(),
|
response: z.any(),
|
||||||
params: z.object({ id: z.coerce.number(), source: z.string() }),
|
params: z.object({ id: z.string(), source: z.string() }),
|
||||||
query: z.object({ isLocal: z.boolean().optional() })
|
query: z.object({ isLocal: z.boolean().optional() })
|
||||||
})
|
})
|
||||||
.delete('/game/:source/:id', async ({ params: { source, id } }) =>
|
.delete('/game/:source/:id', async ({ params: { source, id } }) =>
|
||||||
|
|
@ -253,36 +325,51 @@ export default new Elysia()
|
||||||
|
|
||||||
return status(deleted.length > 0 ? 'OK' : 'Not Modified');
|
return status(deleted.length > 0 ? 'OK' : 'Not Modified');
|
||||||
}, {
|
}, {
|
||||||
params: z.object({ id: z.coerce.number(), source: z.string() }),
|
params: z.object({ id: z.string(), source: z.string() }),
|
||||||
})
|
})
|
||||||
.post('/game/:source/:id/install', async ({ params: { id, source } }) =>
|
.post('/game/:source/:id/install', async ({ params: { id, source } }) =>
|
||||||
{
|
{
|
||||||
if (!taskQueue.hasActive())
|
if (!taskQueue.hasActive())
|
||||||
|
{
|
||||||
|
if (source === 'romm' || source === 'store')
|
||||||
{
|
{
|
||||||
taskQueue.enqueue(`install-rom-${source}-${id}`, new InstallJob(id, source, id));
|
taskQueue.enqueue(`install-rom-${source}-${id}`, new InstallJob(id, source, id));
|
||||||
return status(200);
|
return status(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
return status('Not Implemented');
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
return status('Not Implemented');
|
return status('Not Implemented');
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
params: z.object({ id: z.coerce.number(), source: z.string() }),
|
params: z.object({ id: z.string(), source: z.string() }),
|
||||||
response: z.any()
|
response: z.any()
|
||||||
})
|
})
|
||||||
.post('/game/:source/:id/play', async ({ params: { id, source }, set }) =>
|
.post('/game/:source/:id/play', async ({ params: { id, source }, query, set }) =>
|
||||||
{
|
{
|
||||||
const validCommand = await getValidLaunchCommandsForGame(source, id);
|
const validCommands = await getValidLaunchCommandsForGame(source, id);
|
||||||
if (validCommand)
|
if (validCommands)
|
||||||
{
|
{
|
||||||
if (validCommand instanceof Error)
|
if (validCommands instanceof Error)
|
||||||
{
|
{
|
||||||
return errorToResponse(validCommand, set);
|
return errorToResponse(validCommands, set);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await launchCommand(validCommand.command.command, source, id, validCommand.gameId);
|
const validCommand = query.command_id ? validCommands.commands.find(c => c.id === query.command_id) : validCommands.commands[0];
|
||||||
|
if (validCommand)
|
||||||
|
{
|
||||||
|
// launch command waits for the game to exit, we don't want that.
|
||||||
|
launchCommand(validCommand.command, source, id, validCommands.gameId);
|
||||||
|
return { type: 'application', command: null };
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
return status("Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error)
|
} catch (error)
|
||||||
{
|
{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
@ -291,5 +378,27 @@ export default new Elysia()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
params: z.object({ id: z.coerce.number(), source: z.string() }),
|
params: z.object({ id: z.string(), source: z.string() }),
|
||||||
|
query: z.object({ command_id: z.number().or(z.string()).optional() }),
|
||||||
|
response: z.object({ type: z.enum(['emulatorjs', 'application']), command: z.string().nullable() })
|
||||||
|
})
|
||||||
|
.post("/stop", async ({ }) =>
|
||||||
|
{
|
||||||
|
if (activeGame)
|
||||||
|
{
|
||||||
|
events.emit('activegameexit', {
|
||||||
|
source: 'local', id: String(activeGame.gameId),
|
||||||
|
exitCode: null,
|
||||||
|
signalCode: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get('/emulatorjs/data/cores/*', async ({ params }) =>
|
||||||
|
{
|
||||||
|
const res = await fetch(`https://cdn.emulatorjs.org/latest/data/cores/${params['*']}`);
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.get('/emulatorjs/data/*', async ({ params }) =>
|
||||||
|
{
|
||||||
|
return status("Not Found");
|
||||||
});
|
});
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import Elysia, { status } from "elysia";
|
import Elysia, { status } from "elysia";
|
||||||
import { getPlatformApiPlatformsIdGet, getPlatformsApiPlatformsGet, getRomsApiRomsGet } from "@clients/romm";
|
import { getPlatformApiPlatformsIdGet, getPlatformsApiPlatformsGet, getRomsApiRomsGet } from "@clients/romm";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { count, eq, getTableColumns, notInArray } from "drizzle-orm";
|
import { count, eq, getTableColumns } from "drizzle-orm";
|
||||||
import { db } from "../app";
|
import { db } from "../app";
|
||||||
import { FrontEndPlatformType } from "@shared/constants";
|
import { FrontEndPlatformType } from "@shared/constants";
|
||||||
import * as schema from "../schema/app";
|
import * as schema from "@schema/app";
|
||||||
|
import { CACHE_KEYS, getOrCached } from "../cache";
|
||||||
|
|
||||||
export default new Elysia()
|
export default new Elysia()
|
||||||
.get('/platforms', async () =>
|
.get('/platforms', async () =>
|
||||||
{
|
{
|
||||||
const platforms: FrontEndPlatformType[] = [];
|
const platforms: FrontEndPlatformType[] = [];
|
||||||
let rommPlatformsSet: Set<string> | undefined;
|
let rommPlatformsSet: Set<string> | undefined;
|
||||||
const { data: rommPlatforms } = await getPlatformsApiPlatformsGet();
|
const rommPlatforms = await getOrCached(CACHE_KEYS.ROM_PLATFORMS, () => getPlatformsApiPlatformsGet({ throwOnError: true }), { expireMs: 60 * 60 * 1000 }).then(d => d.data).catch(e => console.error(e));
|
||||||
|
|
||||||
const localPlatforms = await db.select({ ...getTableColumns(schema.platforms), game_count: count(schema.games.id) })
|
const localPlatforms = await db.select({ ...getTableColumns(schema.platforms), game_count: count(schema.games.id) })
|
||||||
.from(schema.platforms)
|
.from(schema.platforms)
|
||||||
|
|
@ -32,7 +33,7 @@ export default new Elysia()
|
||||||
path_cover: `/api/romm/image/romm/assets/platforms/${p.slug}.svg`,
|
path_cover: `/api/romm/image/romm/assets/platforms/${p.slug}.svg`,
|
||||||
game_count: p.rom_count,
|
game_count: p.rom_count,
|
||||||
updated_at: new Date(p.updated_at),
|
updated_at: new Date(p.updated_at),
|
||||||
id: { source: 'romm', id: p.id },
|
id: { source: 'romm', id: String(p.id) },
|
||||||
hasLocal: localPlatformSet.has(p.slug),
|
hasLocal: localPlatformSet.has(p.slug),
|
||||||
paths_screenshots: game.data?.items[0]?.merged_screenshots.map(s => `/api/romm/image/romm/${s}`) ?? []
|
paths_screenshots: game.data?.items[0]?.merged_screenshots.map(s => `/api/romm/image/romm/${s}`) ?? []
|
||||||
};
|
};
|
||||||
|
|
@ -46,7 +47,13 @@ export default new Elysia()
|
||||||
|
|
||||||
platforms.push(...await Promise.all(localPlatforms.filter(p => !rommPlatformsSet?.has(p.slug)).map(async p =>
|
platforms.push(...await Promise.all(localPlatforms.filter(p => !rommPlatformsSet?.has(p.slug)).map(async p =>
|
||||||
{
|
{
|
||||||
const game = await db.query.games.findFirst({ where: eq(schema.games.platform_id, p.id), with: { screenshots: true }, columns: {} });
|
const game = await db.query.games.findFirst({ where: eq(schema.games.platform_id, p.id) });
|
||||||
|
let screenshots: { id: number; }[] = [];
|
||||||
|
if (game)
|
||||||
|
{
|
||||||
|
screenshots = await db.query.screenshots.findMany({ where: eq(schema.screenshots.game_id, game.id), columns: { id: true } });
|
||||||
|
}
|
||||||
|
|
||||||
const platform: FrontEndPlatformType = {
|
const platform: FrontEndPlatformType = {
|
||||||
slug: p.slug,
|
slug: p.slug,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
|
|
@ -54,9 +61,9 @@ export default new Elysia()
|
||||||
path_cover: `/api/romm/platform/local/${p.id}/cover`,
|
path_cover: `/api/romm/platform/local/${p.id}/cover`,
|
||||||
game_count: p.game_count,
|
game_count: p.game_count,
|
||||||
updated_at: p.created_at,
|
updated_at: p.created_at,
|
||||||
id: { source: 'local', id: p.id },
|
id: { source: 'local', id: String(p.id) },
|
||||||
hasLocal: true,
|
hasLocal: true,
|
||||||
paths_screenshots: game?.screenshots?.map(s => `/api/romm/screenshot/${s.id}`) ?? []
|
paths_screenshots: screenshots?.map(s => `/api/romm/screenshot/${s.id}`) ?? []
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -66,13 +73,52 @@ export default new Elysia()
|
||||||
return { platforms };
|
return { platforms };
|
||||||
}).get('/platforms/:source/:id', async ({ params: { source, id } }) =>
|
}).get('/platforms/:source/:id', async ({ params: { source, id } }) =>
|
||||||
{
|
{
|
||||||
const rommPlatform = await getPlatformApiPlatformsIdGet({ path: { id } });
|
if (source === 'romm')
|
||||||
if (rommPlatform.data)
|
|
||||||
{
|
{
|
||||||
return rommPlatform.data;
|
const { data: rommPlatform, response } = await getPlatformApiPlatformsIdGet({ path: { id } });
|
||||||
|
if (rommPlatform)
|
||||||
|
{
|
||||||
|
const platform: FrontEndPlatformType = {
|
||||||
|
slug: rommPlatform.slug,
|
||||||
|
name: rommPlatform.display_name,
|
||||||
|
family_name: rommPlatform.family_name,
|
||||||
|
path_cover: `/api/romm/image/romm/assets/platforms/${rommPlatform.slug}.svg`,
|
||||||
|
game_count: rommPlatform.rom_count,
|
||||||
|
updated_at: new Date(rommPlatform.updated_at),
|
||||||
|
id: { source: 'romm', id: String(rommPlatform.id) },
|
||||||
|
paths_screenshots: [],
|
||||||
|
hasLocal: false
|
||||||
|
};
|
||||||
|
|
||||||
|
return platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
return status("Not Found", rommPlatform.response);
|
return status("Not Found", response);
|
||||||
|
}
|
||||||
|
else if (source === 'local')
|
||||||
|
{
|
||||||
|
const localPlatform = await db.query.platforms.findFirst({ where: eq(schema.platforms.id, id) });
|
||||||
|
if (localPlatform)
|
||||||
|
{
|
||||||
|
const platform: FrontEndPlatformType = {
|
||||||
|
slug: localPlatform.slug,
|
||||||
|
name: localPlatform.name,
|
||||||
|
family_name: localPlatform.family_name,
|
||||||
|
path_cover: `/api/romm/platform/local/${localPlatform.id}/cover`,
|
||||||
|
game_count: 0,
|
||||||
|
updated_at: localPlatform.created_at,
|
||||||
|
id: { source: 'local', id: String(localPlatform.id) },
|
||||||
|
hasLocal: true,
|
||||||
|
paths_screenshots: []
|
||||||
|
};
|
||||||
|
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status("Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return status("Not Implemented");
|
||||||
}, { params: z.object({ source: z.string(), id: z.coerce.number() }) }).get('/platform/local/:id/cover', async ({ params: { id }, set }) =>
|
}, { 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({
|
const coverBlob = await db.query.platforms.findFirst({
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,19 @@ import path from 'node:path';
|
||||||
import { which } from 'bun';
|
import { which } from 'bun';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import * as schema from '../../schema/emulators';
|
import * as schema from '@schema/emulators';
|
||||||
import * as appSchema from "../../schema/app";
|
import * as appSchema from "@schema/app";
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { activeGame, config, db, emulatorsDb, events, setActiveGame } from '../../app';
|
import { activeGame, config, db, emulatorsDb, events, setActiveGame } from '../../app';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { $ } from 'bun';
|
import { $ } from 'bun';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { updateRomUserApiRomsIdPropsPut } from '@/clients/romm';
|
import { updateRomUserApiRomsIdPropsPut } from '@/clients/romm';
|
||||||
|
import { CommandEntry } from '@/shared/constants';
|
||||||
|
|
||||||
export const varRegex = /%([^%]+)%/g;
|
export const varRegex = /%([^%]+)%/g;
|
||||||
|
|
||||||
interface CommandEntry
|
export async function launchCommand (validCommand: string, source: string, sourceId: string, id: number)
|
||||||
{
|
|
||||||
label?: string;
|
|
||||||
command: string;
|
|
||||||
valid: boolean;
|
|
||||||
emulator?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function launchCommand (validCommand: string, source: string, sourceId: number, id: number)
|
|
||||||
{
|
{
|
||||||
if (activeGame && activeGame.process?.killed === false)
|
if (activeGame && activeGame.process?.killed === false)
|
||||||
{
|
{
|
||||||
|
|
@ -69,13 +62,12 @@ export async function launchCommand (validCommand: string, source: string, sourc
|
||||||
|
|
||||||
if (source === 'romm')
|
if (source === 'romm')
|
||||||
{
|
{
|
||||||
updateRommProps(sourceId);
|
updateRommProps(Number(sourceId));
|
||||||
}
|
}
|
||||||
else if (localGame?.source === 'romm' && localGame.source_id)
|
else if (localGame?.source === 'romm' && localGame.source_id)
|
||||||
{
|
{
|
||||||
updateRommProps(localGame.source_id);
|
updateRommProps(Number(localGame.source_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Old spawn lanching, cases issues, needs to be ran as shell
|
/* Old spawn lanching, cases issues, needs to be ran as shell
|
||||||
|
|
@ -117,7 +109,10 @@ export async function getValidLaunchCommands (data: {
|
||||||
}): Promise<CommandEntry[]>
|
}): Promise<CommandEntry[]>
|
||||||
{
|
{
|
||||||
|
|
||||||
const system = await emulatorsDb.query.systems.findFirst({ with: { commands: true }, where: eq(schema.systems.name, data.systemSlug) });
|
const system = await emulatorsDb.query.systems.findFirst({
|
||||||
|
with: { commands: true },
|
||||||
|
where: eq(schema.systems.name, data.systemSlug)
|
||||||
|
});
|
||||||
|
|
||||||
if (!system)
|
if (!system)
|
||||||
{
|
{
|
||||||
|
|
@ -165,7 +160,7 @@ export async function getValidLaunchCommands (data: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedCommands = await Promise.all(system.commands.map(async command =>
|
const formattedCommands = await Promise.all(system.commands.map(async (command, index) =>
|
||||||
{
|
{
|
||||||
const label = command.label;
|
const label = command.label;
|
||||||
let cmd = command.command;
|
let cmd = command.command;
|
||||||
|
|
@ -213,14 +208,14 @@ export async function getValidLaunchCommands (data: {
|
||||||
if (value.startsWith("%EMULATOR_"))
|
if (value.startsWith("%EMULATOR_"))
|
||||||
{
|
{
|
||||||
const emulatorName = value.substring("%EMULATOR_".length, value.length - 1);
|
const emulatorName = value.substring("%EMULATOR_".length, value.length - 1);
|
||||||
let exec = await findExec(emulatorName);
|
let exec = await findExecByName(emulatorName);
|
||||||
if (data.customEmulatorConfig.has(emulatorName))
|
if (data.customEmulatorConfig.has(emulatorName))
|
||||||
{
|
{
|
||||||
exec = data.customEmulatorConfig.get(emulatorName);
|
exec = { path: data.customEmulatorConfig.get(emulatorName)!, type: 'custom' };
|
||||||
}
|
}
|
||||||
|
|
||||||
emulator = emulatorName;
|
emulator = emulatorName;
|
||||||
return [[value, exec ? exec : undefined], ['%EMUDIR%', exec ? $.escape(path.dirname(exec)) : undefined]];
|
return [[value, exec ? exec : undefined], ['%EMUDIR%', exec ? $.escape(path.dirname(exec.path)) : undefined]];
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = value[0].substring(1, value.length - 1);
|
const key = value[0].substring(1, value.length - 1);
|
||||||
|
|
@ -237,6 +232,7 @@ export async function getValidLaunchCommands (data: {
|
||||||
const formattedCommand = cmd.replace(varRegex, (s) => vars[s] ?? '').trim();
|
const formattedCommand = cmd.replace(varRegex, (s) => vars[s] ?? '').trim();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: index,
|
||||||
label: label ?? undefined,
|
label: label ?? undefined,
|
||||||
command: formattedCommand,
|
command: formattedCommand,
|
||||||
valid: !invalid, emulator
|
valid: !invalid, emulator
|
||||||
|
|
@ -246,13 +242,18 @@ export async function getValidLaunchCommands (data: {
|
||||||
return formattedCommands.filter(c => !!c);
|
return formattedCommands.filter(c => !!c);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findExec (emulatorName: string)
|
export async function findExecByName (emulatorName: string)
|
||||||
{
|
{
|
||||||
const emulator = await emulatorsDb.query.emulators.findFirst({ where: eq(schema.emulators.name, emulatorName) });
|
const emulator = await emulatorsDb.query.emulators.findFirst({ where: eq(schema.emulators.name, emulatorName) });
|
||||||
if (!emulator)
|
if (!emulator)
|
||||||
{
|
{
|
||||||
throw new Error(`Could not find emulator ${emulatorName}`);
|
throw new Error(`Could not find emulator ${emulatorName}`);
|
||||||
}
|
}
|
||||||
|
return findExec(emulator);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findExec (emulator: { winregistrypath: string[], systempath: string[], staticpath: string[]; })
|
||||||
|
{
|
||||||
if (os.platform() === 'win32')
|
if (os.platform() === 'win32')
|
||||||
{
|
{
|
||||||
const regValues = emulator.winregistrypath;
|
const regValues = emulator.winregistrypath;
|
||||||
|
|
@ -263,7 +264,7 @@ export async function findExec (emulatorName: string)
|
||||||
const registryValue = await readRegistryValue(node);
|
const registryValue = await readRegistryValue(node);
|
||||||
if (registryValue)
|
if (registryValue)
|
||||||
{
|
{
|
||||||
return registryValue;
|
return { path: registryValue, type: 'registry' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,7 +277,7 @@ export async function findExec (emulatorName: string)
|
||||||
const systemPath = await resolveSystemPath(systempaths);
|
const systemPath = await resolveSystemPath(systempaths);
|
||||||
if (systemPath)
|
if (systemPath)
|
||||||
{
|
{
|
||||||
return systemPath;
|
return { path: systemPath, type: 'system' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,7 +287,7 @@ export async function findExec (emulatorName: string)
|
||||||
const staticPath = await resolveStaticPath(staticPaths);
|
const staticPath = await resolveStaticPath(staticPaths);
|
||||||
if (staticPath)
|
if (staticPath)
|
||||||
{
|
{
|
||||||
return staticPath;
|
return { path: staticPath, type: 'static' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import { GameInstallProgress, GameStatusType, } from "@shared/constants";
|
import { GameInstallProgress, GameStatusType, RPC_URL, } from "@shared/constants";
|
||||||
import { activeGame, config, customEmulators, db, events, taskQueue } from "../../app";
|
import { activeGame, config, customEmulators, db, events, taskQueue } from "../../app";
|
||||||
import { getValidLaunchCommands } from "./launchGameService";
|
import { getValidLaunchCommands } from "./launchGameService";
|
||||||
import * as schema from '../../schema/app';
|
import * as schema from '@schema/app';
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getErrorMessage } from "@/bun/utils";
|
import { getErrorMessage } from "@/bun/utils";
|
||||||
import { getLocalGameMatch } from "./utils";
|
import { getLocalGameMatch } from "./utils";
|
||||||
import { getRomApiRomsIdGet } from "@/clients/romm";
|
import { getRomApiRomsIdGet } from "@/clients/romm";
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { ErrorLike } from "elysia/universal";
|
import { ErrorLike } from "elysia/universal";
|
||||||
|
import { getStoreGameFromId } from "../../store/services/gamesService";
|
||||||
|
import { cores } from "../../emulatorjs/emulatorjs";
|
||||||
|
import { host } from "@/bun/utils/host";
|
||||||
|
|
||||||
class CommandSearchError extends Error
|
class CommandSearchError extends Error
|
||||||
{
|
{
|
||||||
|
|
@ -18,7 +21,7 @@ class CommandSearchError extends Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLocalGame (source: string, id: number)
|
export async function getLocalGame (source: string, id: string)
|
||||||
{
|
{
|
||||||
const localGames = await db.select({ id: schema.games.id, path_fs: schema.games.path_fs, platform_slug: schema.platforms.es_slug })
|
const localGames = await db.select({ id: schema.games.id, path_fs: schema.games.path_fs, platform_slug: schema.platforms.es_slug })
|
||||||
.from(schema.games)
|
.from(schema.games)
|
||||||
|
|
@ -33,7 +36,7 @@ export async function getLocalGame (source: string, id: number)
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getValidLaunchCommandsForGame (source: string, id: number)
|
export async function getValidLaunchCommandsForGame (source: string, id: string)
|
||||||
{
|
{
|
||||||
const localGame = await getLocalGame(source, id);
|
const localGame = await getLocalGame(source, id);
|
||||||
if (localGame)
|
if (localGame)
|
||||||
|
|
@ -42,18 +45,28 @@ export async function getValidLaunchCommandsForGame (source: string, id: number)
|
||||||
{
|
{
|
||||||
if (localGame.path_fs)
|
if (localGame.path_fs)
|
||||||
{
|
{
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const commands = await getValidLaunchCommands({ systemSlug: localGame.platform_slug, customEmulatorConfig: customEmulators, gamePath: localGame.path_fs });
|
const commands = await getValidLaunchCommands({ systemSlug: localGame.platform_slug, customEmulatorConfig: customEmulators, gamePath: localGame.path_fs });
|
||||||
|
|
||||||
|
if (cores[localGame.platform_slug])
|
||||||
|
{
|
||||||
|
const gameUrl = `${RPC_URL(host)}/api/romm/rom/${source}/${id}`;
|
||||||
|
commands.push({
|
||||||
|
id: 'emulatorjs',
|
||||||
|
label: "Emulator JS", command: `core=${cores[localGame.platform_slug]}&gameUrl=${encodeURIComponent(gameUrl)}`, valid: true, emulator: 'emulatorjs'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const validCommand = commands.find(c => c.valid);
|
const validCommand = commands.find(c => c.valid);
|
||||||
if (validCommand)
|
if (validCommand)
|
||||||
{
|
{
|
||||||
return { command: validCommand, gameId: localGame.id, source: source, sourceId: id };
|
return { commands: commands.filter(c => c.valid), gameId: localGame.id, source: source, sourceId: id };
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator).join(',')}`);
|
return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${Array.from(new Set(commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator))).join(', ')}`);
|
||||||
}
|
}
|
||||||
} catch (error)
|
} catch (error)
|
||||||
{
|
{
|
||||||
|
|
@ -76,7 +89,7 @@ export async function getValidLaunchCommandsForGame (source: string, id: number)
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function buildStatusResponse (source: string, id: number)
|
export default async function buildStatusResponse (source: string, id: string)
|
||||||
{
|
{
|
||||||
let cleanup: (() => void) | undefined;
|
let cleanup: (() => void) | undefined;
|
||||||
let closed = false;
|
let closed = false;
|
||||||
|
|
@ -87,6 +100,7 @@ export default async function buildStatusResponse (source: string, id: number)
|
||||||
|
|
||||||
function enqueue (data: GameInstallProgress, event?: 'error' | 'refresh' | 'ping')
|
function enqueue (data: GameInstallProgress, event?: 'error' | 'refresh' | 'ping')
|
||||||
{
|
{
|
||||||
|
if (closed) return;
|
||||||
const evntString = event ? `event: ${event}\n` : '';
|
const evntString = event ? `event: ${event}\n` : '';
|
||||||
controller.enqueue(encoder.encode(`${evntString}data: ${JSON.stringify(data)}\n\n`));
|
controller.enqueue(encoder.encode(`${evntString}data: ${JSON.stringify(data)}\n\n`));
|
||||||
}
|
}
|
||||||
|
|
@ -136,13 +150,14 @@ export default async function buildStatusResponse (source: string, id: number)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
enqueue({ status: 'installed', details: validCommand.command.label });
|
enqueue({ status: 'installed', details: validCommand.commands[0].label, commands: validCommand.commands });
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (source === 'romm')
|
}
|
||||||
|
else if (source === 'romm')
|
||||||
{
|
{
|
||||||
// TODO: Add Caching
|
// TODO: Add Caching
|
||||||
const remoteGame = await getRomApiRomsIdGet({ path: { id } });
|
const remoteGame = await getRomApiRomsIdGet({ path: { id: Number(id) } });
|
||||||
const stats = await fs.statfs(config.get('downloadPath'));
|
const stats = await fs.statfs(config.get('downloadPath'));
|
||||||
if (remoteGame.data?.fs_size_bytes && remoteGame.data?.fs_size_bytes > stats.bsize * stats.bavail)
|
if (remoteGame.data?.fs_size_bytes && remoteGame.data?.fs_size_bytes > stats.bsize * stats.bavail)
|
||||||
{
|
{
|
||||||
|
|
@ -152,6 +167,20 @@ export default async function buildStatusResponse (source: string, id: number)
|
||||||
enqueue({ status: 'install', details: 'Install' });
|
enqueue({ status: 'install', details: 'Install' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (source === 'store')
|
||||||
|
{
|
||||||
|
const storeGame = await getStoreGameFromId(id);
|
||||||
|
const fileResponse = await fetch(storeGame.file, { method: 'HEAD' });
|
||||||
|
const size = Number(fileResponse.headers.get('content-length'));
|
||||||
|
const stats = await fs.statfs(config.get('downloadPath'));
|
||||||
|
|
||||||
|
if (size > stats.bsize * stats.bavail)
|
||||||
|
{
|
||||||
|
enqueue({ status: 'error', error: "Not Enough Free Space" });
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
enqueue({ status: 'install', details: 'Install' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +219,7 @@ export default async function buildStatusResponse (source: string, id: number)
|
||||||
{
|
{
|
||||||
enqueue({
|
enqueue({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error: error
|
error: getErrorMessage(error)
|
||||||
}, 'error');
|
}, 'error');
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import getFolderSize from "get-folder-size";
|
import getFolderSize from "get-folder-size";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { config } from "../../app";
|
import { config, db, emulatorsDb } from "../../app";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import * as schema from "../../schema/app";
|
import * as schema from "@schema/app";
|
||||||
import { FrontEndGameType, FrontEndGameTypeDetailed } from "@shared/constants";
|
import { FrontEndGameType, FrontEndGameTypeDetailed, StoreGameType } from "@shared/constants";
|
||||||
import { DetailedRomSchema, SimpleRomSchema } from "@clients/romm";
|
import { DetailedRomSchema, SimpleRomSchema } from "@clients/romm";
|
||||||
|
import * as emulatorSchema from "@schema/emulators";
|
||||||
|
|
||||||
export async function calculateSize (installPath: string | null)
|
export async function calculateSize (installPath: string | null)
|
||||||
{
|
{
|
||||||
|
|
@ -19,15 +20,15 @@ export async function checkInstalled (installPath: string | null)
|
||||||
return fs.exists(path.join(config.get('downloadPath'), installPath));
|
return fs.exists(path.join(config.get('downloadPath'), installPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLocalGameMatch (id: number, source: string)
|
export function getLocalGameMatch (id: string, source: string)
|
||||||
{
|
{
|
||||||
return source !== 'local' ? and(eq(schema.games.source_id, id), eq(schema.games.source, source)) : eq(schema.games.id, id);
|
return source !== 'local' ? and(eq(schema.games.source_id, id), eq(schema.games.source, source)) : eq(schema.games.id, Number(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertRomToFrontend (rom: SimpleRomSchema): FrontEndGameType
|
export function convertRomToFrontend (rom: SimpleRomSchema): FrontEndGameType
|
||||||
{
|
{
|
||||||
const game: FrontEndGameType = {
|
const game: FrontEndGameType = {
|
||||||
id: { id: rom.id, source: 'romm' },
|
id: { id: String(rom.id), source: 'romm' },
|
||||||
path_cover: `/api/romm/image/romm${rom.path_cover_large}`,
|
path_cover: `/api/romm/image/romm${rom.path_cover_large}`,
|
||||||
last_played: rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null,
|
last_played: rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null,
|
||||||
updated_at: new Date(rom.updated_at),
|
updated_at: new Date(rom.updated_at),
|
||||||
|
|
@ -40,11 +41,131 @@ export function convertRomToFrontend (rom: SimpleRomSchema): FrontEndGameType
|
||||||
source: null,
|
source: null,
|
||||||
source_id: null,
|
source_id: null,
|
||||||
paths_screenshots: rom.merged_screenshots.map(s => `/api/romm/image/romm/${s}`),
|
paths_screenshots: rom.merged_screenshots.map(s => `/api/romm/image/romm/${s}`),
|
||||||
|
platform_slug: rom.platform_slug
|
||||||
};
|
};
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertLocalToFrontend (g: typeof schema.games.$inferSelect & {
|
||||||
|
platform?: typeof schema.platforms.$inferSelect | null;
|
||||||
|
screenshotIds?: number[];
|
||||||
|
})
|
||||||
|
{
|
||||||
|
const game: FrontEndGameType = {
|
||||||
|
platform_display_name: g.platform?.name ?? "Local",
|
||||||
|
id: { id: String(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`,
|
||||||
|
paths_screenshots: g.screenshotIds?.map(s => `/api/romm/screenshot/${s}`) ?? [],
|
||||||
|
path_fs: g.path_fs,
|
||||||
|
last_played: g.last_played,
|
||||||
|
slug: g.slug,
|
||||||
|
name: g.name,
|
||||||
|
platform_id: g.platform_id,
|
||||||
|
platform_slug: g.platform?.slug ?? null
|
||||||
|
};
|
||||||
|
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertLocalToFrontendDetailed (g: typeof schema.games.$inferSelect & {
|
||||||
|
platform?: typeof schema.platforms.$inferSelect | null;
|
||||||
|
screenshotIds?: number[];
|
||||||
|
})
|
||||||
|
{
|
||||||
|
const game: FrontEndGameTypeDetailed = {
|
||||||
|
platform_display_name: g.platform?.name ?? "Local",
|
||||||
|
id: { id: String(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`,
|
||||||
|
paths_screenshots: g.screenshotIds?.map(s => `/api/romm/screenshot/${s}`) ?? [],
|
||||||
|
path_fs: g.path_fs,
|
||||||
|
last_played: g.last_played,
|
||||||
|
slug: g.slug,
|
||||||
|
name: g.name,
|
||||||
|
platform_id: g.platform_id,
|
||||||
|
platform_slug: g.platform?.slug ?? null,
|
||||||
|
summary: g.summary,
|
||||||
|
fs_size_bytes: 0,
|
||||||
|
missing: false,
|
||||||
|
local: true
|
||||||
|
};
|
||||||
|
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function convertStoreToFrontend (system: string, id: string, storeGame: StoreGameType): Promise<FrontEndGameType>
|
||||||
|
{
|
||||||
|
let size: number | null = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const fileResponse = await fetch(storeGame.file, { method: 'HEAD' });
|
||||||
|
size = Number(fileResponse.headers.get('content-length'));
|
||||||
|
} catch (error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
const rommSystem = await emulatorsDb.query.systemMappings.findFirst({
|
||||||
|
where: and(eq(emulatorSchema.systemMappings.system, system), eq(emulatorSchema.systemMappings.source, 'romm'))
|
||||||
|
});
|
||||||
|
|
||||||
|
const platformDef = await emulatorsDb.query.systems.findFirst({
|
||||||
|
where: eq(emulatorSchema.systems.name, system),
|
||||||
|
columns: { fullname: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const gameId = `${system}@${id}`;
|
||||||
|
|
||||||
|
const game: FrontEndGameType = {
|
||||||
|
platform_display_name: platformDef?.fullname ?? system,
|
||||||
|
path_platform_cover: `/api/romm/image/romm/assets/platforms/${rommSystem?.sourceSlug ?? system}.svg`,
|
||||||
|
id: { source: 'store', id: gameId },
|
||||||
|
source: null,
|
||||||
|
source_id: null,
|
||||||
|
path_fs: null,
|
||||||
|
path_cover: `/api/romm/image?url=${encodeURIComponent(storeGame.pictures.titlescreens?.[0])}`,
|
||||||
|
last_played: null,
|
||||||
|
updated_at: new Date(),
|
||||||
|
slug: null,
|
||||||
|
name: storeGame.title,
|
||||||
|
platform_id: null,
|
||||||
|
platform_slug: system,
|
||||||
|
paths_screenshots: storeGame.pictures.screenshots?.map((s: string) => `/api/romm/image?url=${encodeURIComponent(s)}`) ?? []
|
||||||
|
};
|
||||||
|
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function convertStoreToFrontendDetailed (system: string, id: string, storeGame: StoreGameType): Promise<FrontEndGameTypeDetailed>
|
||||||
|
{
|
||||||
|
let size: number | null = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const fileResponse = await fetch(storeGame.file, { method: 'HEAD' });
|
||||||
|
size = Number(fileResponse.headers.get('content-length'));
|
||||||
|
} catch (error)
|
||||||
|
{
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailed: FrontEndGameTypeDetailed = {
|
||||||
|
...await convertStoreToFrontend(system, id, storeGame),
|
||||||
|
summary: storeGame.description,
|
||||||
|
fs_size_bytes: size,
|
||||||
|
missing: false,
|
||||||
|
local: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return detailed;
|
||||||
|
}
|
||||||
|
|
||||||
export function convertRomToFrontendDetailed (rom: DetailedRomSchema)
|
export function convertRomToFrontendDetailed (rom: DetailedRomSchema)
|
||||||
{
|
{
|
||||||
const detailed: FrontEndGameTypeDetailed = {
|
const detailed: FrontEndGameTypeDetailed = {
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,16 @@ import { IJob, JobContext } from "../task-queue";
|
||||||
import { mkdir } from 'node:fs/promises';
|
import { mkdir } from 'node:fs/promises';
|
||||||
import { and, eq, or } from 'drizzle-orm';
|
import { and, eq, or } from 'drizzle-orm';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import * as schema from "../schema/app";
|
import * as schema from "@schema/app";
|
||||||
import * as emulatorSchema from "../schema/emulators";
|
import * as emulatorSchema from "@schema/emulators";
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { getPlatformApiPlatformsIdGet, getRomApiRomsIdGet } from "@clients/romm";
|
import { getPlatformApiPlatformsIdGet, getRomApiRomsIdGet, PlatformSchema } from "@clients/romm";
|
||||||
import { config, db, emulatorsDb, jar } from "../app";
|
import { config, db, emulatorsDb, jar } from "../app";
|
||||||
import unzip from 'unzip-stream';
|
import unzip from 'unzip-stream';
|
||||||
import { Readable, Transform } from "node:stream";
|
import { Readable, Transform } from "node:stream";
|
||||||
|
import { extractStoreGameSourceId, getStoreGameFromId } from "../store/services/gamesService";
|
||||||
|
import * as igdb from 'ts-igdb-client';
|
||||||
|
import secrets from "../secrets";
|
||||||
|
|
||||||
interface JobConfig
|
interface JobConfig
|
||||||
{
|
{
|
||||||
|
|
@ -18,15 +21,15 @@ interface JobConfig
|
||||||
|
|
||||||
export class InstallJob implements IJob
|
export class InstallJob implements IJob
|
||||||
{
|
{
|
||||||
public id: number;
|
public gameId: string;
|
||||||
public source: string;
|
public source: string;
|
||||||
public sourceId: number;
|
public sourceId: string;
|
||||||
|
|
||||||
public config?: JobConfig;
|
public config?: JobConfig;
|
||||||
|
static id = "install-job" as const;
|
||||||
|
|
||||||
constructor(id: number, source: string, sourceId: number, config?: JobConfig)
|
constructor(id: string, source: string, sourceId: string, config?: JobConfig)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.gameId = id;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.sourceId = sourceId;
|
this.sourceId = sourceId;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
|
|
@ -41,6 +44,65 @@ export class InstallJob implements IJob
|
||||||
{
|
{
|
||||||
const downloadPath = config.get('downloadPath');
|
const downloadPath = config.get('downloadPath');
|
||||||
|
|
||||||
|
let downloadUrl: URL;
|
||||||
|
let cookie: string = '';
|
||||||
|
let screenshotUrls: string[];
|
||||||
|
let coverUrl: string;
|
||||||
|
let rommPlatform: PlatformSchema | undefined;
|
||||||
|
let slug: string | null;
|
||||||
|
let path_fs: string | undefined;
|
||||||
|
let summary: string | null;
|
||||||
|
let name: string | null;
|
||||||
|
let last_played: Date | null;
|
||||||
|
let igdb_id: number | null;
|
||||||
|
let ra_id: number | null;
|
||||||
|
let source_id: string;
|
||||||
|
let system_slug: string;
|
||||||
|
let extract_path: string;
|
||||||
|
|
||||||
|
switch (this.source)
|
||||||
|
{
|
||||||
|
case 'romm':
|
||||||
|
|
||||||
|
const rom = (await getRomApiRomsIdGet({ path: { id: Number(this.gameId) }, throwOnError: true })).data;
|
||||||
|
rommPlatform = (await getPlatformApiPlatformsIdGet({ path: { id: rom.platform_id }, throwOnError: true })).data;
|
||||||
|
|
||||||
|
const rommAddress = config.get('rommAddress');
|
||||||
|
coverUrl = `${rommAddress}${rom.path_cover_large}`;
|
||||||
|
screenshotUrls = rom.merged_screenshots.map(s => `${config.get('rommAddress')}${s}`);
|
||||||
|
last_played = rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null;
|
||||||
|
igdb_id = rom.igdb_id;
|
||||||
|
ra_id = rom.ra_id;
|
||||||
|
summary = rom.summary;
|
||||||
|
name = rom.name;
|
||||||
|
path_fs = path.join(rom.fs_path, rom.fs_name);
|
||||||
|
source_id = String(rom.id);
|
||||||
|
slug = rom.slug;
|
||||||
|
system_slug = rommPlatform.slug;
|
||||||
|
extract_path = '';
|
||||||
|
|
||||||
|
downloadUrl = new URL(`${config.get('rommAddress')}/api/roms/download`);
|
||||||
|
downloadUrl.searchParams.set('rom_ids', String(this.gameId));
|
||||||
|
cookie = await jar.getCookieString(config.get('rommAddress') ?? '');
|
||||||
|
break;
|
||||||
|
case 'store':
|
||||||
|
const game = await getStoreGameFromId(this.gameId);
|
||||||
|
const gameId = extractStoreGameSourceId(this.gameId);
|
||||||
|
coverUrl = game.pictures.titlescreens[0];
|
||||||
|
screenshotUrls = game.pictures.screenshots;
|
||||||
|
downloadUrl = new URL(game.file);
|
||||||
|
slug = this.gameId;
|
||||||
|
source_id = this.gameId;
|
||||||
|
name = game.title;
|
||||||
|
summary = game.description;
|
||||||
|
system_slug = gameId.system;
|
||||||
|
extract_path = 'roms', gameId.system;
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unsupported source");
|
||||||
|
}
|
||||||
|
|
||||||
if (this.config?.dryDownload !== true)
|
if (this.config?.dryDownload !== true)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
|
|
@ -92,11 +154,10 @@ export class InstallJob implements IJob
|
||||||
await fs.rm(zipFilePath);*/
|
await fs.rm(zipFilePath);*/
|
||||||
|
|
||||||
cx.setProgress(0, 'download');
|
cx.setProgress(0, 'download');
|
||||||
const downloadUrl = new URL(`${config.get('rommAddress')}/api/roms/download`);
|
|
||||||
downloadUrl.searchParams.set('rom_ids', String(this.id));
|
|
||||||
const res = await fetch(downloadUrl, {
|
const res = await fetch(downloadUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
cookie: await jar.getCookieString(config.get('rommAddress') ?? '')
|
cookie: cookie
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -119,62 +180,99 @@ export class InstallJob implements IJob
|
||||||
|
|
||||||
await new Promise((resolve, reject) =>
|
await new Promise((resolve, reject) =>
|
||||||
{
|
{
|
||||||
Readable.fromWeb(res.body as any).pipe(progressStream).pipe(unzip.Extract({ path: downloadPath })).on('close', resolve).on('error', reject);
|
const extract = unzip.Extract({ path: path.join(downloadPath, extract_path), });
|
||||||
|
(extract as any).unzipStream.on('entry', (entry: any) =>
|
||||||
|
{
|
||||||
|
if (!path_fs)
|
||||||
|
path_fs = path.join(extract_path, entry.path);
|
||||||
|
});
|
||||||
|
Readable.fromWeb(res.body as any).pipe(progressStream)
|
||||||
|
.pipe(extract)
|
||||||
|
.on('close', resolve)
|
||||||
|
.on('error', reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
if (this.config?.dryDownload === true)
|
||||||
{
|
{
|
||||||
rom.files.length;
|
await mkdir(path.join(downloadPath, extract_path), { recursive: true });
|
||||||
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}`);
|
const coverResponse = await fetch(coverUrl);
|
||||||
|
const cover = Buffer.from(await coverResponse.arrayBuffer());
|
||||||
|
|
||||||
if (cx.abortSignal.aborted) return;
|
if (cx.abortSignal.aborted) return;
|
||||||
|
|
||||||
await db.transaction(async (tx) =>
|
await db.transaction(async (tx) =>
|
||||||
{
|
{
|
||||||
// Search for existing platform
|
// Search for existing platform
|
||||||
const platformSearch = [];
|
const platformSearch = [eq(schema.platforms.slug, system_slug)];
|
||||||
if (romPlatform.igdb_id) platformSearch.push(eq(schema.platforms.igdb_id, romPlatform.igdb_id));
|
const esPlatformSearch = [eq(emulatorSchema.systemMappings.system, system_slug)];
|
||||||
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
|
if (rommPlatform)
|
||||||
.select({ slug: emulatorSchema.systemMappings.system, romm_slug: emulatorSchema.systemMappings.sourceSlug })
|
{
|
||||||
.from(emulatorSchema.systemMappings)
|
if (rommPlatform.igdb_id) platformSearch.push(eq(schema.platforms.igdb_id, rommPlatform.igdb_id));
|
||||||
.where(and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.sourceSlug, romPlatform.slug)));
|
if (rommPlatform.igdb_slug) platformSearch.push(eq(schema.platforms.igdb_slug, rommPlatform.igdb_slug));
|
||||||
|
if (rommPlatform.ra_id) platformSearch.push(eq(schema.platforms.ra_id, rommPlatform.ra_id));
|
||||||
|
if (rommPlatform.moby_id) platformSearch.push(eq(schema.platforms.moby_id, rommPlatform.moby_id));
|
||||||
|
|
||||||
const existingPlatform = await tx.query.platforms.findFirst({ where: or(...platformSearch) });
|
esPlatformSearch.push(eq(emulatorSchema.systemMappings.source, 'romm'));
|
||||||
|
esPlatformSearch.push(eq(emulatorSchema.systemMappings.sourceSlug, rommPlatform.slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
const esPlatform = await emulatorsDb.query.systemMappings.findFirst({
|
||||||
|
with: { system: true },
|
||||||
|
where: and(...esPlatformSearch)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (esPlatform)
|
||||||
|
platformSearch.push(eq(schema.platforms.es_slug, esPlatform.system.name));
|
||||||
|
|
||||||
|
let existingPlatform = await tx.query.platforms.findFirst({ where: or(...platformSearch) });
|
||||||
let platformId: number;
|
let platformId: number;
|
||||||
if (!existingPlatform)
|
if (!existingPlatform)
|
||||||
|
{
|
||||||
|
// TODO: use something else than the romm demo as CDN
|
||||||
|
const platformCover = await fetch(`https://demo.romm.app/assets/platforms/${system_slug}.svg`);
|
||||||
|
|
||||||
|
if (!esPlatform && !rommPlatform)
|
||||||
|
{
|
||||||
|
// go to unknown platform
|
||||||
|
existingPlatform = await tx.query.platforms.findFirst({ where: eq(schema.platforms.slug, "unknown") });
|
||||||
|
|
||||||
|
if (existingPlatform)
|
||||||
|
{
|
||||||
|
platformId = existingPlatform.id;
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
const [{ id }] = await tx.insert(schema.platforms).values({
|
||||||
|
slug: 'unknown',
|
||||||
|
name: "Unknown"
|
||||||
|
}).returning({ id: schema.platforms.id });
|
||||||
|
platformId = id;
|
||||||
|
}
|
||||||
|
} else
|
||||||
{
|
{
|
||||||
// Create new local platform
|
// Create new local platform
|
||||||
const platformCover = await fetch(`${rommAddress}/assets/platforms/${romPlatform.slug.toLocaleLowerCase()}.svg`);
|
|
||||||
const platform: typeof schema.platforms.$inferInsert = {
|
const platform: typeof schema.platforms.$inferInsert = {
|
||||||
slug: romPlatform.slug,
|
slug: rommPlatform?.slug ?? esPlatform?.system.name ?? '',
|
||||||
igdb_id: romPlatform.igdb_id,
|
igdb_id: rommPlatform?.igdb_id,
|
||||||
igdb_slug: romPlatform.igdb_slug,
|
igdb_slug: rommPlatform?.igdb_slug,
|
||||||
ra_id: romPlatform.ra_id,
|
ra_id: rommPlatform?.ra_id,
|
||||||
cover: Buffer.from(await platformCover.arrayBuffer()),
|
cover: Buffer.from(await platformCover.arrayBuffer()),
|
||||||
cover_type: platformCover.headers.get('content-type'),
|
cover_type: platformCover.headers.get('content-type'),
|
||||||
name: romPlatform.name,
|
name: rommPlatform?.name ?? esPlatform?.system.fullname ?? '',
|
||||||
family_name: romPlatform.family_name,
|
family_name: rommPlatform?.family_name,
|
||||||
es_slug: esPlatform.length > 0 ? esPlatform[0].slug : undefined
|
es_slug: esPlatform?.system.name ?? undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: add ES slug once I have better way to query ES
|
// 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 });
|
const [{ id }] = await tx.insert(schema.platforms).values(platform).returning({ id: schema.platforms.id });
|
||||||
platformId = id;
|
platformId = id;
|
||||||
|
}
|
||||||
|
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
platformId = existingPlatform.id;
|
platformId = existingPlatform.id;
|
||||||
|
|
@ -182,22 +280,40 @@ export class InstallJob implements IJob
|
||||||
|
|
||||||
// create the rom
|
// create the rom
|
||||||
const game: typeof schema.games.$inferInsert = {
|
const game: typeof schema.games.$inferInsert = {
|
||||||
source_id: rom.id,
|
source_id,
|
||||||
source: 'romm',
|
source: this.source,
|
||||||
slug: rom.slug,
|
slug,
|
||||||
path_fs: path.join(rom.fs_path, rom.fs_name),
|
path_fs,
|
||||||
last_played: rom.rom_user.last_played ? new Date(rom.rom_user.last_played) : null,
|
last_played: last_played,
|
||||||
platform_id: platformId,
|
platform_id: platformId,
|
||||||
igdb_id: rom.igdb_id,
|
igdb_id: igdb_id,
|
||||||
ra_id: rom.ra_id,
|
ra_id: ra_id,
|
||||||
summary: rom.summary,
|
summary: summary,
|
||||||
name: rom.name,
|
name,
|
||||||
cover: Buffer.from(await coverResponse.arrayBuffer()),
|
cover,
|
||||||
cover_type: coverResponse.headers.get('content-type')
|
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 });
|
const [{ id }] = await tx.insert(schema.games).values(game).returning({ id: schema.games.id });
|
||||||
|
|
||||||
|
if (screenshotUrls.length <= 0 && process.env.TWITCH_CLIENT_ID)
|
||||||
|
{
|
||||||
|
const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' });
|
||||||
|
if (access_token)
|
||||||
|
{
|
||||||
|
const client = igdb.igdb(process.env.TWITCH_CLIENT_ID, access_token);
|
||||||
|
|
||||||
|
const { data } = await client.request('artworks').pipe(igdb.fields(['game', 'url']), igdb.where('game', '=', igdb_id)).execute();
|
||||||
|
|
||||||
|
screenshotUrls.push(...data.filter(s => s.url).map(s => s.url!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre-fetch screenshots
|
||||||
|
const screenshots = await Promise.all(screenshotUrls.map(s => fetch(s)));
|
||||||
|
|
||||||
|
if (screenshots.length > 0)
|
||||||
|
{
|
||||||
await tx.insert(schema.screenshots).values(await Promise.all(screenshots.map(async (response) =>
|
await tx.insert(schema.screenshots).values(await Promise.all(screenshots.map(async (response) =>
|
||||||
{
|
{
|
||||||
const screenshot: typeof schema.screenshots.$inferInsert = {
|
const screenshot: typeof schema.screenshots.$inferInsert = {
|
||||||
|
|
@ -208,6 +324,8 @@ export class InstallJob implements IJob
|
||||||
|
|
||||||
return screenshot;
|
return screenshot;
|
||||||
})));
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
80
src/bun/api/jobs/jobs.ts
Normal file
80
src/bun/api/jobs/jobs.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import Elysia from "elysia";
|
||||||
|
import z, { } from "zod";
|
||||||
|
import { taskQueue } from "../app";
|
||||||
|
import { LoginJob } from "./login-job";
|
||||||
|
import TwitchLoginJob from "./twitch-login-job";
|
||||||
|
import UpdateStoreJob from "./update-store";
|
||||||
|
|
||||||
|
function registerJob<const Path extends string, TS, T extends { id: Path, dataSchema?: TS; }> (job: T, path: Path, dataSchema: TS)
|
||||||
|
{
|
||||||
|
return new Elysia().ws(path, {
|
||||||
|
body: z.discriminatedUnion('type', [
|
||||||
|
z.object({ type: z.literal('cancel') })
|
||||||
|
]),
|
||||||
|
response: z.discriminatedUnion('type', [
|
||||||
|
z.object({
|
||||||
|
type: z.literal(['data', 'started', 'progress']),
|
||||||
|
status: z.string(),
|
||||||
|
progress: z.number(),
|
||||||
|
data: dataSchema
|
||||||
|
}),
|
||||||
|
z.object({ type: z.literal(['completed', 'ended']) }),
|
||||||
|
z.object({ type: z.literal('error'), error: z.unknown() })
|
||||||
|
]),
|
||||||
|
open (ws)
|
||||||
|
{
|
||||||
|
const job = taskQueue.findJob(path);
|
||||||
|
if (job)
|
||||||
|
{
|
||||||
|
ws.send({ type: 'data', status: job.status, progress: job.progress, data: job.job.exposeData?.() });
|
||||||
|
}
|
||||||
|
|
||||||
|
(ws.data as any).cleanup = [
|
||||||
|
taskQueue.on('started', ({ id, job }) =>
|
||||||
|
{
|
||||||
|
if (id === path)
|
||||||
|
{
|
||||||
|
ws.send({ type: 'started', status: job.status, progress: job.progress, data: job.job.exposeData?.() });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
taskQueue.on('progress', ({ id, job }) =>
|
||||||
|
{
|
||||||
|
if (id === path)
|
||||||
|
{
|
||||||
|
ws.send({ type: 'started', status: job.status, progress: job.progress, data: job.job.exposeData?.() });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
taskQueue.on('completed', ({ id }) =>
|
||||||
|
{
|
||||||
|
if (id === path)
|
||||||
|
{
|
||||||
|
ws.send({ type: 'completed' });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
taskQueue.on('error', ({ id, error }) =>
|
||||||
|
{
|
||||||
|
if (id === path)
|
||||||
|
{
|
||||||
|
ws.send({ type: 'error', error: error });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
},
|
||||||
|
close (ws)
|
||||||
|
{
|
||||||
|
(ws.data as any).cleanup.forEach((d: Function) => d());
|
||||||
|
},
|
||||||
|
message (ws, message)
|
||||||
|
{
|
||||||
|
if (message.type === 'cancel')
|
||||||
|
{
|
||||||
|
taskQueue.findJob(path)?.abort('cancel');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const jobs = new Elysia({ prefix: '/api/jobs' })
|
||||||
|
.use(registerJob(LoginJob, LoginJob.id, LoginJob.dataSchema))
|
||||||
|
.use(registerJob(TwitchLoginJob, TwitchLoginJob.id, TwitchLoginJob.dataSchema))
|
||||||
|
.use(registerJob(UpdateStoreJob, UpdateStoreJob.id, undefined));
|
||||||
|
|
@ -4,20 +4,27 @@ import { LOGIN_PORT, SERVER_URL } from "@/shared/constants";
|
||||||
import { host, localIp } from "@/bun/utils/host";
|
import { host, localIp } from "@/bun/utils/host";
|
||||||
import cors from "@elysiajs/cors";
|
import cors from "@elysiajs/cors";
|
||||||
import { tryLoginAndSave } from "../auth";
|
import { tryLoginAndSave } from "../auth";
|
||||||
import z from "zod";
|
|
||||||
import { config } from "../app";
|
import { config } from "../app";
|
||||||
|
import z from "zod";
|
||||||
|
import { delay } from "@/shared/utils";
|
||||||
|
|
||||||
export class LoginJob implements IJob
|
export class LoginJob implements IJob
|
||||||
{
|
{
|
||||||
endsAt: Date;
|
endsAt: Date;
|
||||||
|
startedAt: Date;
|
||||||
url: string;
|
url: string;
|
||||||
|
static id = "login-job" as const;
|
||||||
|
static dataSchema = z.object({ endsAt: z.date(), startedAt: z.date(), url: z.url() });
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
this.endsAt = new Date();
|
this.endsAt = new Date(new Date().getTime() + 300000);
|
||||||
|
this.startedAt = new Date();
|
||||||
this.url = `http://${localIp}:${LOGIN_PORT}/`;
|
this.url = `http://${localIp}:${LOGIN_PORT}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exposeData = (): z.infer<typeof LoginJob.dataSchema> => ({ endsAt: this.endsAt, startedAt: this.startedAt, url: this.url });
|
||||||
|
|
||||||
async start (context: JobContext): Promise<any>
|
async start (context: JobContext): Promise<any>
|
||||||
{
|
{
|
||||||
const loginServer = new Elysia({ serve: { hostname: localIp, port: LOGIN_PORT } })
|
const loginServer = new Elysia({ serve: { hostname: localIp, port: LOGIN_PORT } })
|
||||||
|
|
@ -44,12 +51,7 @@ export class LoginJob implements IJob
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
loginServer.listen({});
|
loginServer.listen({});
|
||||||
await new Promise((resolve, reject) =>
|
await delay(this.endsAt, context.abortSignal);
|
||||||
{
|
|
||||||
this.endsAt = new Date(new Date().getTime() + 300000);
|
|
||||||
context.abortSignal.addEventListener('abort', () => reject());
|
|
||||||
setTimeout(() => { reject('timeout'); }, 300000); // auto close after 5 minutes
|
|
||||||
});
|
|
||||||
} catch
|
} catch
|
||||||
{
|
{
|
||||||
} finally
|
} finally
|
||||||
|
|
|
||||||
110
src/bun/api/jobs/twitch-login-job.ts
Normal file
110
src/bun/api/jobs/twitch-login-job.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { IJob, JobContext } from "../task-queue";
|
||||||
|
import secrets from "../secrets";
|
||||||
|
import open from "open";
|
||||||
|
import z from "zod";
|
||||||
|
import { delay } from "@/shared/utils";
|
||||||
|
|
||||||
|
|
||||||
|
interface TwitchDevice
|
||||||
|
{
|
||||||
|
device_code: string,
|
||||||
|
expires_in: number,
|
||||||
|
expires_at: Date,
|
||||||
|
started_at: Date,
|
||||||
|
interval: number,
|
||||||
|
user_code: string,
|
||||||
|
verification_uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TwitchLoginJob implements IJob
|
||||||
|
{
|
||||||
|
twitchScopes = "analytics:read:extensions analytics:read:games user:read:email";
|
||||||
|
device?: TwitchDevice;
|
||||||
|
clientId: string;
|
||||||
|
openInBrowser: boolean;
|
||||||
|
static id = 'twitch-login-job' as const;
|
||||||
|
static dataSchema = z.object({ expires_at: z.date(), started_at: z.date(), url: z.url(), user_code: z.string() }).or(z.undefined());
|
||||||
|
|
||||||
|
constructor(clientId: string, openInBrowser: boolean)
|
||||||
|
{
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.openInBrowser = openInBrowser;
|
||||||
|
}
|
||||||
|
|
||||||
|
exposeData = (): z.infer<typeof TwitchLoginJob.dataSchema> => this.device ? ({
|
||||||
|
expires_at: this.device.expires_at,
|
||||||
|
started_at: this.device.started_at,
|
||||||
|
url: this.device.verification_uri,
|
||||||
|
user_code: this.device.user_code
|
||||||
|
}) : undefined;
|
||||||
|
|
||||||
|
async start (context: JobContext): Promise<any>
|
||||||
|
{
|
||||||
|
context.setProgress(0, "Retrieving Device");
|
||||||
|
let res = await fetch("https://id.twitch.tv/oauth2/device", {
|
||||||
|
method: "POST",
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: this.clientId,
|
||||||
|
scopes: this.twitchScopes
|
||||||
|
}),
|
||||||
|
signal: context.abortSignal
|
||||||
|
});
|
||||||
|
|
||||||
|
const device: TwitchDevice = await res.json();
|
||||||
|
const expiredTimeout = setTimeout(() => context.abort('expired'), device.expires_in * 1000);
|
||||||
|
device.expires_at = new Date(new Date().getTime() + device.expires_in * 1000);
|
||||||
|
device.started_at = new Date();
|
||||||
|
this.device = device;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (this.openInBrowser)
|
||||||
|
open(device.verification_uri);
|
||||||
|
this.device = device;
|
||||||
|
context.setProgress(50, "Waiting For Authentication");
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (context.abortSignal.aborted) break;
|
||||||
|
await delay(device.interval * 1000, context.abortSignal);
|
||||||
|
|
||||||
|
res = await fetch("https://id.twitch.tv/oauth2/token", {
|
||||||
|
method: "POST",
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: this.clientId,
|
||||||
|
scopes: this.twitchScopes,
|
||||||
|
device_code: this.device.device_code,
|
||||||
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
||||||
|
}),
|
||||||
|
signal: context.abortSignal
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200)
|
||||||
|
{
|
||||||
|
const data: {
|
||||||
|
access_token: string,
|
||||||
|
expires_in: number,
|
||||||
|
refresh_token: string,
|
||||||
|
scope: string[],
|
||||||
|
token_type: string;
|
||||||
|
} = await res.json();
|
||||||
|
|
||||||
|
secrets.set({ service: 'gamflow_twitch', name: 'access_token', value: data.access_token });
|
||||||
|
secrets.set({ service: 'gamflow_twitch', name: 'refresh_token', value: data.refresh_token });
|
||||||
|
secrets.set({ service: 'gamflow_twitch', name: 'expires_in', value: new Date(new Date().getTime() + data.expires_in).toString() });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (res.status !== 400)
|
||||||
|
{
|
||||||
|
console.error(res.statusText);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally
|
||||||
|
{
|
||||||
|
clearTimeout(expiredTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
76
src/bun/api/jobs/update-store.ts
Normal file
76
src/bun/api/jobs/update-store.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { ensureDir } from "fs-extra";
|
||||||
|
import { IJob, JobContext } from "../task-queue";
|
||||||
|
import { getStoreFolder } from "../store/store";
|
||||||
|
|
||||||
|
export default class UpdateStoreJob implements IJob
|
||||||
|
{
|
||||||
|
static id = "update-store" as const;
|
||||||
|
static origin = "https://github.com/simeonradivoev/gameflow-store.git";
|
||||||
|
static branch = "master";
|
||||||
|
|
||||||
|
async gitCommand (commands: string[], dir: string)
|
||||||
|
{
|
||||||
|
const proc = Bun.spawn(['git', ...commands], {
|
||||||
|
cwd: dir,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [output] = await Promise.all([
|
||||||
|
new Response(proc.stdout).text(),
|
||||||
|
proc.exited,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return output.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async isGitRepo (dir: string)
|
||||||
|
{
|
||||||
|
return (await this.gitCommand(["rev-parse", "--is-inside-work-tree"], dir)) === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrigin (dir: string)
|
||||||
|
{
|
||||||
|
const origin = await this.gitCommand(["remote", "get-url", "origin"], dir);
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasChanges (dir: string)
|
||||||
|
{
|
||||||
|
return (await this.gitCommand(["status", "--porcelain"], dir)).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start (context: JobContext)
|
||||||
|
{
|
||||||
|
const storeFolder = getStoreFolder();
|
||||||
|
await ensureDir(storeFolder);
|
||||||
|
context.setProgress(10);
|
||||||
|
if (await this.isGitRepo(storeFolder))
|
||||||
|
{
|
||||||
|
const existingOrigin = await this.getOrigin(storeFolder);
|
||||||
|
if (existingOrigin !== UpdateStoreJob.origin)
|
||||||
|
{
|
||||||
|
throw new Error(`Git Repo in downloads is not valid. It has origin of ${existingOrigin}. Repo must be of ${UpdateStoreJob.origin}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for uncommitted changes
|
||||||
|
const status = await this.gitCommand([" status", "--porcelain"], storeFolder);
|
||||||
|
if (status.length > 0)
|
||||||
|
{
|
||||||
|
console.log("Cleaning local changes...");
|
||||||
|
await this.gitCommand(["reset", "--hard"], storeFolder);
|
||||||
|
await this.gitCommand(["clean", "-fd"], storeFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch & reset to remote
|
||||||
|
await this.gitCommand(["fetch", "origin"], storeFolder);
|
||||||
|
await this.gitCommand(["reset", "--hard", `origin/${UpdateStoreJob.branch}`], storeFolder);
|
||||||
|
console.log("Shop Repo updated");
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
context.setProgress(50);
|
||||||
|
await this.gitCommand(["clone", "--depth", "1", "--branch", UpdateStoreJob.branch, UpdateStoreJob.origin, '.'], storeFolder);
|
||||||
|
context.setProgress(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
import { cors } from "@elysiajs/cors";
|
import { cors } from "@elysiajs/cors";
|
||||||
import Elysia from "elysia";
|
import Elysia from "elysia";
|
||||||
import { RPC_PORT } from "../../shared/constants";
|
import { RPC_PORT } from "@shared/constants";
|
||||||
import clients from "./clients";
|
import clients from "./clients";
|
||||||
import { settings } from "./settings";
|
import { settings } from "./settings/settings";
|
||||||
import { system } from "./system";
|
import { system } from "./system";
|
||||||
|
import { store } from "./store/store";
|
||||||
import { host } from "../utils/host";
|
import { host } from "../utils/host";
|
||||||
|
import { jobs } from "./jobs/jobs";
|
||||||
|
|
||||||
const api = new Elysia({ serve: {} })
|
const api = new Elysia({ serve: {} })
|
||||||
.use([cors(), clients, settings, system]);
|
.use([cors(), clients, settings, system, store, jobs]);
|
||||||
|
|
||||||
export type RommAPIType = typeof clients;
|
export type RommAPIType = typeof clients;
|
||||||
export type SettingsAPIType = typeof settings;
|
export type SettingsAPIType = typeof settings;
|
||||||
export type SystemAPIType = typeof system;
|
export type SystemAPIType = typeof system;
|
||||||
|
export type StoreAPIType = typeof store;
|
||||||
|
export type JobsAPIType = typeof jobs;
|
||||||
|
|
||||||
export function RunAPIServer ()
|
export function RunAPIServer ()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { integer, text, sqliteTable, blob } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
export const games = sqliteTable('games', {
|
export const games = sqliteTable('games', {
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||||
source_id: integer('source_id').unique(),
|
source_id: text('source_id'),
|
||||||
source: text("source"),
|
source: text("source"),
|
||||||
igdb_id: integer("igdb_id").unique(),
|
igdb_id: integer("igdb_id").unique(),
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
|
|
|
||||||
10
src/bun/api/schema/cache.ts
Normal file
10
src/bun/api/schema/cache.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
item_cache: sqliteTable('item_cache', {
|
||||||
|
key: text('key').primaryKey(),
|
||||||
|
data: text('data', { mode: 'json' }).notNull(),
|
||||||
|
expire_at: integer("expire_at", { mode: 'timestamp' }).notNull(),
|
||||||
|
updated_at: integer("updated_at", { mode: 'timestamp' }).notNull(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
@ -29,6 +29,10 @@ export const systemMappings = sqliteTable('systemMappings', {
|
||||||
system: text().notNull().references(() => systems.name)
|
system: text().notNull().references(() => systems.name)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const systemMappingsRelations = relations(systemMappings, ({ one }) => ({
|
||||||
|
system: one(systems, { fields: [systemMappings.system], references: [systems.name] })
|
||||||
|
}));
|
||||||
|
|
||||||
export const commands = sqliteTable('commands', {
|
export const commands = sqliteTable('commands', {
|
||||||
system: text().references(() => systems.name, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
system: text().references(() => systems.name, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
label: text(),
|
label: text(),
|
||||||
|
|
@ -36,7 +40,7 @@ export const commands = sqliteTable('commands', {
|
||||||
});
|
});
|
||||||
|
|
||||||
export const commandsRelations = relations(commands, ({ one }) => ({
|
export const commandsRelations = relations(commands, ({ one }) => ({
|
||||||
author: one(systems, {
|
system: one(systems, {
|
||||||
fields: [commands.system],
|
fields: [commands.system],
|
||||||
references: [systems.name],
|
references: [systems.name],
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
import z from "zod";
|
|
||||||
import { LOGIN_PORT, SettingsSchema } from "@shared/constants";
|
|
||||||
import Elysia, { status } from "elysia";
|
|
||||||
import { config, customEmulators, db, emulatorsDb, taskQueue } 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';
|
|
||||||
import { existsSync } from "node:fs";
|
|
||||||
import { InstallJob } from "./jobs/install-job";
|
|
||||||
import { move } from "fs-extra";
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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())
|
|
||||||
})
|
|
||||||
.put('/path/download', async ({ body: { manualPath, drive } }) =>
|
|
||||||
{
|
|
||||||
if (taskQueue.hasActiveOfType(InstallJob))
|
|
||||||
{
|
|
||||||
return status("Forbidden", "Installation in progress");
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldDownloadPath = config.get('downloadPath');
|
|
||||||
if (!existsSync(oldDownloadPath))
|
|
||||||
{
|
|
||||||
return status("Not Found", "Old download path doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isDirEmpty (dirname: string)
|
|
||||||
{
|
|
||||||
const files = await fs.readdir(dirname);
|
|
||||||
return files.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = manualPath ?? drive;
|
|
||||||
|
|
||||||
if (!path)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existsSync(path) && !isDirEmpty(path))
|
|
||||||
{
|
|
||||||
return status("Conflict", "New location already exists and is not empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
await move(oldDownloadPath, path);
|
|
||||||
config.set('downloadPath', manualPath);
|
|
||||||
return manualPath;
|
|
||||||
}, {
|
|
||||||
body: z.object({
|
|
||||||
manualPath: z.string().optional(),
|
|
||||||
drive: z.string().optional()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.get("/:id", async ({ params: { id } }) =>
|
|
||||||
{
|
|
||||||
const value = config.get(id);
|
|
||||||
return { value: value };
|
|
||||||
}, {
|
|
||||||
params: z.object({ id: z.keyof(SettingsSchema) }),
|
|
||||||
}).post('/:id',
|
|
||||||
async ({ params: { id }, body: { value }, }) =>
|
|
||||||
{
|
|
||||||
config.set(id, value);
|
|
||||||
}, {
|
|
||||||
params: z.object({ id: z.keyof(SettingsSchema) }),
|
|
||||||
body: z.object({ value: z.any() }),
|
|
||||||
});
|
|
||||||
|
|
||||||
193
src/bun/api/settings/services.ts
Normal file
193
src/bun/api/settings/services.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
|
||||||
|
import * as appSchema from '@schema/app';
|
||||||
|
import { findExec, findExecByName } from "../games/services/launchGameService";
|
||||||
|
import * as emulatorSchema from "@schema/emulators";
|
||||||
|
import { eq, inArray } from 'drizzle-orm';
|
||||||
|
import { customEmulators, db, emulatorsDb } from '../app';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import { cores } from '../emulatorjs/emulatorjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get emulators based on local games. Only the ones we probably need.
|
||||||
|
* */
|
||||||
|
export async function getRelevantEmulators ()
|
||||||
|
{
|
||||||
|
const localGames = await db.select({ es_slug: appSchema.platforms.es_slug, platform_id: appSchema.platforms.id, platform_name: appSchema.platforms.name })
|
||||||
|
.from(appSchema.games)
|
||||||
|
.leftJoin(appSchema.platforms, eq(appSchema.games.platform_id, appSchema.platforms.id))
|
||||||
|
.groupBy(appSchema.platforms.es_slug);
|
||||||
|
|
||||||
|
const platformLookup = new Map(localGames.filter(g => g.es_slug).map(g => [g.es_slug!, g]));
|
||||||
|
const platformViability = new Map(localGames.filter(g => g.es_slug).map(g => [g.es_slug!, false]));
|
||||||
|
|
||||||
|
// check emulator js
|
||||||
|
for (const platform of platformLookup)
|
||||||
|
{
|
||||||
|
if (cores[platform[0]])
|
||||||
|
platformViability.set(platform[0], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// all commands based on the local games
|
||||||
|
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));
|
||||||
|
|
||||||
|
|
||||||
|
// get all emulators in said commands
|
||||||
|
const emulators = commands
|
||||||
|
.flatMap(command =>
|
||||||
|
{
|
||||||
|
const matches = command.command.match(/(?<=%EMULATOR_)[\w-]+(?=%)/);
|
||||||
|
if (!matches)
|
||||||
|
{
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches?.map(m => ({ emulator: m, system: command.system_slug }));
|
||||||
|
}
|
||||||
|
).filter(c => !!c);
|
||||||
|
|
||||||
|
// Group them by name
|
||||||
|
const groupedEmulators = Map.groupBy(emulators, ({ emulator }) => emulator);
|
||||||
|
const finalEmulators = await Promise.all(Array.from(groupedEmulators.entries()).map(async ([emulator, system_slug]) =>
|
||||||
|
{
|
||||||
|
let execPath: { path: string; type: string, } | undefined;
|
||||||
|
if (customEmulators.has(emulator))
|
||||||
|
{
|
||||||
|
execPath = { path: customEmulators.get(emulator), type: 'custom' };
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
execPath = await findExecByName(emulator);
|
||||||
|
}
|
||||||
|
|
||||||
|
let platform: number | null | undefined = null;
|
||||||
|
const validSystemSlug = system_slug.find(s => s.system);
|
||||||
|
if (validSystemSlug?.system)
|
||||||
|
{
|
||||||
|
platform = platformLookup.get(validSystemSlug.system)?.platform_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if automatic or custom path found existing binary.
|
||||||
|
// This might not be the actual emulator but I don't care.
|
||||||
|
const exists = !!execPath && await fs.exists(execPath.path);
|
||||||
|
const systems = Array.from(new Set(system_slug.filter(s => s.system).map(s => s.system!)));
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
systems.forEach(s => platformViability.set(s, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
emulator: emulator,
|
||||||
|
path: execPath,
|
||||||
|
exists: exists,
|
||||||
|
isCritical: false,
|
||||||
|
path_cover: platform ? `/api/romm/platform/local/${platform}/cover` : null,
|
||||||
|
systems: systems.map(s => platformLookup.get(s)).filter(s => !!s)
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
finalEmulators.push({
|
||||||
|
emulator: 'emulatorjs',
|
||||||
|
exists: true,
|
||||||
|
path: { path: 'localhost', type: 'js' },
|
||||||
|
path_cover: `/api/romm/image?url=${encodeURIComponent('https://emulatorjs.org/logo/EmulatorJS.png')}`,
|
||||||
|
isCritical: false,
|
||||||
|
systems: []
|
||||||
|
});
|
||||||
|
|
||||||
|
return finalEmulators.map(e =>
|
||||||
|
{
|
||||||
|
e.isCritical = !e.systems.filter(s => s?.es_slug).some(s => !!platformViability.get(s?.es_slug!));
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only emulators we strictly need based on local games. Emulator JS is included as bundled.
|
||||||
|
* If there is even single emulator for a system don't include emulators for that system.
|
||||||
|
*/
|
||||||
|
/*export async function getMissingEmulators ()
|
||||||
|
{
|
||||||
|
const localGames = await db.query.games.findMany({
|
||||||
|
columns: {
|
||||||
|
platform_id: true,
|
||||||
|
slug: true
|
||||||
|
},
|
||||||
|
with: {
|
||||||
|
platform: {
|
||||||
|
columns: {
|
||||||
|
name: true,
|
||||||
|
es_slug: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const platformLookup = new Map(localGames.map(g => [g.platform.es_slug, g]));
|
||||||
|
const platformViability = new Map(localGames.map(g => [g.platform.es_slug, false]));
|
||||||
|
|
||||||
|
// all commands based on the local games
|
||||||
|
const commands = await emulatorsDb.query.commands.findMany({
|
||||||
|
columns: { command: true },
|
||||||
|
where: inArray(emulatorSchema.commands.system, Array.from(new Set(localGames.filter(g => g.platform.es_slug).map(s => s.platform.es_slug!)))),
|
||||||
|
with: { system: { columns: { name: true } } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// get all emulators in said commands
|
||||||
|
const emulators = commands
|
||||||
|
.flatMap(command =>
|
||||||
|
{
|
||||||
|
const matches = command.command.match(/(?<=%EMULATOR_)[\w-]+(?=%)/);
|
||||||
|
if (!matches)
|
||||||
|
{
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches?.map(m => ({ emulator: m, system: command.system?.name }));
|
||||||
|
}
|
||||||
|
).filter(c => !!c);
|
||||||
|
|
||||||
|
const groupedEmulators = Map.groupBy(emulators, ({ emulator }) => emulator);
|
||||||
|
const finalEmulators = await Promise.all(Array.from(groupedEmulators.entries()).map(async ([emulator, system_slug]) =>
|
||||||
|
{
|
||||||
|
let execPath: { path: string; type: string, } | undefined;
|
||||||
|
if (customEmulators.has(emulator))
|
||||||
|
{
|
||||||
|
execPath = { path: customEmulators.get(emulator), type: 'custom' };
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
execPath = await findExecByName(emulator);
|
||||||
|
}
|
||||||
|
|
||||||
|
let platform: number | null | undefined = null;
|
||||||
|
if (system_slug.length <= 1)
|
||||||
|
{
|
||||||
|
platform = platformLookup.get(system_slug[0].system)?.platform_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if automatic or custom path found existing binary.
|
||||||
|
// This might not be the actual emulator but I don't care.
|
||||||
|
const exists = !!execPath && await fs.exists(execPath.path);
|
||||||
|
const systems = Array.from(new Set(system_slug.map(s => s.system)));
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
systems.forEach(s => platformViability.set(s, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
emulator: emulator,
|
||||||
|
path: execPath,
|
||||||
|
exists: exists,
|
||||||
|
isCritical: false,
|
||||||
|
path_cover: platform ? `/api/romm/platform/local/${platform}/cover` : null,
|
||||||
|
systems: systems.map(s => platformLookup.get(s)).filter(s => !!s)
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
return finalEmulators.map(e =>
|
||||||
|
{
|
||||||
|
e.isCritical = !e.systems.filter(s => s?.es_slug).some(s => !!platformViability.get(s?.es_slug!));
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}*/
|
||||||
94
src/bun/api/settings/settings.ts
Normal file
94
src/bun/api/settings/settings.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import z from "zod";
|
||||||
|
import { SettingsSchema } from "@shared/constants";
|
||||||
|
import Elysia, { status } from "elysia";
|
||||||
|
import { config, customEmulators, taskQueue } from "../app";
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { InstallJob } from "../jobs/install-job";
|
||||||
|
import { move } from "fs-extra";
|
||||||
|
import { getRelevantEmulators } from "./services";
|
||||||
|
|
||||||
|
export const settings = new Elysia({ prefix: '/api/settings' })
|
||||||
|
.get('/emulators/automatic', async () =>
|
||||||
|
{
|
||||||
|
return getRelevantEmulators();
|
||||||
|
})
|
||||||
|
.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())
|
||||||
|
})
|
||||||
|
.put('/path/download', async ({ body: { manualPath, drive } }) =>
|
||||||
|
{
|
||||||
|
if (taskQueue.hasActiveOfType(InstallJob))
|
||||||
|
{
|
||||||
|
return status("Forbidden", "Installation in progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldDownloadPath = config.get('downloadPath');
|
||||||
|
if (!existsSync(oldDownloadPath))
|
||||||
|
{
|
||||||
|
return status("Not Found", "Old download path doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isDirEmpty (dirname: string)
|
||||||
|
{
|
||||||
|
const files = await fs.readdir(dirname);
|
||||||
|
return files.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = manualPath ?? drive;
|
||||||
|
|
||||||
|
if (!path)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(path) && !isDirEmpty(path))
|
||||||
|
{
|
||||||
|
return status("Conflict", "New location already exists and is not empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
await move(oldDownloadPath, path);
|
||||||
|
config.set('downloadPath', manualPath);
|
||||||
|
return manualPath;
|
||||||
|
}, {
|
||||||
|
body: z.object({
|
||||||
|
manualPath: z.string().optional(),
|
||||||
|
drive: z.string().optional()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get("/:id", async ({ params: { id } }) =>
|
||||||
|
{
|
||||||
|
const value = config.get(id);
|
||||||
|
return { value: value };
|
||||||
|
}, {
|
||||||
|
params: z.object({ id: z.keyof(SettingsSchema) }),
|
||||||
|
}).post('/:id',
|
||||||
|
async ({ params: { id }, body: { value }, }) =>
|
||||||
|
{
|
||||||
|
config.set(id, value);
|
||||||
|
}, {
|
||||||
|
params: z.object({ id: z.keyof(SettingsSchema) }),
|
||||||
|
body: z.object({ value: z.any() }),
|
||||||
|
});
|
||||||
|
|
||||||
59
src/bun/api/store/services/gamesService.ts
Normal file
59
src/bun/api/store/services/gamesService.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { GithubManifestSchema, StoreGameSchema } from "@/shared/constants";
|
||||||
|
import { CACHE_KEYS, getOrCached } from "../../cache";
|
||||||
|
|
||||||
|
export async function getStoreGameManifest ()
|
||||||
|
{
|
||||||
|
return getOrCached(CACHE_KEYS.STORE_GAME_MANIFEST, async () =>
|
||||||
|
{
|
||||||
|
const store = await fetch('https://api.github.com/repos/dragoonDorise/EmuDeck/git/trees/50261b66d69c1758efa28c6d7c54e45259a0c9c5?recursive=true').then(r => r.json()).then(data => GithubManifestSchema.parseAsync(data));
|
||||||
|
|
||||||
|
return store.tree.filter((e: any) =>
|
||||||
|
{
|
||||||
|
if (e.type === 'blob' && e.path !== "featured.json")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStoreGames (gamesManifest: any[], filter?: { limit?: number; offset?: number; })
|
||||||
|
{
|
||||||
|
const offset = filter?.offset ?? 0;
|
||||||
|
const limit = Math.min(50, filter?.limit ?? 10);
|
||||||
|
|
||||||
|
const games = await Promise.all(gamesManifest.slice(offset, Math.min(offset + limit, gamesManifest.length)).map((e: any) =>
|
||||||
|
{
|
||||||
|
return fetch(e.url).then(e => e.json()).then(game => StoreGameSchema.parseAsync(JSON.parse(atob(game.content.replace(/\n/g, "")))));
|
||||||
|
}));
|
||||||
|
|
||||||
|
return games;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractStoreGameSourceId (id: string)
|
||||||
|
{
|
||||||
|
const gameId = id.split('@');
|
||||||
|
if (gameId.length !== 2)
|
||||||
|
throw new Error("Store ID should include platform and name with @ separator");
|
||||||
|
return { system: gameId[0], id: gameId[1] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoreGameFromId (id: string)
|
||||||
|
{
|
||||||
|
const data = extractStoreGameSourceId(id);
|
||||||
|
return getStoreGame(data.system, data.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStoreGame (system: string, id: string)
|
||||||
|
{
|
||||||
|
return getStoreGameFromPath(`${system}/${encodeURIComponent(id)}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStoreGameFromPath (path: string)
|
||||||
|
{
|
||||||
|
const game = await getOrCached(CACHE_KEYS.STORE_GAME(path), () => fetch(`https://cdn.jsdelivr.net/gh/dragoonDorise/EmuDeck/store/${path}`)
|
||||||
|
.then(e => e.json())
|
||||||
|
.then(g => StoreGameSchema.parseAsync(g)));
|
||||||
|
return game;
|
||||||
|
}
|
||||||
201
src/bun/api/store/store.ts
Normal file
201
src/bun/api/store/store.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
|
||||||
|
import Elysia from "elysia";
|
||||||
|
import { config, customEmulators, db } from "../app";
|
||||||
|
import path from "node:path";
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import { EmulatorPackageSchema, EmulatorPackageType, FrontEndEmulator, FrontEndEmulatorDetailed, StoreGameSchema } from "@/shared/constants";
|
||||||
|
import { findExec } from "../games/services/launchGameService";
|
||||||
|
import { emulatorsDb } from '../app';
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import * as emulatorSchema from '@schema/emulators';
|
||||||
|
import * as appSchema from '@schema/app';
|
||||||
|
import z from "zod";
|
||||||
|
import { convertLocalToFrontendDetailed, convertStoreToFrontendDetailed, getLocalGameMatch } from "../games/services/utils";
|
||||||
|
import { getPlatformsApiPlatformsGet } from "@/clients/romm";
|
||||||
|
import { CACHE_KEYS, getOrCached } from "../cache";
|
||||||
|
|
||||||
|
export function getStoreFolder ()
|
||||||
|
{
|
||||||
|
const downlodDir = config.get('downloadPath');
|
||||||
|
return path.join(downlodDir, "store");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllStoreEmulatorPackages ()
|
||||||
|
{
|
||||||
|
const downlodDir = config.get('downloadPath');
|
||||||
|
const emulatorsBucket = path.join(downlodDir, "store", "buckets", "emulators");
|
||||||
|
const emulators = await fs.readdir(emulatorsBucket);
|
||||||
|
const emulatorsRawData = await Promise.all(emulators.map(e => fs.readFile(path.join(emulatorsBucket, e), 'utf-8')));
|
||||||
|
|
||||||
|
const emulatesParsed = emulatorsRawData.map(d => EmulatorPackageSchema.safeParse(JSON.parse(d))).filter(e =>
|
||||||
|
{
|
||||||
|
if (e.error)
|
||||||
|
{
|
||||||
|
console.error(e.error);
|
||||||
|
}
|
||||||
|
return e.data;
|
||||||
|
}).map(e => e.data!);
|
||||||
|
|
||||||
|
return emulatesParsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildSystems (emulator: EmulatorPackageType)
|
||||||
|
{
|
||||||
|
const systems = await Promise.all(emulator.systems.map(async system =>
|
||||||
|
{
|
||||||
|
const rommSystem = await emulatorsDb.query.systemMappings.findFirst({
|
||||||
|
where: and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.system, system))
|
||||||
|
});
|
||||||
|
|
||||||
|
const esSystem = await emulatorsDb.query.systems.findFirst({ where: eq(emulatorSchema.emulators.name, system), columns: { fullname: true } });
|
||||||
|
|
||||||
|
let icon: string = `/api/romm/image/romm/assets/platforms/${rommSystem?.sourceSlug ?? system}.svg`;
|
||||||
|
|
||||||
|
return { id: system, name: esSystem?.fullname ?? system, icon: icon };
|
||||||
|
}));
|
||||||
|
|
||||||
|
return systems;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const store = new Elysia({ prefix: '/api/store' })
|
||||||
|
.get('/emulators', async ({ query }) =>
|
||||||
|
{
|
||||||
|
const rommPlatforms = await getOrCached(CACHE_KEYS.ROM_PLATFORMS, () => getPlatformsApiPlatformsGet({ throwOnError: true }), { expireMs: 60 * 60 * 1000 }).then(d => d.data).catch(e =>
|
||||||
|
{
|
||||||
|
console.error(e);
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
const emulatesParsed = await getAllStoreEmulatorPackages();
|
||||||
|
let frontEndEmulators = await Promise.all(emulatesParsed
|
||||||
|
.filter(e => e.os.includes(process.platform as any))
|
||||||
|
.map(async (emulator) =>
|
||||||
|
{
|
||||||
|
let execPath: { path: string; type: string; } | undefined;
|
||||||
|
const esEmulator = await emulatorsDb.query.emulators.findFirst({ where: eq(emulatorSchema.emulators.name, emulator.name) });
|
||||||
|
|
||||||
|
if (esEmulator)
|
||||||
|
{
|
||||||
|
if (customEmulators.has(emulator?.name))
|
||||||
|
{
|
||||||
|
execPath = { path: customEmulators.get(emulator.name), type: 'custom' };
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
execPath = await findExec(esEmulator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = !!execPath && await fs.exists(execPath.path);
|
||||||
|
const systems = await buildSystems(emulator);
|
||||||
|
|
||||||
|
const gameCounts = await Promise.all(systems.map(async (s) =>
|
||||||
|
{
|
||||||
|
const rommMapping = await emulatorsDb.query.systemMappings.findFirst({ where: and(eq(emulatorSchema.systemMappings.source, 'romm'), eq(emulatorSchema.systemMappings.system, s.id)) });
|
||||||
|
const romPlatform = rommPlatforms?.find(p => p.slug === (rommMapping?.sourceSlug ?? s.id));
|
||||||
|
if (romPlatform)
|
||||||
|
{
|
||||||
|
return romPlatform.rom_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
const gameCount = gameCounts.reduce((a, c) => a + c);
|
||||||
|
|
||||||
|
return { ...emulator, exists, systems, gameCount } satisfies FrontEndEmulator;
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (query.missing)
|
||||||
|
{
|
||||||
|
frontEndEmulators = frontEndEmulators.filter(e => !e.exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.orderBy === 'importance')
|
||||||
|
{
|
||||||
|
frontEndEmulators.sort((a, b) =>
|
||||||
|
{
|
||||||
|
const gameCountDiff = b.gameCount - a.gameCount;
|
||||||
|
if (gameCountDiff !== 0) return gameCountDiff;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.limit)
|
||||||
|
{
|
||||||
|
frontEndEmulators = frontEndEmulators.splice(0, query.limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frontEndEmulators;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: z.object({
|
||||||
|
limit: z.coerce.number().optional(),
|
||||||
|
missing: z.stringbool().optional().describe("Show Only Non Installed emulators"),
|
||||||
|
orderBy: z.enum(['name', 'recently_updated', 'importance']).optional()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/games/featured', async () =>
|
||||||
|
{
|
||||||
|
const response = await fetch('https://cdn.jsdelivr.net/gh/dragoonDorise/EmuDeck/store/featured.json');
|
||||||
|
const games = await z.object({ featured: z.array(StoreGameSchema) }).parseAsync(await response.json());
|
||||||
|
return Promise.all(games.featured.map(async g =>
|
||||||
|
{
|
||||||
|
const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(`${g.system}@${g.title}`, 'store') });
|
||||||
|
if (localGame) return convertLocalToFrontendDetailed(localGame);
|
||||||
|
return convertStoreToFrontendDetailed(g.system, g.title, g);
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.get('/stats', async () =>
|
||||||
|
{
|
||||||
|
const emulatesParsed = await getAllStoreEmulatorPackages();
|
||||||
|
const storeEmulatorCount = emulatesParsed.filter(e => e.os.includes(process.platform as any)).length;
|
||||||
|
const gameCount = await db.$count(appSchema.games);
|
||||||
|
return {
|
||||||
|
storeEmulatorCount,
|
||||||
|
gameCount
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.get('/screenshot/emulator/:id/:name', async ({ params: { id, name } }) =>
|
||||||
|
{
|
||||||
|
const downlodDir = config.get('downloadPath');
|
||||||
|
return Bun.file(path.join(downlodDir, "store", "media", "screenshots", id, name));
|
||||||
|
},
|
||||||
|
{ params: z.object({ id: z.string(), name: z.string() }) })
|
||||||
|
.get('/details/emulator/:id', async ({ params: { id } }) =>
|
||||||
|
{
|
||||||
|
const downlodDir = config.get('downloadPath');
|
||||||
|
const emulatorPath = path.join(downlodDir, "store", "buckets", "emulators", `${id}.json`);
|
||||||
|
const emulatorScreenshotsPath = path.join(downlodDir, "store", "media", "screenshots", id);
|
||||||
|
const emulatorPackage = await EmulatorPackageSchema.parseAsync(JSON.parse(await fs.readFile(emulatorPath, 'utf-8')));
|
||||||
|
|
||||||
|
const systems = await buildSystems(emulatorPackage);
|
||||||
|
let execPath: { path: string; type: string; } | undefined;
|
||||||
|
const esEmulator = await emulatorsDb.query.emulators.findFirst({ where: eq(emulatorSchema.emulators.name, emulatorPackage.name) });
|
||||||
|
|
||||||
|
if (esEmulator)
|
||||||
|
{
|
||||||
|
if (customEmulators.has(emulatorPackage?.name))
|
||||||
|
{
|
||||||
|
execPath = { path: customEmulators.get(emulatorPackage.name), type: 'custom' };
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
execPath = await findExec(esEmulator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const screenshots = await fs.exists(emulatorScreenshotsPath) ? await fs.readdir(emulatorScreenshotsPath) : [];
|
||||||
|
const exists = !!execPath && await fs.exists(execPath.path);
|
||||||
|
const emulator: FrontEndEmulatorDetailed = {
|
||||||
|
...emulatorPackage,
|
||||||
|
systems,
|
||||||
|
exists,
|
||||||
|
status: {
|
||||||
|
source: execPath?.type,
|
||||||
|
location: execPath?.path
|
||||||
|
},
|
||||||
|
screenshots: screenshots.map(s => `/api/store/screenshot/emulator/${id}/${s}`)
|
||||||
|
};
|
||||||
|
|
||||||
|
return emulator;
|
||||||
|
}, { params: z.object({ id: z.string() }) });
|
||||||
|
|
@ -2,7 +2,7 @@ import Elysia from "elysia";
|
||||||
import open from 'open';
|
import open from 'open';
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { config, events } from "./app";
|
import { cachePath, config, events } from "./app";
|
||||||
import { isSteamDeck, openExternal } from "../utils";
|
import { isSteamDeck, openExternal } from "../utils";
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import buildNotificationsStream from "./notifications";
|
import buildNotificationsStream from "./notifications";
|
||||||
|
|
@ -11,6 +11,7 @@ import { DirSchema, DownloadsDrive } from "@/shared/constants";
|
||||||
import { getDevices, getDevicesCurated } from "./drives";
|
import { getDevices, getDevicesCurated } from "./drives";
|
||||||
import getFolderSize from "get-folder-size";
|
import getFolderSize from "get-folder-size";
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
|
import { getStoreFolder } from "./store/store";
|
||||||
|
|
||||||
export const system = new Elysia({ prefix: '/api/system' })
|
export const system = new Elysia({ prefix: '/api/system' })
|
||||||
.post('/show_keyboard', async ({ body: { XPosition, YPosition, Width, Height } }) =>
|
.post('/show_keyboard', async ({ body: { XPosition, YPosition, Width, Height } }) =>
|
||||||
|
|
@ -48,7 +49,9 @@ export const system = new Elysia({ prefix: '/api/system' })
|
||||||
hostname: os.hostname(),
|
hostname: os.hostname(),
|
||||||
steamDeck: process.env.SteamDeck,
|
steamDeck: process.env.SteamDeck,
|
||||||
machine: os.machine(),
|
machine: os.machine(),
|
||||||
source
|
source,
|
||||||
|
cacheSize: (await fs.stat(cachePath)).size,
|
||||||
|
storeSize: (await getFolderSize(getStoreFolder())).size
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.get('/notifications', ({ set }) =>
|
.get('/notifications', ({ set }) =>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export class TaskQueue
|
||||||
setTimeout(this.processQueue);
|
setTimeout(this.processQueue);
|
||||||
});
|
});
|
||||||
return promise;
|
return promise;
|
||||||
|
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
@ -91,8 +92,10 @@ export class TaskQueue
|
||||||
|
|
||||||
export interface EventsList
|
export interface EventsList
|
||||||
{
|
{
|
||||||
|
started: [e: BaseEvent];
|
||||||
progress: [e: ProgressEvent];
|
progress: [e: ProgressEvent];
|
||||||
abort: [e: AbortEvent];
|
abort: [e: AbortEvent];
|
||||||
|
/** Called when the job successfully completes */
|
||||||
completed: [e: CompletedEvent];
|
completed: [e: CompletedEvent];
|
||||||
error: [e: ErrorEvent];
|
error: [e: ErrorEvent];
|
||||||
ended: [e: BaseEvent];
|
ended: [e: BaseEvent];
|
||||||
|
|
@ -101,7 +104,7 @@ export interface EventsList
|
||||||
interface BaseEvent
|
interface BaseEvent
|
||||||
{
|
{
|
||||||
id: string;
|
id: string;
|
||||||
job: IJob;
|
job: IPublicJob;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorEvent extends BaseEvent
|
interface ErrorEvent extends BaseEvent
|
||||||
|
|
@ -128,6 +131,7 @@ interface CompletedEvent extends BaseEvent
|
||||||
export interface IJob
|
export interface IJob
|
||||||
{
|
{
|
||||||
start (context: JobContext): Promise<any>;
|
start (context: JobContext): Promise<any>;
|
||||||
|
exposeData?(): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JobStatus = 'completed' | 'error' | 'running' | 'waiting' | 'aborted';
|
export type JobStatus = 'completed' | 'error' | 'running' | 'waiting' | 'aborted';
|
||||||
|
|
@ -137,7 +141,7 @@ export interface IPublicJob
|
||||||
progress: number;
|
progress: number;
|
||||||
state?: string;
|
state?: string;
|
||||||
status: JobStatus;
|
status: JobStatus;
|
||||||
job: any;
|
job: IJob;
|
||||||
abort: (reason?: any) => void;
|
abort: (reason?: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,7 +156,7 @@ export class JobContext implements IPublicJob
|
||||||
private error?: any;
|
private error?: any;
|
||||||
private events: EventEmitter<EventsList>;
|
private events: EventEmitter<EventsList>;
|
||||||
private abortController: AbortController;
|
private abortController: AbortController;
|
||||||
private m_job: IJob;
|
private readonly m_job: IJob;
|
||||||
|
|
||||||
constructor(id: string, events: EventEmitter<EventsList>, job: IJob)
|
constructor(id: string, events: EventEmitter<EventsList>, job: IJob)
|
||||||
{
|
{
|
||||||
|
|
@ -162,7 +166,7 @@ export class JobContext implements IPublicJob
|
||||||
this.abortController.signal.addEventListener('abort', () =>
|
this.abortController.signal.addEventListener('abort', () =>
|
||||||
{
|
{
|
||||||
this.aborted = true;
|
this.aborted = true;
|
||||||
this.events.emit('abort', { id: this.m_id, reason: this.abortController.signal.reason, job: this.m_job } satisfies AbortEvent);
|
this.events.emit('abort', { id: this.m_id, reason: this.abortController.signal.reason, job: this } satisfies AbortEvent);
|
||||||
});
|
});
|
||||||
this.events = events;
|
this.events = events;
|
||||||
}
|
}
|
||||||
|
|
@ -171,19 +175,24 @@ export class JobContext implements IPublicJob
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
this.events.emit('started', { id: this.m_id, job: this });
|
||||||
await this.m_job.start(this);
|
await this.m_job.start(this);
|
||||||
this.completed = true;
|
this.completed = true;
|
||||||
this.events.emit('completed', { id: this.m_id, job: this.m_job });
|
this.events.emit('completed', { id: this.m_id, job: this });
|
||||||
|
|
||||||
} catch (error)
|
} catch (error)
|
||||||
|
{
|
||||||
|
if (error !== 'cancel')
|
||||||
{
|
{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.events.emit('error', { id: this.m_id, job: this.m_job, error });
|
}
|
||||||
|
|
||||||
|
this.events.emit('error', { id: this.m_id, job: this, error });
|
||||||
this.error = error;
|
this.error = error;
|
||||||
} finally
|
} finally
|
||||||
{
|
{
|
||||||
this.running = false;
|
this.running = false;
|
||||||
this.events.emit('ended', { id: this.m_id, job: this.m_job });
|
this.events.emit('ended', { id: this.m_id, job: this });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +220,7 @@ export class JobContext implements IPublicJob
|
||||||
this.m_progress = progress;
|
this.m_progress = progress;
|
||||||
if (state)
|
if (state)
|
||||||
this.m_state = state;
|
this.m_state = state;
|
||||||
this.events.emit('progress', { id: this.m_id, progress, state: state ?? this.m_state, job: this.m_job });
|
this.events.emit('progress', { id: this.m_id, progress, state: state ?? this.m_state, job: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
public abort (reason?: any)
|
public abort (reason?: any)
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,42 @@
|
||||||
import { killBrowser, spawnBrowser } from './utils/browser-spawner';
|
import { killBrowser, spawnBrowser } from './utils/browser-spawner';
|
||||||
import { BuildParams } from './utils/browser-params';
|
import { BrowserParams, BuildParams } from './utils/browser-params';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { EventEmitter } from 'node:stream';
|
import { EventEmitter } from 'node:stream';
|
||||||
import { config } from './api/app';
|
|
||||||
import { dirname } from 'node:path';
|
|
||||||
|
|
||||||
export default async function init (events: EventEmitter, forceBrowser: boolean)
|
export default async function init (events: EventEmitter, forceBrowser: boolean, params: BrowserParams)
|
||||||
{
|
{
|
||||||
if (forceBrowser)
|
if (forceBrowser)
|
||||||
{
|
{
|
||||||
await runBrowser(events);
|
await runBrowser(events, params);
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await runWebview(events);
|
await runWebview(events, params);
|
||||||
} catch (error)
|
} catch (error)
|
||||||
{
|
{
|
||||||
await runBrowser(events);
|
await runBrowser(events, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runWebview (events: EventEmitter)
|
async function runWebview (events: EventEmitter, params: BrowserParams)
|
||||||
{
|
{
|
||||||
const webviewWorker = new Worker(new URL(`./webview/${os.platform()}`, import.meta.url).href, {
|
const webviewPath = process.env.IS_BINARY ? `./webview/${os.platform()}` : new URL(`./webview/${os.platform()}`, import.meta.url).href;
|
||||||
|
console.log("Launching Webview Worker at: ", webviewPath);
|
||||||
|
const config: Record<string, string> = {};
|
||||||
|
if (params.windowSize)
|
||||||
|
{
|
||||||
|
config.WINDOW_WIDTH = String(params.windowSize?.width);
|
||||||
|
config.WINDOW_HEIGHT = String(params.windowSize?.height);
|
||||||
|
}
|
||||||
|
const webviewWorker = new Worker(webviewPath, {
|
||||||
smol: true,
|
smol: true,
|
||||||
ref: false
|
ref: false,
|
||||||
|
env: {
|
||||||
|
...config,
|
||||||
|
...process.env as any
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise((resolve, reject) =>
|
||||||
|
|
@ -39,8 +49,9 @@ async function runWebview (events: EventEmitter)
|
||||||
|
|
||||||
webviewWorker.addEventListener('message', (e) =>
|
webviewWorker.addEventListener('message', (e) =>
|
||||||
{
|
{
|
||||||
if (e.data === 'destroyed')
|
if (e.data.data === 'destroyed')
|
||||||
{
|
{
|
||||||
|
console.log("Webview Destroyed");
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -48,14 +59,15 @@ async function runWebview (events: EventEmitter)
|
||||||
events.on('exitapp', () =>
|
events.on('exitapp', () =>
|
||||||
{
|
{
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
console.log("Terminating Webview Worker");
|
||||||
webviewWorker.terminate();
|
webviewWorker.terminate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runBrowser (events: EventEmitter)
|
async function runBrowser (events: EventEmitter, params: BrowserParams)
|
||||||
{
|
{
|
||||||
const browserParams = await BuildParams({ configPath: dirname(config.path) });
|
const browserParams = await BuildParams(params);
|
||||||
if (!browserParams)
|
if (!browserParams)
|
||||||
{
|
{
|
||||||
console.error("Could not find valid browser");
|
console.error("Could not find valid browser");
|
||||||
|
|
@ -72,7 +84,7 @@ async function runBrowser (events: EventEmitter)
|
||||||
detached: false,
|
detached: false,
|
||||||
execPath: browserParams.browser.path,
|
execPath: browserParams.browser.path,
|
||||||
source: browserParams.browser.source,
|
source: browserParams.browser.source,
|
||||||
configPath: dirname(config.path),
|
configPath: params.configPath,
|
||||||
ipc (message)
|
ipc (message)
|
||||||
{
|
{
|
||||||
console.log(message);
|
console.log(message);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { RunBunServer } from './server';
|
import { RunBunServer } from './server';
|
||||||
import { RunAPIServer } from './api/rpc';
|
import { RunAPIServer } from './api/rpc';
|
||||||
import { cleanup as appCleanup, events } from './api/app';
|
import { cleanup as appCleanup, config, events } from './api/app';
|
||||||
import init from './browser';
|
import init from './browser';
|
||||||
|
import { dirname } from 'pathe';
|
||||||
|
import { createInterface } from 'readline';
|
||||||
|
|
||||||
const api = RunAPIServer();
|
const api = RunAPIServer();
|
||||||
let bunServer: { stop: () => void; url: URL; } | undefined;
|
let bunServer: { stop: () => void; } | undefined;
|
||||||
|
|
||||||
if (!Bun.env.PUBLIC_ACCESS)
|
if (!Bun.env.PUBLIC_ACCESS)
|
||||||
{
|
{
|
||||||
|
|
@ -16,21 +18,25 @@ async function cleanup ()
|
||||||
console.log("Cleaning Up");
|
console.log("Cleaning Up");
|
||||||
await appCleanup();
|
await appCleanup();
|
||||||
bunServer?.stop();
|
bunServer?.stop();
|
||||||
await api.apiServer.stop();
|
await api.apiServer.stop(true);
|
||||||
await api.cleanup();
|
await api.cleanup();
|
||||||
|
console.log("Finished Cleaning Up");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Bun.env.HEADLESS)
|
if (Bun.env.HEADLESS)
|
||||||
{
|
{
|
||||||
// Called by outside force
|
const rl = createInterface({ input: process.stdin });
|
||||||
process.on('message', ({ type }) =>
|
|
||||||
|
rl.on("line", async (line) =>
|
||||||
{
|
{
|
||||||
if (type === 'exitapp')
|
if (line.trim() === "shutdown")
|
||||||
{
|
{
|
||||||
cleanup();
|
console.log("Graceful Shutdown");
|
||||||
|
await cleanup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Called by user
|
// Called by user
|
||||||
events.on('exitapp', () =>
|
events.on('exitapp', () =>
|
||||||
{
|
{
|
||||||
|
|
@ -39,7 +45,11 @@ if (Bun.env.HEADLESS)
|
||||||
});
|
});
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
await init(events, !!Bun.env.FORCE_BROWSER);
|
await init(events, Bun.env.FORCE_BROWSER === "true", {
|
||||||
|
configPath: dirname(config.path),
|
||||||
|
windowPosition: config.get('windowPosition'),
|
||||||
|
windowSize: config.get('windowSize')
|
||||||
|
});
|
||||||
await cleanup();
|
await cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,44 @@
|
||||||
import { SERVER_PORT } from "../shared/constants";
|
import { SERVER_PORT } from "@shared/constants";
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import appInfo from '../../package.json';
|
import appInfo from '~/package.json';
|
||||||
import { host } from "./utils/host";
|
import { host } from "./utils/host";
|
||||||
import { appPath } from "./utils";
|
import { appPath } from "./utils";
|
||||||
|
import Elysia, { file } from "elysia";
|
||||||
|
import cors from "@elysiajs/cors";
|
||||||
|
import staticPlugin from "@elysiajs/static";
|
||||||
|
|
||||||
export function RunBunServer ()
|
export function RunBunServer ()
|
||||||
{
|
{
|
||||||
console.log("Launching Server on port ", SERVER_PORT);
|
console.log("Launching Server on port ", SERVER_PORT);
|
||||||
return Bun.serve({
|
return new Elysia()
|
||||||
|
.use(cors())
|
||||||
|
.get("/", ({ set }) =>
|
||||||
|
{
|
||||||
|
set.headers['cross-origin-opener-policy'] = 'same-origin';
|
||||||
|
set.headers['cross-origin-embedder-policy'] = 'require-corp';
|
||||||
|
return file("./dist/index.html");
|
||||||
|
})
|
||||||
|
.get('/emulatorjs', ({ set }) =>
|
||||||
|
{
|
||||||
|
set.headers['cross-origin-opener-policy'] = 'same-origin';
|
||||||
|
set.headers['cross-origin-embedder-policy'] = 'require-corp';
|
||||||
|
set.headers['cross-origin-resource-policy'] = 'cross-origin';
|
||||||
|
return file('./dist/emulatorjs/index.html');
|
||||||
|
})
|
||||||
|
.use(staticPlugin({
|
||||||
|
indexHTML: false,
|
||||||
|
assets: "dist",
|
||||||
|
prefix: "/",
|
||||||
|
alwaysStatic: true
|
||||||
|
})).listen({ port: SERVER_PORT, hostname: host }, console.log);
|
||||||
|
/*return Bun.serve({
|
||||||
port: SERVER_PORT,
|
port: SERVER_PORT,
|
||||||
hostname: host,
|
hostname: host,
|
||||||
routes: {
|
routes: {
|
||||||
"/": Bun.file(appPath("./dist/index.html")),
|
"/": Bun.file(appPath("./dist/index.html")),
|
||||||
// Serve a file by lazily loading it into memory
|
// Serve a file by lazily loading it into memory
|
||||||
"/favicon.ico": Bun.file(appPath("./dist/favicon.ico")),
|
"/favicon.ico": Bun.file(appPath("./dist/favicon.ico")),
|
||||||
|
"/emulatorjs/": Bun.file(appPath("./dist/emulatorjs/index.html")),
|
||||||
"/.well-known/appspecific/com.chrome.devtools.json": new Response(
|
"/.well-known/appspecific/com.chrome.devtools.json": new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
name: appInfo.name,
|
name: appInfo.name,
|
||||||
|
|
@ -33,5 +58,5 @@ export function RunBunServer ()
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
return new Response(Bun.file(appPath(`./${path.join('dist', url.pathname)}`)));
|
return new Response(Bun.file(appPath(`./${path.join('dist', url.pathname)}`)));
|
||||||
},
|
},
|
||||||
});
|
});*/
|
||||||
}
|
}
|
||||||
26
src/bun/types/types.d.ts
vendored
26
src/bun/types/types.d.ts
vendored
|
|
@ -8,3 +8,29 @@ export type ActiveGame = {
|
||||||
name: string;
|
name: string;
|
||||||
command: string;
|
command: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ObjectConstructor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Groups members of an iterable according to the return value of the passed callback.
|
||||||
|
* @param items An iterable.
|
||||||
|
* @param keySelector A callback which will be invoked for each item in items.
|
||||||
|
*/
|
||||||
|
groupBy<K extends PropertyKey, T> (
|
||||||
|
items: Iterable<T>,
|
||||||
|
keySelector: (item: T, index: number) => K,
|
||||||
|
): Partial<Record<K, T[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapConstructor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Groups members of an iterable according to the return value of the passed callback.
|
||||||
|
* @param items An iterable.
|
||||||
|
* @param keySelector A callback which will be invoked for each item in items.
|
||||||
|
*/
|
||||||
|
groupBy<K, T> (
|
||||||
|
items: Iterable<T>,
|
||||||
|
keySelector: (item: T, index: number) => K,
|
||||||
|
): Map<K, T[]>;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import { $ } from 'bun';
|
import { $ } from 'bun';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
import { SERVER_URL } from "../../shared/constants";
|
import { SERVER_URL } from "@shared/constants";
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { getBrowserPath } from "./get-browser";
|
import { getBrowserPath } from "./get-browser";
|
||||||
import { isSteamDeckGameMode } from "../utils";
|
import { isSteamDeckGameMode } from "../utils";
|
||||||
import { config } from "../api/app";
|
|
||||||
import { ensureDir } from 'fs-extra';
|
import { ensureDir } from 'fs-extra';
|
||||||
import { host } from "./host";
|
import { host } from "./host";
|
||||||
|
|
||||||
export async function BuildParams (data: { configPath: string; })
|
export interface BrowserParams
|
||||||
|
{
|
||||||
|
configPath: string;
|
||||||
|
windowPosition?: { x: number, y: number; };
|
||||||
|
windowSize?: { width?: number, height?: number; };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function BuildParams (data: BrowserParams)
|
||||||
{
|
{
|
||||||
const validBrowser = await getBrowserPath({
|
const validBrowser = await getBrowserPath({
|
||||||
browserOrder: Bun.env.BROWSER_PRIORITY ? Bun.env.BROWSER_PRIORITY.split(',') as any : ['chrome', 'chromium']
|
browserOrder: Bun.env.BROWSER_PRIORITY ? Bun.env.BROWSER_PRIORITY.split(',') as any : ['chrome', 'chromium']
|
||||||
|
|
@ -52,9 +58,9 @@ export async function BuildParams (data: { configPath: string; })
|
||||||
if (isSteamDeckGameMode())
|
if (isSteamDeckGameMode())
|
||||||
{
|
{
|
||||||
args.push('--kiosk');
|
args.push('--kiosk');
|
||||||
} else
|
} else if (data.windowSize)
|
||||||
{
|
{
|
||||||
args.push(`--window-size=${config.get('windowSize.width')},${config.get('windowSize.height')}`);
|
args.push(`--window-size=${data.windowSize.width},${data.windowSize.height}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
args.push('--password-store=basic');
|
args.push('--password-store=basic');
|
||||||
|
|
@ -71,9 +77,9 @@ export async function BuildParams (data: { configPath: string; })
|
||||||
args.push('--remote-debugging-port=9222');
|
args.push('--remote-debugging-port=9222');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.has('windowPosition'))
|
if (data.windowPosition)
|
||||||
{
|
{
|
||||||
args.push(`--window-position=${config.get('windowPosition.x')},${config.get('windowPosition.y')}`);
|
args.push(`--window-position=${data.windowPosition.x},${data.windowPosition.y}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEdge)
|
if (isEdge)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { SERVER_URL } from "@/shared/constants";
|
import { SERVER_URL } from "@shared/constants";
|
||||||
import { host } from "../utils/host";
|
import { host } from "../utils/host";
|
||||||
|
|
||||||
export default function (webview: { navigate: (url: string) => void; run: () => void; destroy: () => void; })
|
export default function (webview: { navigate: (url: string) => void; run: () => void; destroy: () => void; })
|
||||||
|
|
@ -14,4 +14,5 @@ export default function (webview: { navigate: (url: string) => void; run: () =>
|
||||||
};
|
};
|
||||||
webview.navigate(SERVER_URL(host));
|
webview.navigate(SERVER_URL(host));
|
||||||
webview.run();
|
webview.run();
|
||||||
|
postMessage({ data: 'destroyed' });
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Webview } from 'webview-bun';
|
import { Size, SizeHint, Webview } from 'webview-bun';
|
||||||
import webviewWorkerBase from "./base";
|
import webviewWorkerBase from "./base";
|
||||||
|
|
||||||
if (process.env.FLATPAK_BUILD === "true")
|
if (process.env.FLATPAK_BUILD === "true")
|
||||||
|
|
@ -28,6 +28,9 @@ if (process.env.FLATPAK_BUILD === "true")
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
console.log("Launching Webview");
|
console.log("Launching Webview");
|
||||||
const webview = new Webview(import.meta.env.NODE_ENV === 'development');
|
let size: Size | undefined = undefined;
|
||||||
|
if (process.env.WINDOW_WIDTH && process.env.WINDOW_HEIGHT)
|
||||||
|
size = { width: Number(process.env.WINDOW_WIDTH), height: Number(process.env.WINDOW_HEIGHT), hint: SizeHint.NONE };
|
||||||
|
const webview = new Webview(process.env.NODE_ENV === 'development', size);
|
||||||
webviewWorkerBase(webview);
|
webviewWorkerBase(webview);
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
|
||||||
import { Webview } from 'webview-bun';
|
import { Size, SizeHint, Webview } from 'webview-bun';
|
||||||
import webviewWorkerBase from "./base";
|
import webviewWorkerBase from "./base";
|
||||||
|
|
||||||
const webview = new Webview(import.meta.env.NODE_ENV === 'development');
|
let size: Size | undefined = undefined;
|
||||||
|
if (process.env.WINDOW_WIDTH && process.env.WINDOW_HEIGHT)
|
||||||
|
size = { width: Number(process.env.WINDOW_WIDTH), height: Number(process.env.WINDOW_HEIGHT), hint: SizeHint.NONE };
|
||||||
|
const webview = new Webview(process.env.NODE_ENV === 'development', size);
|
||||||
webviewWorkerBase(webview);
|
webviewWorkerBase(webview);
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { createContext, JSX, Ref, useContext, useEffect, useState } from 'react';
|
import { CSSProperties, JSX, Ref, useEffect, useRef, useState } from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { useSessionStorage } from 'usehooks-ts';
|
import { useSessionStorage } from 'usehooks-ts';
|
||||||
import { useLocalSetting } from '../scripts/utils';
|
import { mobileCheck, useLocalSetting } from '../scripts/utils';
|
||||||
|
import { AnimatedBackgroundContext } from '../scripts/contexts';
|
||||||
export const AnimatedBackgroundContext = createContext({} as { setBackground: (url: string) => void; });
|
|
||||||
|
|
||||||
export function AnimatedBackground (data: {
|
export function AnimatedBackground (data: {
|
||||||
children?: any;
|
children?: any;
|
||||||
|
|
@ -15,26 +14,43 @@ export function AnimatedBackground (data: {
|
||||||
className?: string;
|
className?: string;
|
||||||
animated?: boolean,
|
animated?: boolean,
|
||||||
scrolling?: boolean;
|
scrolling?: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
const animateBackground = true;
|
const animateBackground = useLocalSetting('backgroundAnimation');
|
||||||
|
const [backgroundUrl, setBackgroundUrl] = data.backgroundKey ?
|
||||||
const [backgroundUrl, setBackgroundUrl] = data.backgroundKey ? useSessionStorage<string | undefined>(
|
useSessionStorage<string | undefined>(
|
||||||
data.backgroundKey!,
|
data.backgroundKey,
|
||||||
data.backgroundUrl ? (data.backgroundUrl instanceof URL ? data.backgroundUrl.href : data.backgroundUrl) : undefined,
|
data.backgroundUrl ? (data.backgroundUrl instanceof URL ? data.backgroundUrl.href : data.backgroundUrl) : undefined,
|
||||||
) : useState<string | undefined>();
|
)
|
||||||
|
: useState<string | undefined>();
|
||||||
|
|
||||||
|
const [lastBackgroundUrl, setLastBackgroundUrl] = useState<string | undefined>(undefined);
|
||||||
|
const backgroundElementRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
const lastBg = backgroundUrl;
|
||||||
|
|
||||||
|
if (data.backgroundUrl != backgroundUrl)
|
||||||
{
|
{
|
||||||
setBackgroundUrl(data.backgroundUrl ? (data.backgroundUrl instanceof URL ? data.backgroundUrl.href : data.backgroundUrl) : undefined);
|
setBackgroundUrl(data.backgroundUrl ? (data.backgroundUrl instanceof URL ? data.backgroundUrl.href : data.backgroundUrl) : undefined);
|
||||||
|
setLastBackgroundUrl(lastBg);
|
||||||
|
}
|
||||||
}, [data.backgroundUrl]);
|
}, [data.backgroundUrl]);
|
||||||
|
|
||||||
let finalBackgroundUrl;
|
let finalBackgroundUrl: URL | undefined;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
finalBackgroundUrl = backgroundUrl ? new URL(backgroundUrl) : undefined;
|
finalBackgroundUrl = backgroundUrl ? new URL(backgroundUrl) : undefined;
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
|
let finalLastBackgroundUrl: URL | undefined;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
finalLastBackgroundUrl = lastBackgroundUrl ? new URL(lastBackgroundUrl) : undefined;
|
||||||
|
} catch { }
|
||||||
|
|
||||||
const blur = useLocalSetting('backgroundBlur');
|
const blur = useLocalSetting('backgroundBlur');
|
||||||
if (blur)
|
if (blur)
|
||||||
{
|
{
|
||||||
|
|
@ -43,11 +59,41 @@ export function AnimatedBackground (data: {
|
||||||
finalBackgroundUrl?.searchParams.set('blur', String(24));
|
finalBackgroundUrl?.searchParams.set('blur', String(24));
|
||||||
}
|
}
|
||||||
|
|
||||||
finalBackgroundUrl?.searchParams.set('height', String(320));
|
if (!finalLastBackgroundUrl?.searchParams.has('blur'))
|
||||||
|
{
|
||||||
|
finalLastBackgroundUrl?.searchParams.set('blur', String(24));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalBackgroundUrl?.searchParams.set('height', String(320));
|
||||||
|
finalLastBackgroundUrl?.searchParams.set('height', String(320));
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if (finalBackgroundUrl && backgroundElementRef.current)
|
||||||
|
{
|
||||||
|
const finalBackgroundImg = new Image();
|
||||||
|
finalBackgroundImg.addEventListener('load', e =>
|
||||||
|
{
|
||||||
|
if (backgroundElementRef.current)
|
||||||
|
{
|
||||||
|
backgroundElementRef.current.style.backgroundImage = `url('${finalBackgroundUrl.href}')`;
|
||||||
|
backgroundElementRef.current.style.opacity = "1";
|
||||||
|
backgroundElementRef.current.style.backgroundSize = "100%";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
finalBackgroundImg.src = finalBackgroundUrl.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}, [finalBackgroundUrl]);
|
||||||
|
|
||||||
|
const isMobile = mobileCheck();
|
||||||
|
|
||||||
function handleSetBackground (url: string)
|
function handleSetBackground (url: string)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
setLastBackgroundUrl(backgroundUrl);
|
||||||
setBackgroundUrl(url);
|
setBackgroundUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,30 +116,40 @@ export function AnimatedBackground (data: {
|
||||||
return (
|
return (
|
||||||
<AnimatedBackgroundContext value={{ setBackground: handleSetBackground }}>
|
<AnimatedBackgroundContext value={{ setBackground: handleSetBackground }}>
|
||||||
<div ref={data.ref}
|
<div ref={data.ref}
|
||||||
className={twMerge("w-full h-full flex flex-col", data.scrolling ? "overflow-y-scroll animate-bg-zoom-scroll" : "overflow-hidden", data.className)}
|
style={data.style}
|
||||||
style={data.scrolling ? {
|
className={twMerge("relative w-full h-full flex flex-col", data.scrolling ? "overflow-y-scroll animate-bg-zoom-scroll" : "overflow-hidden", data.className)}
|
||||||
backgroundImage: `url('${finalBackgroundUrl?.href}')`,
|
|
||||||
backgroundAttachment: 'local',
|
|
||||||
backgroundSize: '100%',
|
|
||||||
backgroundPositionY: 'bottom',
|
|
||||||
backgroundPositionX: 'center',
|
|
||||||
backgroundBlendMode: blur ? 'normal' : 'soft-light',
|
|
||||||
backgroundColor: "var(--color-base-100)",
|
|
||||||
} : {}}
|
|
||||||
>
|
>
|
||||||
{!data.scrolling && <div className='absolute top-0 left-0 overflow-hidden w-full h-full'>
|
{!data.scrolling && <div className='absolute top-0 left-0 right-0 bottom-0 overflow-hidden'>
|
||||||
{<img
|
<div className='fixed bg-base-100 top-0 left-0 right-0 bottom-0 -z-5'></div>
|
||||||
|
{blur && finalLastBackgroundUrl && <img className='absolute w-full h-full object-cover object-center -z-4' src={finalLastBackgroundUrl.href}></img>}
|
||||||
|
{finalBackgroundUrl ? <img
|
||||||
key={finalBackgroundUrl?.href}
|
key={finalBackgroundUrl?.href}
|
||||||
className={classNames('absolute w-full h-full object-cover object-center opacity-0 -z-3')}
|
className={'absolute w-full h-full object-cover object-center opacity-0 -z-3'}
|
||||||
src={finalBackgroundUrl?.href}
|
src={finalBackgroundUrl?.href}
|
||||||
onLoad={e => e.currentTarget.classList.add(blur ? "animate-bg-zoom-big" : "animate-bg-zoom")}
|
onLoad={e => e.currentTarget.classList.add(blur ? "animate-bg-zoom-big" : "animate-bg-zoom")}
|
||||||
></img>}
|
></img> : <><div className='mobile:hidden bg-gradient'></div></>}
|
||||||
<div className='absolute w-full h-full bg-linear-to-b from-base-100/60 to-base-300/80 -z-2' />
|
<div className='absolute top-0 left-0 right-0 bottom-0 bg-linear-to-b from-base-100/60 to-base-300/80 -z-2' />
|
||||||
|
<div className='mobile:hidden bg-noise'></div>
|
||||||
</div>}
|
</div>}
|
||||||
{data.animated && animateBackground && <div className="absolute overflow-hidden w-full h-full" style={{ zIndex: -1 }}>
|
{data.animated && animateBackground && <div className="fixed overflow-hidden top-0 left-0 right-0 bottom-0" style={{ zIndex: -1 }}>
|
||||||
{backgroundElements}
|
{backgroundElements}
|
||||||
</div>}
|
</div>}
|
||||||
{data.children}
|
{data.children}
|
||||||
|
{!!data.scrolling && <>
|
||||||
|
<div key={finalBackgroundUrl?.href} ref={backgroundElementRef} className='absolute top-0 bottom-0 left-0 right-0' style={data.scrolling ? {
|
||||||
|
backgroundAttachment: 'local',
|
||||||
|
backgroundSize: '105%',
|
||||||
|
opacity: 0,
|
||||||
|
transition: 'all ease-out',
|
||||||
|
backgroundPositionY: 'bottom',
|
||||||
|
backgroundPositionX: 'center',
|
||||||
|
transitionDuration: "400ms",
|
||||||
|
backgroundBlendMode: blur ? 'normal' : 'soft-light',
|
||||||
|
backgroundColor: "var(--color-base-300)",
|
||||||
|
} : {}}></div>
|
||||||
|
<div className='mobile:hidden bg-noise opacity-30 z-1'></div>
|
||||||
|
</>}
|
||||||
</div>
|
</div>
|
||||||
</AnimatedBackgroundContext >
|
</AnimatedBackgroundContext >
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
import { doesFocusableExist, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
import { doesFocusableExist, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function AutoFocus (data: { focus: () => void; force?: boolean; delay?: number; })
|
export function AutoFocus (data: {
|
||||||
|
parentKey?: string;
|
||||||
|
focus: () => void;
|
||||||
|
force?: boolean;
|
||||||
|
delay?: number;
|
||||||
|
})
|
||||||
{
|
{
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
let delayTimeout: number | undefined;
|
let delayTimeout: number | undefined;
|
||||||
|
|
||||||
if (data.force || !getCurrentFocusKey() || !doesFocusableExist(getCurrentFocusKey()))
|
if (data.force || !getCurrentFocusKey() || getCurrentFocusKey() === data.parentKey || !doesFocusableExist(getCurrentFocusKey()))
|
||||||
{
|
{
|
||||||
if (data.delay)
|
if (data.delay)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,10 @@ export interface GameCardParams
|
||||||
className?: string;
|
className?: string;
|
||||||
onFocus?: GameCardFocusHandler;
|
onFocus?: GameCardFocusHandler;
|
||||||
onBlur?: (id: string) => void;
|
onBlur?: (id: string) => void;
|
||||||
onAction?: () => void;
|
|
||||||
clickFocuses?: boolean;
|
clickFocuses?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GameCard (data: GameCardParams)
|
export default function CardElement (data: GameCardParams & InteractParams)
|
||||||
{
|
{
|
||||||
const { ref, focused, focusSelf } = useFocusable({
|
const { ref, focused, focusSelf } = useFocusable({
|
||||||
focusKey: data.focusKey,
|
focusKey: data.focusKey,
|
||||||
|
|
@ -57,40 +56,35 @@ export default function GameCard (data: GameCardParams)
|
||||||
scrollSnapAlign: "center"
|
scrollSnapAlign: "center"
|
||||||
}}
|
}}
|
||||||
onFocus={focusSelf}
|
onFocus={focusSelf}
|
||||||
onDoubleClick={data.onAction}
|
onDoubleClick={e => data.onAction?.(e.nativeEvent)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
{
|
{
|
||||||
focusSelf();
|
focusSelf();
|
||||||
data.onAction?.();
|
data.onAction?.();
|
||||||
}}
|
}}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`game-card bg-base-300 game-card-height flex flex-col justify-end z-5 ring-primary`,
|
"relative game-card bg-base-300 flex flex-col z-5 overflow-hidden transition-all duration-200 not-mobile:drop-shadow-lg cursor-pointer focusable focusable-primary focusable-hover select-none focused focused:not-control-mouse:animate-wiggle focused:not-control-mouse:bg-base-content focused:not-control-mouse:text-base-300 focused:not-control-mouse:drop-shadow-xl focused:not-control-mouse:drop-shadow-black/30 focused:not-control-mouse:scale-102 focused:not-control-mouse:z-10 group control-mouse:hover:bg-base-200 h-full [--tw-border-style:inset] border-2 border-base-content/5 backdrop-opacity-0 active:bg-base-content! active:text-base-100 active:transition-none",
|
||||||
'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",
|
|
||||||
classNames({
|
|
||||||
"focused animate-wiggle ring-7 bg-base-content text-base-300 drop-shadow-xl drop-shadow-black/30 scale-102 z-10": focused && !isPointer,
|
|
||||||
"group hover:focused hover:animate-wiggle sm:hover:ring-4 md:hover:ring-7 hover:bg-base-content hover:text-base-300 hover:drop-shadow-xl hover:drop-shadow-black/30 hover:scale-102 hover:z-10": isMouse,
|
|
||||||
"h-(--game-card-height)": typeof data.preview === "string"
|
|
||||||
}),
|
|
||||||
data.className
|
data.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={twMerge(
|
<div id="preview" className={twMerge(
|
||||||
"overflow-hidden bg-base-400 h-full rounded-t-xl rounded-b-md transition-all",
|
"overflow-hidden bg-base-400 rounded-t-xl rounded-b-md transition-all",
|
||||||
focused ? "sm:mt-1 sm:mx-1" : "sm:mt-1 sm:mx-1",
|
focused ? "sm:mt-1 sm:mx-1" : "sm:mt-1 sm:mx-1",
|
||||||
focused ? "md:mt-2 md:mx-2" : "md:mt-2 md:mx-2",
|
focused ? "md:mt-2 md:mx-2" : "md:mt-2 md:mx-2",
|
||||||
|
classNames({ "h-full": typeof data.preview === "string" })
|
||||||
)}>
|
)}>
|
||||||
{typeof data.preview === "string" ? (
|
{typeof data.preview === "string" ? (
|
||||||
<img className={classNames("object-cover w-full h-full", { "animate-rotate-small": focused && !isPointer })} src={data.preview} ></img>
|
<img draggable={false} className={classNames("object-cover w-full h-full", { "animate-rotate-small": focused && !isPointer })} src={data.preview} ></img>
|
||||||
) : (
|
) : (
|
||||||
typeof data.preview === 'function' ? data.preview({ focused }) : data.preview
|
typeof data.preview === 'function' ? data.preview({ focused }) : data.preview
|
||||||
)}</div>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="h-0 flex pr-2 justify-end items-center sm:gap-1 md:gap-2">
|
<div className="h-0 flex pr-2 justify-end items-center sm:gap-1 md:gap-2 z-2">
|
||||||
{data.badges?.map((b, i) =>
|
{data.badges?.map((b, i) =>
|
||||||
<div key={i}
|
<div key={i}
|
||||||
className={
|
className={
|
||||||
twMerge("bg-base-100 text-base-content drop-shadow-lg overflow-hidden rounded-full p-1 sm:last:mr-1 md:last:mr-4 transition-colors",
|
twMerge("bg-base-100 text-base-content not-mobile:not-in-focused:drop-shadow-lg sm:border-3 md:border-6 border-base-300 in-focused:border-base-content overflow-hidden rounded-full sm:last:mr-1 md:last:mr-4 transition-colors",
|
||||||
classNames({
|
classNames({
|
||||||
"bg-primary text-primary-content": focused && !isPointer,
|
"bg-primary text-primary-content": focused && !isPointer,
|
||||||
"group-hover:bg-primary group-hover:text-primary-content": isPointer
|
"group-hover:bg-primary group-hover:text-primary-content": isPointer
|
||||||
|
|
@ -100,7 +94,7 @@ export default function GameCard (data: GameCardParams)
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:p-2 md:p-4">
|
<div className="flex flex-col sm:p-2 grow md:p-4 justify-center">
|
||||||
<div className="md:text-xl sm:text-sm font-bold text-nowrap text-ellipsis overflow-hidden">
|
<div className="md:text-xl sm:text-sm font-bold text-nowrap text-ellipsis overflow-hidden">
|
||||||
{data.title}
|
{data.title}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
FocusContext,
|
FocusContext,
|
||||||
FocusDetails,
|
|
||||||
useFocusable,
|
useFocusable,
|
||||||
} from "@noriginmedia/norigin-spatial-navigation";
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { GameMeta } from "../../shared/constants";
|
import { GameMeta } from "../../shared/constants";
|
||||||
import GameCard, { GameCardFocusHandler, GameCardParams } from "./GameCard";
|
import CardElement, { GameCardFocusHandler, GameCardParams } from "./CardElement";
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
|
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
|
||||||
|
|
@ -47,7 +46,7 @@ export function CardList (data: {
|
||||||
useShortcuts(g.focusKey, () => [{ label: "Select", button: GamePadButtonCode.A, action: handleAction }]);
|
useShortcuts(g.focusKey, () => [{ label: "Select", button: GamePadButtonCode.A, action: handleAction }]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GameCard
|
<CardElement
|
||||||
key={g.id}
|
key={g.id}
|
||||||
type={data.type}
|
type={data.type}
|
||||||
index={i}
|
index={i}
|
||||||
|
|
@ -74,9 +73,9 @@ export function CardList (data: {
|
||||||
id={`card-list-${data.id}`}
|
id={`card-list-${data.id}`}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
save-child-focus="session"
|
save-child-focus="session"
|
||||||
className={twMerge("items-center justify-center-safe landscape:h-(--game-card-height) ",
|
className={twMerge("items-center justify-center-safe h-full",
|
||||||
data.grid ? "grid h-fit sm:gap-2 md:gap-5 auto-rows-(--game-card-height) grid-cols-[repeat(auto-fill,var(--game-card-width))]" :
|
data.grid ? "grid h-fit sm:gap-2 md:gap-5 auto-rows-min grid-cols-[repeat(auto-fill,var(--game-card-width))]" :
|
||||||
'landscape:flex sm:gap-2 md:gap-6 portrait:grid portrait:auto-rows-(--game-card-height) portrait:grid-cols-[repeat(auto-fill,var(--game-card-width))]',
|
'landscape:grid landscape:grid-flow-col landscape:auto-cols-min auto-rows-[1fr] sm:gap-2 md:gap-4 portrait:grid portrait:auto-rows-min portrait:grid-cols-[repeat(auto-fill,var(--game-card-width))] *:portrait:aspect-8/10 *:landscape:aspect-8/12 sm:landscape:max-h-84 md:max-h-128!',
|
||||||
data.className
|
data.className
|
||||||
)}
|
)}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function Clock() {
|
export default function Clock ()
|
||||||
|
{
|
||||||
const locale = "en";
|
const locale = "en";
|
||||||
const [today, setDate] = useState(new Date());
|
const [today, setDate] = useState(new Date());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
const timer = setInterval(() => {
|
{
|
||||||
|
const timer = setInterval(() =>
|
||||||
|
{
|
||||||
setDate(new Date());
|
setDate(new Date());
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
|
|
||||||
return () => {
|
return () =>
|
||||||
|
{
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,15 @@ import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { CardList, GameMetaExtra } from "./CardList";
|
import { CardList, GameMetaExtra } from "./CardList";
|
||||||
import { SaveSource } from "../scripts/spatialNavigation";
|
import { SaveSource } from "../scripts/spatialNavigation";
|
||||||
import { GameCardFocusHandler } from "./GameCard";
|
import { GameCardFocusHandler } from "./CardElement";
|
||||||
|
import { getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
|
||||||
export default function CollectionList (data: {
|
export default function CollectionList (data: {
|
||||||
id: string,
|
id: string,
|
||||||
setBackground: (url: string) => void;
|
setBackground: (url: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
onFocus?: GameCardFocusHandler;
|
onFocus?: GameCardFocusHandler;
|
||||||
|
onSelect?: (id: string) => void;
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -20,6 +22,12 @@ export default function CollectionList (data: {
|
||||||
staleTime: DefaultRommStaleTime
|
staleTime: DefaultRommStaleTime
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleDefaultSelect = (id: string) =>
|
||||||
|
{
|
||||||
|
SaveSource('game-list', { search: { focus: getCurrentFocusKey() } });
|
||||||
|
navigate({ to: `/collection/${id}`, viewTransition: { types: ['zoom-in'] } });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardList
|
<CardList
|
||||||
type="collection"
|
type="collection"
|
||||||
|
|
@ -38,11 +46,7 @@ export default function CollectionList (data: {
|
||||||
</span>
|
</span>
|
||||||
],
|
],
|
||||||
} satisfies GameMetaExtra))}
|
} satisfies GameMetaExtra))}
|
||||||
onSelectGame={(id) =>
|
onSelectGame={data.onSelect ? data.onSelect : handleDefaultSelect}
|
||||||
{
|
|
||||||
SaveSource('game-list');
|
|
||||||
navigate({ to: `/collection/${id}`, viewTransition: { types: ['zoom-in'] } });
|
|
||||||
}}
|
|
||||||
onGameFocus={(id, node, details) =>
|
onGameFocus={(id, node, details) =>
|
||||||
{
|
{
|
||||||
data.setBackground(
|
data.setBackground(
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { AnimatedBackground } from './AnimatedBackground';
|
import { AnimatedBackground } from './AnimatedBackground';
|
||||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { HeaderUI } from './Header';
|
import { HeaderUI } from './Header';
|
||||||
import { GameList, GameListFilter } from './GameList';
|
import { GameList } from './GameList';
|
||||||
import { Search, Settings2 } from 'lucide-react';
|
import { Search, Settings2 } from 'lucide-react';
|
||||||
import { JSX, Suspense } from 'react';
|
import { JSX, Suspense } from 'react';
|
||||||
import Shortcuts from './Shortcuts';
|
import Shortcuts from './Shortcuts';
|
||||||
import { AutoFocus } from './AutoFocus';
|
import { AutoFocus } from './AutoFocus';
|
||||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||||
import { Router } from '..';
|
import { Router } from '..';
|
||||||
import { PopSource } from '../scripts/spatialNavigation';
|
import { PopNavigateSource, PopSource } from '../scripts/spatialNavigation';
|
||||||
import { GameListFilterType } from '@/shared/constants';
|
import { GameListFilterType } from '@/shared/constants';
|
||||||
import { GameCardFocusHandler } from './GameCard';
|
import { GameCardFocusHandler } from './CardElement';
|
||||||
|
|
||||||
export interface CollectionsDetailParams
|
export interface CollectionsDetailParams
|
||||||
{
|
{
|
||||||
|
|
@ -22,16 +22,6 @@ export interface CollectionsDetailParams
|
||||||
footer?: JSX.Element;
|
footer?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
function HandleGoBack ()
|
|
||||||
{
|
|
||||||
const source = PopSource('game-list');
|
|
||||||
if (source)
|
|
||||||
{
|
|
||||||
console.log("Found source ", source, " to go back to");
|
|
||||||
}
|
|
||||||
Router.navigate({ to: source ?? "/", viewTransition: { types: ['zoom-out'] } });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CollectionsDetail (data: CollectionsDetailParams)
|
export function CollectionsDetail (data: CollectionsDetailParams)
|
||||||
{
|
{
|
||||||
const focusKey = `game-list-${data.id}-${data.filters ? Object.values(data.filters).map(f => String(f)).join(",") : ''}`;
|
const focusKey = `game-list-${data.id}-${data.filters ? Object.values(data.filters).map(f => String(f)).join(",") : ''}`;
|
||||||
|
|
@ -40,7 +30,7 @@ export function CollectionsDetail (data: CollectionsDetailParams)
|
||||||
preferredChildFocusKey: `${focusKey}-list`,
|
preferredChildFocusKey: `${focusKey}-list`,
|
||||||
});
|
});
|
||||||
|
|
||||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: () => PopNavigateSource('game-list', '/') }]);
|
||||||
const { shortcuts } = useShortcutContext();
|
const { shortcuts } = useShortcutContext();
|
||||||
|
|
||||||
const handleScroll: GameCardFocusHandler = (id, node, details) =>
|
const handleScroll: GameCardFocusHandler = (id, node, details) =>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
import { FocusContext, FocusDetails, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { FocusContext, FocusDetails, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { createContext, JSX, useContext, useEffect } from "react";
|
import { JSX, useContext, useEffect } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
|
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
|
||||||
|
import { ContextDialogContext } from "../scripts/contexts";
|
||||||
const ContextDialogContext = createContext({} as {
|
|
||||||
close: () => void,
|
|
||||||
id: string;
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ContextList (data: { options?: DialogEntry[]; className?: string; showCloseButton?: boolean; })
|
export function ContextList (data: { options?: DialogEntry[]; className?: string; showCloseButton?: boolean; })
|
||||||
{
|
{
|
||||||
|
|
@ -35,12 +31,12 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class
|
||||||
trackChildren: typeof data.content !== 'string'
|
trackChildren: typeof data.content !== 'string'
|
||||||
});
|
});
|
||||||
const colors = {
|
const colors = {
|
||||||
primary: classNames("hover:bg-primary/40", { "bg-primary text-primary-content": focused || hasFocusedChild }),
|
primary: "active:bg-primary control-pointer:hover:bg-primary focused:bg-primary focused:text-primary-content in-focused:bg-primary in-focused:text-primary-content",
|
||||||
secondary: classNames("hover:bg-secondary/40", { "bg-secondary text-secondary-content": focused || hasFocusedChild }),
|
secondary: "active:bg-secondary control-pointer:hover:bg-secondary focused:bg-secondary focused:text-secondary-content in-focused:bg-secondary in-focused:text-secondary-content",
|
||||||
accent: classNames("hover:bg-accent/40", { "bg-accent text-accent-content": focused || hasFocusedChild }),
|
accent: "active:bg-accent control-pointer:hover:bg-accent focused:bg-accent focused:text-accent-content in-focused:bg-accent in-focused:text-accent-content",
|
||||||
info: classNames("hover:bg-info/40", { "bg-info text-info-content": focused || hasFocusedChild }),
|
info: "active:bg-info control-pointer:hover:bg-info focused:bg-info focused:text-info-content in-focused:bg-info in-focused:text-info-content",
|
||||||
warning: classNames("hover:bg-warning/40", { "bg-warning text-warning-content": focused || hasFocusedChild }),
|
warning: "active:bg-warning control-pointer:hover:bg-warning focused:bg-warning focused:text-warning-content in-focused:bg-warning in-focused:text-warning-content",
|
||||||
error: classNames("hover:bg-error/40", { "bg-error text-error-content": focused || hasFocusedChild })
|
error: "active:bg-error control-pointer:hover:bg-error focused:bg-error focused:text-error-content in-focused:bg-error in-focused:text-error-content"
|
||||||
};
|
};
|
||||||
if (data.shortcuts)
|
if (data.shortcuts)
|
||||||
{
|
{
|
||||||
|
|
@ -51,8 +47,7 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class
|
||||||
className={
|
className={
|
||||||
twMerge("flex cursor-pointer sm:text-sm md:text-base")}>
|
twMerge("flex cursor-pointer sm:text-sm md:text-base")}>
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
<div className={twMerge("flex w-full sm:h-12 md:h-14 items-center px-4 rounded-2xl transition-all gap-2",
|
<div className={twMerge("flex w-full sm:h-12 md:h-14 items-center px-4 rounded-2xl transition-all gap-2 active:animate-scale in-focused:font-semibold",
|
||||||
classNames({ "font-semibold": focused || hasFocusedChild }),
|
|
||||||
data.className,
|
data.className,
|
||||||
colors[data.type])}>
|
colors[data.type])}>
|
||||||
{data.icon}
|
{data.icon}
|
||||||
|
|
@ -105,7 +100,7 @@ export function ContextDialog (data: {
|
||||||
}] : [], [data.open]);
|
}] : [], [data.open]);
|
||||||
|
|
||||||
return <dialog ref={ref} open={data.open} closedby="any" className={
|
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",
|
twMerge("fixed 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 }))
|
classNames({ "opacity-0": !data.open }))
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|
|
||||||
33
src/mainview/components/Error.tsx
Normal file
33
src/mainview/components/Error.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
import { Home, TriangleAlert } from "lucide-react";
|
||||||
|
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
|
||||||
|
import { Router } from "..";
|
||||||
|
import Shortcuts from "./Shortcuts";
|
||||||
|
import { Button } from "./options/Button";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { ErrorComponentProps } from "@tanstack/react-router";
|
||||||
|
import { mobileCheck } from "../scripts/utils";
|
||||||
|
|
||||||
|
export default function Error (data: ErrorComponentProps)
|
||||||
|
{
|
||||||
|
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "not-found" });
|
||||||
|
const handleReturn = () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } });
|
||||||
|
useShortcuts(focusKey, () => [{ label: "Return Home", button: GamePadButtonCode.B, action: handleReturn }]);
|
||||||
|
const { shortcuts } = useShortcutContext();
|
||||||
|
|
||||||
|
useEffect(() => { focusSelf(); }, []);
|
||||||
|
|
||||||
|
return <div ref={ref} className="absolute flex flex-col justify-center items-center w-full h-full gap-4">
|
||||||
|
<FocusContext value={focusKey}>
|
||||||
|
<p className="flex gap-2 items-center text-4xl text-error text-shadow-lg">
|
||||||
|
<TriangleAlert className="size-12" />
|
||||||
|
{data.error.message}
|
||||||
|
</p>
|
||||||
|
<p className="flex gap-2 text-lg text-base-content/50 text-shadow-lg">{window.location.href} </p>
|
||||||
|
<Button className="text-2xl! p-6! focusable focusable-primary" id="return" onAction={handleReturn}><Home />Return Home</Button>
|
||||||
|
<div className="mobile:hidden bg-gradient"></div>
|
||||||
|
<div className="mobile:hidden bg-noise"></div>
|
||||||
|
<div className="flex justify-end fixed bottom-4 left-4 right-4"><Shortcuts shortcuts={shortcuts} /></div>
|
||||||
|
</FocusContext>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { ContextList, DialogEntry, OptionElement } from "./ContextDialog";
|
import { ContextList, DialogEntry } from "./ContextDialog";
|
||||||
import { systemApi } from "../scripts/clientApi";
|
import { systemApi } from "../scripts/clientApi";
|
||||||
import { createContext, useContext, useRef, useState } from "react";
|
import { useContext, useRef, useState } from "react";
|
||||||
import path from "pathe";
|
import path from "pathe";
|
||||||
import { Check, File, Folder, FolderClosed, FolderInput, FolderOutput, FolderPlus, HardDrive, Plus, Save, Undo, Usb, X } from "lucide-react";
|
import { Check, Folder, FolderInput, FolderOutput, FolderPlus, HardDrive, Usb, X } from "lucide-react";
|
||||||
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { DirType, Drive } from "@/shared/constants";
|
import { DirType } from "@/shared/constants";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
|
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
|
||||||
|
|
@ -13,17 +13,8 @@ import SvgIcon from "./SvgIcon";
|
||||||
import { Button } from "./options/Button";
|
import { Button } from "./options/Button";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { drivesQuery, filesQuery } from "../scripts/queries";
|
import { drivesQuery, filesQuery } from "../scripts/queries";
|
||||||
|
import { FilePickerContext } from "../scripts/contexts";
|
||||||
const FilePickerContext = createContext<{
|
import useActiveControl from "../scripts/gamepads";
|
||||||
allowNewFolderCreation: boolean;
|
|
||||||
isDirectoryPicker: boolean;
|
|
||||||
setCurrentPath: (path: string) => void;
|
|
||||||
currentPath: string | undefined,
|
|
||||||
startingPath: string | undefined;
|
|
||||||
refetchFiles: () => void;
|
|
||||||
drives: Drive[],
|
|
||||||
activeDrive: Drive | undefined;
|
|
||||||
}>({} as any);
|
|
||||||
|
|
||||||
function List (data: {
|
function List (data: {
|
||||||
id: string,
|
id: string,
|
||||||
|
|
@ -137,7 +128,7 @@ function NewFolderOption (data: { id: string, dirname: string; })
|
||||||
});
|
});
|
||||||
return <div className="flex gap-2 grow -ml-2">
|
return <div className="flex gap-2 grow -ml-2">
|
||||||
<NewFolderInput className="grow" id={`${data.id}-input`} setName={setName} name={name} />
|
<NewFolderInput className="grow" id={`${data.id}-input`} setName={setName} name={name} />
|
||||||
<Button id={`${data.id}-create`} onAction={createMutation.mutate} type="button" ><FolderPlus /></Button>
|
<Button id={`${data.id}-create`} onAction={e => createMutation.mutate()} type="button" ><FolderPlus /></Button>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,7 +140,7 @@ function OptionButtons (data: {
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
const { ref, focusKey } = useFocusable({ focusKey: `options-${data.id}`, onEnterPress: data.onSelect });
|
const { ref, focusKey } = useFocusable({ focusKey: `options-${data.id}`, onEnterPress: data.onSelect });
|
||||||
return <div ref={ref} className="flex md:inline h-12 w-full justify-end gap-2">
|
return <div ref={ref} className="flex h-12 w-full justify-end gap-2">
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
{data.showConfirm && <Button className="p-6 ring-accent-content" onAction={data.onSelect} id={`${data.id}-select`} focusClassName="ring-7" type="button" ><Check />Select</Button>}
|
{data.showConfirm && <Button className="p-6 ring-accent-content" onAction={data.onSelect} id={`${data.id}-select`} focusClassName="ring-7" type="button" ><Check />Select</Button>}
|
||||||
<Button className="md:p-6 ring-warning-content" onAction={data.onCancel} id={`${data.id}-cancel`} type="button" focusClassName="ring-7 btn-warning" ><X />Cancel</Button>
|
<Button className="md:p-6 ring-warning-content" onAction={data.onCancel} id={`${data.id}-cancel`} type="button" focusClassName="ring-7 btn-warning" ><X />Cancel</Button>
|
||||||
|
|
@ -252,6 +243,8 @@ export default function FilePicker (data: {
|
||||||
[<><HardDrive />{activeDrive?.label}</>, ...fullPath.substring(activeDriveMount?.length ?? 0).split(path.sep)] :
|
[<><HardDrive />{activeDrive?.label}</>, ...fullPath.substring(activeDriveMount?.length ?? 0).split(path.sep)] :
|
||||||
fullPath.substring(activeDriveMount?.length ?? 0).split(path.sep);
|
fullPath.substring(activeDriveMount?.length ?? 0).split(path.sep);
|
||||||
|
|
||||||
|
const { isPointer } = useActiveControl();
|
||||||
|
|
||||||
return <div className="flex flex-col h-full max-h-full gap-3">
|
return <div className="flex flex-col h-full max-h-full gap-3">
|
||||||
<FilePickerContext value={{
|
<FilePickerContext value={{
|
||||||
setCurrentPath,
|
setCurrentPath,
|
||||||
|
|
@ -271,8 +264,9 @@ export default function FilePicker (data: {
|
||||||
setCurrentPath(path.join(...fullPath.slice(-i)))
|
setCurrentPath(path.join(...fullPath.slice(-i)))
|
||||||
}>{p}</a>
|
}>{p}</a>
|
||||||
</li>)}
|
</li>)}
|
||||||
|
|
||||||
|
{(filesLoading || drivesLoading) && <li className="mr-2 loading loading-spinner sm:loading-md md:loading-sm"></li>}
|
||||||
</ul>
|
</ul>
|
||||||
{(filesLoading || drivesLoading) && <span className="loading loading-spinner sm:loading-md md:loading-lg"></span>}
|
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
<ListWithDrives
|
<ListWithDrives
|
||||||
|
|
@ -281,11 +275,11 @@ export default function FilePicker (data: {
|
||||||
onSelect={data.onSelect}
|
onSelect={data.onSelect}
|
||||||
parentPath={files?.parentPath ?? ''}
|
parentPath={files?.parentPath ?? ''}
|
||||||
/>
|
/>
|
||||||
<OptionButtons
|
{isPointer && <OptionButtons
|
||||||
showConfirm={!!data.isDirectoryPicker}
|
showConfirm={!!data.isDirectoryPicker}
|
||||||
onCancel={data.cancel}
|
onCancel={data.cancel}
|
||||||
onSelect={() => currentPath ? data.onSelect(currentPath) : undefined}
|
onSelect={() => currentPath ? data.onSelect(currentPath) : undefined}
|
||||||
id={data.id} />
|
id={data.id} />}
|
||||||
</FilePickerContext>
|
</FilePickerContext>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -4,10 +4,7 @@ import
|
||||||
useFocusable,
|
useFocusable,
|
||||||
} from "@noriginmedia/norigin-spatial-navigation";
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import SvgIcon from "./SvgIcon";
|
import SvgIcon from "./SvgIcon";
|
||||||
import classNames from "classnames";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { useSearch } from "@tanstack/react-router";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import useActiveControl from "../scripts/gamepads";
|
|
||||||
|
|
||||||
function FilterCat (
|
function FilterCat (
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -25,31 +22,12 @@ function FilterCat (
|
||||||
onEnterPress: data.onAction
|
onEnterPress: data.onAction
|
||||||
});
|
});
|
||||||
|
|
||||||
const { filter } = useSearch({ from: '/' });
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
if (filter == data.id && data.hasFocusedPeer)
|
|
||||||
{
|
|
||||||
focusSelf();
|
|
||||||
}
|
|
||||||
}, [filter]);
|
|
||||||
|
|
||||||
const { isMouse } = useActiveControl();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
aria-selected={data.active}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={focusSelf}
|
onClick={focusSelf}
|
||||||
className={classNames(
|
className={"sm:text-sm sm:px-2 flex md:px-4 items-center justify-center rounded-full transition-all md:text-lg focusable focusable-primary hover:not-focused:not-aria-selected:bg-base-content/40 not-focused:cursor-pointer aria-selected:bg-base-content aria-selected:text-base-300 aria-selected:drop-shadow aria-selected:cursor-default active:bg-accent! active:text-accent-content! active:ring-offset-7 active:ring-offset-base-content select-none"}
|
||||||
"sm:text-sm sm:px-2",
|
|
||||||
"flex md:px-4 items-center justify-center rounded-full transition-all md:text-lg",
|
|
||||||
{
|
|
||||||
"bg-base-content px-3 text-base-300 drop-shadow cursor-default":
|
|
||||||
focused || data.active,
|
|
||||||
"ring-primary ring-7": focused && !isMouse,
|
|
||||||
"hover:bg-base-content/40 cursor-pointer": !focused,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{data.children ?? data.label}
|
{data.children ?? data.label}
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -59,35 +37,37 @@ function FilterCat (
|
||||||
export function FilterUI (data: {
|
export function FilterUI (data: {
|
||||||
id: string;
|
id: string;
|
||||||
options: Record<string, FilterOption>;
|
options: Record<string, FilterOption>;
|
||||||
selected: string;
|
|
||||||
setSelected: (id: string) => void;
|
setSelected: (id: string) => void;
|
||||||
|
containerClassName?: string;
|
||||||
|
className?: string;
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
|
const defaultFocus = Object.entries(data.options).filter(o => o[1].selected)[0]?.[0];
|
||||||
const { ref, focusKey, hasFocusedChild } = useFocusable({
|
const { ref, focusKey, hasFocusedChild } = useFocusable({
|
||||||
focusKey: `filter-${data.id}`,
|
focusKey: data.id,
|
||||||
saveLastFocusedChild: false,
|
saveLastFocusedChild: false,
|
||||||
autoRestoreFocus: false,
|
autoRestoreFocus: false,
|
||||||
preferredChildFocusKey: data.selected,
|
preferredChildFocusKey: `${data.id}-${defaultFocus}`,
|
||||||
trackChildren: true
|
trackChildren: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
save-child-focus="session"
|
className={data.containerClassName}
|
||||||
>
|
>
|
||||||
<FocusContext.Provider value={focusKey}>
|
<FocusContext.Provider value={focusKey}>
|
||||||
<ul className="flex flex-row bg-base-100 rounded-full p-1 drop-shadow-sm sm:h-9 md:h-14">
|
<ul className={twMerge("flex flex-row bg-base-100 rounded-full p-1 drop-shadow-sm sm:portrait:h-12 sm:landscape:h-9 md:h-14!", data.className)}>
|
||||||
<li className=" flex px-4 items-center justify-center rounded-full">
|
<li className=" flex px-4 items-center justify-center rounded-full">
|
||||||
<SvgIcon className="sm:size-5 md:size-8" icon="steamdeck_button_l1_outline" />
|
<SvgIcon className="sm:size-5 md:size-8" icon="steamdeck_button_l1_outline" />
|
||||||
</li>
|
</li>
|
||||||
{Object.entries(data.options)?.map(([id, option]) => (
|
{Object.entries(data.options)?.map(([id, option]) => (
|
||||||
<FilterCat
|
<FilterCat
|
||||||
hasFocusedPeer={hasFocusedChild}
|
hasFocusedPeer={hasFocusedChild}
|
||||||
id={id}
|
id={`${data.id}-${id}`}
|
||||||
key={id}
|
key={id}
|
||||||
onFocus={() => data.setSelected(id)}
|
onFocus={() => data.setSelected(id)}
|
||||||
active={id === data.selected}
|
active={option.selected}
|
||||||
{...option}
|
{...option}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
21
src/mainview/components/FocusDots.tsx
Normal file
21
src/mainview/components/FocusDots.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { setFocus } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { useGlobalFocus } from "../scripts/spatialNavigation";
|
||||||
|
|
||||||
|
export default function FocusDots (data: {
|
||||||
|
elements: string[];
|
||||||
|
|
||||||
|
})
|
||||||
|
{
|
||||||
|
const focusedKey = useGlobalFocus();
|
||||||
|
|
||||||
|
return <div className="divider opacity-20"><div className="flex gap-2 py-6 justify-center items-center h-3">{data.elements.map((em, i) =>
|
||||||
|
{
|
||||||
|
const focused = em === focusedKey;
|
||||||
|
return <button key={i} onClick={(e) => setFocus(em, { nativeEvent: e.nativeEvent })}
|
||||||
|
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></div>;
|
||||||
|
}
|
||||||
46
src/mainview/components/FrontEndGameCard.tsx
Normal file
46
src/mainview/components/FrontEndGameCard.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { FrontEndGameType, FrontEndId, RPC_URL } from "@/shared/constants";
|
||||||
|
import CardElement from "./CardElement";
|
||||||
|
import { SaveSource } from "../scripts/spatialNavigation";
|
||||||
|
import { Router } from "..";
|
||||||
|
import { HardDrive } from "lucide-react";
|
||||||
|
import { JSX } from "react";
|
||||||
|
import { FOCUS_KEYS } from "../scripts/types";
|
||||||
|
|
||||||
|
export default function FrontEndGameCard (data: { index: number, game: FrontEndGameType; } & FocusParams & InteractParams)
|
||||||
|
{
|
||||||
|
function handleDefaultSelect (id: FrontEndId, source: string | null, sourceId: string | null)
|
||||||
|
{
|
||||||
|
SaveSource('details', { search: { focus: FOCUS_KEYS.GAME_CARD(data.game.id.id) } });
|
||||||
|
console.log({ id: String(sourceId ?? id.id), source: source ?? id.source });
|
||||||
|
Router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source }, viewTransition: { types: ['zoom-in'] } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const platformUrl = new URL(`${RPC_URL(__HOST__)}${data.game.path_platform_cover}`);
|
||||||
|
platformUrl.searchParams.set('width', "64");
|
||||||
|
const subtitle = <div className="flex gap-1 items-center">
|
||||||
|
{!!data.game.path_platform_cover && <img className="sm:hidden md:inline size-4" src={platformUrl.href} />}
|
||||||
|
<p className="opacity-80">{data.game.platform_display_name}</p>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
const previewUrl = new URL(`${RPC_URL(__HOST__)}${data.game.path_cover}`);
|
||||||
|
previewUrl.searchParams.delete('ts');
|
||||||
|
previewUrl.searchParams.set('width', "640");
|
||||||
|
|
||||||
|
const badges: JSX.Element[] = [];
|
||||||
|
if (data.game.id.source === 'local')
|
||||||
|
{
|
||||||
|
badges.push(<HardDrive className="sm:size-4 md:size-8 m-1" />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <CardElement
|
||||||
|
badges={badges}
|
||||||
|
onFocus={data.onFocus}
|
||||||
|
onAction={(e) => data.onAction ? data.onAction(e) : handleDefaultSelect(data.game.id, data.game.source, data.game.source_id)}
|
||||||
|
preview={previewUrl.href}
|
||||||
|
title={data.game.name ?? ""}
|
||||||
|
subtitle={subtitle}
|
||||||
|
focusKey={FOCUS_KEYS.GAME_CARD(data.game.id.id)}
|
||||||
|
index={data.index}
|
||||||
|
id={`game-${data.game.id.source}-${data.game.id.id}`}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,7 @@ import { SaveSource } from "../scripts/spatialNavigation";
|
||||||
import { rommApi } from "../scripts/clientApi";
|
import { rommApi } from "../scripts/clientApi";
|
||||||
import { HardDrive } from "lucide-react";
|
import { HardDrive } from "lucide-react";
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
import { GameCardFocusHandler } from "./GameCard";
|
import { GameCardFocusHandler } from "./CardElement";
|
||||||
import { gameQuery } from "../scripts/queries";
|
|
||||||
import { useLocalSetting } from "../scripts/utils";
|
import { useLocalSetting } from "../scripts/utils";
|
||||||
|
|
||||||
export interface GameListParams
|
export interface GameListParams
|
||||||
|
|
@ -16,7 +15,7 @@ export interface GameListParams
|
||||||
filters?: GameListFilterType,
|
filters?: GameListFilterType,
|
||||||
grid?: boolean,
|
grid?: boolean,
|
||||||
setBackground?: (url: string) => void;
|
setBackground?: (url: string) => void;
|
||||||
onGameSelect?: (id: FrontEndId) => void;
|
onGameSelect?: (id: FrontEndId, source: string | null, sourceId: string | null) => void;
|
||||||
onFocus?: GameCardFocusHandler;
|
onFocus?: GameCardFocusHandler;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +32,7 @@ export function GameList (data: GameListParams)
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const blur = useLocalSetting('backgroundBlur');
|
const blur = useLocalSetting('backgroundBlur');
|
||||||
|
|
||||||
const handleFocus = (id: FrontEndId, source: string | null, sourceId: number | null) =>
|
const handleFocus = (id: FrontEndId, source: string | null, sourceId: string | null) =>
|
||||||
{
|
{
|
||||||
const game = games.data?.games.find((g) => g.id === id);
|
const game = games.data?.games.find((g) => g.id === id);
|
||||||
if (game)
|
if (game)
|
||||||
|
|
@ -52,7 +51,7 @@ export function GameList (data: GameListParams)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleDefaultSelect (id: FrontEndId, source: string | null, sourceId: number | null)
|
function handleDefaultSelect (id: FrontEndId, source: string | null, sourceId: string | null)
|
||||||
{
|
{
|
||||||
SaveSource('details');
|
SaveSource('details');
|
||||||
navigator({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source }, viewTransition: { types: ['zoom-in'] } });
|
navigator({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source }, viewTransition: { types: ['zoom-in'] } });
|
||||||
|
|
@ -73,11 +72,11 @@ export function GameList (data: GameListParams)
|
||||||
const badges: JSX.Element[] = [];
|
const badges: JSX.Element[] = [];
|
||||||
if (g.id.source === 'local')
|
if (g.id.source === 'local')
|
||||||
{
|
{
|
||||||
badges.push(<HardDrive className="sm:size-4 md:size-8 m-1" />);
|
badges.push(<HardDrive className="sm:size-4 md:size-8 md:p-1 m-1" />);
|
||||||
}
|
}
|
||||||
const previewUrl = new URL(`${RPC_URL(__HOST__)}${g.path_cover}`);
|
const previewUrl = new URL(`${RPC_URL(__HOST__)}${g.path_cover}`);
|
||||||
previewUrl.searchParams.delete('ts');
|
previewUrl.searchParams.delete('ts');
|
||||||
previewUrl.searchParams.set('width', "640");
|
previewUrl.searchParams.set('width', "16");
|
||||||
const platformUrl = new URL(`${RPC_URL(__HOST__)}${g.path_platform_cover}`);
|
const platformUrl = new URL(`${RPC_URL(__HOST__)}${g.path_platform_cover}`);
|
||||||
platformUrl.searchParams.set('width', "64");
|
platformUrl.searchParams.set('width', "64");
|
||||||
|
|
||||||
|
|
@ -93,7 +92,7 @@ export function GameList (data: GameListParams)
|
||||||
),
|
),
|
||||||
previewUrl: previewUrl.href,
|
previewUrl: previewUrl.href,
|
||||||
badges: badges,
|
badges: badges,
|
||||||
onSelect: () => data.onGameSelect ? data.onGameSelect(g.id) : handleDefaultSelect(g.id, g.source, g.source_id),
|
onSelect: () => data.onGameSelect ? data.onGameSelect(g.id, g.source, g.source_id) : handleDefaultSelect(g.id, g.source, g.source_id),
|
||||||
onFocus: () => handleFocus(g.id, g.source, g.source_id)
|
onFocus: () => handleFocus(g.id, g.source, g.source_id)
|
||||||
} satisfies GameMetaExtra;
|
} satisfies GameMetaExtra;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ import
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { RoundButton } from "./RoundButton";
|
import { RoundButton } from "./RoundButton";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getCurrentUserApiUsersMeGetOptions, statsApiStatsGetOptions } from "../../clients/romm/@tanstack/react-query.gen";
|
import { getCurrentUserApiUsersMeGetOptions, statsApiStatsGetOptions } from "@clients/romm/@tanstack/react-query.gen";
|
||||||
import { RPC_URL } from "../../shared/constants";
|
import { RPC_URL } from "../../shared/constants";
|
||||||
import { JSX, useEffect, useRef } from "react";
|
import { JSX, useEffect, useRef } from "react";
|
||||||
import { SaveSource } from "../scripts/spatialNavigation";
|
import { SaveSource, useFocusableDynamic } from "../scripts/spatialNavigation";
|
||||||
import { systemApi } from "../scripts/clientApi";
|
import { systemApi } from "../scripts/clientApi";
|
||||||
import { Router } from "..";
|
import { Router } from "..";
|
||||||
|
|
||||||
|
|
@ -54,14 +54,14 @@ function HeaderAvatar (data: {
|
||||||
id={data.id}
|
id={data.id}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={data.onSelect}
|
onClick={data.onSelect}
|
||||||
|
style={{ viewTransitionName: `header-account-${data.id}` }}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`avatar indicator ring-base-100 ring-offset-base-100 sm:size-8 md:size-14 rounded-full flex items-center justify-center`,
|
`avatar indicator ring-offset-base-100 sm:size-8 md:size-14 rounded-full flex items-center justify-center`,
|
||||||
bgColors[data.type ?? "none"],
|
bgColors[data.type ?? "none"],
|
||||||
"text-base-content cursor-pointer transition-all drop-shadow-md",
|
"text-base-content cursor-pointer transition-all drop-shadow-md",
|
||||||
"hover:ring-primary hover:ring-7",
|
"hover:ring-primary hover:ring-7 focusable focusable-primary focused:ring-offset-base-100",
|
||||||
{
|
{
|
||||||
"ring-5 hover:ring-offset-5": data.active,
|
"ring-5 hover:ring-offset-5": data.active,
|
||||||
"sm:ring-4 md:ring-7 ring-primary ring-offset-base-100": focused,
|
|
||||||
"ring-offset-5": focused && data.active,
|
"ring-offset-5": focused && data.active,
|
||||||
},
|
},
|
||||||
data.className,
|
data.className,
|
||||||
|
|
@ -276,7 +276,7 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; })
|
||||||
export function HeaderStatusBar (data: { buttons?: HeaderButton[]; buttonElements?: JSX.Element[] | JSX.Element; })
|
export function HeaderStatusBar (data: { buttons?: HeaderButton[]; buttonElements?: JSX.Element[] | JSX.Element; })
|
||||||
{
|
{
|
||||||
return <div className="flex items-center sm:gap-1 md:gap-2 text drop-shadow-sm">
|
return <div className="flex items-center sm:gap-1 md:gap-2 text drop-shadow-sm">
|
||||||
<div className="flex sm:gap-2 md:gap-5 items-center">
|
<div className="flex sm:gap-2 md:gap-5 items-center" style={{ viewTransitionName: 'status-bar-icons' }}>
|
||||||
<ClockStatus />
|
<ClockStatus />
|
||||||
<WiFiStatus />
|
<WiFiStatus />
|
||||||
<BluetoothStatus />
|
<BluetoothStatus />
|
||||||
|
|
@ -289,22 +289,29 @@ export function HeaderStatusBar (data: { buttons?: HeaderButton[]; buttonElement
|
||||||
key={b.id}
|
key={b.id}
|
||||||
className="header-icon sm:size-10 md:size-16"
|
className="header-icon sm:size-10 md:size-16"
|
||||||
id={b.id}
|
id={b.id}
|
||||||
icon={b.icon}
|
|
||||||
external={b.external}
|
external={b.external}
|
||||||
action={b.action}
|
style={{ viewTransitionName: `header-button-${b.id}` }}
|
||||||
/>)}
|
onAction={b.action}
|
||||||
|
>{b.icon}</RoundButton>)}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeaderUI (data: { buttons?: HeaderButton[]; accounts?: HeaderAccount[]; buttonElements?: JSX.Element[] | JSX.Element; title?: JSX.Element; })
|
export function HeaderUI (data: {
|
||||||
|
buttons?: HeaderButton[];
|
||||||
|
accounts?: HeaderAccount[];
|
||||||
|
buttonElements?: JSX.Element[] | JSX.Element;
|
||||||
|
title?: JSX.Element;
|
||||||
|
preferredChildFocusKey?: string;
|
||||||
|
})
|
||||||
{
|
{
|
||||||
const { ref, focusKey } = useFocusable({ focusKey: "header-elements" });
|
const { ref, focusKey } = useFocusable({ focusKey: "header-elements", preferredChildFocusKey: data.preferredChildFocusKey });
|
||||||
return (
|
return (
|
||||||
<FocusContext.Provider value={focusKey}>
|
<FocusContext.Provider value={focusKey}>
|
||||||
<header
|
<header
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`flex items-center justify-between text-base-content`}
|
className="flex items-center justify-between text-base-content"
|
||||||
|
style={{ viewTimelineName: 'header' }}
|
||||||
>
|
>
|
||||||
<HeaderAccounts accounts={data.accounts} />
|
<HeaderAccounts accounts={data.accounts} />
|
||||||
{data.title}
|
{data.title}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { GameCardSkeleton } from './GameCard';
|
import { GameCardSkeleton } from './CardElement';
|
||||||
|
|
||||||
export default function LoadingCardList (data: { placeholderCount: number, grid?: boolean; })
|
export default function LoadingCardList (data: { placeholderCount: number, grid?: boolean; })
|
||||||
{
|
{
|
||||||
|
|
|
||||||
31
src/mainview/components/NotFound.tsx
Normal file
31
src/mainview/components/NotFound.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
import { Home, TriangleAlert } from "lucide-react";
|
||||||
|
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
|
||||||
|
import { Router } from "..";
|
||||||
|
import Shortcuts from "./Shortcuts";
|
||||||
|
import { Button } from "./options/Button";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function NotFound ()
|
||||||
|
{
|
||||||
|
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "not-found" });
|
||||||
|
const handleReturn = () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } });
|
||||||
|
useShortcuts(focusKey, () => [{ label: "Return Home", button: GamePadButtonCode.B, action: handleReturn }]);
|
||||||
|
const { shortcuts } = useShortcutContext();
|
||||||
|
|
||||||
|
useEffect(() => { focusSelf(); }, []);
|
||||||
|
|
||||||
|
return <div ref={ref} className="absolute flex flex-col justify-center items-center w-full h-full gap-4">
|
||||||
|
<FocusContext value={focusKey}>
|
||||||
|
<p className="flex gap-2 items-center text-4xl text-error text-shadow-lg">
|
||||||
|
<TriangleAlert className="size-12" />
|
||||||
|
Not found
|
||||||
|
</p>
|
||||||
|
<p className="flex gap-2 text-lg text-base-content/50 text-shadow-lg">{window.location.href} </p>
|
||||||
|
<Button className="text-2xl! p-6! focusable focusable-primary" id="return" onAction={handleReturn}><Home />Return Home</Button>
|
||||||
|
<div className="mobile:hidden bg-gradient"></div>
|
||||||
|
<div className="mobile:hidden bg-noise"></div>
|
||||||
|
<div className="flex justify-end fixed bottom-4 left-4 right-4"><Shortcuts shortcuts={shortcuts} /></div>
|
||||||
|
</FocusContext>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { DefaultRommStaleTime, RPC_URL } from "../../shared/constants";
|
import { DefaultRommStaleTime, RPC_URL } from "@shared/constants";
|
||||||
import { CardList, GameMetaExtra } from "./CardList";
|
import { CardList, GameMetaExtra } from "./CardList";
|
||||||
import classNames from "classnames";
|
|
||||||
import { rommApi } from "../scripts/clientApi";
|
import { rommApi } from "../scripts/clientApi";
|
||||||
import { SaveSource } from "../scripts/spatialNavigation";
|
import { SaveSource } from "../scripts/spatialNavigation";
|
||||||
import { JSX, useMemo } from "react";
|
import { JSX, useMemo } from "react";
|
||||||
import { HardDrive } from "lucide-react";
|
import { HardDrive } from "lucide-react";
|
||||||
import { GameCardFocusHandler } from "./GameCard";
|
import { GameCardFocusHandler } from "./CardElement";
|
||||||
|
import { mobileCheck } from "../scripts/utils";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function PlatformsList (data: { id: string, setBackground: (url: string) => void; className?: string; onFocus?: GameCardFocusHandler; grid?: boolean; })
|
export function PlatformsList (data: {
|
||||||
|
id: string,
|
||||||
|
setBackground: (url: string) => void;
|
||||||
|
className?: string;
|
||||||
|
onFocus?: GameCardFocusHandler;
|
||||||
|
grid?: boolean;
|
||||||
|
onSelect?: (source: string, id: string) => void;
|
||||||
|
})
|
||||||
{
|
{
|
||||||
|
const isMobile = mobileCheck();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: platforms } = useSuspenseQuery(
|
const { data: platforms } = useSuspenseQuery(
|
||||||
{
|
{
|
||||||
|
|
@ -25,6 +34,12 @@ export function PlatformsList (data: { id: string, setBackground: (url: string)
|
||||||
staleTime: DefaultRommStaleTime,
|
staleTime: DefaultRommStaleTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleDefaultSelect = (source: string, id: string) =>
|
||||||
|
{
|
||||||
|
SaveSource('game-list');
|
||||||
|
navigate({ to: `/platform/${source}/${id}`, viewTransition: { types: ['zoom-in'] } });
|
||||||
|
};
|
||||||
|
|
||||||
const platformsMapped = useMemo(() => platforms.sort((a, b) => a.updated_at.getTime() - b.updated_at.getTime())
|
const platformsMapped = useMemo(() => platforms.sort((a, b) => a.updated_at.getTime() - b.updated_at.getTime())
|
||||||
.map((g, i) =>
|
.map((g, i) =>
|
||||||
{
|
{
|
||||||
|
|
@ -44,13 +59,9 @@ export function PlatformsList (data: { id: string, setBackground: (url: string)
|
||||||
onFocus: () => data.setBackground(
|
onFocus: () => data.setBackground(
|
||||||
g.paths_screenshots.length > 0 ? `${RPC_URL(__HOST__)}${g.paths_screenshots[new Date().getMinutes() % g.paths_screenshots.length]}` : `${RPC_URL(__HOST__)}/api/romm/image?url=https://picsum.photos/id/${10 + i}/1280/720.webp`,
|
g.paths_screenshots.length > 0 ? `${RPC_URL(__HOST__)}${g.paths_screenshots[new Date().getMinutes() % g.paths_screenshots.length]}` : `${RPC_URL(__HOST__)}/api/romm/image?url=https://picsum.photos/id/${10 + i}/1280/720.webp`,
|
||||||
),
|
),
|
||||||
onSelect: () =>
|
onSelect: () => data.onSelect ? data.onSelect(g.id.source, g.id.id) : handleDefaultSelect(g.id.source, g.id.id),
|
||||||
{
|
|
||||||
SaveSource('game-list');
|
|
||||||
navigate({ to: `/platform/${g.id.source}/${g.id.id}`, viewTransition: { types: ['zoom-in'] } });
|
|
||||||
},
|
|
||||||
preview:
|
preview:
|
||||||
({ focused }) => <div
|
() => <div
|
||||||
className="flex p-6 bg-base-100 justify-center"
|
className="flex p-6 bg-base-100 justify-center"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(
|
background: `linear-gradient(
|
||||||
|
|
@ -58,11 +69,11 @@ export function PlatformsList (data: { id: string, setBackground: (url: string)
|
||||||
color-mix(in srgb, var(--color-base-300) 60%, transparent)
|
color-mix(in srgb, var(--color-base-300) 60%, transparent)
|
||||||
), url(https://picsum.photos/id/${10 + i}/100/100.webp?blur=10) center / cover`,
|
), url(https://picsum.photos/id/${10 + i}/100/100.webp?blur=10) center / cover`,
|
||||||
|
|
||||||
backgroundBlendMode: "screen",
|
backgroundBlendMode: isMobile ? undefined : "screen",
|
||||||
boxShadow: 'inset 0 0 32px rgba(0,0,0,0.6)'
|
boxShadow: isMobile ? undefined : 'inset 0 0 32px rgba(0,0,0,0.6)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img className={classNames("drop-shadow-2xl", { "animate-rotate": focused })}
|
<img draggable={false} className={"not-mobile:drop-shadow-2xl in-focus:animate-rotate"}
|
||||||
src={coverUrl.href}
|
src={coverUrl.href}
|
||||||
></img>
|
></img>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -76,7 +87,7 @@ export function PlatformsList (data: { id: string, setBackground: (url: string)
|
||||||
type="platform"
|
type="platform"
|
||||||
id={data.id}
|
id={data.id}
|
||||||
grid={data.grid}
|
grid={data.grid}
|
||||||
className={data.className}
|
className={twMerge('*:aspect-8/10! md:py-12', data.className)}
|
||||||
onGameFocus={data.onFocus}
|
onGameFocus={data.onFocus}
|
||||||
games={platformsMapped}
|
games={platformsMapped}
|
||||||
onSelectGame={(id) =>
|
onSelectGame={(id) =>
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,20 @@
|
||||||
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { CSSProperties, JSX } from "react";
|
||||||
import classNames from "classnames";
|
|
||||||
import { JSX } from "react";
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import { Button, ButtonStyle } from "./options/Button";
|
||||||
|
|
||||||
export function RoundButton (data: {
|
export function RoundButton (data: {
|
||||||
id: string;
|
id: string;
|
||||||
icon: JSX.Element;
|
children?: any;
|
||||||
className?: string;
|
className?: string;
|
||||||
external?: boolean;
|
external?: boolean;
|
||||||
action?: () => void;
|
style?: ButtonStyle;
|
||||||
})
|
} & InteractParams & FocusParams)
|
||||||
{
|
{
|
||||||
const { ref, focused } = useFocusable({
|
|
||||||
focusKey: data.id,
|
|
||||||
onEnterPress: data.action,
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Button onFocus={data.onFocus} id={data.id} style={data.style} className={twMerge("rounded-full", data.external && "focusable focusable-primary focusable-hover", data.className)} onAction={data.onAction}>
|
||||||
id={data.id}
|
{data.children}
|
||||||
ref={ref}
|
</Button>
|
||||||
onClick={data.action}
|
|
||||||
className={classNames(twMerge(
|
|
||||||
"rounded-full size-14 flex items-center justify-center bg-base-100 text-base-content cursor-pointer transition-all drop-shadow-sm",
|
|
||||||
data.className, classNames(data.external === true
|
|
||||||
? {
|
|
||||||
"hover:ring-7 hover:ring-primary hover:bg-base-content hover:text-base-300": true,
|
|
||||||
"ring-7 ring-primary bg-base-content text-base-100": focused,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
"hover:bg-primary hover:text-primary-content": true,
|
|
||||||
"bg-primary text-primary-content": focused,
|
|
||||||
},)),
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{data.icon}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
49
src/mainview/components/Screenshots.tsx
Normal file
49
src/mainview/components/Screenshots.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { RPC_URL } from "@/shared/constants";
|
||||||
|
import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import FocusDots from "./FocusDots";
|
||||||
|
import { scrollIntoNearestParent, useDragScroll } from "../scripts/utils";
|
||||||
|
import { Fullscreen } from "lucide-react";
|
||||||
|
|
||||||
|
function Screenshot (data: { path: string; index: number; setFocused?: (index: number) => void; })
|
||||||
|
{
|
||||||
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
|
const { ref, focused, focusSelf } = useFocusable({
|
||||||
|
focusKey: `screenshot-${data.index}`,
|
||||||
|
onEnterPress: () => (ref.current as HTMLElement).requestFullscreen(),
|
||||||
|
onFocus: (e, p, details) =>
|
||||||
|
{
|
||||||
|
data.setFocused?.(data.index);
|
||||||
|
scrollIntoNearestParent(ref.current, { behavior: details.instant ? 'instant' : 'smooth' });
|
||||||
|
}
|
||||||
|
}); 4096;
|
||||||
|
return <div ref={ref} className="group relative flex min-w-fit aspect-video max-h-[60vh] rounded-3xl focusable focusable-accent not-focused:cursor-pointer overflow-hidden">
|
||||||
|
<img ref={imageRef} draggable={false} className="object-cover w-full h-full" onClick={e => focusSelf({ nativeEvent: e.nativeEvent })} src={`${RPC_URL(__HOST__)}${data.path}`} loading="lazy" />
|
||||||
|
<div className="absolute flex justify-center items-center bottom-2 right-2 size-10 rounded-full bg-base-100 hover:bg-base-content hover:text-base-300 cursor-pointer opacity-60 not-control-mouse:hidden invisible group-has-hover:visible" onClick={() => imageRef.current?.requestFullscreen()}> <Fullscreen /> </div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Screenshots (data: { screenshots: string[]; } & FocusParams)
|
||||||
|
{
|
||||||
|
const scrollRef = useRef(null);
|
||||||
|
const { ref, focusKey } = useFocusable({
|
||||||
|
focusKey: 'screenshot-list',
|
||||||
|
onFocus: (e, p, details) =>
|
||||||
|
{
|
||||||
|
data.onFocus?.(focusKey, ref.current, details);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
useDragScroll(scrollRef);
|
||||||
|
|
||||||
|
return <div ref={ref} className="flex flex-col w-full z-0 min-h-0">
|
||||||
|
<FocusContext value={focusKey}>
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="flex gap-6 px-16 py-2 sm:overflow-scroll md:overflow-hidden no-scrollbar justify-center-safe"
|
||||||
|
>
|
||||||
|
{data.screenshots.map((s, i) => <Screenshot key={s} index={i} path={s} />)}
|
||||||
|
</div>
|
||||||
|
<FocusDots elements={data.screenshots.map((_, i) => `screenshot-${i}`)} />
|
||||||
|
</FocusContext>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ export default function ShortcutPrompt (data: {
|
||||||
onClick={data.onClick}
|
onClick={data.onClick}
|
||||||
style={{ viewTransitionName: data.id }}
|
style={{ viewTransitionName: data.id }}
|
||||||
className={twMerge("xs:text-xs sm:p-1 sm:text-sm",
|
className={twMerge("xs:text-xs sm:p-1 sm:text-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",
|
"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 active:text-base-300 active:bg-base-content",
|
||||||
data.className,
|
data.className,
|
||||||
classNames({
|
classNames({
|
||||||
"hover:bg-base-300 cursor-pointer": !!data.onClick,
|
"hover:bg-base-300 cursor-pointer": !!data.onClick,
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export default function Shortcuts (data: { shortcuts?: Shortcut[]; })
|
||||||
const { control } = useActiveControl();
|
const { control } = useActiveControl();
|
||||||
const showKeyboard = control === 'keyboard' || control === 'mouse';
|
const showKeyboard = control === 'keyboard' || control === 'mouse';
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 z-1000 h-10">
|
<div className="flex gap-2 z-1000" style={{ viewTimelineName: "shortcuts" }}>
|
||||||
{data.shortcuts?.filter(s => !!s.label).map((s, i) => <ShortcutPrompt
|
{data.shortcuts?.filter(s => !!s.label).map((s, i) => <ShortcutPrompt
|
||||||
key={s.button}
|
key={s.button}
|
||||||
id={`shortcut-${s.button}`}
|
id={`shortcut-${s.button}`}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,26 @@ import
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { GamePadButtonCode, Shortcut, useShortcuts } from "@/mainview/scripts/shortcuts";
|
import { GamePadButtonCode, Shortcut, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||||
|
|
||||||
|
export type ButtonStyle = 'base' | 'accent' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error';
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
base: 'bg-base-200 text-base-content active:bg-base-300! active:text-base-content! active:ring-offset-base-content',
|
||||||
|
accent: "bg-accent text-accent-content active:bg-base-content! active:text-base-content active:ring-offset-accent",
|
||||||
|
primary: "bg-primary text-primary-content active:bg-base-content! active:text-base-content! active:ring-offset-primary",
|
||||||
|
secondary: "bg-secondary text-secondary-content active:bg-base-content! active:text-base-content! active:ring-offset-secondary",
|
||||||
|
info: "bg-info text-info-content active:bg-base-content! active:text-base-content! active:ring-offset-info",
|
||||||
|
success: "bg-success text-success-content active:bg-base-content! active:text-base-content! active:ring-offset-success",
|
||||||
|
warning: "bg-warning text-warning-content active:bg-base-content! active:text-base-content! active:ring-offset-warning",
|
||||||
|
error: "bg-error text-error-content active:bg-base-content! active:text-base-content! active:ring-offset-error",
|
||||||
|
};
|
||||||
|
|
||||||
export function Button (data: {
|
export function Button (data: {
|
||||||
id: string,
|
id: string,
|
||||||
children?: any,
|
children?: any,
|
||||||
className?: string,
|
className?: string,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
type?: "reset" | "button" | "submit";
|
type?: "reset" | "button" | "submit";
|
||||||
|
style?: ButtonStyle,
|
||||||
shortcutLabel?: string;
|
shortcutLabel?: string;
|
||||||
focusClassName?: string;
|
focusClassName?: string;
|
||||||
} & InteractParams & FocusParams)
|
} & InteractParams & FocusParams)
|
||||||
|
|
@ -20,7 +34,7 @@ export function Button (data: {
|
||||||
const { ref, focused, focusKey } = useFocusable({
|
const { ref, focused, focusKey } = useFocusable({
|
||||||
focusKey: data.id,
|
focusKey: data.id,
|
||||||
onEnterPress: data.onAction,
|
onEnterPress: data.onAction,
|
||||||
onFocus: data.onFocus,
|
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details),
|
||||||
focusable: !data.disabled
|
focusable: !data.disabled
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -31,9 +45,10 @@ export function Button (data: {
|
||||||
|
|
||||||
return <button
|
return <button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={data.onAction}
|
onClick={e => data.onAction?.(e.nativeEvent)}
|
||||||
disabled={data.disabled}
|
disabled={data.disabled}
|
||||||
className={twMerge("btn rounded-full focus:bg-base-content focus:text-base-300 md:text-lg",
|
className={twMerge("flex items-center justify-center px-4 py-2 disabled:bg-base-200/40 disabled:text-base-content/40 cursor-pointer rounded-3xl md:text-lg not-control-mouse:focused:drop-shadow-lg border border-base-content/5 not-control-mouse:focused:bg-base-content not-control-mouse:focused:text-base-100 control-mouse:hover:bg-base-content control-mouse:hover:text-base-100 active:transition-none active:ring-offset-4",
|
||||||
|
styles[data.style ?? 'base'],
|
||||||
focused ? data.focusClassName : undefined,
|
focused ? data.focusClassName : undefined,
|
||||||
classNames({
|
classNames({
|
||||||
"btn-accent": focused,
|
"btn-accent": focused,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { HTMLInputTypeAttribute, JSX } from "react";
|
import { HTMLInputTypeAttribute, JSX } from "react";
|
||||||
import { LocalSettingsSchema, LocalSettingsType } from "../../../shared/constants";
|
import { LocalSettingsSchema, LocalSettingsType } from "@shared/constants";
|
||||||
import { OptionSpace } from "./OptionSpace";
|
import { OptionSpace } from "./OptionSpace";
|
||||||
import { OptionInput } from "./OptionInput";
|
import { OptionInput } from "./OptionInput";
|
||||||
import { useLocalStorage } from "usehooks-ts";
|
import { useLocalStorage } from "usehooks-ts";
|
||||||
|
|
@ -18,7 +18,7 @@ export function LocalOption (data: {
|
||||||
const [localValue, setLocalValue] = useLocalStorage<any>(data.id, LocalSettingsSchema.shape[data.id].parse(undefined), { deserializer: (v) => LocalSettingsSchema.shape[data.id].parse(JSON.parse(v)) });
|
const [localValue, setLocalValue] = useLocalStorage<any>(data.id, LocalSettingsSchema.shape[data.id].parse(undefined), { deserializer: (v) => LocalSettingsSchema.shape[data.id].parse(JSON.parse(v)) });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionSpace label={data.label}>
|
<OptionSpace id={`${data.id}-space`} label={data.label}>
|
||||||
{data.type === 'dropdown' && data.values && <OptionDropdown values={data.values} icon={data.icon}
|
{data.type === 'dropdown' && data.values && <OptionDropdown values={data.values} icon={data.icon}
|
||||||
name={data.id ?? ""}
|
name={data.id ?? ""}
|
||||||
type={data.type}
|
type={data.type}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import classNames from "classnames";
|
import { FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef, useState } from "react";
|
||||||
import { ChangeEventHandler, FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef, useState } from "react";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { useOptionContext } from "./OptionSpace";
|
import { useOptionContext } from "./OptionSpace";
|
||||||
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { systemApi } from "../../scripts/clientApi";
|
|
||||||
import { ContextDialog, ContextList, DialogEntry } from "../ContextDialog";
|
import { ContextDialog, ContextList, DialogEntry } from "../ContextDialog";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
|
@ -39,16 +37,13 @@ export function OptionDropdown (data: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<label ref={ref} className={twMerge("flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent",
|
<label ref={ref} className={twMerge("flex group-focusable items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent")}>
|
||||||
classNames({ "[&_button]:not-focus:ring-7 [&_button]:not-focus:ring-accent": focused }))}>
|
{!!data.icon && <span className={"text-base-content/80 is-focused:text-primary-content"}>{data.icon}</span>}
|
||||||
{!!data.icon && <span className={twMerge("text-base-content/80", classNames({
|
|
||||||
"text-primary-content": option.focused
|
|
||||||
}))}>{data.icon}</span>}
|
|
||||||
<button onClick={() =>
|
<button onClick={() =>
|
||||||
{
|
{
|
||||||
console.log("Open");
|
console.log("Open");
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}} className={classNames('btn input rounded-full cursor-pointer grow', { "bg-base-200": !focused })}>{data.value}<ChevronDown /></button>
|
}} className={'flex items-center justify-center border h-10 border-base-content/30 px-4 py-2 rounded-full cursor-pointer grow not-in-focused:bg-base-200 focusable focusable-accent hover:border-base-content hover:bg-base-content hover:text-base-300'}>{data.value}<ChevronDown /></button>
|
||||||
</label>
|
</label>
|
||||||
{open && <ContextDialog id={`${data.name}-context`} open={true} close={handleClose}>
|
{open && <ContextDialog id={`${data.name}-context`} open={true} close={handleClose}>
|
||||||
<ContextList options={data.values.map((v, i) => ({
|
<ContextList options={data.values.map((v, i) => ({
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import classNames from "classnames";
|
import { FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef } from "react";
|
||||||
import { ChangeEventHandler, FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef } from "react";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { useOptionContext } from "./OptionSpace";
|
import { useOptionContext } from "./OptionSpace";
|
||||||
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { systemApi } from "../../scripts/clientApi";
|
import { systemApi } from "../../scripts/clientApi";
|
||||||
import { Check, CheckIcon, X } from "lucide-react";
|
import { CheckIcon, X } from "lucide-react";
|
||||||
|
|
||||||
export function OptionInput (data: {
|
export function OptionInput (data: {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -52,11 +51,8 @@ export function OptionInput (data: {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label ref={ref} className={twMerge("flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent",
|
<label ref={ref} className={`flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent group-focusable`}>
|
||||||
classNames({ "[&_.focus-target]:not-focus:ring-7 [&_.focus-target]:not-focus:ring-accent": focused, "pl-1": data.type === 'checkbox' }))}>
|
{!!data.icon && <span className="text-base-content/80">{data.icon}</span>}
|
||||||
{!!data.icon && <span className={twMerge("text-base-content/80", classNames({
|
|
||||||
"text-primary-content": option.focused
|
|
||||||
}))}>{data.icon}</span>}
|
|
||||||
{data.type !== 'checkbox' && <input
|
{data.type !== 'checkbox' && <input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
id={data.name}
|
id={data.name}
|
||||||
|
|
@ -72,17 +68,11 @@ export function OptionInput (data: {
|
||||||
onBlur={data.onBlur}
|
onBlur={data.onBlur}
|
||||||
defaultChecked={typeof data.defaultValue === 'boolean' ? data.defaultValue : undefined}
|
defaultChecked={typeof data.defaultValue === 'boolean' ? data.defaultValue : undefined}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"focus-target text-base-content",
|
"flex text-base-content px-4 py-2 items-center justify-center border border-base-content/20 grow rounded-full focus:ring-base-content in-focused:bg-base-200 focusable focusable-accent focus:not-focused:ring-7 control-mouse:ring-0! hover:border-base-content",
|
||||||
"input grow rounded-full ring-primary-content focus:ring-7", classNames({
|
|
||||||
"bg-base-200": !focused
|
|
||||||
}),
|
|
||||||
data.className
|
data.className
|
||||||
)}
|
)}
|
||||||
/>}
|
/>}
|
||||||
{data.type === 'checkbox' && <div className={classNames("toggle focus-target toggle-primary toggle-xl border-base-content/30 rounded-full before:rounded-full text-base-content", {
|
{data.type === 'checkbox' && <div className="toggle toggle-xl before:size-6 h-8 border-base-content/30 rounded-full before:rounded-full text-base-content not-in-focus:bg-base-200 focused-child:border-0 ml-1 ring-7 hover:border-base-content focusable focusable-accent">
|
||||||
"bg-base-200": !focused,
|
|
||||||
"border-0": focused,
|
|
||||||
})}>
|
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
id={data.name}
|
id={data.name}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,9 @@
|
||||||
import { FocusContext, FocusDetails, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { OptionContext } from "@/mainview/scripts/contexts";
|
||||||
|
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { createContext, JSX, useContext, useEffect, useMemo } from "react";
|
import { JSX, useContext, useEffect, useMemo } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export const OptionContext = createContext(
|
|
||||||
{} as {
|
|
||||||
focused: boolean;
|
|
||||||
focus: (focusDetails?: FocusDetails | undefined) => void;
|
|
||||||
eventTarget: EventTarget;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export function useOptionContext (params?: { onOptionEnterPress?: () => void; })
|
export function useOptionContext (params?: { onOptionEnterPress?: () => void; })
|
||||||
{
|
{
|
||||||
const context = useContext(OptionContext);
|
const context = useContext(OptionContext);
|
||||||
|
|
@ -81,11 +74,7 @@ export function OptionSpace (data: {
|
||||||
<OptionContext value={{ focused, focus: focusSelf, eventTarget }}>
|
<OptionContext value={{ focused, focus: focusSelf, eventTarget }}>
|
||||||
<li
|
<li
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={twMerge("flex portrait:flex-col portrait:gap-2 portrait:p-4 md:flex-row sm:p-2 md:p-4 md:pl-8! rounded-3xl border-b border-base-content/5",
|
className={twMerge("flex portrait:flex-col portrait:gap-2 portrait:p-4 md:flex-row sm:p-2 md:p-4 md:pl-8! rounded-3xl border-b border-base-content/5 focused:bg-base-300 focused-child:bg-base-300",
|
||||||
classNames(
|
|
||||||
{
|
|
||||||
"bg-base-300": focused || hasFocusedChild,
|
|
||||||
}),
|
|
||||||
data.className,
|
data.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ export function PathSettingsOptionBase (data: PathSettingsOptionParams & {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionSpace id={data.id} className="gap-2" label={<>{data.label}{changed && <Pen />}</>}>
|
<OptionSpace id={`${data.id}-space`} className="gap-2" label={<>{data.label}{changed && <Pen />}</>}>
|
||||||
<OptionInput
|
<OptionInput
|
||||||
icon={data.icon}
|
icon={data.icon}
|
||||||
name={`${data.id}-input`}
|
name={`${data.id}-input`}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export const { useAppForm: useSettingsForm, useTypedAppFormContext: useSettingsF
|
||||||
function FormOption (data: { type: HTMLInputTypeAttribute, icon?: JSX.Element; label?: string | JSX.Element; placeholder?: string; })
|
function FormOption (data: { type: HTMLInputTypeAttribute, icon?: JSX.Element; label?: string | JSX.Element; placeholder?: string; })
|
||||||
{
|
{
|
||||||
const field = useFieldContext<string>();
|
const field = useFieldContext<string>();
|
||||||
return <OptionSpace label={<div className="flex flex-1 gap-2">
|
return <OptionSpace id={`${field.name}-space`} label={<div className="flex flex-1 gap-2">
|
||||||
{data.label}
|
{data.label}
|
||||||
{field.getMeta().errors.length > 0 && <div className="badge badge-error">
|
{field.getMeta().errors.length > 0 && <div className="badge badge-error">
|
||||||
{field.state.meta.errors.map(e => e.message).join(',')}
|
{field.state.meta.errors.map(e => e.message).join(',')}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export function SettingsOption (data: {
|
||||||
}, [dirty, setDirty, localValue]);
|
}, [dirty, setDirty, localValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionSpace label={data.label}>
|
<OptionSpace id={`${data.id}-space`} label={data.label}>
|
||||||
<OptionInput
|
<OptionInput
|
||||||
icon={data.icon}
|
icon={data.icon}
|
||||||
name={data.id ?? ""}
|
name={data.id ?? ""}
|
||||||
|
|
|
||||||
76
src/mainview/components/store/EmulatorsSection.tsx
Normal file
76
src/mainview/components/store/EmulatorsSection.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { useRef } from "react";
|
||||||
|
import
|
||||||
|
{
|
||||||
|
useFocusable,
|
||||||
|
FocusContext,
|
||||||
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
import { ChevronRight, Joystick } from "lucide-react";
|
||||||
|
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||||
|
import { scrollIntoNearestParent, useDragScroll } from "@/mainview/scripts/utils";
|
||||||
|
import FocusDots from "../FocusDots";
|
||||||
|
import { Router } from "@/mainview";
|
||||||
|
import { StoreEmulatorCard } from "./StoreEmulatorCard";
|
||||||
|
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||||
|
import { FrontEndEmulator } from "@/shared/constants";
|
||||||
|
|
||||||
|
function SeeAllCard (data: { id: string; onAction: () => void; onFocus?: (details: { node: HTMLElement, instant: boolean; }) => void; })
|
||||||
|
{
|
||||||
|
const { ref, focusKey } = useFocusable({
|
||||||
|
focusKey: data.id,
|
||||||
|
onFocus: (_l, _p, details) => data.onFocus?.({ node: ref.current, instant: details.instant }),
|
||||||
|
onEnterPress: data.onAction
|
||||||
|
});
|
||||||
|
useShortcuts(focusKey, () => [{ button: GamePadButtonCode.A, label: "See All", action: data.onAction }], []);
|
||||||
|
return <div
|
||||||
|
ref={ref}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={data.onAction}
|
||||||
|
className={"flex focusable focusable-info bg-base-100 rounded-4xl transition-shadow focused:animate-scale-small p-4 justify-center items-center min-w-2xs gap-2 hover:bg-base-300 cursor-pointer"}
|
||||||
|
>
|
||||||
|
See All Emulators <ChevronRight />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmulatorsSection (data: {
|
||||||
|
id: string;
|
||||||
|
emulators: FrontEndEmulator[];
|
||||||
|
onSelect?: (id: string, focusKey: string) => void;
|
||||||
|
header?: any;
|
||||||
|
} & FocusParams)
|
||||||
|
{
|
||||||
|
const { ref, focusKey } = useFocusable({
|
||||||
|
focusKey: FOCUS_KEYS.EMULATOR_SECTION(data.id),
|
||||||
|
trackChildren: true,
|
||||||
|
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details)
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
useDragScroll(containerRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusContext.Provider value={focusKey}>
|
||||||
|
<section ref={ref} className="px-2 py-4">
|
||||||
|
<div className="flex items-center gap-3 px-4 mb-4 text-info">
|
||||||
|
{data.header ?? <>
|
||||||
|
<div className="w-2 h-5 rounded-full bg-info shadow-sm shadow-error/40" />
|
||||||
|
<Joystick />
|
||||||
|
<h2 className="font-bold uppercase tracking-widest">
|
||||||
|
Recommended Emulators
|
||||||
|
</h2>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
|
<div ref={containerRef} className="flex *:min-w-[18rem] overflow-y-hidden overflow-x-scroll scrollbar-none py-2 px-4 gap-4 select-none">
|
||||||
|
{data.emulators?.map((em) => (
|
||||||
|
<StoreEmulatorCard id={`${data.id}-${em.name}`} key={em.name} emulator={em} onSelect={(id, focusKey) => data.onSelect?.(em.name, focusKey)} onFocus={({ node, details }) =>
|
||||||
|
{
|
||||||
|
scrollIntoNearestParent(node, { behavior: details.instant ? 'instant' : 'smooth' });
|
||||||
|
}} />
|
||||||
|
))}
|
||||||
|
<SeeAllCard id={`${FOCUS_KEYS.EMULATOR_SECTION}-see-all`} onAction={() => Router.navigate({ to: '/store/tab/emulators' })} onFocus={({ node, instant }) => scrollIntoNearestParent(node, { behavior: instant ? 'instant' : 'smooth' })} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{!!data.emulators && <FocusDots elements={data.emulators.map(e => FOCUS_KEYS.EMULATOR_CARD(e.name))} />}
|
||||||
|
</FocusContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/mainview/components/store/GamesSection.tsx
Normal file
49
src/mainview/components/store/GamesSection.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { useRef } from "react";
|
||||||
|
import
|
||||||
|
{
|
||||||
|
useFocusable,
|
||||||
|
FocusContext,
|
||||||
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
import { Gamepad2 } from "lucide-react";
|
||||||
|
import { useDragScroll } from "@/mainview/scripts/utils";
|
||||||
|
import FocusDots from "../FocusDots";
|
||||||
|
import { FrontEndGameType, FrontEndId } from "@/shared/constants";
|
||||||
|
import FrontEndGameCard from "../FrontEndGameCard";
|
||||||
|
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||||
|
|
||||||
|
export function GamesSection ({ games, onSelect, onFocus }: {
|
||||||
|
games: FrontEndGameType[];
|
||||||
|
onSelect?: (id: FrontEndId, focusKey: string) => void;
|
||||||
|
} & FocusParams)
|
||||||
|
{
|
||||||
|
const { ref, focusKey } = useFocusable({
|
||||||
|
focusKey: FOCUS_KEYS.GAME_SECTION,
|
||||||
|
trackChildren: true,
|
||||||
|
onFocus: (_l, _p, details) => onFocus?.(focusKey, ref.current, details)
|
||||||
|
});
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
useDragScroll(containerRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusContext.Provider value={focusKey}>
|
||||||
|
<section ref={ref} className="px-6 py-3 select-none">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-2 h-5 rounded-full bg-accent shadow-sm shadow-error/40" />
|
||||||
|
<Gamepad2 className="text-accent" />
|
||||||
|
<h2 className="font-bold uppercase tracking-widest text-accent grow">
|
||||||
|
Featured Games
|
||||||
|
</h2>
|
||||||
|
<div className="badge badge-xl badge-accent badge-soft">Curated picks</div>
|
||||||
|
</div>
|
||||||
|
<div ref={containerRef} className="grid grid-flow-col auto-cols-[18rem] overflow-y-hidden overflow-x-auto hide-scrollbar p-4 gap-4 justify-center-safe">
|
||||||
|
{games.map((g, i) => <FrontEndGameCard
|
||||||
|
key={g.id.id}
|
||||||
|
game={g}
|
||||||
|
onAction={() => onSelect?.(g.id, FOCUS_KEYS.GAME_CARD(g.id.id))}
|
||||||
|
index={i} />)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<FocusDots elements={games.map(e => FOCUS_KEYS.GAME_CARD(e.id.id))} />
|
||||||
|
</FocusContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
src/mainview/components/store/MissingEmulatorsSection.tsx
Normal file
98
src/mainview/components/store/MissingEmulatorsSection.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import
|
||||||
|
{
|
||||||
|
useFocusable,
|
||||||
|
FocusContext,
|
||||||
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
import { Button } from "../options/Button";
|
||||||
|
import useActiveControl from "@/mainview/scripts/gamepads";
|
||||||
|
import { ChevronRight, CircleQuestionMark, SearchAlert } from "lucide-react";
|
||||||
|
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||||
|
import { FrontEndEmulator, RPC_URL } from "@/shared/constants";
|
||||||
|
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||||
|
|
||||||
|
// ── Single missing-emulator card ───────────────────────────────────────────
|
||||||
|
interface MissingCardProps
|
||||||
|
{
|
||||||
|
emulator: FrontEndEmulator;
|
||||||
|
onSelect?: (id: string, focusKey: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MissingCard ({ emulator: em, onSelect }: MissingCardProps)
|
||||||
|
{
|
||||||
|
const handleSelect = () => onSelect?.(em.name, focusKey);
|
||||||
|
|
||||||
|
const { ref, focusKey } = useFocusable({
|
||||||
|
focusKey: FOCUS_KEYS.MISSING_CARD(em.name),
|
||||||
|
onEnterPress: handleSelect,
|
||||||
|
});
|
||||||
|
useShortcuts(focusKey, () => [{ button: GamePadButtonCode.A, label: "Details", action: handleSelect }], [handleSelect]);
|
||||||
|
const { isMouse } = useActiveControl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={handleSelect}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleSelect}
|
||||||
|
className={"focusable focusable-accent bg-base-100 rounded-4xl transition-all focused:animate-scale-small shadow-lg"}
|
||||||
|
>
|
||||||
|
<div className="card-body p-5 gap-3">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div
|
||||||
|
className={`size-14 bg-base-content rounded-full flex items-center justify-center text-2xl shadow-md shrink-0 text-base-300`}
|
||||||
|
>
|
||||||
|
{em.logo ?
|
||||||
|
<img className='size-6 drop-shadow drop-shadow-black/20' src={`${RPC_URL(__HOST__)}${em.logo}`}></img> :
|
||||||
|
<CircleQuestionMark />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="grow">
|
||||||
|
<p className="font-bold text-base-content text-xl leading-tight">{em.name}</p>
|
||||||
|
<p className="text-base-content/40 mt-0.5">{em.systems?.map(s => s.name).join(',')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center grow h-8">
|
||||||
|
<p className="text-xs text-error/80 leading-relaxed">{em.name}</p>
|
||||||
|
{isMouse && <Button className="hover:btn-error hover:text-primary-content text-base-content/40 font-normal md:text-base" onAction={handleSelect} id={`details-${em.name}`}>Details<ChevronRight /></Button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MissingEmulatorsSection ({
|
||||||
|
emulators,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
emulators: FrontEndEmulator[];
|
||||||
|
onSelect?: (id: string, focusKey: string) => void;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
const { ref, focusKey } = useFocusable({
|
||||||
|
focusKey: FOCUS_KEYS.MISSING_SECTION,
|
||||||
|
trackChildren: true,
|
||||||
|
onFocus: (_l, _p, details) => (ref.current as HTMLElement)?.scrollIntoView({ behavior: details.instant ? 'instant' : 'smooth', block: 'end' })
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusContext.Provider value={focusKey}>
|
||||||
|
<section ref={ref} className="px-6 pt-5 pb-2">
|
||||||
|
<div className="flex items-center gap-3 mb-4 text-error">
|
||||||
|
<div className="w-2 h-5 rounded-full bg-error shadow-sm shadow-error/40" />
|
||||||
|
<SearchAlert />
|
||||||
|
<h2 className="font-bold uppercase tracking-widest">
|
||||||
|
Missing Emulators
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid sm:grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{emulators.map((em) => (
|
||||||
|
<MissingCard key={em.name} emulator={em} onSelect={onSelect} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div className="divider opacity-20" />
|
||||||
|
</FocusContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/mainview/components/store/StatsSection.tsx
Normal file
52
src/mainview/components/store/StatsSection.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { storeApi } from "@/mainview/scripts/clientApi";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Joystick, LibraryBig, Save, TriangleAlert } from "lucide-react";
|
||||||
|
|
||||||
|
interface StatsSectionProps
|
||||||
|
{
|
||||||
|
romCount: number;
|
||||||
|
missingCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatsSection ({
|
||||||
|
romCount,
|
||||||
|
missingCount,
|
||||||
|
}: StatsSectionProps)
|
||||||
|
{
|
||||||
|
|
||||||
|
const { data: stats } = useQuery({
|
||||||
|
queryKey: ['store', 'stats'], queryFn: async () =>
|
||||||
|
{
|
||||||
|
const { data, error } = await storeApi.api.store.stats.get();
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="px-6 pt-3 pb-4">
|
||||||
|
<div className="stats stats-horizontal w-full rounded-2xl text-shadow-sm">
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-figure text-2xl text-primary shadow-2xl"><Joystick /></div>
|
||||||
|
<div className="stat-value text-xl font-black text-primary shadow-2xl">{stats?.storeEmulatorCount}</div>
|
||||||
|
<div className="stat-desc ">Emulators Available</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-figure text-2xl text-secondary"><Save /></div>
|
||||||
|
<div className="stat-value text-xl font-black text-secondary">{romCount.toLocaleString()}+</div>
|
||||||
|
<div className="stat-desc">ROMs in Store</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-figure text-2xl text-success"><LibraryBig /></div>
|
||||||
|
<div className="stat-value text-xl font-black text-success">{stats?.gameCount}</div>
|
||||||
|
<div className="stat-desc">Your Library</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-figure text-2xl text-warning"><TriangleAlert /></div>
|
||||||
|
<div className="stat-value text-xl font-black text-warning">{missingCount}</div>
|
||||||
|
<div className="stat-desc">Missing Emulators</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
src/mainview/components/store/StoreEmulatorCard.tsx
Normal file
84
src/mainview/components/store/StoreEmulatorCard.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { FrontEndEmulator, RPC_URL } from "@/shared/constants";
|
||||||
|
import { Button } from "../options/Button";
|
||||||
|
import useActiveControl from "@/mainview/scripts/gamepads";
|
||||||
|
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||||
|
import { ChevronRight, EllipsisVertical, HardDrive } from "lucide-react";
|
||||||
|
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||||
|
|
||||||
|
export function StoreEmulatorCard (data: {
|
||||||
|
id: string;
|
||||||
|
emulator: FrontEndEmulator;
|
||||||
|
onSelect?: (id: string, focusKey: string) => void;
|
||||||
|
onFocus?: (data: { id: string; node: HTMLElement; details: Record<string, any>; }) => void;
|
||||||
|
className?: string;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
const handleSelect = () => data.onSelect?.(data.emulator.name, focusKey);
|
||||||
|
|
||||||
|
const { ref, focusKey } = useFocusable({
|
||||||
|
focusKey: FOCUS_KEYS.EMULATOR_CARD(data.id),
|
||||||
|
onEnterPress: handleSelect,
|
||||||
|
onFocus: (_l, _p, details) =>
|
||||||
|
{
|
||||||
|
data.onFocus?.({ id: data.emulator.name, node: ref.current as HTMLElement, details });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useShortcuts(focusKey, () => [{ button: GamePadButtonCode.A, label: "Details", action: handleSelect }], [handleSelect]);
|
||||||
|
const { isMouse, isTouch } = useActiveControl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
data-installed={data.emulator.exists ? true : undefined}
|
||||||
|
onClick={isTouch ? handleSelect : undefined}
|
||||||
|
className={twMerge("relative focusable focusable-info bg-base-100 rounded-4xl transition-shadow focused:not-control-mouse:animate-scale-small shadow-lg border border-base-content/10 active:ring-4 active:ring-base-content active:transition-none", data.className)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col justify-between p-4 gap-2 h-full">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div
|
||||||
|
data-installed={data.emulator.exists}
|
||||||
|
className={`size-14 p-2 rounded-full bg-info flex items-center justify-center text-xl shadow-lg data-[installed=true]:bg-success`}
|
||||||
|
>
|
||||||
|
<img draggable={false} src={data.emulator.logo}></img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p data-installed={data.emulator.exists} className="font-bold text-base-content text-xl leading-snug data-[installed=true]:text-success">{data.emulator.name}</p>
|
||||||
|
<ul className="flex flex-wrap gap-1">
|
||||||
|
{data.emulator.systems.map(({ id, name, icon }) =>
|
||||||
|
{
|
||||||
|
return <div key={id} className="flex gap-1 items-center text-base-content/35 mt-0.5">
|
||||||
|
{!!icon && <img draggable={false} className="size-6 p-1 bg-base-200 rounded-full" src={`${RPC_URL(__HOST__)}${icon}`} />}
|
||||||
|
<p className="text-nowrap text-ellipsis overflow-hidden">{name}</p>
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-0.5 mt-1 h-10 items-center">
|
||||||
|
{data.emulator.exists && <div className="tooltip" data-tip="Installed">
|
||||||
|
<div className="flex items-center justify-center rounded-full p-1 size-8 bg-success text-success-content"><HardDrive /></div>
|
||||||
|
</div>}
|
||||||
|
{<div className="tooltip" data-tip="Game Count">
|
||||||
|
<div className="flex items-center justify-center rounded-full font-semibold size-9 p-2 bg-base-200 text-base-content/40">{data.emulator.gameCount}</div>
|
||||||
|
</div>}
|
||||||
|
{isMouse && <>
|
||||||
|
<Button onAction={handleSelect} style="base" className="grow text-base-content/40" id={`${data.emulator.name}-details`} >Details<ChevronRight /></Button>
|
||||||
|
<Button className="bg-transparent border-none shadow-none w-6 p-0" id={`${data.emulator.name}-options`} ><EllipsisVertical /></Button>
|
||||||
|
</>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
src/mainview/emulatorjs/emulator.ts
Normal file
63
src/mainview/emulatorjs/emulator.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { RPC_URL } from '@/shared/constants';
|
||||||
|
import { basename } from 'pathe';
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
Array.from(params.entries()).forEach(([key, value]) =>
|
||||||
|
{
|
||||||
|
(window as any)[`EJS_${key}`] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('message', (e) =>
|
||||||
|
{
|
||||||
|
switch (e.data.type)
|
||||||
|
{
|
||||||
|
case 'pause':
|
||||||
|
if (e.data.data === true)
|
||||||
|
{
|
||||||
|
window.EJS_emulator.pause();
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
window.EJS_emulator.play();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'restart':
|
||||||
|
window.EJS_emulator.elements.bottomBar.restart[0].click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
window.EJS_player = "#game";
|
||||||
|
window.EJS_lightgun = false;
|
||||||
|
window.EJS_startOnLoaded = true;
|
||||||
|
// For core downloads, it either redirects to CDN or uses local if downloaded
|
||||||
|
window.EJS_pathtodata = `${RPC_URL(__HOST__)}/api/romm/emulatorjs/data`;
|
||||||
|
window.EJS_Buttons = {
|
||||||
|
exitEmulation: {
|
||||||
|
visible: true,
|
||||||
|
displayName: "Exit",
|
||||||
|
callback: () =>
|
||||||
|
{
|
||||||
|
window.parent.postMessage(
|
||||||
|
{ type: "exit" },
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const moduleUrls = import.meta.glob
|
||||||
|
(['../../../node_modules/@emulatorjs/emulatorjs/data/**/*.js',
|
||||||
|
'../../../node_modules/@emulatorjs/emulatorjs/data/**/*.css',
|
||||||
|
'../../../node_modules/@emulatorjs/emulatorjs/data/**/*.wasm',
|
||||||
|
'../../../node_modules/@emulatorjs/emulatorjs/data/localization/en-US.json'
|
||||||
|
], {
|
||||||
|
query: '?url',
|
||||||
|
import: 'default',
|
||||||
|
});
|
||||||
|
|
||||||
|
// emulatorjs expects basenames instead of paths for some reason
|
||||||
|
window.EJS_paths = Object.fromEntries(await Promise.all(Object.entries(moduleUrls).map(async ([key, value]) => [basename(key), await value()])));
|
||||||
|
|
||||||
|
await import('@emulatorjs/emulatorjs/data/loader.js');
|
||||||
20
src/mainview/emulatorjs/index.html
Normal file
20
src/mainview/emulatorjs/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" href="../assets/favicon.ico" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Alan+Sans:wght@300..900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link href="./style.css" rel="stylesheet" />
|
||||||
|
<title>GameFlow</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" src="./emulator.ts"></script>
|
||||||
|
<div id="game-wrapper">
|
||||||
|
<div id="game"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
src/mainview/emulatorjs/style.css
Normal file
22
src/mainview/emulatorjs/style.css
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
62
src/mainview/emulatorjs/types.d.ts
vendored
Normal file
62
src/mainview/emulatorjs/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
export declare global
|
||||||
|
{
|
||||||
|
interface Window
|
||||||
|
{
|
||||||
|
EJS_emulator: any,
|
||||||
|
EJS_player: string,
|
||||||
|
EJS_gameUrl: string,
|
||||||
|
EJS_pathtodata: string,
|
||||||
|
EJS_language: string,
|
||||||
|
EJS_disableAutoLang: boolean,
|
||||||
|
EJS_paths: Record<string, string>,
|
||||||
|
EJS_volume: number,
|
||||||
|
EJS_gameName: string,
|
||||||
|
EJS_cheats: string[][],
|
||||||
|
EJS_fullscreenOnLoaded: boolean,
|
||||||
|
EJS_startOnLoaded: boolean,
|
||||||
|
EJS_core: string,
|
||||||
|
EJS_lightgun: boolean,
|
||||||
|
EJS_biosUrl: string,
|
||||||
|
EJS_color: string,
|
||||||
|
EJS_AdUrl: string,
|
||||||
|
EJS_AdMode: string,
|
||||||
|
EJS_AdTimer: number,
|
||||||
|
EJS_AdSize: number,
|
||||||
|
EJS_alignStartButton: boolean,
|
||||||
|
EJS_VirtualGamepadSettings,
|
||||||
|
EJS_Buttons,
|
||||||
|
EJS_defaultControls,
|
||||||
|
EJS_loadStateURL: string,
|
||||||
|
EJS_CacheLimit: number,
|
||||||
|
EJS_cacheConfig,
|
||||||
|
EJS_cheatPath: string,
|
||||||
|
EJS_defaultOptions,
|
||||||
|
EJS_gamePatchUrl: string,
|
||||||
|
EJS_gameParentUrl: string,
|
||||||
|
EJS_netplayServer,
|
||||||
|
EJS_netplayICEServers,
|
||||||
|
EJS_gameID: string,
|
||||||
|
EJS_backgroundImage: string,
|
||||||
|
EJS_backgroundBlur,
|
||||||
|
EJS_backgroundColor,
|
||||||
|
EJS_controlScheme,
|
||||||
|
EJS_threads: boolean,
|
||||||
|
EJS_disableCue,
|
||||||
|
EJS_startButtonName,
|
||||||
|
EJS_softLoad,
|
||||||
|
EJS_screenCapture,
|
||||||
|
EJS_externalFiles,
|
||||||
|
EJS_dontExtractRom,
|
||||||
|
EJS_dontExtractBIOS,
|
||||||
|
EJS_disableLocalStorage: boolean,
|
||||||
|
EJS_forceLegacyCores: boolean,
|
||||||
|
EJS_noAutoFocus: boolean,
|
||||||
|
EJS_videoRotation,
|
||||||
|
EJS_hideSettings,
|
||||||
|
EJS_browserMode,
|
||||||
|
EJS_shaders,
|
||||||
|
EJS_fixedSaveInterval,
|
||||||
|
EJS_disableAutoUnload,
|
||||||
|
EJS_disableBatchBootup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,9 +17,15 @@ import { Route as SettingsDirectoriesRouteImport } from './../routes/settings/di
|
||||||
import { Route as SettingsAccountsRouteImport } from './../routes/settings/accounts'
|
import { Route as SettingsAccountsRouteImport } from './../routes/settings/accounts'
|
||||||
import { Route as SettingsAboutRouteImport } from './../routes/settings/about'
|
import { Route as SettingsAboutRouteImport } from './../routes/settings/about'
|
||||||
import { Route as CollectionIdRouteImport } from './../routes/collection.$id'
|
import { Route as CollectionIdRouteImport } from './../routes/collection.$id'
|
||||||
|
import { Route as StoreTabRouteRouteImport } from './../routes/store/tab/route'
|
||||||
|
import { Route as StoreTabIndexRouteImport } from './../routes/store/tab/index'
|
||||||
|
import { Route as StoreTabGamesRouteImport } from './../routes/store/tab/games'
|
||||||
|
import { Route as StoreTabEmulatorsRouteImport } from './../routes/store/tab/emulators'
|
||||||
import { Route as PlatformSourceIdRouteImport } from './../routes/platform.$source.$id'
|
import { Route as PlatformSourceIdRouteImport } from './../routes/platform.$source.$id'
|
||||||
import { Route as LauncherSourceIdRouteImport } from './../routes/launcher.$source.$id'
|
import { Route as LauncherSourceIdRouteImport } from './../routes/launcher.$source.$id'
|
||||||
import { Route as GameSourceIdRouteImport } from './../routes/game/$source.$id'
|
import { Route as GameSourceIdRouteImport } from './../routes/game/$source.$id'
|
||||||
|
import { Route as EmbeddedSourceIdRouteImport } from './../routes/embedded.$source.$id'
|
||||||
|
import { Route as StoreDetailsEmulatorIdRouteImport } from './../routes/store/details.emulator.$id'
|
||||||
|
|
||||||
const SettingsRouteRoute = SettingsRouteRouteImport.update({
|
const SettingsRouteRoute = SettingsRouteRouteImport.update({
|
||||||
id: '/settings',
|
id: '/settings',
|
||||||
|
|
@ -61,6 +67,26 @@ const CollectionIdRoute = CollectionIdRouteImport.update({
|
||||||
path: '/collection/$id',
|
path: '/collection/$id',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const StoreTabRouteRoute = StoreTabRouteRouteImport.update({
|
||||||
|
id: '/store/tab',
|
||||||
|
path: '/store/tab',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const StoreTabIndexRoute = StoreTabIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => StoreTabRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const StoreTabGamesRoute = StoreTabGamesRouteImport.update({
|
||||||
|
id: '/games',
|
||||||
|
path: '/games',
|
||||||
|
getParentRoute: () => StoreTabRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const StoreTabEmulatorsRoute = StoreTabEmulatorsRouteImport.update({
|
||||||
|
id: '/emulators',
|
||||||
|
path: '/emulators',
|
||||||
|
getParentRoute: () => StoreTabRouteRoute,
|
||||||
|
} as any)
|
||||||
const PlatformSourceIdRoute = PlatformSourceIdRouteImport.update({
|
const PlatformSourceIdRoute = PlatformSourceIdRouteImport.update({
|
||||||
id: '/platform/$source/$id',
|
id: '/platform/$source/$id',
|
||||||
path: '/platform/$source/$id',
|
path: '/platform/$source/$id',
|
||||||
|
|
@ -76,19 +102,35 @@ const GameSourceIdRoute = GameSourceIdRouteImport.update({
|
||||||
path: '/game/$source/$id',
|
path: '/game/$source/$id',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const EmbeddedSourceIdRoute = EmbeddedSourceIdRouteImport.update({
|
||||||
|
id: '/embedded/$source/$id',
|
||||||
|
path: '/embedded/$source/$id',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const StoreDetailsEmulatorIdRoute = StoreDetailsEmulatorIdRouteImport.update({
|
||||||
|
id: '/store/details/emulator/$id',
|
||||||
|
path: '/store/details/emulator/$id',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/settings': typeof SettingsRouteRouteWithChildren
|
'/settings': typeof SettingsRouteRouteWithChildren
|
||||||
|
'/store/tab': typeof StoreTabRouteRouteWithChildren
|
||||||
'/collection/$id': typeof CollectionIdRoute
|
'/collection/$id': typeof CollectionIdRoute
|
||||||
'/settings/about': typeof SettingsAboutRoute
|
'/settings/about': typeof SettingsAboutRoute
|
||||||
'/settings/accounts': typeof SettingsAccountsRoute
|
'/settings/accounts': typeof SettingsAccountsRoute
|
||||||
'/settings/directories': typeof SettingsDirectoriesRoute
|
'/settings/directories': typeof SettingsDirectoriesRoute
|
||||||
'/settings/emulators': typeof SettingsEmulatorsRoute
|
'/settings/emulators': typeof SettingsEmulatorsRoute
|
||||||
'/settings/interface': typeof SettingsInterfaceRoute
|
'/settings/interface': typeof SettingsInterfaceRoute
|
||||||
|
'/embedded/$source/$id': typeof EmbeddedSourceIdRoute
|
||||||
'/game/$source/$id': typeof GameSourceIdRoute
|
'/game/$source/$id': typeof GameSourceIdRoute
|
||||||
'/launcher/$source/$id': typeof LauncherSourceIdRoute
|
'/launcher/$source/$id': typeof LauncherSourceIdRoute
|
||||||
'/platform/$source/$id': typeof PlatformSourceIdRoute
|
'/platform/$source/$id': typeof PlatformSourceIdRoute
|
||||||
|
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
|
||||||
|
'/store/tab/games': typeof StoreTabGamesRoute
|
||||||
|
'/store/tab/': typeof StoreTabIndexRoute
|
||||||
|
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
|
@ -99,38 +141,55 @@ export interface FileRoutesByTo {
|
||||||
'/settings/directories': typeof SettingsDirectoriesRoute
|
'/settings/directories': typeof SettingsDirectoriesRoute
|
||||||
'/settings/emulators': typeof SettingsEmulatorsRoute
|
'/settings/emulators': typeof SettingsEmulatorsRoute
|
||||||
'/settings/interface': typeof SettingsInterfaceRoute
|
'/settings/interface': typeof SettingsInterfaceRoute
|
||||||
|
'/embedded/$source/$id': typeof EmbeddedSourceIdRoute
|
||||||
'/game/$source/$id': typeof GameSourceIdRoute
|
'/game/$source/$id': typeof GameSourceIdRoute
|
||||||
'/launcher/$source/$id': typeof LauncherSourceIdRoute
|
'/launcher/$source/$id': typeof LauncherSourceIdRoute
|
||||||
'/platform/$source/$id': typeof PlatformSourceIdRoute
|
'/platform/$source/$id': typeof PlatformSourceIdRoute
|
||||||
|
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
|
||||||
|
'/store/tab/games': typeof StoreTabGamesRoute
|
||||||
|
'/store/tab': typeof StoreTabIndexRoute
|
||||||
|
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/settings': typeof SettingsRouteRouteWithChildren
|
'/settings': typeof SettingsRouteRouteWithChildren
|
||||||
|
'/store/tab': typeof StoreTabRouteRouteWithChildren
|
||||||
'/collection/$id': typeof CollectionIdRoute
|
'/collection/$id': typeof CollectionIdRoute
|
||||||
'/settings/about': typeof SettingsAboutRoute
|
'/settings/about': typeof SettingsAboutRoute
|
||||||
'/settings/accounts': typeof SettingsAccountsRoute
|
'/settings/accounts': typeof SettingsAccountsRoute
|
||||||
'/settings/directories': typeof SettingsDirectoriesRoute
|
'/settings/directories': typeof SettingsDirectoriesRoute
|
||||||
'/settings/emulators': typeof SettingsEmulatorsRoute
|
'/settings/emulators': typeof SettingsEmulatorsRoute
|
||||||
'/settings/interface': typeof SettingsInterfaceRoute
|
'/settings/interface': typeof SettingsInterfaceRoute
|
||||||
|
'/embedded/$source/$id': typeof EmbeddedSourceIdRoute
|
||||||
'/game/$source/$id': typeof GameSourceIdRoute
|
'/game/$source/$id': typeof GameSourceIdRoute
|
||||||
'/launcher/$source/$id': typeof LauncherSourceIdRoute
|
'/launcher/$source/$id': typeof LauncherSourceIdRoute
|
||||||
'/platform/$source/$id': typeof PlatformSourceIdRoute
|
'/platform/$source/$id': typeof PlatformSourceIdRoute
|
||||||
|
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
|
||||||
|
'/store/tab/games': typeof StoreTabGamesRoute
|
||||||
|
'/store/tab/': typeof StoreTabIndexRoute
|
||||||
|
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
|
| '/store/tab'
|
||||||
| '/collection/$id'
|
| '/collection/$id'
|
||||||
| '/settings/about'
|
| '/settings/about'
|
||||||
| '/settings/accounts'
|
| '/settings/accounts'
|
||||||
| '/settings/directories'
|
| '/settings/directories'
|
||||||
| '/settings/emulators'
|
| '/settings/emulators'
|
||||||
| '/settings/interface'
|
| '/settings/interface'
|
||||||
|
| '/embedded/$source/$id'
|
||||||
| '/game/$source/$id'
|
| '/game/$source/$id'
|
||||||
| '/launcher/$source/$id'
|
| '/launcher/$source/$id'
|
||||||
| '/platform/$source/$id'
|
| '/platform/$source/$id'
|
||||||
|
| '/store/tab/emulators'
|
||||||
|
| '/store/tab/games'
|
||||||
|
| '/store/tab/'
|
||||||
|
| '/store/details/emulator/$id'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
|
|
@ -141,31 +200,45 @@ export interface FileRouteTypes {
|
||||||
| '/settings/directories'
|
| '/settings/directories'
|
||||||
| '/settings/emulators'
|
| '/settings/emulators'
|
||||||
| '/settings/interface'
|
| '/settings/interface'
|
||||||
|
| '/embedded/$source/$id'
|
||||||
| '/game/$source/$id'
|
| '/game/$source/$id'
|
||||||
| '/launcher/$source/$id'
|
| '/launcher/$source/$id'
|
||||||
| '/platform/$source/$id'
|
| '/platform/$source/$id'
|
||||||
|
| '/store/tab/emulators'
|
||||||
|
| '/store/tab/games'
|
||||||
|
| '/store/tab'
|
||||||
|
| '/store/details/emulator/$id'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
|
| '/store/tab'
|
||||||
| '/collection/$id'
|
| '/collection/$id'
|
||||||
| '/settings/about'
|
| '/settings/about'
|
||||||
| '/settings/accounts'
|
| '/settings/accounts'
|
||||||
| '/settings/directories'
|
| '/settings/directories'
|
||||||
| '/settings/emulators'
|
| '/settings/emulators'
|
||||||
| '/settings/interface'
|
| '/settings/interface'
|
||||||
|
| '/embedded/$source/$id'
|
||||||
| '/game/$source/$id'
|
| '/game/$source/$id'
|
||||||
| '/launcher/$source/$id'
|
| '/launcher/$source/$id'
|
||||||
| '/platform/$source/$id'
|
| '/platform/$source/$id'
|
||||||
|
| '/store/tab/emulators'
|
||||||
|
| '/store/tab/games'
|
||||||
|
| '/store/tab/'
|
||||||
|
| '/store/details/emulator/$id'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
SettingsRouteRoute: typeof SettingsRouteRouteWithChildren
|
SettingsRouteRoute: typeof SettingsRouteRouteWithChildren
|
||||||
|
StoreTabRouteRoute: typeof StoreTabRouteRouteWithChildren
|
||||||
CollectionIdRoute: typeof CollectionIdRoute
|
CollectionIdRoute: typeof CollectionIdRoute
|
||||||
|
EmbeddedSourceIdRoute: typeof EmbeddedSourceIdRoute
|
||||||
GameSourceIdRoute: typeof GameSourceIdRoute
|
GameSourceIdRoute: typeof GameSourceIdRoute
|
||||||
LauncherSourceIdRoute: typeof LauncherSourceIdRoute
|
LauncherSourceIdRoute: typeof LauncherSourceIdRoute
|
||||||
PlatformSourceIdRoute: typeof PlatformSourceIdRoute
|
PlatformSourceIdRoute: typeof PlatformSourceIdRoute
|
||||||
|
StoreDetailsEmulatorIdRoute: typeof StoreDetailsEmulatorIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
|
|
@ -226,6 +299,34 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof CollectionIdRouteImport
|
preLoaderRoute: typeof CollectionIdRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/store/tab': {
|
||||||
|
id: '/store/tab'
|
||||||
|
path: '/store/tab'
|
||||||
|
fullPath: '/store/tab'
|
||||||
|
preLoaderRoute: typeof StoreTabRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/store/tab/': {
|
||||||
|
id: '/store/tab/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/store/tab/'
|
||||||
|
preLoaderRoute: typeof StoreTabIndexRouteImport
|
||||||
|
parentRoute: typeof StoreTabRouteRoute
|
||||||
|
}
|
||||||
|
'/store/tab/games': {
|
||||||
|
id: '/store/tab/games'
|
||||||
|
path: '/games'
|
||||||
|
fullPath: '/store/tab/games'
|
||||||
|
preLoaderRoute: typeof StoreTabGamesRouteImport
|
||||||
|
parentRoute: typeof StoreTabRouteRoute
|
||||||
|
}
|
||||||
|
'/store/tab/emulators': {
|
||||||
|
id: '/store/tab/emulators'
|
||||||
|
path: '/emulators'
|
||||||
|
fullPath: '/store/tab/emulators'
|
||||||
|
preLoaderRoute: typeof StoreTabEmulatorsRouteImport
|
||||||
|
parentRoute: typeof StoreTabRouteRoute
|
||||||
|
}
|
||||||
'/platform/$source/$id': {
|
'/platform/$source/$id': {
|
||||||
id: '/platform/$source/$id'
|
id: '/platform/$source/$id'
|
||||||
path: '/platform/$source/$id'
|
path: '/platform/$source/$id'
|
||||||
|
|
@ -247,6 +348,20 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof GameSourceIdRouteImport
|
preLoaderRoute: typeof GameSourceIdRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/embedded/$source/$id': {
|
||||||
|
id: '/embedded/$source/$id'
|
||||||
|
path: '/embedded/$source/$id'
|
||||||
|
fullPath: '/embedded/$source/$id'
|
||||||
|
preLoaderRoute: typeof EmbeddedSourceIdRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/store/details/emulator/$id': {
|
||||||
|
id: '/store/details/emulator/$id'
|
||||||
|
path: '/store/details/emulator/$id'
|
||||||
|
fullPath: '/store/details/emulator/$id'
|
||||||
|
preLoaderRoute: typeof StoreDetailsEmulatorIdRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,13 +385,32 @@ const SettingsRouteRouteWithChildren = SettingsRouteRoute._addFileChildren(
|
||||||
SettingsRouteRouteChildren,
|
SettingsRouteRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface StoreTabRouteRouteChildren {
|
||||||
|
StoreTabEmulatorsRoute: typeof StoreTabEmulatorsRoute
|
||||||
|
StoreTabGamesRoute: typeof StoreTabGamesRoute
|
||||||
|
StoreTabIndexRoute: typeof StoreTabIndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const StoreTabRouteRouteChildren: StoreTabRouteRouteChildren = {
|
||||||
|
StoreTabEmulatorsRoute: StoreTabEmulatorsRoute,
|
||||||
|
StoreTabGamesRoute: StoreTabGamesRoute,
|
||||||
|
StoreTabIndexRoute: StoreTabIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const StoreTabRouteRouteWithChildren = StoreTabRouteRoute._addFileChildren(
|
||||||
|
StoreTabRouteRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
SettingsRouteRoute: SettingsRouteRouteWithChildren,
|
SettingsRouteRoute: SettingsRouteRouteWithChildren,
|
||||||
|
StoreTabRouteRoute: StoreTabRouteRouteWithChildren,
|
||||||
CollectionIdRoute: CollectionIdRoute,
|
CollectionIdRoute: CollectionIdRoute,
|
||||||
|
EmbeddedSourceIdRoute: EmbeddedSourceIdRoute,
|
||||||
GameSourceIdRoute: GameSourceIdRoute,
|
GameSourceIdRoute: GameSourceIdRoute,
|
||||||
LauncherSourceIdRoute: LauncherSourceIdRoute,
|
LauncherSourceIdRoute: LauncherSourceIdRoute,
|
||||||
PlatformSourceIdRoute: PlatformSourceIdRoute,
|
PlatformSourceIdRoute: PlatformSourceIdRoute,
|
||||||
|
StoreDetailsEmulatorIdRoute: StoreDetailsEmulatorIdRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|
|
||||||
|
|
@ -464,7 +464,7 @@ const assets = new Set<string>([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Store basePath resolved from Vite config
|
// Store basePath resolved from Vite config
|
||||||
const BASE_PATH = "./";
|
const BASE_PATH = "/";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,18 @@
|
||||||
--breakpoint-sm: 0px;
|
--breakpoint-sm: 0px;
|
||||||
--breakpoint-md: 1280px;
|
--breakpoint-md: 1280px;
|
||||||
--page-scroll-bg: transparent;
|
--page-scroll-bg: transparent;
|
||||||
|
--animation-size: 1;
|
||||||
|
|
||||||
--animate-wiggle: wiggle 0.3s ease-in-out 1;
|
--animate-wiggle: wiggle 0.3s ease-in-out 1;
|
||||||
--animate-rotate: rotate 0.3s ease-in-out 1 0.2s;
|
--animate-rotate-instant: rotate 0.3s ease-in-out 1;
|
||||||
--animate-rotate-small: rotate-small 0.3s ease-in-out 1 0.2s;
|
--animate-rotate: rotate 0.3s ease-in-out 1 200ms;
|
||||||
|
--animate-rotate-small: rotate-small 0.3s ease-in-out 1 200ms;
|
||||||
--animate-scale: scale 0.3s ease-in-out 1;
|
--animate-scale: scale 0.3s ease-in-out 1;
|
||||||
--animate-slide-up: slide-up 0.2s 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-delayed: scale 0.3s ease-in-out 1 100ms;
|
||||||
--animate-scale-small: scale-small 0.3s ease-in-out 1;
|
--animate-scale-small: scale-small 0.3s ease-in-out 1;
|
||||||
--animate-fade-out: fade-out 0.3s ease-out 1;
|
--animate-fade-out: fade-out 0.3s ease-out 1;
|
||||||
|
--animate-fade-in: fade-out 0.6s ease-out 1 reverse forwards;
|
||||||
--animate-bg-zoom: zoom-in-scale 0.6s ease-out 1 forwards;
|
--animate-bg-zoom: zoom-in-scale 0.6s ease-out 1 forwards;
|
||||||
--animate-bg-zoom-big: zoom-in-scale-big 0.6s ease-out 1 forwards;
|
--animate-bg-zoom-big: zoom-in-scale-big 0.6s ease-out 1 forwards;
|
||||||
--animate-bg-zoom-scroll: zoom-in-bg 0.6s ease-out 1 forwards;
|
--animate-bg-zoom-scroll: zoom-in-bg 0.6s ease-out 1 forwards;
|
||||||
|
|
@ -116,11 +119,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
25% {
|
25% {
|
||||||
transform: rotate(1deg);
|
transform: rotate(calc(1deg * var(--animation-size)));
|
||||||
}
|
}
|
||||||
|
|
||||||
75% {
|
75% {
|
||||||
transform: rotate(-1deg);
|
transform: rotate(calc(-1deg * var(--animation-size)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,8 +200,88 @@ body {
|
||||||
font-family: 'Alan Sans', sans-serif;
|
font-family: 'Alan Sans', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility focusable-* {
|
||||||
|
--focus-ring-color: --value(--color-*);
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility hide-scrollbar {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility no-scrollbar {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility scrollbar-* {
|
||||||
|
scrollbar-width: --value(integer);
|
||||||
|
scrollbar-width: --value([integer]);
|
||||||
|
scrollbar-width: --value("none");
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility animation-size-* {
|
||||||
|
--animation-size: --value(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility animation-delay-* {
|
||||||
|
--tw-animation-delay: --value([*]);
|
||||||
|
--tw-animation-delay: --value(integer)ms;
|
||||||
|
animation-delay: var(--tw-animation-delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@custom-variant focused {
|
||||||
|
|
||||||
|
&:where([data-active-control="gamepad"] &[data-focused=true]),
|
||||||
|
&:where([data-active-control="keyboard"] &[data-focused=true]) {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@custom-variant focused-child {
|
||||||
|
|
||||||
|
&:where([data-active-control="gamepad"] &:has([data-focused=true])),
|
||||||
|
&:where([data-active-control="keyboard"] &:has([data-focused=true])) {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@custom-variant in-focused {
|
||||||
|
|
||||||
|
&:where([data-active-control="gamepad"] [data-focused=true] *),
|
||||||
|
&:where([data-active-control="keyboard"] [data-focused=true] *) {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@custom-variant control-mouse (&:where([data-active-control="mouse"] *));
|
||||||
|
@custom-variant control-touch (&:where([data-active-control="touch"] *));
|
||||||
|
@custom-variant control-keyboard (&:where([data-active-control="keyboard"] *));
|
||||||
|
@custom-variant control-gamepad (&:where([data-active-control="gamepad"] *));
|
||||||
|
@custom-variant control-pointer (&:where([data-active-control="mouse"] *), &:where([data-active-control="touch"] *));
|
||||||
|
@custom-variant mobile (&:where([data-device="mobile"] *));
|
||||||
|
|
||||||
|
@container scroll-state(stuck: top) {
|
||||||
|
.sticky-header {
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
|
||||||
|
.focusable {
|
||||||
|
--focus-ring-color: --value(--color-accent);
|
||||||
|
@apply md:ring-14 sm:ring-8 ring-transparent transition-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-active-control="keyboard"] .group-focusable[data-focused="true"] .focusable,
|
||||||
|
[data-active-control="gamepad"] .group-focusable[data-focused="true"] .focusable,
|
||||||
|
[data-active-control="keyboard"] .focusable[data-focused=true],
|
||||||
|
[data-active-control="gamepad"] .focusable[data-focused=true],
|
||||||
|
[data-active-control="mouse"] .group-focusable:hover .focusable-hover,
|
||||||
|
[data-active-control="mouse"] .focusable-hover:hover {
|
||||||
|
@apply md:ring-7 sm:ring-4 ring-(--focus-ring-color);
|
||||||
|
}
|
||||||
|
|
||||||
.background {
|
.background {
|
||||||
-webkit-backface-visibility: hidden;
|
-webkit-backface-visibility: hidden;
|
||||||
-webkit-perspective: 1000;
|
-webkit-perspective: 1000;
|
||||||
|
|
@ -210,6 +293,17 @@ body {
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container-scroll {
|
||||||
|
container-type: scroll-state;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.game-card {
|
.game-card {
|
||||||
@apply rounded-2xl;
|
@apply rounded-2xl;
|
||||||
}
|
}
|
||||||
|
|
@ -245,8 +339,52 @@ body {
|
||||||
@apply grid pb-4;
|
@apply grid pb-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-scrollbar {
|
.bg-gradient {
|
||||||
scrollbar-width: none;
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
--bg-gradient-opacity: 15%;
|
||||||
|
|
||||||
|
background:
|
||||||
|
radial-gradient(at 10% 20%, rgb(from var(--color-error) r g b / var(--bg-gradient-opacity)), transparent 60%),
|
||||||
|
radial-gradient(at 80% 30%, rgb(from var(--color-info) r g b / var(--bg-gradient-opacity)), transparent 60%),
|
||||||
|
radial-gradient(at 40% 90%, rgb(from var(--color-success) r g b / var(--bg-gradient-opacity)), transparent 60%),
|
||||||
|
radial-gradient(at 90% 80%, rgb(from var(--color-warning) r g b / var(--bg-gradient-opacity)), transparent 60%);
|
||||||
|
|
||||||
|
background-blend-mode: lighten;
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
@apply mobile:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-noise {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
background-image: url("https://momentsingraphics.de/Media/BlueNoise/BlueNoise470.png");
|
||||||
|
mix-blend-mode: color-dodge;
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-back {
|
||||||
|
|
||||||
|
--bg-opacity: 90%;
|
||||||
|
background:
|
||||||
|
radial-gradient(at 10% 20%, color-mix(in srgb, var(--color-secondary), transparent var(--bg-opacity)), transparent 60%),
|
||||||
|
radial-gradient(at 80% 30%, color-mix(in srgb, var(--color-info), transparent var(--bg-opacity)), transparent 60%),
|
||||||
|
radial-gradient(at 40% 90%, color-mix(in srgb, var(--color-success), transparent var(--bg-opacity)), transparent 60%),
|
||||||
|
radial-gradient(at 90% 80%, color-mix(in srgb, var(--color-warning), transparent var(--bg-opacity)), transparent 60%);
|
||||||
|
|
||||||
|
background-blend-mode: screen;
|
||||||
|
background-repeat: repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll-bg {
|
||||||
|
to {
|
||||||
|
transform: translateY(800px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::view-transition-old(.game-card),
|
&::view-transition-old(.game-card),
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import "./scripts/gamepads";
|
||||||
import "./scripts/windowEvents";
|
import "./scripts/windowEvents";
|
||||||
import { client as rommClient } from "../clients/romm/client.gen";
|
import { client as rommClient } from "../clients/romm/client.gen";
|
||||||
import "./scripts/spatialNavigation";
|
import "./scripts/spatialNavigation";
|
||||||
|
import NotFound from "./components/NotFound";
|
||||||
|
import Error from "./components/Error";
|
||||||
|
|
||||||
const hashHistory = createHashHistory({});
|
const hashHistory = createHashHistory({});
|
||||||
|
|
||||||
|
|
@ -38,15 +40,9 @@ export const Router = createRouter({
|
||||||
defaultPreload: "intent",
|
defaultPreload: "intent",
|
||||||
context: { queryClient },
|
context: { queryClient },
|
||||||
scrollRestoration: false,
|
scrollRestoration: false,
|
||||||
defaultNotFoundComponent: () =>
|
defaultNotFoundComponent: NotFound,
|
||||||
{
|
defaultPendingMs: 300,
|
||||||
return (
|
defaultErrorComponent: Error
|
||||||
<div>
|
|
||||||
<p> {window.location.href} Not found!</p>
|
|
||||||
<Link to="/">Go home</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register things for typesafety
|
// Register things for typesafety
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
||||||
import { RouterContext } from "..";
|
import { RouterContext } from "..";
|
||||||
import Notifications from "../components/Notifications";
|
import Notifications from "../components/Notifications";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { mobileCheck, useLocalSetting } from "../scripts/utils";
|
import { mobileCheck, useLocalSetting } from "../scripts/utils";
|
||||||
|
import useActiveControl from "../scripts/gamepads";
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||||
component: RootComponent,
|
component: RootComponent,
|
||||||
|
|
@ -14,18 +13,19 @@ function RootComponent ()
|
||||||
{
|
{
|
||||||
const isMobile = mobileCheck();
|
const isMobile = mobileCheck();
|
||||||
const theme = useLocalSetting('theme');
|
const theme = useLocalSetting('theme');
|
||||||
|
const { control } = useActiveControl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-theme={theme === 'auto' ? undefined : theme} className="w-screen h-screen overflow-hidden">
|
<div data-theme={theme === 'auto' ? undefined : theme} data-device={isMobile ? 'mobile' : ''} data-active-control={control} className="w-screen h-screen overflow-hidden">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<Toaster containerStyle={{ viewTimelineName: 'toasters' }} />
|
<Toaster containerStyle={{ viewTimelineName: 'toasters' }} />
|
||||||
{import.meta.env.DEV && !isMobile &&
|
{/*import.meta.env.DEV && !isMobile &&
|
||||||
<>
|
<>
|
||||||
<TanStackRouterDevtools position="top-left" />
|
<TanStackRouterDevtools position="top-left" />
|
||||||
<ReactQueryDevtools buttonPosition="top-right" />
|
<ReactQueryDevtools buttonPosition="top-right" />
|
||||||
</>
|
</>
|
||||||
}
|
*/}
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
import { useSessionStorage } from 'usehooks-ts';
|
|
||||||
import { CollectionsDetail } from '../components/CollectionsDetail';
|
import { CollectionsDetail } from '../components/CollectionsDetail';
|
||||||
import { getCollectionApiCollectionsIdGetOptions, getRomsApiRomsGetOptions } from '../../clients/romm/@tanstack/react-query.gen';
|
import { getCollectionApiCollectionsIdGetOptions, getRomsApiRomsGetOptions } from '@clients/romm/@tanstack/react-query.gen';
|
||||||
import { DefaultRommStaleTime } from '../../shared/constants';
|
import { DefaultRommStaleTime } from '@shared/constants';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { AnimatedBackgroundContext } from '../scripts/contexts';
|
||||||
|
|
||||||
export const Route = createFileRoute('/collection/$id')({
|
export const Route = createFileRoute('/collection/$id')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|
@ -17,12 +18,9 @@ function RouteComponent ()
|
||||||
{
|
{
|
||||||
const { id } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
const { data: collection } = useQuery({ ...getCollectionApiCollectionsIdGetOptions({ path: { id: Number(id) } }) });
|
const { data: collection } = useQuery({ ...getCollectionApiCollectionsIdGetOptions({ path: { id: Number(id) } }) });
|
||||||
const [, setBackground] = useSessionStorage<string | undefined>(
|
const animatedBgContext = useContext(AnimatedBackgroundContext);
|
||||||
"home-background",
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollectionsDetail setBackground={setBackground} title={<div className="divider font-semibold text-2xl">{collection?.name}</div>} filters={{ collection_id: Number(id) }} />
|
<CollectionsDetail setBackground={animatedBgContext.setBackground} title={<div className="divider font-semibold text-2xl">{collection?.name}</div>} filters={{ collection_id: Number(id) }} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
185
src/mainview/routes/embedded.$source.$id.tsx
Normal file
185
src/mainview/routes/embedded.$source.$id.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
import { EMULATORJS_URL, RPC_URL, SERVER_URL } from '@/shared/constants';
|
||||||
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
import { gameQuery } from '../scripts/queries';
|
||||||
|
import { zodValidator } from '@tanstack/zod-adapter';
|
||||||
|
import z from 'zod';
|
||||||
|
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||||
|
import { Router } from '..';
|
||||||
|
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
|
import { Button, ButtonStyle } from '../components/options/Button';
|
||||||
|
import { DoorOpen, Home, RefreshCw, Undo } from 'lucide-react';
|
||||||
|
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||||
|
import Shortcuts from '../components/Shortcuts';
|
||||||
|
import { useEventListener, useTimeout } from 'usehooks-ts';
|
||||||
|
import { GetFocusedElement, useGlobalFocus } from '../scripts/spatialNavigation';
|
||||||
|
import useActiveControl from '../scripts/gamepads';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import { HeaderAccounts, HeaderStatusBar } from '../components/Header';
|
||||||
|
import { RoundButton } from '../components/RoundButton';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/embedded/$source/$id')({
|
||||||
|
component: RouteComponent,
|
||||||
|
loader: async (ctx) =>
|
||||||
|
{
|
||||||
|
const data = await ctx.context.queryClient.fetchQuery(gameQuery(ctx.params.source, ctx.params.id));
|
||||||
|
return { data };
|
||||||
|
},
|
||||||
|
validateSearch: zodValidator(z.record(z.string(), z.string().optional().nullable()))
|
||||||
|
});
|
||||||
|
|
||||||
|
function OverlayButton (data: {
|
||||||
|
id: string,
|
||||||
|
style: ButtonStyle,
|
||||||
|
tooltip: string, setTooltip: (tooltip: string) => void,
|
||||||
|
className?: string;
|
||||||
|
children?: any;
|
||||||
|
} & InteractParams)
|
||||||
|
{
|
||||||
|
return <div className="tooltip tooltip-bottom" data-tip={data.tooltip}>
|
||||||
|
<RoundButton external onFocus={() => data.setTooltip(data.tooltip)} style={data.style} className={twMerge("", data.className)} id={data.id} onAction={data.onAction} >
|
||||||
|
{data.children}
|
||||||
|
</RoundButton>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Overlay (data: {
|
||||||
|
open: boolean;
|
||||||
|
iframeRef: RefObject<HTMLIFrameElement | null>;
|
||||||
|
close: () => void;
|
||||||
|
goBack: () => void;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
const { ref, focusSelf, focusKey } = useFocusable({ focusable: data.open, focusKey: 'overlay', forceFocus: true, isFocusBoundary: true });
|
||||||
|
const [tooltip, setTooltip] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
useShortcuts(focusKey, () => data.open ? [{ label: 'Return', button: GamePadButtonCode.B, action: data.close }] : [], [data.open, data.close]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if (data.open)
|
||||||
|
{
|
||||||
|
focusSelf();
|
||||||
|
}
|
||||||
|
}, [data.open]);
|
||||||
|
|
||||||
|
const { isPointer } = useActiveControl();
|
||||||
|
const handleEvent = (type: string, value?: any) => data.iframeRef.current?.contentWindow?.postMessage({ type, data: value });
|
||||||
|
|
||||||
|
return <div data-open={data.open} className='flex group w-full flex-col gap-2 transition-opacity p-4 not-data-[open=true]:pointer-events-none not-data-[open=true]:opacity-0'>
|
||||||
|
<div className='grid grid-cols-3 justify-between items-start'>
|
||||||
|
<div className='flex justify-start'>
|
||||||
|
<HeaderAccounts />
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
<ul ref={ref} className='flex rounded-4xl bg-base-100 justify-end gap-2 p-4 group-data-[open=true]:animate-scale'>
|
||||||
|
<FocusContext value={focusKey}>
|
||||||
|
<OverlayButton id="return" style='primary' tooltip='Return' setTooltip={setTooltip} onAction={data.close} ><Undo /></OverlayButton>
|
||||||
|
<OverlayButton id="restart" style='secondary' tooltip='Restart' setTooltip={setTooltip} onAction={() =>
|
||||||
|
{
|
||||||
|
data.close();
|
||||||
|
handleEvent('restart');
|
||||||
|
}} ><RefreshCw /></OverlayButton>
|
||||||
|
<OverlayButton id="exit" style='warning' tooltip='Exit' setTooltip={setTooltip} onAction={data.goBack} ><DoorOpen /></OverlayButton>
|
||||||
|
</FocusContext>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-end'>
|
||||||
|
<HeaderStatusBar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
{!!tooltip && data.open && !isPointer && <div className='bg-accent text-accent-content rounded-full font-semibold py-1 px-4'>{tooltip}</div>}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Frame (data: { ref: RefObject<HTMLIFrameElement | null>; })
|
||||||
|
{
|
||||||
|
const { ref } = useFocusable({ focusKey: 'frame' });
|
||||||
|
const { data: game } = Route.useLoaderData();
|
||||||
|
|
||||||
|
const search = Route.useSearch();
|
||||||
|
search['gameName'] = game.name;
|
||||||
|
search['backgroundImage'] = `${RPC_URL(__HOST__)}${game.path_cover}`;
|
||||||
|
search['backgroundBlur'] = "true";
|
||||||
|
|
||||||
|
if (!__PUBLIC__)
|
||||||
|
{
|
||||||
|
search['threads'] = "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = Object.entries(search)
|
||||||
|
.filter(kvp => kvp[1] !== null && kvp[1] !== undefined)
|
||||||
|
.map(kvp => `${kvp[0]}=${encodeURIComponent(kvp[1]!)}`).join('&');
|
||||||
|
|
||||||
|
return <iframe ref={r =>
|
||||||
|
{
|
||||||
|
ref.current = r;
|
||||||
|
data.ref.current = r;
|
||||||
|
}}
|
||||||
|
allow='fullscreen; cross-origin-isolated'
|
||||||
|
className='absolute w-full h-full transition-[padding]' src={
|
||||||
|
__PUBLIC__ ? `${SERVER_URL(__HOST__)}/emulatorjs/?${params}` : `${EMULATORJS_URL(__HOST__)}/?${params}`
|
||||||
|
}></iframe>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RouteComponent ()
|
||||||
|
{
|
||||||
|
const { ref, focusSelf, focusKey } = useFocusable({
|
||||||
|
focusKey: 'emulatorjs',
|
||||||
|
preferredChildFocusKey: 'frame',
|
||||||
|
forceFocus: true
|
||||||
|
});
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
const [overlayOpen, setOverlayOpen] = useState(false);
|
||||||
|
const { source, id } = Route.useParams();
|
||||||
|
|
||||||
|
function HandleGoBack ()
|
||||||
|
{
|
||||||
|
Router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
useEventListener('message', e =>
|
||||||
|
{
|
||||||
|
if (e.data.type === 'exit')
|
||||||
|
{
|
||||||
|
HandleGoBack();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useShortcuts(focusKey, () => [{
|
||||||
|
button: GamePadButtonCode.Steam, action: () =>
|
||||||
|
{
|
||||||
|
setOverlayOpen(!overlayOpen);
|
||||||
|
}
|
||||||
|
}], [overlayOpen, setOverlayOpen]);
|
||||||
|
|
||||||
|
const setPaused = (paused: boolean) =>
|
||||||
|
{
|
||||||
|
if (paused) iframeRef.current?.contentWindow?.postMessage({ type: 'pause', data: true });
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// we want to prevent input from closing the overlay spilling
|
||||||
|
setTimeout(() => iframeRef.current?.contentWindow?.postMessage({ type: 'pause', data: false }), 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => setPaused(overlayOpen), [overlayOpen]);
|
||||||
|
const { shortcuts } = useShortcutContext();
|
||||||
|
useEffect(() => { if (!overlayOpen) focusSelf(); }, [overlayOpen]);
|
||||||
|
function handleClose ()
|
||||||
|
{
|
||||||
|
setOverlayOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div ref={ref} className='absolute w-full h-full'>
|
||||||
|
<FocusContext value={focusKey}>
|
||||||
|
<Frame ref={iframeRef} />
|
||||||
|
<div className='flex fixed left-0 right-0 top-0'>
|
||||||
|
<Overlay iframeRef={iframeRef} goBack={HandleGoBack} open={overlayOpen} close={handleClose} />
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-end fixed bottom-4 right-4 left-4 z-10'>
|
||||||
|
<Shortcuts shortcuts={shortcuts} />
|
||||||
|
</div>
|
||||||
|
</FocusContext>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute, ErrorComponentProps } from "@tanstack/react-router";
|
||||||
import { FrontEndGameTypeDetailed, GameInstallProgress, GameStatusType, RPC_URL } from "@shared/constants";
|
import { CommandEntry, FrontEndGameTypeDetailed, GameInstallProgress, GameStatusType, RPC_URL } from "@shared/constants";
|
||||||
import { twJoin, twMerge } from "tailwind-merge";
|
import { twJoin, twMerge } from "tailwind-merge";
|
||||||
import { JSX, RefObject, useEffect, useRef, useState } from "react";
|
import { JSX, RefObject, useEffect, useRef, useState } from "react";
|
||||||
import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
|
@ -17,32 +17,124 @@ import { ContextDialog, ContextList, DialogEntry } from "../../components/Contex
|
||||||
import Shortcuts from "../../components/Shortcuts";
|
import Shortcuts from "../../components/Shortcuts";
|
||||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||||
import { gameQuery } from "@/mainview/scripts/queries";
|
import { gameQuery } from "@/mainview/scripts/queries";
|
||||||
|
import Screenshots from "@/mainview/components/Screenshots";
|
||||||
|
import { delay, useSticky, useStickyDataAttr } from "@/mainview/scripts/utils";
|
||||||
|
import useActiveControl from "@/mainview/scripts/gamepads";
|
||||||
|
|
||||||
export const Route = createFileRoute("/game/$source/$id")({
|
export const Route = createFileRoute("/game/$source/$id")({
|
||||||
loader: ({ params, context }) =>
|
loader: async ({ params, context }) =>
|
||||||
{
|
{
|
||||||
context.queryClient.prefetchQuery(gameQuery(params.source, Number(params.id)));
|
const data = await context.queryClient.fetchQuery(gameQuery(params.source, params.id));
|
||||||
|
return { data };
|
||||||
},
|
},
|
||||||
component: GameDetailsUI,
|
component: GameDetailsUI,
|
||||||
pendingComponent: GameDetailsUIPending,
|
pendingComponent: GameDetailsUIPending,
|
||||||
|
errorComponent: Error
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function Error (data: ErrorComponentProps)
|
||||||
|
{
|
||||||
|
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details-error", preferredChildFocusKey: "main-details" });
|
||||||
|
|
||||||
|
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||||
|
const { shortcuts } = useShortcutContext();
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
focusSelf();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <AnimatedBackground ref={ref} backgroundKey="game-details">
|
||||||
|
<div className="relative z-10 h-full">
|
||||||
|
<FocusContext value={focusKey}>
|
||||||
|
<div className="h-0" />
|
||||||
|
<div className="fixed group top-0 left-0 right-0 bg-base-100/40 group p-2 z-15 transition-colors data-stuck:backdrop-blur-3xl">
|
||||||
|
<HeaderUI />
|
||||||
|
</div>
|
||||||
|
<div className="absolute w-full flex flex-col justify-center items-center h-full overflow-hidden bg-linear-to-t from-base-100 to-base-100/40">
|
||||||
|
<div className="flex gap-2 items-center text-4xl text-error"><TriangleAlert className="size-12" /> {data.error.message}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-base-200">
|
||||||
|
|
||||||
|
<footer className="fixed left-0 right-0 bottom-0 w-full p-4 flex items-center justify-end z-10">
|
||||||
|
<Shortcuts shortcuts={shortcuts} />
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</FocusContext>
|
||||||
|
</div>
|
||||||
|
</AnimatedBackground>;
|
||||||
|
}
|
||||||
|
|
||||||
function GameDetailsUIPending ()
|
function GameDetailsUIPending ()
|
||||||
{
|
{
|
||||||
return <AnimatedBackground>
|
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details-error", preferredChildFocusKey: "main-details" });
|
||||||
<div className="flex flex-col p-2 px-3 w-full h-full">
|
|
||||||
|
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||||
|
const { shortcuts } = useShortcutContext();
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
focusSelf();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <AnimatedBackground ref={ref} backgroundKey="game-details">
|
||||||
|
<div className="z-10">
|
||||||
|
<FocusContext value={focusKey}>
|
||||||
|
<div className="h-0" />
|
||||||
|
<div className="sticky group top-0 bg-base-100/40 group p-2 z-15 transition-colors data-stuck:backdrop-blur-3xl">
|
||||||
<HeaderUI />
|
<HeaderUI />
|
||||||
<div className="flex flex-col justify-center items-center grow">
|
|
||||||
<span className="loading loading-dots loading-xl"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col h-[80vh] overflow-hidden bg-linear-to-t from-base-100 to-base-100/40">
|
||||||
|
<main ref={ref} className="flex p-3 flex-col flex-1 min-h-0">
|
||||||
|
<section className="flex portrait:flex-col my-4 sm:p-0 md:px-12 md:pb-8 pt-4 sm:gap-8 md:gap-12 portrait:w-full h-full min-h-0 rounded-4xl flex-1 z-0 sm:text-sm md:text-base">
|
||||||
|
<div className="flex gap-6 overflow-hidden bg-base-100 justify-end portrait:w-full rounded-3xl aspect-3/4 portrait:h-24 p-4">
|
||||||
|
<div className="skeleton w-full h-full"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-2 flex flex-col sm:gap-1 md:gap-6 sm:pt-2 md:pt-16 min-h-0">
|
||||||
|
<div className="flex flex-wrap sm:gap-4 md:gap-6 shrink-0">
|
||||||
|
<Detail icon={<Clock />} ></Detail>
|
||||||
|
<Detail icon={<div className="skeleton size-6" />} ><div className="skeleton h-4 w-32"></div></Detail>
|
||||||
|
<Detail icon={
|
||||||
|
<Store />
|
||||||
|
} >
|
||||||
|
|
||||||
|
</Detail>
|
||||||
|
</div>
|
||||||
|
<div className="md:hidden divider divider-vertical m-0"></div>
|
||||||
|
<div className="text-base-content/80 flex-1 min-h-0 leading-relaxed grow text-wrap whitespace-break-spaces text-ellipsis overflow-hidden text-lg">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<div className="bg-base-200">
|
||||||
|
<div className="divider m-0 pb-12"><div className="flex items-center gap-3 opacity-60"><Image className="sm:size-4 md:size-6" />Screenshots</div></div>
|
||||||
|
<div className="flex flex-col w-full z-0 min-h-0">
|
||||||
|
<div
|
||||||
|
className="flex gap-6 px-16 py-2 sm:overflow-scroll md:overflow-hidden no-scrollbar justify-center-safe"
|
||||||
|
>
|
||||||
|
{Array.from({ length: 5 }).map((s, i) => <div key={i} className="skeleton h-64 w-lg"></div>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer className="fixed left-0 right-0 bottom-0 w-full p-4 flex items-center justify-end z-10">
|
||||||
|
<Shortcuts shortcuts={shortcuts} />
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</FocusContext>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedBackground>;
|
</AnimatedBackground>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function HandleGoBack ()
|
function HandleGoBack ()
|
||||||
{
|
{
|
||||||
const source = PopSource('details');
|
const { to, search } = PopSource('details');
|
||||||
Router.navigate({ to: source ?? '/', viewTransition: { types: ['zoom-out'] } });
|
Router.navigate({ to: to ?? '/', viewTransition: { types: ['zoom-out'] }, search });
|
||||||
}
|
}
|
||||||
|
|
||||||
function Details (data: { mainAreaRef: RefObject<HTMLDivElement | null>, game?: FrontEndGameTypeDetailed; })
|
function Details (data: { mainAreaRef: RefObject<HTMLDivElement | null>, game?: FrontEndGameTypeDetailed; })
|
||||||
|
|
@ -50,7 +142,7 @@ function Details (data: { mainAreaRef: RefObject<HTMLDivElement | null>, game?:
|
||||||
const { ref, focusKey } = useFocusable({
|
const { ref, focusKey } = useFocusable({
|
||||||
focusKey: 'main-details', onFocus: () =>
|
focusKey: 'main-details', onFocus: () =>
|
||||||
{
|
{
|
||||||
data.mainAreaRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
data.mainAreaRef.current?.scrollIntoView({ block: 'end', behavior: 'smooth' });
|
||||||
},
|
},
|
||||||
preferredChildFocusKey: "play-btn",
|
preferredChildFocusKey: "play-btn",
|
||||||
saveLastFocusedChild: false
|
saveLastFocusedChild: false
|
||||||
|
|
@ -77,10 +169,10 @@ function Details (data: { mainAreaRef: RefObject<HTMLDivElement | null>, game?:
|
||||||
|
|
||||||
return <main ref={ref} className="flex p-3 flex-col flex-1 min-h-0">
|
return <main ref={ref} className="flex p-3 flex-col flex-1 min-h-0">
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
<section className="flex portrait:flex-col my-4 sm:p-0 md:p-12 pt-4 sm:gap-8 md:gap-12 portrait:w-full h-full min-h-0 rounded-4xl flex-1 z-0 sm:text-sm md:text-base">
|
<section className="flex portrait:flex-col my-4 sm:p-0 md:px-12 md:pb-8 pt-4 sm:gap-8 md:gap-12 portrait:w-full h-full min-h-0 rounded-4xl flex-1 z-0 sm:text-sm md:text-base">
|
||||||
<div className="flex gap-6 overflow-hidden bg-base-300 justify-end portrait:w-full rounded-3xl aspect-3/4 portrait:h-24">
|
<div className="flex gap-6 overflow-hidden bg-base-100 justify-end portrait:w-full rounded-3xl aspect-3/4 portrait:h-24 p-4">
|
||||||
{gameCoverImg ?
|
{gameCoverImg ?
|
||||||
<img className="drop-shadow-2xl drop-shadow-base-300/40 w-full object-cover" src={gameCoverImg}></img> :
|
<img className="drop-shadow-2xl drop-shadow-base-300/40 w-full object-cover rounded-2xl" src={gameCoverImg}></img> :
|
||||||
<div className="skeleton w-full h-full"></div>
|
<div className="skeleton w-full h-full"></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -101,7 +193,7 @@ function Details (data: { mainAreaRef: RefObject<HTMLDivElement | null>, game?:
|
||||||
{data.game?.local && <small className="text-base-content/60 font-semibold">local</small>}</Detail>
|
{data.game?.local && <small className="text-base-content/60 font-semibold">local</small>}</Detail>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:hidden divider divider-vertical m-0"></div>
|
<div className="md:hidden divider divider-vertical m-0"></div>
|
||||||
<div className="text-base-content/80 flex-1 min-h-0 leading-relaxed grow text-wrap whitespace-break-spaces text-ellipsis overflow-hidden ">
|
<div className="text-base-content/80 flex-1 min-h-0 leading-relaxed grow text-wrap whitespace-break-spaces text-ellipsis overflow-hidden text-lg">
|
||||||
{data.game?.summary ?? <div className="flex flex-col gap-4 w-full">
|
{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-[30%]"></div>
|
||||||
<div className="skeleton h-4 w-[80%]"></div>
|
<div className="skeleton h-4 w-[80%]"></div>
|
||||||
|
|
@ -118,60 +210,6 @@ function Details (data: { mainAreaRef: RefObject<HTMLDivElement | null>, game?:
|
||||||
</main>;
|
</main>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Screenshot (data: { path: string; index: number; setFocused?: (index: number) => void; })
|
|
||||||
{
|
|
||||||
const { ref, focused, focusSelf } = useFocusable({
|
|
||||||
focusKey: `screenshot-${data.index}`,
|
|
||||||
onFocus: (e, p, details) =>
|
|
||||||
{
|
|
||||||
data.setFocused?.(data.index);
|
|
||||||
(ref.current as HTMLElement).scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' });
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}); 4096;
|
|
||||||
return <img className={twJoin("max-h-[60vh] rounded-3xl", classNames({
|
|
||||||
"sm:ring-4 md:ring-7 ring-primary": focused,
|
|
||||||
"cursor-pointer": !focused
|
|
||||||
}))} onClick={e => focusSelf({ nativeEvent: e.nativeEvent })} 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: (e, p, details) =>
|
|
||||||
{
|
|
||||||
if (!(details.nativeEvent instanceof TouchEvent))
|
|
||||||
{
|
|
||||||
(ref.current as HTMLElement).scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onBlur: () => setFocusedScreenshot(-1)
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div ref={ref} className="flex flex-col w-full z-0">
|
|
||||||
<FocusContext value={focusKey}>
|
|
||||||
<div
|
|
||||||
ref={scrollRef}
|
|
||||||
className="flex gap-6 px-16 py-2 sm:overflow-scroll md:overflow-hidden no-scrollbar 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={(e) => setFocus(`screenshot-${i}`, { nativeEvent: e.nativeEvent })}
|
|
||||||
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; })
|
function AchievementsInfo (data: { game: FrontEndGameTypeDetailed; })
|
||||||
{
|
{
|
||||||
if (!data.game.achievements)
|
if (!data.game.achievements)
|
||||||
|
|
@ -221,6 +259,7 @@ function MainActions (data: { game: FrontEndGameTypeDetailed; })
|
||||||
const [status, setStatus] = useState<GameStatusType | undefined>(undefined);
|
const [status, setStatus] = useState<GameStatusType | undefined>(undefined);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
const [details, setDetails] = useState<string | undefined>(undefined);
|
const [details, setDetails] = useState<string | undefined>(undefined);
|
||||||
|
const [commands, setCommands] = useState<CommandEntry[] | undefined>(undefined);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
|
|
@ -233,13 +272,14 @@ function MainActions (data: { game: FrontEndGameTypeDetailed; })
|
||||||
setProgress(stats.progress);
|
setProgress(stats.progress);
|
||||||
setStatus(stats.status);
|
setStatus(stats.status);
|
||||||
setDetails(stats.details);
|
setDetails(stats.details);
|
||||||
|
setCommands(stats.commands);
|
||||||
setError(stats.error);
|
setError(stats.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
es.addEventListener('refresh', () =>
|
es.addEventListener('refresh', () =>
|
||||||
{
|
{
|
||||||
queryClient.invalidateQueries({ queryKey: ['game', data.game.id] });
|
queryClient.invalidateQueries({ queryKey: ['game', data.game.id] });
|
||||||
location.reload();
|
Router.navigate({ to: '/game/$source/$id', params: { id, source } });
|
||||||
});
|
});
|
||||||
|
|
||||||
es.addEventListener('error', (e) =>
|
es.addEventListener('error', (e) =>
|
||||||
|
|
@ -248,6 +288,7 @@ function MainActions (data: { game: FrontEndGameTypeDetailed; })
|
||||||
{
|
{
|
||||||
const stats = JSON.parse((e as any).data) as GameInstallProgress;
|
const stats = JSON.parse((e as any).data) as GameInstallProgress;
|
||||||
toast.error(stats.error);
|
toast.error(stats.error);
|
||||||
|
setError(stats.error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -257,6 +298,7 @@ function MainActions (data: { game: FrontEndGameTypeDetailed; })
|
||||||
if (error)
|
if (error)
|
||||||
{
|
{
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
|
setError(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -278,10 +320,19 @@ function MainActions (data: { game: FrontEndGameTypeDetailed; })
|
||||||
if (status === 'installed')
|
if (status === 'installed')
|
||||||
{
|
{
|
||||||
mainButton = <ActionButton onAction={() =>
|
mainButton = <ActionButton onAction={() =>
|
||||||
|
{
|
||||||
|
const firstValid = commands?.find(c => c.valid);
|
||||||
|
if (firstValid?.emulator === 'emulatorjs')
|
||||||
|
{
|
||||||
|
const params = new URLSearchParams(firstValid.command);
|
||||||
|
Router.navigate({ to: '/embedded/$source/$id', viewTransition: { types: ['zoom-in'] }, params: { source, id }, search: Object.fromEntries(params.entries()) });
|
||||||
|
} else
|
||||||
{
|
{
|
||||||
playMutation.mutate();
|
playMutation.mutate();
|
||||||
SaveSource('launch');
|
SaveSource('launch');
|
||||||
Router.navigate({ to: '/launcher/$source/$id', viewTransition: { types: ['zoom-in'] }, params: { source, id } });
|
Router.navigate({ to: '/launcher/$source/$id', viewTransition: { types: ['zoom-in'] }, params: { source, id } });
|
||||||
|
}
|
||||||
|
|
||||||
}} tooltip={details} key="primary" type='primary' id="mainAction"><Play /></ActionButton>;
|
}} tooltip={details} key="primary" type='primary' id="mainAction"><Play /></ActionButton>;
|
||||||
}
|
}
|
||||||
else if (error)
|
else if (error)
|
||||||
|
|
@ -383,6 +434,8 @@ function ActionButtons (data: { game: FrontEndGameTypeDetailed; })
|
||||||
|
|
||||||
}, ref);
|
}, ref);
|
||||||
|
|
||||||
|
const { isPointer } = useActiveControl();
|
||||||
|
|
||||||
const tooltipStyles = {
|
const tooltipStyles = {
|
||||||
base: 'bg-base-100 text-base-content',
|
base: 'bg-base-100 text-base-content',
|
||||||
accent: 'bg-accent text-accent-content',
|
accent: 'bg-accent text-accent-content',
|
||||||
|
|
@ -403,7 +456,7 @@ function ActionButtons (data: { game: FrontEndGameTypeDetailed; })
|
||||||
}}>
|
}}>
|
||||||
<ContextList options={contextOptions} />
|
<ContextList options={contextOptions} />
|
||||||
</ContextDialog>
|
</ContextDialog>
|
||||||
{!!hoverText && <p className={twMerge("flex sm:hidden md:inline py-1 md:py-2 md:px-4 rounded-4xl text-wrap wrap-anywhere text-base", (tooltipStyles as any)[hoverTextType])}>{hoverText}</p>}
|
{!!hoverText && !isPointer && <p className={twMerge("flex sm:hidden md:inline py-1 md:py-2 md:px-4 rounded-4xl text-wrap wrap-anywhere text-base", (tooltipStyles as any)[hoverTextType])}>{hoverText}</p>}
|
||||||
</FocusContext>
|
</FocusContext>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -434,69 +487,62 @@ function ActionButton (data: {
|
||||||
{
|
{
|
||||||
const { ref, focused } = useFocusable({ focusKey: data.id, onFocus: data.onFocus, onEnterPress: data.onAction, focusable: data.disabled !== true });
|
const { ref, focused } = useFocusable({ focusKey: data.id, onFocus: data.onFocus, onEnterPress: data.onAction, focusable: data.disabled !== true });
|
||||||
const styles = {
|
const styles = {
|
||||||
primary: twMerge("bg-primary text-primary-content",
|
primary: "bg-primary text-primary-content focused:bg-base-content focused:text-base-300 focusable focusable-primary",
|
||||||
classNames({
|
base: " text-base-content border-dashed border-base-content/20 border-2 focused:bg-base-content focused:text-base-300 focusable focusable-primary",
|
||||||
"bg-base-content text-base-300 sm:ring-4 md:ring-7 ring-primary": focused
|
accent: "bg-primary text-primary-content focusable focusable-primary focusable:bg-base-content focusable:text-base-300",
|
||||||
})),
|
error: "bg-error text-error-content focused:bg-error focused:text-error-content",
|
||||||
base: twMerge(" text-base-content border-dashed border-base-content/20 border-2", classNames({
|
|
||||||
"bg-base-content text-base-300 sm:ring-4 md:ring-7 ring-primary": focused
|
|
||||||
})),
|
|
||||||
accent: twMerge("bg-primary text-primary-content ", classNames({
|
|
||||||
"bg-base-content text-base-300 sm:ring-4 md:ring-7 ring-primary": focused
|
|
||||||
})),
|
|
||||||
error: twMerge("bg-error text-error-content ", classNames({
|
|
||||||
"bg-error text-error-content sm:ring-4 md:ring-7 ring-primary": focused
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
<div className="tooltip tooltip-accent tooltip-right" data-tip={data.tooltip}>
|
||||||
<button
|
<button
|
||||||
disabled={data.disabled}
|
disabled={data.disabled}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={data.onAction}
|
onClick={data.onAction}
|
||||||
data-tooltip={data.tooltip}
|
data-tooltip={data.tooltip}
|
||||||
data-tooltip_type={data.tooltip_type}
|
data-tooltip_type={data.tooltip_type}
|
||||||
className={twMerge("header-icon flex flex-col gap-2 md:px-5 md:py-4 rounded-3xl md:text-2xl justify-center items-center cursor-pointer disabled:opacity-30",
|
className={twMerge("header-icon flex flex-col gap-2 md:px-5 md:py-4 rounded-3xl md:text-2xl justify-center items-center cursor-pointer disabled:opacity-30 active:bg-base-100 active:transition-none active:text-base-content",
|
||||||
"hover:ring-7 hover:ring-primary", styles[data.type], classNames({ "rounded-full sm:size-14 md:size-21 hover:bg-base-content hover:text-base-300 hover:ring-7 hover:ring-primary": !data.square }), data.className)}>
|
"hover:ring-7 hover:ring-primary", styles[data.type], classNames({ "rounded-full sm:size-14 md:size-21 hover:bg-base-content hover:text-base-300 hover:ring-7 hover:ring-primary": !data.square }), data.className)}>
|
||||||
{data.icon}
|
{data.icon}
|
||||||
{data.children}
|
{data.children}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GameDetailsUI ()
|
export default function GameDetailsUI ()
|
||||||
{
|
{
|
||||||
const { source, id } = Route.useParams();
|
const { data } = Route.useLoaderData();
|
||||||
const { data, isSuccess } = useQuery(gameQuery(source, Number(id)));
|
|
||||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details" });
|
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details" });
|
||||||
const backgroundImage = data?.path_cover ? new URL(`${RPC_URL(__HOST__)}${data?.path_cover}`) : undefined;
|
const headerRef = useRef(null);
|
||||||
|
const sentinelRef = useRef(null);
|
||||||
|
const backgroundImage = data.path_cover ? new URL(`${RPC_URL(__HOST__)}${data?.path_cover}`) : undefined;
|
||||||
const mainAreaRef = useRef<HTMLDivElement>(null);
|
const mainAreaRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||||
const { shortcuts } = useShortcutContext();
|
const { shortcuts } = useShortcutContext();
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
|
||||||
if (isSuccess)
|
|
||||||
{
|
{
|
||||||
focusSelf();
|
focusSelf();
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
}, [isSuccess]);
|
useStickyDataAttr(headerRef, sentinelRef, ref);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedBackground ref={ref} backgroundKey="game-details" backgroundUrl={backgroundImage} scrolling>
|
<AnimatedBackground ref={ref} backgroundKey="game-details" backgroundUrl={backgroundImage} scrolling>
|
||||||
<div className="z-0">
|
<div className="z-10">
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
<div className="flex flex-col px-3 py-2 h-[90vh] overflow-hidden bg-linear-to-t from-base-100 to-base-100/40" ref={mainAreaRef}>
|
<div ref={sentinelRef} className="h-0" />
|
||||||
|
<div ref={headerRef} className="sticky group top-0 bg-base-100/40 group p-2 z-15 transition-colors data-stuck:backdrop-blur-3xl">
|
||||||
<HeaderUI />
|
<HeaderUI />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col h-[80vh] overflow-hidden bg-linear-to-t from-base-100 to-base-100/40" ref={mainAreaRef}>
|
||||||
<Details mainAreaRef={mainAreaRef} game={data} />
|
<Details mainAreaRef={mainAreaRef} game={data} />
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-base-200">
|
<div className="bg-base-200">
|
||||||
<div className="divider m-0 pb-12"><div className="flex items-center gap-3 opacity-60"><Image className="sm:size-4 md:size-6" />Screenshots</div></div>
|
<div className="divider m-0 pb-12"><div className="flex items-center gap-3 opacity-60"><Image className="sm:size-4 md:size-6" />Screenshots</div></div>
|
||||||
{!!data && <Screenshots screenshots={data.paths_screenshots} />}
|
{!!data && <Screenshots screenshots={data.paths_screenshots} onFocus={(_, node) => node.scrollIntoView({ behavior: 'smooth', block: 'center' })} />}
|
||||||
<footer className="absolute left-0 bottom-0 w-full p-2 flex items-center justify-between z-10">
|
<footer className="fixed left-0 right-0 bottom-0 w-full p-4 flex items-center justify-end z-10">
|
||||||
<div className="flex gap-2 text-sm">
|
|
||||||
</div>
|
|
||||||
<Shortcuts shortcuts={shortcuts} />
|
<Shortcuts shortcuts={shortcuts} />
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import
|
||||||
Gamepad2,
|
Gamepad2,
|
||||||
Settings,
|
Settings,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
ShoppingBag,
|
|
||||||
Image,
|
Image,
|
||||||
Search,
|
Search,
|
||||||
Power,
|
Power,
|
||||||
OctagonAlert,
|
OctagonAlert,
|
||||||
Maximize,
|
Maximize,
|
||||||
|
Store,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
|
|
@ -21,13 +21,14 @@ import
|
||||||
{
|
{
|
||||||
FocusContext,
|
FocusContext,
|
||||||
FocusDetails,
|
FocusDetails,
|
||||||
|
getCurrentFocusKey,
|
||||||
useFocusable,
|
useFocusable,
|
||||||
} from "@noriginmedia/norigin-spatial-navigation";
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useEventListener } from "usehooks-ts";
|
import { useEventListener } from "usehooks-ts";
|
||||||
import { HeaderAccounts, HeaderStatusBar, HeaderUI } from "../components/Header";
|
import { HeaderAccounts, HeaderStatusBar } from "../components/Header";
|
||||||
import { FilterUI } from "../components/Filters";
|
import { FilterUI } from "../components/Filters";
|
||||||
import { AnimatedBackground, AnimatedBackgroundContext } from "../components/AnimatedBackground";
|
import { AnimatedBackground } from "../components/AnimatedBackground";
|
||||||
import { GameList } from "../components/GameList";
|
import { GameList } from "../components/GameList";
|
||||||
import { SaveSource } from "../scripts/spatialNavigation";
|
import { SaveSource } from "../scripts/spatialNavigation";
|
||||||
import LoadingCardList from "../components/LoadingCardList";
|
import LoadingCardList from "../components/LoadingCardList";
|
||||||
|
|
@ -43,7 +44,9 @@ import z from "zod";
|
||||||
import { Router } from "..";
|
import { Router } from "..";
|
||||||
import CollectionList from "../components/CollectionList";
|
import CollectionList from "../components/CollectionList";
|
||||||
import { zodValidator } from '@tanstack/zod-adapter';
|
import { zodValidator } from '@tanstack/zod-adapter';
|
||||||
import { mobileCheck } from "../scripts/utils";
|
import { mobileCheck, useDragScroll } from "../scripts/utils";
|
||||||
|
import { AnimatedBackgroundContext } from "../scripts/contexts";
|
||||||
|
import { FrontEndId } from "@/shared/constants";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: ConsoleHomeUI,
|
component: ConsoleHomeUI,
|
||||||
|
|
@ -93,6 +96,7 @@ function HomeList (data: {
|
||||||
{
|
{
|
||||||
const [initFocus, setInitFocus] = useState(false);
|
const [initFocus, setInitFocus] = useState(false);
|
||||||
const bg = useContext(AnimatedBackgroundContext);
|
const bg = useContext(AnimatedBackgroundContext);
|
||||||
|
const { } = Route.useSearch;
|
||||||
const { ref, focused, focusKey, focusSelf } = useFocusable({
|
const { ref, focused, focusKey, focusSelf } = useFocusable({
|
||||||
focusKey: "home-list",
|
focusKey: "home-list",
|
||||||
preferredChildFocusKey: `${data.selectedFilter}-list`
|
preferredChildFocusKey: `${data.selectedFilter}-list`
|
||||||
|
|
@ -103,18 +107,54 @@ function HomeList (data: {
|
||||||
const isMounseEvent = details.nativeEvent instanceof MouseEvent;
|
const isMounseEvent = details.nativeEvent instanceof MouseEvent;
|
||||||
if (!isMounseEvent)
|
if (!isMounseEvent)
|
||||||
{
|
{
|
||||||
node?.scrollIntoView({ inline: 'center', behavior: initFocus ? 'smooth' : 'instant' });
|
node?.scrollIntoView({ inline: 'center', block: 'center', behavior: initFocus ? 'smooth' : 'instant' });
|
||||||
}
|
}
|
||||||
|
|
||||||
setInitFocus(true);
|
setInitFocus(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const lists: Record<string, JSX.Element> = {
|
function handleGameSelect (id: FrontEndId, source: string | null, sourceId: string | null)
|
||||||
consoles: <PlatformsList onFocus={handleNodeFocus} className="animate-slide-up" key="consoles-list" id="consoles-list" setBackground={bg.setBackground} />,
|
{
|
||||||
games: <GameList onFocus={handleNodeFocus} className="animate-slide-up" key="games-list" id="games-list" setBackground={bg.setBackground} />,
|
SaveSource('details', { search: { filter: data.selectedFilter } });
|
||||||
collections: <CollectionList onFocus={handleNodeFocus} className="animate-slide-up" key="collections-list" id="collections-list" setBackground={bg.setBackground} />,
|
Router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source }, viewTransition: { types: ['zoom-in'] } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCollectionSelect = (id: string) =>
|
||||||
|
{
|
||||||
|
SaveSource('game-list', { search: { filter: data.selectedFilter } });
|
||||||
|
Router.navigate({ to: `/collection/${id}`, viewTransition: { types: ['zoom-in'] } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlatformSelect = (source: string, id: string) =>
|
||||||
|
{
|
||||||
|
SaveSource('game-list', { search: { filter: data.selectedFilter } });
|
||||||
|
Router.navigate({ to: `/platform/${source}/${id}`, viewTransition: { types: ['zoom-in'] } });
|
||||||
|
};
|
||||||
|
|
||||||
|
let activeList: JSX.Element;
|
||||||
|
switch (data.selectedFilter)
|
||||||
|
{
|
||||||
|
case 'consoles':
|
||||||
|
activeList = <>
|
||||||
|
<PlatformsList onSelect={handlePlatformSelect} onFocus={handleNodeFocus} className="animate-slide-up" key="consoles-list" id="consoles-list" setBackground={bg.setBackground} />
|
||||||
|
<AutoFocus parentKey={focusKey} focus={focusSelf} delay={10} />
|
||||||
|
</>;
|
||||||
|
break;
|
||||||
|
case 'collections':
|
||||||
|
activeList = <>
|
||||||
|
<CollectionList onSelect={handleCollectionSelect} onFocus={handleNodeFocus} className="animate-slide-up" key="collections-list" id="collections-list" setBackground={bg.setBackground} />
|
||||||
|
<AutoFocus parentKey={focusKey} focus={focusSelf} delay={10} />
|
||||||
|
</>;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
activeList = <>
|
||||||
|
<GameList onGameSelect={handleGameSelect} onFocus={handleNodeFocus} className="animate-slide-up" key="games-list" id="games-list" setBackground={bg.setBackground} />
|
||||||
|
<AutoFocus parentKey={focusKey} focus={focusSelf} delay={10} />
|
||||||
|
</>;
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
useEventListener('wheel', e =>
|
useEventListener('wheel', e =>
|
||||||
{
|
{
|
||||||
const deltaY = e.deltaY;
|
const deltaY = e.deltaY;
|
||||||
|
|
@ -138,17 +178,18 @@ function HomeList (data: {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useDragScroll(ref);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
<div ref={ref} className="flex h-full w-full landscape:overflow-x-scroll portrait:overflow-y-scroll overflow-hidden no-scrollbar justify-center-safe sm:pt-2 md:py-6 md:pb-3 md:mb-1" style={{
|
<div ref={ref} className="flex h-full w-full landscape:overflow-x-scroll portrait:overflow-y-scroll overflow-hidden no-scrollbar justify-center-safe sm:py-2 md:py-6 md:pb-6 md:mb-1 not-mobile:sm:pb-4" style={{
|
||||||
mask: `linear-gradient(to right, rgba(0,0,0,0.8) 0%, black 10%, black 90%, rgba(0,0,0,0.8) 100%)`
|
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="landscape:px-16 portrait:min-h-fit portrait:h-fit portrait:pb-32 portrait:w-full landscape:h-full">
|
<div className="landscape:flex landscape:px-16 portrait:min-h-fit portrait:h-fit portrait:pb-32 portrait:w-full landscape:h-full landscape:items-center">
|
||||||
<ErrorBoundary fallback={<HomeListError focused={focused} />}>
|
<ErrorBoundary fallback={<HomeListError focused={focused} />}>
|
||||||
<Suspense key={data.selectedFilter} fallback={<LoadingCardList placeholderCount={8} />}>
|
<Suspense key={data.selectedFilter} fallback={<LoadingCardList placeholderCount={8} />}>
|
||||||
{lists[data.selectedFilter]}
|
{activeList}
|
||||||
<SaveScroll id={`card-list-${data.selectedFilter}`} ref={ref} />
|
<SaveScroll id={`card-list-${data.selectedFilter}`} ref={ref} />
|
||||||
<AutoFocus focus={focusSelf} delay={10} />
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -179,7 +220,7 @@ function MainMenu (data: {})
|
||||||
type="secondary"
|
type="secondary"
|
||||||
/>
|
/>
|
||||||
<CircleIcon icon={<MessageSquare />} label="News" />
|
<CircleIcon icon={<MessageSquare />} label="News" />
|
||||||
<CircleIcon icon={<ShoppingBag />} label="Shop" />
|
<CircleIcon type="info" icon={<Store />} action={() => navigate({ to: "/store/tab", viewTransition: { types: ['zoom-in'] } })} label="Shop" />
|
||||||
<CircleIcon icon={<Image />} label="Album" />
|
<CircleIcon icon={<Image />} label="Album" />
|
||||||
<CircleIcon
|
<CircleIcon
|
||||||
icon={<Gamepad2 />}
|
icon={<Gamepad2 />}
|
||||||
|
|
@ -202,7 +243,7 @@ function MainMenu (data: {})
|
||||||
|
|
||||||
function CircleIcon (data: {
|
function CircleIcon (data: {
|
||||||
action?: () => void;
|
action?: () => void;
|
||||||
type?: "secondary" | "accent";
|
type?: "secondary" | "accent" | "info";
|
||||||
label?: string;
|
label?: string;
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
})
|
})
|
||||||
|
|
@ -215,6 +256,7 @@ function CircleIcon (data: {
|
||||||
const typeClasses = {
|
const typeClasses = {
|
||||||
secondary: "bg-secondary text-secondary-content",
|
secondary: "bg-secondary text-secondary-content",
|
||||||
accent: "bg-accent text-accent-content",
|
accent: "bg-accent text-accent-content",
|
||||||
|
info: "bg-info text-info-content",
|
||||||
none: "bg-base-content",
|
none: "bg-base-content",
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|
@ -222,15 +264,9 @@ function CircleIcon (data: {
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={data.action}
|
onClick={data.action}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`portrait:sm:size-12 sm:w-14 sm:h-10 menu-icon text-base-300 md:w-20 md:h-20 rounded-full flex items-center justify-center drop-shadow-lg cursor-pointer transition-all`,
|
`portrait:sm:size-12 sm:w-14 sm:h-10 menu-icon text-base-300 md:w-20 md:h-20 rounded-full flex items-center justify-center drop-shadow-lg cursor-pointer transition-all focusable focusable-primary focused:drop-shadow-2xl focused:animate-scale focusable-hover bg-base-content border-6 md:border-12 border-base-content focused:border-0 hover:border-0 z-1 active:border-0 active:bg-base-300 active:text-base-content active:transition-none`, typeClasses[data.type ?? 'none'])}
|
||||||
typeClasses[data.type ?? "none"], classNames(
|
|
||||||
{
|
|
||||||
"focus ring-7 ring-primary drop-shadow-2xl animate-scale": focused,
|
|
||||||
"hover:ring-7 hover:ring-primary": true,
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{data.icon}
|
<div className="in-focused:animate-rotate-instant animation-size-5">{data.icon}</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -291,11 +327,11 @@ export default function ConsoleHomeUI ()
|
||||||
<div className="sm:landscape:hidden md:landscape:inline sm:portrait:col-start-1 md:inline flex col-span-1 md:pl-2 md:pt-2">
|
<div className="sm:landscape:hidden md:landscape:inline sm:portrait:col-start-1 md:inline flex col-span-1 md:pl-2 md:pt-2">
|
||||||
<HeaderAccounts />
|
<HeaderAccounts />
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:portrait:*:justify-center sm:portrait:col-span-3 sm:landscape:*:justify-start sm:px-2 sm:pt-2 md:row-start-2 md:col-start-1 sm:landscape:col-span-1 md:landscape:col-span-3 flex items-center md:*:justify-center! md:ml-0 gap-2 *:w-full *:flex">
|
<div className=" sm:portrait:col-span-3 sm:px-2 sm:pt-2 md:row-start-2 md:col-start-1 sm:landscape:col-span-1 md:landscape:col-span-3 flex items-center md:ml-0 gap-2">
|
||||||
<FilterUI
|
<FilterUI
|
||||||
id="home"
|
id="home"
|
||||||
options={filters}
|
containerClassName="flex w-full sm:landscape:justify-start sm:portrait:justify-center md:justify-center!"
|
||||||
selected={filter ? filter : 'games'}
|
options={Object.fromEntries(Object.entries(filters).map(([key, value]) => [key, { ...value, selected: key === filter }]))}
|
||||||
setSelected={setFilter}
|
setSelected={setFilter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -311,7 +347,7 @@ export default function ConsoleHomeUI ()
|
||||||
<MainMenu />
|
<MainMenu />
|
||||||
</div>
|
</div>
|
||||||
<footer className={twMerge(
|
<footer className={twMerge(
|
||||||
"sm:portrait:hidden sm:col-span-1 md:col-start-2 md:col-span-2 md:relative px-2 pb-2 flex items-end justify-end",
|
"fixed bottom-4 left-4 right-4 sm:portrait:hidden sm:col-span-1 md:col-start-2 md:col-span-2 flex items-end justify-end",
|
||||||
)}>
|
)}>
|
||||||
<Shortcuts shortcuts={shortcuts} />
|
<Shortcuts shortcuts={shortcuts} />
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@ import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
import { GameInstallProgress, RPC_URL } from '@/shared/constants';
|
import { GameInstallProgress, RPC_URL } from '@/shared/constants';
|
||||||
import DotsLoading from '../components/backgrounds/dots';
|
import DotsLoading from '../components/backgrounds/dots';
|
||||||
import { useEventListener } from 'usehooks-ts';
|
|
||||||
import { Router } from '..';
|
import { Router } from '..';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { rommApi } from '../scripts/clientApi';
|
import { rommApi } from '../scripts/clientApi';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useEventListener, useSessionStorage } from "usehooks-ts";
|
|
||||||
import { CollectionsDetail } from "../components/CollectionsDetail";
|
import { CollectionsDetail } from "../components/CollectionsDetail";
|
||||||
import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { DefaultRommStaleTime, RPC_URL } from "../../shared/constants";
|
import { DefaultRommStaleTime, RPC_URL } from "../../shared/constants";
|
||||||
import { Suspense } from "react";
|
import { useContext } from "react";
|
||||||
import { rommApi } from "../scripts/clientApi";
|
import { rommApi } from "../scripts/clientApi";
|
||||||
|
import { AnimatedBackgroundContext } from "../scripts/contexts";
|
||||||
|
|
||||||
export const Route = createFileRoute("/platform/$source/$id")({
|
export const Route = createFileRoute("/platform/$source/$id")({
|
||||||
component: RouteComponent
|
component: RouteComponent
|
||||||
});
|
});
|
||||||
|
|
||||||
function PlatformTitle (data: { platformSlug?: string, platformName?: string; })
|
function PlatformTitle (data: { pathCover: string | null, platformName?: string; })
|
||||||
{
|
{
|
||||||
return <div className="sm:landscape:hidden flex flex-col gap-2 pl-2 text-2xl font-semibold text-base-content justify-center drop-shadow">
|
return <div className="sm:landscape:hidden 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">
|
<div className="divider mb-6 mt-0">
|
||||||
{!!data.platformSlug && <img className="size-14 rounded-full p-2" src={`${RPC_URL(__HOST__)}/api/romm/assets/platforms/${data.platformSlug.toLocaleLowerCase()}.svg`} ></img>}
|
{!!data.pathCover && <img className="size-14 rounded-full p-2" src={`${RPC_URL(__HOST__)}${data.pathCover}`} ></img>}
|
||||||
{data.platformName}
|
{data.platformName}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
@ -33,16 +33,13 @@ function RouteComponent ()
|
||||||
}, staleTime: DefaultRommStaleTime
|
}, staleTime: DefaultRommStaleTime
|
||||||
});
|
});
|
||||||
|
|
||||||
const [, setBackground] = useSessionStorage<string | undefined>(
|
const animatedBgContext = useContext(AnimatedBackgroundContext);
|
||||||
"home-background",
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
{!!platform && <CollectionsDetail
|
{!!platform && <CollectionsDetail
|
||||||
title={<PlatformTitle platformSlug={platform.slug} platformName={platform.name} />}
|
title={<PlatformTitle pathCover={platform.path_cover} platformName={platform.name} />}
|
||||||
setBackground={setBackground}
|
setBackground={animatedBgContext.setBackground}
|
||||||
filters={{ platform_id: Number(id), platform_slug: platform.slug, platform_source: source }}
|
filters={{ platform_id: Number(id), platform_slug: platform.slug, platform_source: source }}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { systemApi } from '@/mainview/scripts/clientApi';
|
import { systemApi } from '@/mainview/scripts/clientApi';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
|
||||||
export const Route = createFileRoute('/settings/about')({
|
export const Route = createFileRoute('/settings/about')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|
@ -9,8 +10,7 @@ export const Route = createFileRoute('/settings/about')({
|
||||||
function RouteComponent ()
|
function RouteComponent ()
|
||||||
{
|
{
|
||||||
const { data: systemInfo } = useQuery({ queryKey: ['system-info'], queryFn: () => systemApi.api.system.info.get() });
|
const { data: systemInfo } = useQuery({ queryKey: ['system-info'], queryFn: () => systemApi.api.system.info.get() });
|
||||||
return <div className="overflow-x-auto">
|
return <table className="table">
|
||||||
<table className="table">
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Agent</th>
|
<th>Agent</th>
|
||||||
|
|
@ -50,6 +50,10 @@ function RouteComponent ()
|
||||||
<th>Machine</th>
|
<th>Machine</th>
|
||||||
<td>{systemInfo?.data?.machine}</td>
|
<td>{systemInfo?.data?.machine}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Sizes</th>
|
||||||
|
<td>Cache: {prettyBytes(systemInfo?.data?.cacheSize ?? 0)}, Store: {prettyBytes(systemInfo?.data?.storeSize ?? 0)}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Source</th>
|
<th>Source</th>
|
||||||
<td>{systemInfo?.data?.source}</td>
|
<td>{systemInfo?.data?.source}</td>
|
||||||
|
|
@ -59,6 +63,5 @@ function RouteComponent ()
|
||||||
<td>{systemInfo?.data?.steamDeck ?? 'false'}</td>
|
<td>{systemInfo?.data?.steamDeck ?? 'false'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>;
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,18 @@ import
|
||||||
import { useIsMutating, useMutation, useQuery } from "@tanstack/react-query";
|
import { useIsMutating, useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Key, Link, Lock, Save, ScanQrCode, Trash, User, X } from "lucide-react";
|
import { Key, Link, Lock, LogOut, Save, ScanQrCode, Trash, User, X } from "lucide-react";
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { RPC_URL } from "../../../shared/constants";
|
import { RPC_URL } from "@shared/constants";
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
getCurrentUserApiUsersMeGetOptions,
|
getCurrentUserApiUsersMeGetOptions,
|
||||||
statsApiStatsGetOptions,
|
statsApiStatsGetOptions,
|
||||||
} from "../../../clients/romm/@tanstack/react-query.gen";
|
} from "@clients/romm/@tanstack/react-query.gen";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { OptionSpace } from "../../components/options/OptionSpace";
|
import { OptionSpace } from "../../components/options/OptionSpace";
|
||||||
|
|
@ -26,20 +27,95 @@ import { rommApi, settingsApi } from "../../scripts/clientApi";
|
||||||
import { Button } from "../../components/options/Button";
|
import { Button } from "../../components/options/Button";
|
||||||
import { ContextDialog } from "@/mainview/components/ContextDialog";
|
import { ContextDialog } from "@/mainview/components/ContextDialog";
|
||||||
import QRCode from "react-qr-code";
|
import QRCode from "react-qr-code";
|
||||||
import { useAsyncGenerator } from "@/mainview/scripts/utils";
|
import { useJobStatus } from "@/mainview/scripts/utils";
|
||||||
|
import { useInterval } from "usehooks-ts";
|
||||||
|
import { TwitchIcon } from "@/mainview/scripts/brandIcons";
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings/accounts")({
|
export const Route = createFileRoute("/settings/accounts")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function LoginQR (data: { id: string, isOpen: boolean, cancel: () => void, url: string; endsAt: Date; })
|
function LoginQR (data: { id: string, isOpen: boolean, cancel: () => void, url: string; endsAt: Date; startedAt: Date; code?: string; })
|
||||||
{
|
{
|
||||||
|
const progressRef = useRef<HTMLProgressElement>(null);
|
||||||
|
useInterval(() =>
|
||||||
|
{
|
||||||
|
if (progressRef.current)
|
||||||
|
{
|
||||||
|
const time = data.endsAt.getTime() - data.startedAt.getTime();
|
||||||
|
progressRef.current.value = ((data.endsAt.getTime() - new Date().getTime()) / time) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
return <ContextDialog id={data.id} open={data.isOpen} close={() => data.cancel()} className="flex flex-col justify-center items-center gap-2">
|
return <ContextDialog id={data.id} open={data.isOpen} close={() => data.cancel()} className="flex flex-col justify-center items-center gap-2">
|
||||||
<QRCode value={data.url} />
|
<QRCode value={data.url} />
|
||||||
|
<progress ref={progressRef} className="progress w-56" max="100"></progress>
|
||||||
|
{!!data.code && <p> Code: {data.code} </p>}
|
||||||
<Button id="qr-login-cancel" focusClassName="btn-warning" type="button" onAction={() => data.cancel()}><X /> Cancel</Button>
|
<Button id="qr-login-cancel" focusClassName="btn-warning" type="button" onAction={() => data.cancel()}><X /> Cancel</Button>
|
||||||
</ContextDialog>;
|
</ContextDialog>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TwitchLogin (data: {})
|
||||||
|
{
|
||||||
|
|
||||||
|
const loginStatus = useQuery({
|
||||||
|
queryKey: ['twitch', 'login', 'status'],
|
||||||
|
retry (failureCount, error)
|
||||||
|
{
|
||||||
|
if (error.status === 404)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return failureCount < 3;
|
||||||
|
},
|
||||||
|
queryFn: async () =>
|
||||||
|
{
|
||||||
|
const { data, error, status } = await rommApi.api.romm.login.twitch.get();
|
||||||
|
if (error) throw { ...error, status };
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginMutation = useMutation({
|
||||||
|
mutationKey: ['twitch', 'login'],
|
||||||
|
mutationFn: (openInBrowser: boolean) =>
|
||||||
|
{
|
||||||
|
return rommApi.api.romm.login.twitch.post({ openInBrowser });
|
||||||
|
},
|
||||||
|
onSuccess: () => loginStatus.refetch()
|
||||||
|
});
|
||||||
|
|
||||||
|
const logoutMutation = useMutation({
|
||||||
|
mutationKey: ['twitch', 'logout'],
|
||||||
|
mutationFn: () =>
|
||||||
|
{
|
||||||
|
return rommApi.api.romm.logout.twitch.post();
|
||||||
|
},
|
||||||
|
onSuccess: () => loginStatus.refetch()
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: loginData, wsRef } = useJobStatus('twitch-login-job', { onEnded: () => loginStatus.refetch() });
|
||||||
|
|
||||||
|
return <div className="flex flex-wrap gap-1 items-center justify-center-safe">
|
||||||
|
{loginStatus.isSuccess ?
|
||||||
|
<div className="badge badge-success badge-lg rounded-full gap-2"><b>{loginStatus.data.login}</b></div> :
|
||||||
|
<div className={classNames("badge gap-2 tooltip", { "badge-error": loginStatus.error })} data-tip={loginStatus.error?.message}>
|
||||||
|
{loginStatus.isError || loginStatus.isRefetchError ? <Lock className="size-4" /> : <span className="loading loading-spinner loading-sm"></span>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<Button id="twitch-login-btn-qr" disabled={loginMutation.isPending} onAction={() => loginMutation.mutate(false)} >
|
||||||
|
<ScanQrCode />
|
||||||
|
</Button>
|
||||||
|
<Button id="twitch-login-btn" disabled={loginMutation.isPending} onAction={() => loginMutation.mutate(true)} >
|
||||||
|
{TwitchIcon}
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
{loginStatus.isSuccess && <Button id="twitch-logout-btn" onAction={() => logoutMutation.mutate()} ><LogOut /> Logout</Button>}
|
||||||
|
{!!loginData && <LoginQR code={loginData.user_code} url={loginData.url} cancel={() => wsRef.current?.send({ type: 'cancel' })} id='twitch-login-qr' isOpen={true} endsAt={loginData.expires_at} startedAt={loginData.started_at} />}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
function LoginControls (data: { hasPassword: boolean; })
|
function LoginControls (data: { hasPassword: boolean; })
|
||||||
{
|
{
|
||||||
const user = useQuery({
|
const user = useQuery({
|
||||||
|
|
@ -48,42 +124,30 @@ function LoginControls (data: { hasPassword: boolean; })
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
retry: 0
|
retry: 0
|
||||||
});
|
});
|
||||||
const { data: qrLoginStatusGen, refetch } = useQuery({
|
|
||||||
queryKey: ['login', 'qr'], queryFn: async () =>
|
|
||||||
{
|
|
||||||
const { data, error } = await rommApi.api.romm.login.remote.status.get();
|
|
||||||
if (error) throw error;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const statusValue = useAsyncGenerator(qrLoginStatusGen, [qrLoginStatusGen]);
|
const loginMutation = useMutation({
|
||||||
const cancelQrMutation = useMutation({
|
|
||||||
mutationKey: ['login', 'qr', 'cancel'],
|
mutationKey: ['login', 'qr', 'cancel'],
|
||||||
mutationFn: () => rommApi.api.romm.login.remote.cancel.post(),
|
mutationFn: () => rommApi.api.romm.login.romm.post()
|
||||||
onSuccess: () => refetch()
|
|
||||||
});
|
|
||||||
const requestQrLoginMutation = useMutation({
|
|
||||||
mutationKey: ['login', 'qr'],
|
|
||||||
mutationFn: () => rommApi.api.romm.login.remote.start.post(),
|
|
||||||
onSuccess: () => refetch()
|
|
||||||
});
|
});
|
||||||
|
const { data: statusValue, error: loginError, wsRef } = useJobStatus('login-job');
|
||||||
const context = useSettingsFormContext({});
|
const context = useSettingsFormContext({});
|
||||||
const isMutatingRomm = useIsMutating({ mutationKey: ["romm", "auth"] }) > 0;
|
const isMutatingRomm = useIsMutating({ mutationKey: ["romm", "auth"] }) > 0;
|
||||||
const logoutMutation = useMutation({
|
const logoutMutation = useMutation({
|
||||||
mutationKey: ["romm", "auth", "logout"], mutationFn: () => rommApi.api.romm.logout.post(),
|
mutationKey: ["romm", "auth", "logout"], mutationFn: () => rommApi.api.romm.logout.post(),
|
||||||
onSuccess: async (d, v, r, c) =>
|
onSuccess: async (d, v, r, c) =>
|
||||||
{
|
{
|
||||||
|
user.refetch();
|
||||||
c.client.invalidateQueries({ queryKey: ["romm", "auth"] });
|
c.client.invalidateQueries({ queryKey: ["romm", "auth"] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return <div className="flex gap-2 items-center flex-wrap">
|
return <div className="flex gap-2 items-center flex-wrap justify-center-safe">
|
||||||
{user.isError && <div className="badge badge-error gap-2 tooltip" data-tip={(user.error as any)?.detail ?? ''}>
|
{user.isSuccess ?
|
||||||
<Lock className="size-4" /></div>}
|
<div className="badge badge-success badge-lg rounded-full gap-2"> <p className="sm:hidden md:inline">Logged In As:</p> <img className="size-6 rounded-full" src={`${RPC_URL(__HOST__)}/api/romm/assets/romm/assets/${user.data?.avatar_path}`} /><b>{user.data?.username}</b></div> :
|
||||||
{user.isSuccess && <>
|
<div className={classNames("badge gap-2 tooltip", { "badge-error": user.error })} data-tip={user.error?.message}>
|
||||||
<div className="badge badge-success badge-lg rounded-full gap-2"> <p className="sm:hidden md:inline">Logged In As:</p> <img className="size-6 rounded-full" src={`${RPC_URL(__HOST__)}/api/romm/assets/romm/assets/${user.data?.avatar_path}`} /><b>{user.data?.username}</b></div>
|
{user.isError ? <Lock className="size-4" /> : <span className="loading loading-spinner loading-sm"></span>}
|
||||||
</>}
|
</div>
|
||||||
<Button id="qr-login" type="button" onAction={() => requestQrLoginMutation.mutate()}><ScanQrCode /> </Button>
|
}
|
||||||
|
<Button id="qr-login" type="button" disabled={loginMutation.isPending} onAction={() => loginMutation.mutate()}><ScanQrCode /> </Button>
|
||||||
<Button id="can-submit" disabled={!context.state.canSubmit || !context.state.isDirty} type="submit" onAction={() => context.handleSubmit()} >
|
<Button id="can-submit" disabled={!context.state.canSubmit || !context.state.isDirty} type="submit" onAction={() => context.handleSubmit()} >
|
||||||
<Save /> Save
|
<Save /> Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -99,11 +163,11 @@ function LoginControls (data: { hasPassword: boolean; })
|
||||||
<Button id="cancel" disabled={context.state.isDefaultValue} type="reset" onAction={() => context.reset()}>
|
<Button id="cancel" disabled={context.state.isDefaultValue} type="reset" onAction={() => context.reset()}>
|
||||||
<X /> Cancel
|
<X /> Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{statusValue?.data?.endsAt && <LoginQR id="qr-login-context" endsAt={statusValue.data.endsAt} isOpen={true} cancel={() =>
|
{!!statusValue && <LoginQR startedAt={statusValue.startedAt} id="qr-login-context" endsAt={statusValue.endsAt} isOpen={true} cancel={() =>
|
||||||
{
|
{
|
||||||
setFocus(`qr-login`);
|
setFocus(`qr-login`);
|
||||||
cancelQrMutation.mutate();
|
wsRef.current?.send({ type: 'cancel' });
|
||||||
}} url={statusValue?.data?.url ?? ''} />}
|
}} url={statusValue?.url ?? ''} />}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,7 +247,7 @@ function RouteComponent ()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusContext.Provider value={focusKey}>
|
<FocusContext.Provider value={focusKey}>
|
||||||
<ul ref={ref} className="list rounded-box gap-2">
|
<ul ref={ref} className="list relative rounded-box gap-2">
|
||||||
<div className="divider text-2xl mt-0 md:mt-4">
|
<div className="divider text-2xl mt-0 md:mt-4">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h3>Romm</h3>
|
<h3>Romm</h3>
|
||||||
|
|
@ -218,12 +282,24 @@ function RouteComponent ()
|
||||||
<loginForm.AppField name="password" children={(field) =>
|
<loginForm.AppField name="password" children={(field) =>
|
||||||
<field.FormOption label={"Romm Password"} icon={<Key />} type="password" placeholder={hasPassword ? '*****' : "Password"} />} />
|
<field.FormOption label={"Romm Password"} icon={<Key />} type="password" placeholder={hasPassword ? '*****' : "Password"} />} />
|
||||||
<loginForm.Subscribe children={(form) =>
|
<loginForm.Subscribe children={(form) =>
|
||||||
<OptionSpace className="justify-end">
|
<OptionSpace id="login-controls-space" className="justify-end border-0">
|
||||||
<LoginControls hasPassword={hasPassword === true} />
|
<LoginControls hasPassword={hasPassword === true} />
|
||||||
</OptionSpace>} />
|
</OptionSpace>} />
|
||||||
</form>
|
</form>
|
||||||
</loginForm.AppForm>
|
</loginForm.AppForm>
|
||||||
|
<div className="divider text-2xl mt-0 md:mt-4">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{TwitchIcon}
|
||||||
|
<h3> Twitch</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
|
<OptionSpace label={<div className="flex flex-col">
|
||||||
|
Twitch Login
|
||||||
|
<small className="text-base-content/40">for IGDB Metadata</small>
|
||||||
|
</div>} id="twitch-login-space" className="justify-end border-0">
|
||||||
|
<TwitchLogin />
|
||||||
|
</OptionSpace>
|
||||||
</FocusContext.Provider>
|
</FocusContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spat
|
||||||
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||||
import FilePicker from '@/mainview/components/FilePicker';
|
import FilePicker from '@/mainview/components/FilePicker';
|
||||||
import { dirname } from 'pathe';
|
import { dirname } from 'pathe';
|
||||||
|
import { autoEmulatorsQuery } from '@/mainview/scripts/queries';
|
||||||
|
|
||||||
export const Route = createFileRoute('/settings/emulators')({
|
export const Route = createFileRoute('/settings/emulators')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|
@ -75,7 +76,7 @@ function NewEmulatorPath (data: { addOverride: (emulator: string) => void; isAdd
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return <OptionSpace label={"Custom Emulator Path"}>
|
return <OptionSpace id={'custom-emulator-path-option'} label={"Custom Emulator Path"}>
|
||||||
<Button disabled={data.isAddingOverride} id='emulator' type='button' onAction={() => setNewEmulatorTypeOpen(true)} >
|
<Button disabled={data.isAddingOverride} id='emulator' type='button' onAction={() => setNewEmulatorTypeOpen(true)} >
|
||||||
Emulator
|
Emulator
|
||||||
<ChevronDown />
|
<ChevronDown />
|
||||||
|
|
@ -155,7 +156,7 @@ function EmulatorPath (data: { id: string; })
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionSpace label={
|
<OptionSpace id={`${data.id}-space`} label={
|
||||||
focus => <>
|
focus => <>
|
||||||
<p className='font-semibold'>{data.id}</p>
|
<p className='font-semibold'>{data.id}</p>
|
||||||
<small className='opacity-40'>{emulators[data.id]}</small>
|
<small className='opacity-40'>{emulators[data.id]}</small>
|
||||||
|
|
@ -211,6 +212,7 @@ function EmulatorBadge (data: {
|
||||||
path?: string,
|
path?: string,
|
||||||
exists: boolean,
|
exists: boolean,
|
||||||
emulator: string;
|
emulator: string;
|
||||||
|
isCritical: boolean;
|
||||||
pathCover?: string;
|
pathCover?: string;
|
||||||
addOverride: (emulator: string) => void;
|
addOverride: (emulator: string) => void;
|
||||||
})
|
})
|
||||||
|
|
@ -229,16 +231,16 @@ function EmulatorBadge (data: {
|
||||||
|
|
||||||
return <div className={classNames("tooltip tooltip-primary", { "tooltip-open": focused })} data-tip={`${emulators[data.emulator]}`}>
|
return <div className={classNames("tooltip tooltip-primary", { "tooltip-open": focused })} data-tip={`${emulators[data.emulator]}`}>
|
||||||
<div ref={ref} className={
|
<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',
|
twMerge('flex flex-col rounded-3xl bg-base-300 justify-center items-center p-4 overflow-hidden h-full',
|
||||||
classNames({
|
classNames({
|
||||||
"bg-base-200": !data.path,
|
"bg-base-200": !data.path,
|
||||||
"border-dashed border-base-content/40 border-2": !data.path && !focused,
|
"border-dashed border-base-content/40 border-2": !data.path && data.isCritical && !focused,
|
||||||
"border-dashed border-accent border-4": focused
|
"border-dashed border-accent border-4": focused
|
||||||
|
|
||||||
}))
|
}))
|
||||||
}>
|
}>
|
||||||
<p className='flex gap-2 font-semibold'>
|
<p className='flex gap-2 font-semibold'>
|
||||||
{data.path ? data.exists ? <Check /> : <TriangleAlert className='text-error' /> : <SearchAlert className='text-warning' />}
|
{data.path ? data.exists ? <Check /> : <TriangleAlert className='text-error' /> : <SearchAlert className={data.isCritical ? 'text-warning' : 'text-base-content/40'} />}
|
||||||
{!!data.pathCover && <img className='size-6 drop-shadow drop-shadow-black/20' src={`${RPC_URL(__HOST__)}${data.pathCover}`}></img>}
|
{!!data.pathCover && <img className='size-6 drop-shadow drop-shadow-black/20' src={`${RPC_URL(__HOST__)}${data.pathCover}`}></img>}
|
||||||
{data.emulator}
|
{data.emulator}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -249,11 +251,11 @@ function EmulatorBadge (data: {
|
||||||
|
|
||||||
function EmulatorBadges (data: { path?: string; addOverride: (emulator: string) => void; })
|
function EmulatorBadges (data: { path?: string; addOverride: (emulator: string) => void; })
|
||||||
{
|
{
|
||||||
const { data: autoEmulators } = useQuery({ queryKey: ['auto-emulators'], queryFn: async () => settingsApi.api.settings.emulators.automatic.get() });
|
const { data: autoEmulators } = useQuery(autoEmulatorsQuery);
|
||||||
const { ref, focusKey } = useFocusable({ focusKey: `emulator-badges`, focusable: !!autoEmulators?.data && autoEmulators.data.length > 0 });
|
const { ref, focusKey } = useFocusable({ focusKey: `emulator-badges`, focusable: !!autoEmulators && autoEmulators.length > 0 });
|
||||||
return <div ref={ref} className='flex flex-wrap gap-2 justify-center-safe'>
|
return <div ref={ref} className='grid grid-cols-[repeat(auto-fit,14rem)] auto-rows-[4rem] gap-2 justify-center-safe'>
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
{autoEmulators?.data?.map(e => <EmulatorBadge key={e.emulator} addOverride={data.addOverride} pathCover={e.path_cover ?? undefined} path={e.path} exists={e.exists} emulator={e.emulator} />)}
|
{autoEmulators?.map(e => <EmulatorBadge key={e.emulator} isCritical={e.isCritical} addOverride={data.addOverride} pathCover={e.path_cover ?? undefined} path={e.path?.path} exists={e.exists} emulator={e.emulator} />)}
|
||||||
</FocusContext>
|
</FocusContext>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ function RouteComponent ()
|
||||||
return <ul ref={ref} className="list rounded-box gap-2">
|
return <ul ref={ref} className="list rounded-box gap-2">
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
<LocalOption id="backgroundBlur" label="Background Blur" type='checkbox'></LocalOption>
|
<LocalOption id="backgroundBlur" label="Background Blur" type='checkbox'></LocalOption>
|
||||||
|
<LocalOption id="backgroundAnimation" label="Background Animation" type='checkbox'></LocalOption>
|
||||||
<LocalOption id="theme" label="Theme" type='dropdown' values={['dark', 'light', 'auto']}></LocalOption>
|
<LocalOption id="theme" label="Theme" type='dropdown' values={['dark', 'light', 'auto']}></LocalOption>
|
||||||
</FocusContext>
|
</FocusContext>
|
||||||
</ul>;
|
</ul>;
|
||||||
|
|
|
||||||
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