feat: Implemented audio effects
This commit is contained in:
parent
fe0ab3b498
commit
edbc390d14
125 changed files with 1137 additions and 217 deletions
3
.gitattributes
vendored
3
.gitattributes
vendored
|
|
@ -4,3 +4,6 @@
|
|||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||
*.svg filter=lfs diff=lfs merge=lfs -text
|
||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
||||
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||
|
|
@ -90,3 +90,9 @@ Focused on building a simple user experience and intuitive UI as a curated commu
|
|||
- [elysia](https://elysiajs.com/) for the APIs
|
||||
- [webview](https://github.com/webview/webview) for launching existing system webviews instead of full browser if possible.
|
||||
- [emulatorjs](https://emulatorjs.org/) for playing lots of roms inside the app without having to deal with external emulators
|
||||
|
||||
### Credits
|
||||
|
||||
- UI Sounds
|
||||
- [CC BY 4.0 - Credit: JC Sounds](https://opengameart.org/content/jc-sounds-ui-utility-pack-vol-1)
|
||||
- [Sounds by: Chhoff](https://chhoffmusic.itch.io/classic-ui-sfx)
|
||||
|
|
|
|||
56
bun.lock
56
bun.lock
|
|
@ -50,6 +50,7 @@
|
|||
"@tanstack/router-plugin": "^1.157.16",
|
||||
"@tanstack/zod-adapter": "^1.162.4",
|
||||
"@types/adm-zip": "^0.5.8",
|
||||
"@types/audiosprite": "^0.7.3",
|
||||
"@types/bun": "latest",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/howler": "^2.2.12",
|
||||
|
|
@ -63,6 +64,7 @@
|
|||
"adm-zip": "^0.5.16",
|
||||
"animate.css": "^4.1.1",
|
||||
"app-builder-bin": "^5.0.0-alpha.13",
|
||||
"audiosprite": "^0.7.2",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"concurrently": "^9.2.1",
|
||||
|
|
@ -590,6 +592,8 @@
|
|||
|
||||
"@types/adm-zip": ["@types/adm-zip@0.5.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q=="],
|
||||
|
||||
"@types/audiosprite": ["@types/audiosprite@0.7.3", "", {}, "sha512-P4rUuHPt2kWPMqyObfh1SfqS2H/ZuTxByh00ecuI2tOdvP5b8NznuBeQgemDXV9v8b4pewFPB9G3BuYRONqD7A=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
|
@ -674,12 +678,14 @@
|
|||
|
||||
"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=="],
|
||||
"async": ["async@0.9.2", "", {}, "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="],
|
||||
|
||||
"audiosprite": ["audiosprite@0.7.2", "", { "dependencies": { "async": "~0.9.0", "glob": "^6.0.4", "mkdirp": "^0.5.0", "optimist": "~0.6.1", "underscore": "~1.8.3", "winston": "~1.0.0" }, "bin": { "audiosprite": "./cli.js" } }, "sha512-9Z6UwUuv4To5nUQNRIw5/Q3qA7HYm0ANzoW5EDGPEsU2oIRVgmIlLlm9YZfpPKoeUxt54vMStl2/762189VmJw=="],
|
||||
|
||||
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
|
||||
|
||||
"axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
|
||||
|
|
@ -762,6 +768,8 @@
|
|||
|
||||
"colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="],
|
||||
|
||||
"colors": ["colors@1.0.3", "", {}, "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
|
||||
|
|
@ -840,6 +848,8 @@
|
|||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"cycle": ["cycle@1.0.3", "", {}, "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA=="],
|
||||
|
||||
"daisyui": ["daisyui@5.5.14", "", {}, "sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg=="],
|
||||
|
||||
"dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="],
|
||||
|
|
@ -954,6 +964,8 @@
|
|||
|
||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||
|
||||
"eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
|
@ -1016,7 +1028,7 @@
|
|||
|
||||
"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": ["glob@6.0.4", "", { "dependencies": { "inflight": "^1.0.4", "inherits": "2", "minimatch": "2 || 3", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
|
|
@ -1066,6 +1078,8 @@
|
|||
|
||||
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||
|
||||
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
|
||||
|
|
@ -1104,6 +1118,8 @@
|
|||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="],
|
||||
|
||||
"jackspeak": ["jackspeak@2.3.6", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ=="],
|
||||
|
||||
"jimp": ["jimp@1.6.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=="],
|
||||
|
|
@ -1278,12 +1294,16 @@
|
|||
|
||||
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"opener": ["opener@1.5.2", "", { "bin": { "opener": "bin/opener-bin.js" } }, "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="],
|
||||
|
||||
"optimist": ["optimist@0.6.1", "", { "dependencies": { "minimist": "~0.0.1", "wordwrap": "~0.0.2" } }, "sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
|
@ -1308,6 +1328,8 @@
|
|||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
|
@ -1332,6 +1354,8 @@
|
|||
|
||||
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||
|
||||
"pkginfo": ["pkginfo@0.3.1", "", {}, "sha512-yO5feByMzAp96LtP58wvPKSbaKAi/1C4kV9XpTctr6EepnP6F33RBNOiVrdz9BrPA98U2BMFsTNHo44TWcbQ2A=="],
|
||||
|
||||
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
||||
|
||||
"portfinder": ["portfinder@1.0.38", "", { "dependencies": { "async": "^3.2.6", "debug": "^4.3.6" } }, "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg=="],
|
||||
|
|
@ -1516,6 +1540,8 @@
|
|||
|
||||
"split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="],
|
||||
|
||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||
|
||||
"standard-version": ["standard-version@9.5.0", "", { "dependencies": { "chalk": "^2.4.2", "conventional-changelog": "3.1.25", "conventional-changelog-config-spec": "2.1.0", "conventional-changelog-conventionalcommits": "4.6.3", "conventional-recommended-bump": "6.1.0", "detect-indent": "^6.0.0", "detect-newline": "^3.1.0", "dotgitignore": "^2.1.0", "figures": "^3.1.0", "find-up": "^5.0.0", "git-semver-tags": "^4.0.0", "semver": "^7.1.1", "stringify-package": "^1.0.1", "yargs": "^16.0.0" }, "bin": { "standard-version": "bin/cli.js" } }, "sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
|
@ -1618,6 +1644,8 @@
|
|||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"underscore": ["underscore@1.8.3", "", {}, "sha512-5WsVTFcH1ut/kkhAaHf4PVgI8c7++GiVcpCGxPouI6ZVjsqPnSDf8h/8HtVqc0t4fzRXwnMK70EcZeAs3PIddg=="],
|
||||
|
||||
"undici": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
|
@ -1672,12 +1700,16 @@
|
|||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
|
||||
"winston": ["winston@1.0.2", "", { "dependencies": { "async": "~1.0.0", "colors": "1.0.x", "cycle": "1.0.x", "eyes": "0.1.x", "isstream": "0.1.x", "pkginfo": "0.3.x", "stack-trace": "0.0.x" } }, "sha512-BLxJH3KCgJ2paj2xKYTQLpxdKr9URPDDDLJnRVcbud7izT+m8Xzt5Rod6mnNgEcfT0fRvhEy2Cj3cEnnQpa6qA=="],
|
||||
|
||||
"wordwrap": ["wordwrap@0.0.3", "", {}, "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||
|
|
@ -1752,6 +1784,8 @@
|
|||
|
||||
"@jimp/wasm-webp/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@node-minify/core/glob": ["glob@10.3.3", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.0.3", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { "glob": "dist/cjs/src/bin.js" } }, "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw=="],
|
||||
|
||||
"@node-minify/core/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"@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=="],
|
||||
|
|
@ -1808,10 +1842,12 @@
|
|||
|
||||
"gitconfiglocal/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||
"glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"handlebars/wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
|
||||
|
||||
"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=="],
|
||||
|
|
@ -1832,6 +1868,8 @@
|
|||
|
||||
"nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="],
|
||||
|
||||
"optimist/minimist": ["minimist@0.0.10", "", {}, "sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw=="],
|
||||
|
||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
|
@ -1840,6 +1878,8 @@
|
|||
|
||||
"pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
||||
|
||||
"portfinder/async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"read-pkg/normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="],
|
||||
|
||||
"read-pkg-up/find-up": ["find-up@2.1.0", "", { "dependencies": { "locate-path": "^2.0.0" } }, "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ=="],
|
||||
|
|
@ -1870,6 +1910,8 @@
|
|||
|
||||
"vite-static-assets-plugin/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"winston/async": ["async@1.0.0", "", {}, "sha512-5mO7DX4CbJzp9zjaFXusQQ4tzKJARjNB1Ih1pVBi8wkbmXy/xzIDgEMXxWePLzt2OdFwaxfneIlT1nCiXubrPQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||
|
|
@ -1924,6 +1966,8 @@
|
|||
|
||||
"@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/core/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||
|
||||
"@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=="],
|
||||
|
|
@ -1938,8 +1982,6 @@
|
|||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
|
@ -2068,6 +2110,8 @@
|
|||
|
||||
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
|
||||
"@node-minify/core/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"dotgitignore/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||
|
||||
"dotgitignore/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@
|
|||
"version:generate": "standard-version --sign",
|
||||
"package:Linux": "bun run build:prod:appimage",
|
||||
"package:Windows": "bun run build:prod",
|
||||
"download:chromium": "bun scripts/download-chromium.ts --out=./bin/chromium"
|
||||
"download:chromium": "bun scripts/download-chromium.ts --out=./bin/chromium",
|
||||
"build:audiosprites": "bun ./scripts/generate-audio-sprites.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"7zip-bin": "^5.2.0",
|
||||
|
|
@ -88,6 +89,7 @@
|
|||
"@tanstack/router-plugin": "^1.157.16",
|
||||
"@tanstack/zod-adapter": "^1.162.4",
|
||||
"@types/adm-zip": "^0.5.8",
|
||||
"@types/audiosprite": "^0.7.3",
|
||||
"@types/bun": "latest",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/howler": "^2.2.12",
|
||||
|
|
@ -101,6 +103,7 @@
|
|||
"adm-zip": "^0.5.16",
|
||||
"animate.css": "^4.1.1",
|
||||
"app-builder-bin": "^5.0.0-alpha.13",
|
||||
"audiosprite": "^0.7.2",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"concurrently": "^9.2.1",
|
||||
|
|
|
|||
24
scripts/generate-audio-sprites.ts
Normal file
24
scripts/generate-audio-sprites.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import audioSprite from 'audiosprite';
|
||||
import { $, which } from 'bun';
|
||||
import fs from "node:fs/promises";
|
||||
import path from 'node:path';
|
||||
|
||||
var files = await Array.fromAsync(new Bun.Glob('*.{ogg,wav}').scan({ cwd: './src/sounds' }));
|
||||
console.log("Loaded", files.join(","));
|
||||
|
||||
await new Promise((resolve) =>
|
||||
{
|
||||
audioSprite(
|
||||
files.map(f => path.join(path.resolve('./src/sounds'), f)),
|
||||
{
|
||||
output: path.resolve('./src/mainview/assets/sounds'),
|
||||
path: path.resolve('./src/sounds'),
|
||||
format: 'howler',
|
||||
export: 'ogg'
|
||||
}, async function (err, obj: any)
|
||||
{
|
||||
if (err) return console.error(err);
|
||||
delete obj.urls;
|
||||
Bun.file('./src/mainview/assets/sounds.json').write(JSON.stringify(obj, null, 2)).then(r => resolve(true));
|
||||
});
|
||||
});
|
||||
48
src/mainview/App.tsx
Normal file
48
src/mainview/App.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { Router } from ".";
|
||||
import { useEffect } from "react";
|
||||
import audioCallbacks from "./scripts/audio/audioCallbacks";
|
||||
import { client as rommClient } from "../clients/romm/client.gen";
|
||||
import { RPC_URL } from "@/shared/constants";
|
||||
|
||||
export const focusQueue: string[] = [];
|
||||
|
||||
export default function App (data: { children: any; })
|
||||
{
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const focusMap = new Map<number, string>();
|
||||
rommClient.setConfig({
|
||||
baseUrl: `${RPC_URL(__HOST__)}/api/romm`,
|
||||
credentials: "include",
|
||||
mode: "cors",
|
||||
});
|
||||
|
||||
const unsub = Router.history.subscribe((op) =>
|
||||
{
|
||||
if (op.action.type === 'PUSH')
|
||||
{
|
||||
focusMap.set(op.location.state.__TSR_index - 1, getCurrentFocusKey());
|
||||
} else if (op.action.type === 'BACK')
|
||||
{
|
||||
if (focusMap.has(op.location.state.__TSR_index))
|
||||
{
|
||||
focusQueue.pop();
|
||||
focusQueue.push(focusMap.get(op.location.state.__TSR_index)!);
|
||||
focusMap.delete(op.location.state.__TSR_index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const audio = audioCallbacks();
|
||||
|
||||
return () =>
|
||||
{
|
||||
unsub();
|
||||
audio.cleanup();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <>{data.children}</>;
|
||||
}
|
||||
304
src/mainview/assets/sounds.json
Normal file
304
src/mainview/assets/sounds.json
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
{
|
||||
"sprite": {
|
||||
"Classic UI SFX - Chords #1": [
|
||||
0,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #10": [
|
||||
6000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #11": [
|
||||
12000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #12": [
|
||||
18000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #13": [
|
||||
24000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #14": [
|
||||
30000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #15": [
|
||||
36000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #16": [
|
||||
42000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #17": [
|
||||
48000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #18": [
|
||||
54000,
|
||||
4005.215419501134
|
||||
],
|
||||
"Classic UI SFX - Chords #19": [
|
||||
60000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #2": [
|
||||
66000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #20": [
|
||||
72000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #3": [
|
||||
78000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #4": [
|
||||
84000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #5": [
|
||||
90000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #6": [
|
||||
96000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #7": [
|
||||
102000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #8": [
|
||||
108000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Chords #9": [
|
||||
114000,
|
||||
4005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Short - High #1": [
|
||||
120000,
|
||||
2546.893424036284
|
||||
],
|
||||
"Classic UI SFX - Short - High #10": [
|
||||
124000,
|
||||
2552.0861678004535
|
||||
],
|
||||
"Classic UI SFX - Short - High #11": [
|
||||
128000,
|
||||
2927.0975056689394
|
||||
],
|
||||
"Classic UI SFX - Short - High #12": [
|
||||
132000,
|
||||
2927.0975056689394
|
||||
],
|
||||
"Classic UI SFX - Short - High #13": [
|
||||
136000,
|
||||
3000
|
||||
],
|
||||
"Classic UI SFX - Short - High #14": [
|
||||
140000,
|
||||
2802.0861678004394
|
||||
],
|
||||
"Classic UI SFX - Short - High #15": [
|
||||
144000,
|
||||
2723.9455782312803
|
||||
],
|
||||
"Classic UI SFX - Short - High #16": [
|
||||
148000,
|
||||
2927.0975056689394
|
||||
],
|
||||
"Classic UI SFX - Short - High #17": [
|
||||
152000,
|
||||
2880.226757369627
|
||||
],
|
||||
"Classic UI SFX - Short - High #18": [
|
||||
156000,
|
||||
2359.387755102034
|
||||
],
|
||||
"Classic UI SFX - Short - High #19": [
|
||||
160000,
|
||||
3052.0861678004394
|
||||
],
|
||||
"Classic UI SFX - Short - High #2": [
|
||||
165000,
|
||||
2843.7641723355964
|
||||
],
|
||||
"Classic UI SFX - Short - High #20": [
|
||||
169000,
|
||||
2015.6462585034092
|
||||
],
|
||||
"Classic UI SFX - Short - High #21": [
|
||||
173000,
|
||||
2005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Short - High #22": [
|
||||
177000,
|
||||
2489.5918367346894
|
||||
],
|
||||
"Classic UI SFX - Short - High #23": [
|
||||
181000,
|
||||
2458.3446712018144
|
||||
],
|
||||
"Classic UI SFX - Short - High #24": [
|
||||
185000,
|
||||
2093.7641723355964
|
||||
],
|
||||
"Classic UI SFX - Short - High #25": [
|
||||
189000,
|
||||
2005.215419501127
|
||||
],
|
||||
"Classic UI SFX - Short - High #3": [
|
||||
193000,
|
||||
2864.6031746031613
|
||||
],
|
||||
"Classic UI SFX - Short - High #4": [
|
||||
197000,
|
||||
3031.2698412698464
|
||||
],
|
||||
"Classic UI SFX - Short - High #5": [
|
||||
202000,
|
||||
2598.9795918367236
|
||||
],
|
||||
"Classic UI SFX - Short - High #6": [
|
||||
206000,
|
||||
2427.0975056689394
|
||||
],
|
||||
"Classic UI SFX - Short - High #7": [
|
||||
210000,
|
||||
2468.752834467125
|
||||
],
|
||||
"Classic UI SFX - Short - High #8": [
|
||||
214000,
|
||||
2916.666666666657
|
||||
],
|
||||
"Classic UI SFX - Short - High #9": [
|
||||
218000,
|
||||
2250
|
||||
],
|
||||
"Classic UI SFX - Short - Low #1": [
|
||||
222000,
|
||||
2010.4308390022538
|
||||
],
|
||||
"Classic UI SFX - Short - Low #10": [
|
||||
226000,
|
||||
3020.8390022675644
|
||||
],
|
||||
"Classic UI SFX - Short - Low #11": [
|
||||
231000,
|
||||
2458.3446712018144
|
||||
],
|
||||
"Classic UI SFX - Short - Low #12": [
|
||||
235000,
|
||||
2901.0430839002197
|
||||
],
|
||||
"Classic UI SFX - Short - Low #13": [
|
||||
239000,
|
||||
2843.7641723355964
|
||||
],
|
||||
"Classic UI SFX - Short - Low #14": [
|
||||
243000,
|
||||
3135.4195011337824
|
||||
],
|
||||
"Classic UI SFX - Short - Low #15": [
|
||||
248000,
|
||||
2703.1292517006877
|
||||
],
|
||||
"Classic UI SFX - Short - Low #16": [
|
||||
252000,
|
||||
2875.011337868472
|
||||
],
|
||||
"Classic UI SFX - Short - Low #17": [
|
||||
256000,
|
||||
2927.0975056689394
|
||||
],
|
||||
"Classic UI SFX - Short - Low #18": [
|
||||
260000,
|
||||
3057.2789115646515
|
||||
],
|
||||
"Classic UI SFX - Short - Low #19": [
|
||||
265000,
|
||||
2473.9455782312803
|
||||
],
|
||||
"Classic UI SFX - Short - Low #2": [
|
||||
269000,
|
||||
2583.3333333333144
|
||||
],
|
||||
"Classic UI SFX - Short - Low #20": [
|
||||
273000,
|
||||
2515.646258503409
|
||||
],
|
||||
"Classic UI SFX - Short - Low #21": [
|
||||
277000,
|
||||
2604.172335600879
|
||||
],
|
||||
"Classic UI SFX - Short - Low #22": [
|
||||
281000,
|
||||
3031.2698412698182
|
||||
],
|
||||
"Classic UI SFX - Short - Low #23": [
|
||||
286000,
|
||||
2937.50566893425
|
||||
],
|
||||
"Classic UI SFX - Short - Low #24": [
|
||||
290000,
|
||||
2609.387755102034
|
||||
],
|
||||
"Classic UI SFX - Short - Low #25": [
|
||||
294000,
|
||||
2625.0113378685
|
||||
],
|
||||
"Classic UI SFX - Short - Low #3": [
|
||||
298000,
|
||||
2828.140589569159
|
||||
],
|
||||
"Classic UI SFX - Short - Low #4": [
|
||||
302000,
|
||||
2614.6031746031895
|
||||
],
|
||||
"Classic UI SFX - Short - Low #5": [
|
||||
306000,
|
||||
3161.4739229024735
|
||||
],
|
||||
"Classic UI SFX - Short - Low #6": [
|
||||
311000,
|
||||
2333.3333333333144
|
||||
],
|
||||
"Classic UI SFX - Short - Low #7": [
|
||||
315000,
|
||||
2536.4625850340303
|
||||
],
|
||||
"Classic UI SFX - Short - Low #8": [
|
||||
319000,
|
||||
2630.2267573695985
|
||||
],
|
||||
"Classic UI SFX - Short - Low #9": [
|
||||
323000,
|
||||
2697.936507936504
|
||||
],
|
||||
"UI_Single_Set 16_01": [
|
||||
327000,
|
||||
309.5918367346826
|
||||
],
|
||||
"UI_Single_Set 16_02": [
|
||||
329000,
|
||||
309.5918367346826
|
||||
],
|
||||
"UI_Single_Set 16_03": [
|
||||
331000,
|
||||
309.5918367346826
|
||||
],
|
||||
"UI_TwoNote_Set 15_01": [
|
||||
333000,
|
||||
335.2380952380827
|
||||
],
|
||||
"UI_TwoNote_Set 15_02": [
|
||||
335000,
|
||||
309.5918367346826
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
src/mainview/assets/sounds.ogg
(Stored with Git LFS)
Normal file
BIN
src/mainview/assets/sounds.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
import { doesFocusableExist, FocusDetails, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useLayoutEffect } from "react";
|
||||
|
||||
export function AutoFocus (data: {
|
||||
parentKey?: string;
|
||||
|
|
@ -8,7 +8,7 @@ export function AutoFocus (data: {
|
|||
delay?: number;
|
||||
})
|
||||
{
|
||||
useEffect(() =>
|
||||
useLayoutEffect(() =>
|
||||
{
|
||||
let delayTimeout: number | undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import classNames from "classnames";
|
|||
import { JSX } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import useActiveControl from "../scripts/gamepads";
|
||||
import { oneShot } from "../scripts/audio/audio";
|
||||
|
||||
export function GameCardSkeleton ()
|
||||
{
|
||||
|
|
@ -38,10 +39,15 @@ export interface GameCardParams
|
|||
|
||||
export default function CardElement (data: GameCardParams & InteractParams)
|
||||
{
|
||||
const handleAction = () =>
|
||||
{
|
||||
data.onAction?.();
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref, focused, focusSelf } = useFocusable({
|
||||
focusKey: data.focusKey,
|
||||
onFocus: (l, p, details) => data.onFocus?.(data.id, ref.current as any, details),
|
||||
onEnterPress: () => data.onAction?.(),
|
||||
onEnterPress: handleAction,
|
||||
onBlur: () => data.onBlur?.(data.id),
|
||||
});
|
||||
const { isPointer } = useActiveControl();
|
||||
|
|
@ -57,11 +63,10 @@ export default function CardElement (data: GameCardParams & InteractParams)
|
|||
scrollSnapAlign: isPointer ? "center" : "none"
|
||||
}}
|
||||
onFocus={focusSelf}
|
||||
onDoubleClick={e => data.onAction?.(e.nativeEvent)}
|
||||
onClick={() =>
|
||||
{
|
||||
focusSelf();
|
||||
data.onAction?.();
|
||||
handleAction();
|
||||
}}
|
||||
className={twMerge(
|
||||
"relative game-card light:bg-base-100 dark:bg-base-300 flex flex-col z-5 overflow-hidden transition-all duration-200 not-mobile:drop-shadow-lg cursor-pointer focusable focusable-primary focusable-hover select-none focused focused:not-control-mouse:animate-wiggle focused:not-control-mouse:bg-base-content focused:not-control-mouse:text-base-300 focused:not-control-mouse:drop-shadow-lg focused:not-control-mouse:drop-shadow-black/30 focused:not-control-mouse:scale-102 focused:not-control-mouse:z-10 group control-mouse:hover:bg-base-200 h-full [--tw-border-style:inset] border-2 border-base-content/5 backdrop-opacity-0 active:bg-base-content! active:text-base-100 active:transition-none",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useSuspenseQuery } from "@tanstack/react-query";
|
|||
import { CardList, GameMetaExtra } from "./CardList";
|
||||
import { GameCardFocusHandler } from "./CardElement";
|
||||
import { getCollectionsQuery } from "@queries/romm";
|
||||
import { Router } from "..";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
export default function CollectionList (data: {
|
||||
id: string,
|
||||
|
|
@ -14,12 +14,13 @@ export default function CollectionList (data: {
|
|||
saveChildFocus?: 'session' | 'local';
|
||||
})
|
||||
{
|
||||
const router = useRouter();
|
||||
const { data: collections } = useSuspenseQuery(getCollectionsQuery);
|
||||
|
||||
const handleDefaultSelect = (gameId: string) =>
|
||||
{
|
||||
const [source, id] = gameId.split('@');
|
||||
Router.navigate({
|
||||
router.navigate({
|
||||
to: `/collection/$source/$id`,
|
||||
params: { source, id },
|
||||
search: { countHint: collections.find(c => c.id.id === id && c.id.source === source)?.game_count }
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { AnimatedBackground } from './AnimatedBackground';
|
||||
import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { HeaderUI, StickyHeaderUI } from './Header';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { StickyHeaderUI } from './Header';
|
||||
import { GameList } from './GameList';
|
||||
import { Search, Settings2 } from 'lucide-react';
|
||||
import { JSX, Suspense, useEffect } from 'react';
|
||||
import { JSX, Suspense } from 'react';
|
||||
import Shortcuts from './Shortcuts';
|
||||
import { AutoFocus } from './AutoFocus';
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||
import { GameListFilterType } from '@/shared/constants';
|
||||
import { GameCardFocusHandler } from './CardElement';
|
||||
import { HandleGoBack, useStickyDataAttr } from '../scripts/utils';
|
||||
import { HandleGoBack } from '../scripts/utils';
|
||||
import LoadingCardList from './LoadingCardList';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { gameQuery } from '../scripts/queries/romm';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
|
||||
export interface CollectionsDetailParams
|
||||
{
|
||||
|
|
@ -29,6 +29,7 @@ export interface CollectionsDetailParams
|
|||
|
||||
export function CollectionsDetail (data: CollectionsDetailParams)
|
||||
{
|
||||
const router = useRouter();
|
||||
const builtData = useQuery({
|
||||
queryKey: ['filter', data.id], queryFn: async () =>
|
||||
{
|
||||
|
|
@ -42,7 +43,7 @@ export function CollectionsDetail (data: CollectionsDetailParams)
|
|||
preferredChildFocusKey: `${focusKey}-list`
|
||||
});
|
||||
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: () => HandleGoBack(router) }], [router]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
const handleScroll: GameCardFocusHandler = (cardId, node, details) =>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { X } from "lucide-react";
|
|||
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
|
||||
import { ContextDialogContext } from "../scripts/contexts";
|
||||
import { FOCUS_KEYS } from "../scripts/types";
|
||||
import { oneShot } from "../scripts/audio/audio";
|
||||
|
||||
export function ContextList (data: {
|
||||
options?: DialogEntry[];
|
||||
|
|
@ -34,6 +35,7 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class
|
|||
{
|
||||
if (data.disabled === true) return;
|
||||
data.action?.({ close: context.close, focus: focusSelf });
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref, focusSelf, focusKey } = useFocusable({
|
||||
focusKey: FOCUS_KEYS.CONTEXT_DIALOG_OPTION(context.id, data.id),
|
||||
|
|
@ -57,6 +59,7 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class
|
|||
onClick={handleAction}
|
||||
data-selected={data.selected}
|
||||
aria-disabled={data.disabled}
|
||||
data-sound-category={"menu"}
|
||||
className={
|
||||
twMerge("flex cursor-pointer sm:text-sm md:text-base group-focusable scroll-m-4")}>
|
||||
<FocusContext value={focusKey}>
|
||||
|
|
@ -100,10 +103,10 @@ export function useContextDialog (id: string, data: { content?: JSX.Element; cla
|
|||
data.onClose?.();
|
||||
if (newSourceFocusKey)
|
||||
{
|
||||
setFocus(newSourceFocusKey);
|
||||
setFocus(newSourceFocusKey, { instant: true });
|
||||
} else if (sourceFocusKey)
|
||||
{
|
||||
setFocus(sourceFocusKey);
|
||||
setFocus(sourceFocusKey, { instant: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,12 +140,14 @@ export function ContextDialog (data: {
|
|||
const handleClose = () =>
|
||||
{
|
||||
data.close(false);
|
||||
oneShot('closeContext');
|
||||
};
|
||||
useEffect(() =>
|
||||
{
|
||||
if (data.open)
|
||||
{
|
||||
focusSelf();
|
||||
focusSelf({ instant: true });
|
||||
oneShot('openContext');
|
||||
}
|
||||
}, [data.open]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
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 { ErrorComponentProps, useRouter } from "@tanstack/react-router";
|
||||
|
||||
export default function Error (data: ErrorComponentProps)
|
||||
{
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "not-found" });
|
||||
const handleReturn = () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } });
|
||||
const router = useRouter();
|
||||
const handleReturn = () => router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } });
|
||||
useShortcuts(focusKey, () => [{ label: "Return Home", button: GamePadButtonCode.B, action: handleReturn }]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
useEffect(() => { focusSelf(); }, []);
|
||||
useEffect(() => { focusSelf({ instant: true }); }, []);
|
||||
|
||||
return <div ref={ref} className="absolute flex flex-col justify-center items-center w-full h-full gap-4">
|
||||
<FocusContext value={focusKey}>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import SvgIcon from "./SvgIcon";
|
|||
import { twMerge } from "tailwind-merge";
|
||||
import { useEffect } from "react";
|
||||
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
|
||||
import { oneShot } from "../scripts/audio/audio";
|
||||
|
||||
function FilterCat (
|
||||
data: {
|
||||
|
|
@ -19,7 +20,10 @@ function FilterCat (
|
|||
{
|
||||
const { ref, focusSelf } = useFocusable({
|
||||
focusKey: data.id,
|
||||
onFocus: (l, p, details) => data.onFocus?.(data.id, ref.current, details),
|
||||
onFocus: (l, p, details) =>
|
||||
{
|
||||
data.onFocus?.(data.id, ref.current, details);
|
||||
},
|
||||
onEnterPress: data.onAction
|
||||
});
|
||||
|
||||
|
|
@ -27,7 +31,8 @@ function FilterCat (
|
|||
<li
|
||||
aria-selected={data.active}
|
||||
ref={ref}
|
||||
onClick={focusSelf}
|
||||
onClick={e => focusSelf({ event: e.nativeEvent })}
|
||||
data-sound-category={data.active ? undefined : "filter"}
|
||||
className={"sm:text-sm sm:px-2 flex md:px-4 items-center justify-center rounded-full transition-all md:text-lg focusable focusable-primary hover:not-focused:not-aria-selected:bg-base-content/40 not-focused:cursor-pointer aria-selected:bg-base-content aria-selected:text-base-300 aria-selected:drop-shadow aria-selected:cursor-default active:bg-accent! active:text-accent-content! active:ring-offset-7 active:ring-offset-base-content select-none gap-1"}
|
||||
>
|
||||
{data.icon ? <><div className="sm:portrait:px-2">{data.icon}</div><div className="sm:portrait:hidden md:inline">{data.children ?? data.label}</div></> : <div>{data.children ?? data.label}</div>}
|
||||
|
|
@ -68,6 +73,10 @@ export function FilterUI (data: {
|
|||
if (!data.options[newFilter].selected)
|
||||
{
|
||||
data.setSelected(newFilter);
|
||||
oneShot('selectFilter');
|
||||
} else
|
||||
{
|
||||
oneShot('invalidNavigation');
|
||||
}
|
||||
},
|
||||
button: GamePadButtonCode.R1
|
||||
|
|
@ -80,7 +89,13 @@ export function FilterUI (data: {
|
|||
const selectedFilterIndex = Math.max(0, filterIndex - 1,);
|
||||
const newFilter = filterKeys[selectedFilterIndex];
|
||||
if (!data.options[newFilter].selected)
|
||||
{
|
||||
data.setSelected(newFilter);
|
||||
oneShot('selectFilter');
|
||||
} else
|
||||
{
|
||||
oneShot('invalidNavigation');
|
||||
}
|
||||
},
|
||||
button: GamePadButtonCode.L1
|
||||
}], [data.options]);
|
||||
|
|
@ -90,7 +105,7 @@ export function FilterUI (data: {
|
|||
{
|
||||
if (hasFocusedChild)
|
||||
{
|
||||
setFocus(`${data.id}-${defaultFocus}`);
|
||||
setFocus(`${data.id}-${defaultFocus}`, { instant: true });
|
||||
}
|
||||
}, [hasFocusedChild, defaultFocus, data.id]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { RPC_URL } from "@/shared/constants";
|
||||
import CardElement from "./CardElement";
|
||||
import { Router } from "..";
|
||||
import { FileQuestion, HardDrive, Store } from "lucide-react";
|
||||
import { JSX } from "react";
|
||||
import { FOCUS_KEYS } from "../scripts/types";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
export default function FrontEndGameCard (data: { index: number, game: FrontEndGameType; showSource?: boolean; } & FocusParams & InteractParams)
|
||||
{
|
||||
const router = useRouter();
|
||||
function handleDefaultSelect (id: FrontEndId, source: string | null, sourceId: string | null)
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } });
|
||||
router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } });
|
||||
};
|
||||
|
||||
const platformUrl = new URL(`${RPC_URL(__HOST__)}${data.game.path_platform_cover}`);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import
|
|||
Bell,
|
||||
Bluetooth,
|
||||
Clock,
|
||||
Plug,
|
||||
Settings,
|
||||
Wifi,
|
||||
WifiHigh,
|
||||
|
|
@ -23,17 +22,15 @@ import
|
|||
} from "lucide-react";
|
||||
import { RoundButton } from "./RoundButton";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { RPC_URL, SystemInfoType } from "../../shared/constants";
|
||||
import { JSX, RefObject, useContext, useEffect, useRef, useState } from "react";
|
||||
import { systemApi } from "../scripts/clientApi";
|
||||
import { Router } from "..";
|
||||
import { useStickyDataAttr } from "../scripts/utils";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { TwitchIcon } from "../scripts/brandIcons";
|
||||
import { rommLoggedInQuery, rommUserQuery } from "../scripts/queries/romm";
|
||||
import { rommLoggedInQuery } from "../scripts/queries/romm";
|
||||
import { twitchLoginVerificationQuery } from "../scripts/queries/settings";
|
||||
import { da } from "zod/v4/locales";
|
||||
import { SystemInfoContext } from "../scripts/contexts";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import { oneShot } from "../scripts/audio/audio";
|
||||
|
||||
function HeaderAvatar (data: {
|
||||
id: string;
|
||||
|
|
@ -206,19 +203,23 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; })
|
|||
placeholderData: keepPreviousData
|
||||
});
|
||||
|
||||
const { ref } = useFocusable({ focusKey: 'accounts' });
|
||||
const handleSelect = () =>
|
||||
{
|
||||
router.navigate({ to: '/settings/accounts' });
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref } = useFocusable({
|
||||
focusKey: 'accounts', onEnterPress: handleSelect
|
||||
});
|
||||
|
||||
const accounts: HeaderAccount[] = [];
|
||||
if (data.accounts) accounts.push(...data.accounts);
|
||||
const router = useRouter();
|
||||
|
||||
if (rommUser.data?.hasLogin || rommUser.isError)
|
||||
{
|
||||
accounts.push({
|
||||
id: 'romm', preview: `https://romm.app/_ipx/q_80/images/blocks/logos/romm.svg`,
|
||||
action: () =>
|
||||
{
|
||||
Router.navigate({ to: '/settings/accounts', search: { focus: 'rommAddress' } });
|
||||
},
|
||||
className: rommUser.data?.hasLogin && !rommUser.isError ? undefined : "border-error",
|
||||
type: 'secondary'
|
||||
});
|
||||
|
|
@ -228,15 +229,11 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; })
|
|||
{
|
||||
accounts.push({
|
||||
id: 'twitch', preview: TwitchIcon,
|
||||
action: () =>
|
||||
{
|
||||
Router.navigate({ to: '/settings/accounts', search: { focus: 'rommAddress' } });
|
||||
},
|
||||
type: 'secondary'
|
||||
});
|
||||
}
|
||||
|
||||
return <div ref={ref} className="avatar-group cursor-pointer -space-x-6 w-fit flex items-center gap-2 drop-shadow-sm overflow-visible rounded-3xl focusable focusable-hover ">
|
||||
return <div onClick={handleSelect} ref={ref} style={{ viewTimelineName: "header-accounts" }} className="avatar-group cursor-pointer -space-x-6 w-fit flex items-center gap-2 drop-shadow-sm overflow-visible rounded-3xl focusable focusable-hover ">
|
||||
{accounts?.map(a => <HeaderAvatar
|
||||
key={`header-avatar-${a.id}`}
|
||||
id={`account-${a.id}`}
|
||||
|
|
@ -285,9 +282,10 @@ interface HeaderUIParams
|
|||
export function HeaderUI (data: HeaderUIParams)
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({ focusKey: "header-elements", focusable: data.focusable, preferredChildFocusKey: data.preferredChildFocusKey });
|
||||
const router = useRouter();
|
||||
const goToSettings = () =>
|
||||
{
|
||||
Router.navigate({ to: '/settings/accounts' });
|
||||
router.navigate({ to: '/settings/accounts' });
|
||||
};
|
||||
return (
|
||||
<FocusContext.Provider value={focusKey}>
|
||||
|
|
@ -296,7 +294,7 @@ export function HeaderUI (data: HeaderUIParams)
|
|||
className="flex items-center justify-between text-base-content"
|
||||
style={{ viewTimelineName: 'header' }}
|
||||
>
|
||||
<HeaderAccounts accounts={data.accounts} />
|
||||
<HeaderAccounts key={"header-accounts"} accounts={data.accounts} />
|
||||
{data.title}
|
||||
<HeaderStatusBar key={"header-status-bar"} buttonElements={data.buttonElements} buttons={[...data.buttons ?? [], { icon: <Settings />, id: "settings", action: goToSettings, external: true }]} />
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export default function LoadMoreButton (data: { isFetching: boolean; lastId?: Fr
|
|||
};
|
||||
|
||||
const { ref, focusKey, focused } = useFocusable({
|
||||
focusable: !data.isFetching,
|
||||
focusKey: 'load-more-btn',
|
||||
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details),
|
||||
onEnterPress: handleAction
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
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 { useRouter } from "@tanstack/react-router";
|
||||
|
||||
export default function NotFound ()
|
||||
{
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "not-found" });
|
||||
const handleReturn = () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } });
|
||||
const router = useRouter();
|
||||
const handleReturn = () => router.navigate({ to: '/', viewTransition: { types: ['zoom-in'] } });
|
||||
useShortcuts(focusKey, () => [{ label: "Return Home", button: GamePadButtonCode.B, action: handleReturn }]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
useEffect(() => { focusSelf(); }, []);
|
||||
useEffect(() => { focusSelf({ instant: true }); }, []);
|
||||
|
||||
return <div ref={ref} className="absolute flex flex-col justify-center items-center w-full h-full gap-4">
|
||||
<FocusContext value={focusKey}>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export default function Screenshots (data: { screenshots?: string[]; className?:
|
|||
const closest = findClosestElementToCenter(scrollRef.current);
|
||||
if (!closest) return;
|
||||
const closestIndex = Array.from(scrollRef.current.children).indexOf(closest);
|
||||
setFocus(`screenshot-${closestIndex}`);
|
||||
setFocus(`screenshot-${closestIndex}`, { instant: true });
|
||||
}
|
||||
}, [focused, hasFocusedChild, scrollRef.current]);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ import MainActions from "./MainActions";
|
|||
import ActionButton from "./ActionButton";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import FocusTooltip from "../FocusTooltip";
|
||||
import { Router } from "@/mainview";
|
||||
import { useBlocker } from "@tanstack/react-router";
|
||||
import { useBlocker, useRouter } from "@tanstack/react-router";
|
||||
|
||||
function AchievementsInfo (data: { game: FrontEndGameTypeDetailed; } & InteractParams)
|
||||
{
|
||||
|
|
@ -35,11 +34,12 @@ export default function ActionButtons (data: { game?: FrontEndGameTypeDetailed,
|
|||
const [, setDetailsSection] = useLocalStorage('details-section', 'screenshots');
|
||||
|
||||
const { ref, focusKey, hasFocusedChild } = useFocusable({ focusKey: 'actions', forceFocus: true, trackChildren: true, preferredChildFocusKey: 'mainAction' });
|
||||
const router = useRouter();
|
||||
const deleteMutation = useMutation({
|
||||
...deleteGameMutation({ id: data.id, source: data.source }),
|
||||
onSuccess: (d, v, r, ctx) =>
|
||||
{
|
||||
ctx.client.invalidateQueries(gameInvalidationQuery(data.id, data.source)).then(() => Router.history.back());
|
||||
ctx.client.invalidateQueries(gameInvalidationQuery(data.id, data.source)).then(() => router.history.back());
|
||||
},
|
||||
onError (error)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Router } from "@/mainview";
|
||||
import { rommApi } from "@/mainview/scripts/clientApi";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { JSX, useEffect, useRef, useState } from "react";
|
||||
|
|
@ -9,10 +8,12 @@ import { ContextList, DialogEntry, useContextDialog } from "../ContextDialog";
|
|||
import { Clock, Download, EllipsisVertical, Import, PackageOpen, Play, TriangleAlert } from "lucide-react";
|
||||
import { gameInvalidationQuery, installMutation, playMutation } from "@/mainview/scripts/queries/romm";
|
||||
import ActionButton from "./ActionButton";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
export default function MainActions (data: { game?: FrontEndGameTypeDetailed, source: string, id: string; })
|
||||
{
|
||||
const installMut = useMutation(installMutation(data.source, data.id));
|
||||
const router = useRouter();
|
||||
const playMut = useMutation({
|
||||
...playMutation, onError (error)
|
||||
{
|
||||
|
|
@ -20,7 +21,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
},
|
||||
onSuccess (data, { source, id }, onMutateResult, context)
|
||||
{
|
||||
Router.navigate({ to: '/launcher/$source/$id', params: { source: source, id: id }, replace: true });
|
||||
router.navigate({ to: '/launcher/$source/$id', params: { source: source, id: id }, replace: true });
|
||||
},
|
||||
});
|
||||
const ws = useRef<{ send: (data: string) => void; }>(undefined);
|
||||
|
|
@ -58,10 +59,10 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
{
|
||||
if (localId)
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { id: String(localId), source: 'local' }, replace: true });
|
||||
router.navigate({ to: '/game/$source/$id', params: { id: String(localId), source: 'local' }, replace: true });
|
||||
} else
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { id: data.id, source: data.source }, replace: true });
|
||||
router.navigate({ to: '/game/$source/$id', params: { id: data.id, source: data.source }, replace: true });
|
||||
}
|
||||
});
|
||||
} else if (e.data.status === 'error')
|
||||
|
|
@ -78,7 +79,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
sub.close();
|
||||
ws.current = undefined;
|
||||
};
|
||||
}, [data.source, data.id]);
|
||||
}, [data.source, data.id, router]);
|
||||
|
||||
let progressIcon: JSX.Element | undefined = undefined;
|
||||
switch (status)
|
||||
|
|
@ -107,7 +108,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
if (cmd.emulator === 'EMULATORJS')
|
||||
{
|
||||
const params = new URLSearchParams(cmd.command);
|
||||
Router.navigate({ to: '/embedded/$source/$id', params: { source: data.source, id: data.id }, search: Object.fromEntries(params.entries()), replace: true });
|
||||
router.navigate({ to: '/embedded/$source/$id', params: { source: data.source, id: data.id }, search: Object.fromEntries(params.entries()), replace: true });
|
||||
} else
|
||||
{
|
||||
playMut.mutate({ source: data.source, id: data.id, command_id: cmd.id });
|
||||
|
|
@ -142,7 +143,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
|
|||
{
|
||||
if (status === 'missing-emulator')
|
||||
{
|
||||
Router.navigate({ to: '/settings/directories' });
|
||||
router.navigate({ to: '/settings/directories' });
|
||||
}
|
||||
}}
|
||||
id="mainAction">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import
|
|||
import classNames from "classnames";
|
||||
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { CSSProperties } from "react";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
export type ButtonStyle = 'base' | 'accent' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
|
|
@ -35,9 +36,14 @@ export function Button (data: {
|
|||
tooltipType?: "base" | "accent" | "error";
|
||||
} & InteractParams & FocusParams)
|
||||
{
|
||||
const handleAction = (e?: any) =>
|
||||
{
|
||||
data.onAction?.(e);
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref, focused, focusKey } = useFocusable({
|
||||
focusKey: data.id,
|
||||
onEnterPress: data.onAction,
|
||||
onEnterPress: () => handleAction(),
|
||||
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details),
|
||||
focusable: !data.disabled
|
||||
});
|
||||
|
|
@ -49,7 +55,7 @@ export function Button (data: {
|
|||
|
||||
return <button
|
||||
ref={ref}
|
||||
onClick={e => data.onAction?.(e.nativeEvent)}
|
||||
onClick={handleAction}
|
||||
disabled={data.disabled}
|
||||
data-tooltip={data.tooltip}
|
||||
data-tooltip_type={data.tooltipType}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
|||
import { ContextDialog, ContextList, DialogEntry } from "../ContextDialog";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
export function OptionDropdown (data: {
|
||||
name: string;
|
||||
|
|
@ -23,6 +24,7 @@ export function OptionDropdown (data: {
|
|||
const handlePress = () =>
|
||||
{
|
||||
setOpen(true);
|
||||
oneShot('click');
|
||||
};
|
||||
const handleClose = () => setOpen(false);
|
||||
const { ref } = useFocusable({
|
||||
|
|
@ -33,11 +35,7 @@ export function OptionDropdown (data: {
|
|||
<>
|
||||
<label ref={ref} className={twMerge("flex group-focusable items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent")}>
|
||||
{!!data.icon && <span className={"text-base-content/80 is-focused:text-primary-content"}>{data.icon}</span>}
|
||||
<button onClick={() =>
|
||||
{
|
||||
console.log("Open");
|
||||
setOpen(true);
|
||||
}} 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>
|
||||
<button onClick={handlePress} 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 active:bg-primary active:text-primary-content active:border-0'}>{data.value}<ChevronDown /></button>
|
||||
</label>
|
||||
{open && <ContextDialog id={`${data.name}-context`} preferredChildFocusKey={FOCUS_KEYS.CONTEXT_DIALOG_OPTION(`${data.name}-context`, String(data.values.indexOf(data.value ?? '')))} open={true} close={handleClose}>
|
||||
<ContextList options={data.values.map((v, i) => ({
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useOptionContext } from "./OptionSpace";
|
|||
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { systemApi } from "../../scripts/clientApi";
|
||||
import { CheckIcon, X } from "lucide-react";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
export function OptionInput (data: {
|
||||
name: string;
|
||||
|
|
@ -27,6 +28,7 @@ export function OptionInput (data: {
|
|||
{
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref } = useFocusable({
|
||||
focusKey: data.name, onEnterPress: handlePress
|
||||
|
|
@ -79,12 +81,14 @@ export function OptionInput (data: {
|
|||
name={data.name}
|
||||
checked={Boolean(data.value)}
|
||||
type={data.type}
|
||||
onClick={() => { oneShot("click"); }}
|
||||
autoComplete={data.autocomplete}
|
||||
onFocus={handleFocus}
|
||||
placeholder={data.placeholder}
|
||||
onChange={e => data.onChange?.(e.target.checked)}
|
||||
onBlur={data.onBlur}
|
||||
className={twMerge(
|
||||
"active:bg-base-content rounded-full",
|
||||
data.className
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export function PathSettingsOptionBase (data: PathSettingsOptionParams & {
|
|||
const handleCloseSeatch = () =>
|
||||
{
|
||||
setIsBrowsing(false);
|
||||
setFocus(`${data.id}-browse`);
|
||||
setFocus(`${data.id}-browse`, { instant: true });
|
||||
};
|
||||
|
||||
const handleInputBlur = () =>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ 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 Carousel from "../Carousel";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
function SeeAllCard (data: { id: string; onAction: () => void; onFocus?: (details: { node: HTMLElement, instant: boolean; }) => void; })
|
||||
function SeeAllCard (data: { id: string; onAction: () => void; onFocus?: (details: { node: HTMLElement, instant?: boolean; }) => void; })
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: data.id,
|
||||
|
|
@ -39,6 +39,7 @@ export function EmulatorsSection (data: {
|
|||
header?: any;
|
||||
} & FocusParams)
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: FOCUS_KEYS.EMULATOR_SECTION(data.id),
|
||||
trackChildren: true,
|
||||
|
|
@ -68,7 +69,7 @@ export function EmulatorsSection (data: {
|
|||
scrollIntoNearestParent(node, { behavior: details.instant ? 'instant' : 'smooth' });
|
||||
}} />
|
||||
)) ?? Array.from({ length: 8 }).map((_, i) => <div key={i} className="skeleton h-38 w-full rounded-4xl" />)}
|
||||
<SeeAllCard id={`${FOCUS_KEYS.EMULATOR_SECTION}-see-all`} onAction={() => Router.navigate({ to: '/store/tab/emulators', viewTransition: { types: ['zoom-in'] } })} onFocus={({ node, instant }) => scrollIntoNearestParent(node, { behavior: instant ? 'instant' : 'smooth' })} />
|
||||
<SeeAllCard id={`${FOCUS_KEYS.EMULATOR_SECTION}-see-all`} onAction={() => router.navigate({ to: '/store/tab/emulators', viewTransition: { types: ['zoom-in'] } })} onFocus={({ node, instant }) => scrollIntoNearestParent(node, { behavior: instant ? 'instant' : 'smooth' })} />
|
||||
</Carousel>
|
||||
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function GamesSection (data: {
|
|||
useEffect(() =>
|
||||
{
|
||||
if (focused)
|
||||
focusSelf();
|
||||
focusSelf({ instant: true });
|
||||
}, [!!data.games]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { ChevronRight, CircleQuestionMark, SearchAlert } from "lucide-react";
|
|||
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { RPC_URL } from "@/shared/constants";
|
||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
// ── Single missing-emulator card ───────────────────────────────────────────
|
||||
interface MissingCardProps
|
||||
|
|
@ -19,7 +20,11 @@ interface MissingCardProps
|
|||
|
||||
function MissingCard ({ emulator: em, onSelect }: MissingCardProps)
|
||||
{
|
||||
const handleSelect = () => onSelect?.(em.name, focusKey);
|
||||
const handleSelect = () =>
|
||||
{
|
||||
onSelect?.(em.name, focusKey);
|
||||
oneShot('click');
|
||||
};
|
||||
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: FOCUS_KEYS.MISSING_CARD(em.name),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { BadgeCheck, ChevronRight, EllipsisVertical, FileQuestion, IceCream2, Pa
|
|||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||
import { FlatpackIcon } from "@/mainview/scripts/brandIcons";
|
||||
import { JSX } from "react";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
export const emulatorStatusIcons: Record<string, JSX.Element> = {
|
||||
store: <Store />,
|
||||
|
|
@ -26,7 +27,11 @@ export function StoreEmulatorCard (data: {
|
|||
className?: string;
|
||||
})
|
||||
{
|
||||
const handleSelect = () => data.onSelect?.(data.emulator.name, focusKey);
|
||||
const handleSelect = () =>
|
||||
{
|
||||
data.onSelect?.(data.emulator.name, focusKey);
|
||||
oneShot('click');
|
||||
};
|
||||
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: FOCUS_KEYS.EMULATOR_CARD(data.id),
|
||||
|
|
@ -45,6 +50,7 @@ export function StoreEmulatorCard (data: {
|
|||
ref={ref}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
data-sound-category="emulator"
|
||||
data-installed={data.emulator.validSources.some(s => s.exists)}
|
||||
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)}
|
||||
|
|
@ -87,7 +93,7 @@ export function StoreEmulatorCard (data: {
|
|||
</div>;
|
||||
})}
|
||||
{isMouse && <>
|
||||
<Button onAction={handleSelect} style="base" className="grow text-base-content/40" id={`${data.emulator.name}-details`} >Details<ChevronRight /></Button>
|
||||
<Button onAction={e => data.onSelect?.(data.emulator.name, focusKey)} style="base" className="grow text-base-content/40" id={`${data.emulator.name}-details`} >Details<ChevronRight /></Button>
|
||||
</>}
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -464,7 +464,7 @@ const assets = new Set<string>([
|
|||
]);
|
||||
|
||||
// Store basePath resolved from Vite config
|
||||
const BASE_PATH = "./";
|
||||
const BASE_PATH = "/";
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,15 +9,13 @@ import
|
|||
} from "@tanstack/react-router";
|
||||
import { routeTree } from "./gen/routeTree.gen";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RPC_URL } from "../shared/constants";
|
||||
import "./scripts/gamepads";
|
||||
import "./scripts/windowEvents";
|
||||
import { client as rommClient } from "../clients/romm/client.gen";
|
||||
import "./scripts/spatialNavigation";
|
||||
import NotFound from "./components/NotFound";
|
||||
import Error from "./components/Error";
|
||||
import serviceWorker from './scripts/serviceWorker?worker&url';
|
||||
import { getCurrentFocusKey, setFocus } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import App from "./App";
|
||||
|
||||
if ('serviceWorker' in navigator)
|
||||
{
|
||||
|
|
@ -26,12 +24,6 @@ if ('serviceWorker' in navigator)
|
|||
|
||||
const hashHistory = createHashHistory({});
|
||||
|
||||
rommClient.setConfig({
|
||||
baseUrl: `${RPC_URL(__HOST__)}/api/romm`,
|
||||
credentials: "include",
|
||||
mode: "cors",
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export interface RouterContext
|
||||
|
|
@ -66,25 +58,6 @@ export const Router = createRouter({
|
|||
}
|
||||
});
|
||||
|
||||
const focusMap = new Map<number, string>();
|
||||
export const focusQueue: string[] = [];
|
||||
|
||||
Router.history.subscribe((op) =>
|
||||
{
|
||||
if (op.action.type === 'PUSH')
|
||||
{
|
||||
focusMap.set(op.location.state.__TSR_index - 1, getCurrentFocusKey());
|
||||
} else if (op.action.type === 'BACK')
|
||||
{
|
||||
if (focusMap.has(op.location.state.__TSR_index))
|
||||
{
|
||||
focusQueue.pop();
|
||||
focusQueue.push(focusMap.get(op.location.state.__TSR_index)!);
|
||||
focusMap.delete(op.location.state.__TSR_index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register things for typesafety
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register
|
||||
|
|
@ -100,9 +73,11 @@ if (!rootElement.innerHTML)
|
|||
const root = createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={Router} />
|
||||
</QueryClientProvider>
|
||||
<App>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={Router} />
|
||||
</QueryClientProvider>
|
||||
</App>
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ import Notifications from "../components/Notifications";
|
|||
import { Toaster } from "react-hot-toast";
|
||||
import { mobileCheck, useLocalSetting } from "../scripts/utils";
|
||||
import useActiveControl from "../scripts/gamepads";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SystemInfoContext } from "../scripts/contexts";
|
||||
import { SystemInfoType } from "@/shared/constants";
|
||||
import { systemApi } from "../scripts/clientApi";
|
||||
import { useEffect } from "react";
|
||||
import AppCommunication from "../components/AppCommunication";
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { RPC_URL, SERVER_URL } from '@/shared/constants';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import z from 'zod';
|
||||
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||
import { Router } from '..';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { ButtonStyle } from '../components/options/Button';
|
||||
import { DoorOpen, RefreshCw, Undo } from 'lucide-react';
|
||||
|
|
@ -57,7 +56,7 @@ function Overlay (data: {
|
|||
{
|
||||
if (data.open)
|
||||
{
|
||||
focusSelf();
|
||||
focusSelf({ instant: true });
|
||||
}
|
||||
}, [data.open]);
|
||||
|
||||
|
|
@ -122,6 +121,7 @@ function Frame (data: { ref: RefObject<HTMLIFrameElement | null>; })
|
|||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusSelf, focusKey } = useFocusable({
|
||||
focusKey: 'emulatorjs',
|
||||
preferredChildFocusKey: 'frame',
|
||||
|
|
@ -133,7 +133,7 @@ function RouteComponent ()
|
|||
|
||||
function HandleGoBack ()
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { source, id }, replace: true });
|
||||
router.navigate({ to: '/game/$source/$id', params: { source, id }, replace: true });
|
||||
}
|
||||
|
||||
useEventListener('message', e =>
|
||||
|
|
@ -173,7 +173,7 @@ function RouteComponent ()
|
|||
};
|
||||
useEffect(() => setPaused(overlayOpen), [overlayOpen]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
useEffect(() => { if (!overlayOpen) focusSelf(); }, [overlayOpen]);
|
||||
useEffect(() => { if (!overlayOpen) focusSelf({ instant: true }); }, [overlayOpen]);
|
||||
function handleClose ()
|
||||
{
|
||||
setOverlayOpen(false);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { createFileRoute, ErrorComponentProps } from "@tanstack/react-router";
|
||||
import { createFileRoute, ErrorComponentProps, useRouter, useRouterState } from "@tanstack/react-router";
|
||||
import { RPC_URL } from "@shared/constants";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { Calendar, Clock, Folder, Gamepad2, Image, Info, Store, TriangleAlert, Trophy } from "lucide-react";
|
||||
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { Calendar, Folder, Gamepad2, Image, Info, TriangleAlert, Trophy } from "lucide-react";
|
||||
import { HeaderUI } from "../../components/Header";
|
||||
import { AnimatedBackground } from "../../components/AnimatedBackground";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Router } from "../..";
|
||||
import Shortcuts from "../../components/Shortcuts";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import Screenshots from "@/mainview/components/Screenshots";
|
||||
import { HandleGoBack, scrollIntoViewHandler, useStickyDataAttr } from "@/mainview/scripts/utils";
|
||||
import { HandleGoBack, scrollIntoViewHandler, useOnNavigateBack, useStickyDataAttr } from "@/mainview/scripts/utils";
|
||||
import { FilterUI } from "@/mainview/components/Filters";
|
||||
import StatList, { StatEntry } from "@/mainview/components/StatList";
|
||||
import { useIntersectionObserver, useLocalStorage } from "usehooks-ts";
|
||||
|
|
@ -21,7 +20,7 @@ import Achievements from "@/mainview/components/game/Achievements";
|
|||
import { GameDetailsContext } from "@/mainview/scripts/contexts";
|
||||
import { gameQuery, gamesRecommendedBasedOnGameQuery } from "@queries/romm";
|
||||
import { GamesSection } from "@/mainview/components/store/GamesSection";
|
||||
import Details, { DetailElement } from "@/mainview/components/game/Details";
|
||||
import Details from "@/mainview/components/game/Details";
|
||||
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
||||
|
||||
export const Route = createFileRoute("/game/$source/$id")({
|
||||
|
|
@ -31,7 +30,11 @@ export const Route = createFileRoute("/game/$source/$id")({
|
|||
},
|
||||
component: RouteComponent,
|
||||
errorComponent: Error,
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() })),
|
||||
staticData: {
|
||||
enterSound: 'openDetails',
|
||||
goBackSound: "returnDetails"
|
||||
},
|
||||
});
|
||||
|
||||
function useDetailsSection ()
|
||||
|
|
@ -45,10 +48,6 @@ function Error (data: ErrorComponentProps)
|
|||
|
||||
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">
|
||||
|
|
@ -68,6 +67,7 @@ function Error (data: ErrorComponentProps)
|
|||
</div>
|
||||
</FocusContext>
|
||||
</div>
|
||||
<AutoFocus force focus={focusSelf} />
|
||||
</AnimatedBackground>;
|
||||
}
|
||||
|
||||
|
|
@ -139,10 +139,10 @@ function Divider (data: { rootFocusKey: string; showShortcuts: boolean; game: Fr
|
|||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const [recommendedGamesVisible, setRecommendedGamesVisible] = useState(false);
|
||||
const { source, id } = Route.useParams();
|
||||
const { data } = useQuery(gameQuery(source, id));
|
||||
const { focus } = Route.useSearch();
|
||||
const [, setUpdate] = useState(0);
|
||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details", forceFocus: true });
|
||||
const headerRef = useRef(null);
|
||||
|
|
@ -150,7 +150,12 @@ function RouteComponent ()
|
|||
const backgroundImage = data ? new URL(`${RPC_URL(__HOST__)}${data.path_cover}`) : undefined;
|
||||
const { data: recommendedGames } = useQuery({ ...gamesRecommendedBasedOnGameQuery(data?.id.source ?? source, data?.id.id ?? id), enabled: !!data && recommendedGamesVisible });
|
||||
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||
useShortcuts(focusKey, () => [{
|
||||
label: "Back", button: GamePadButtonCode.B, action: () => HandleGoBack(router)
|
||||
}], [router]);
|
||||
|
||||
useOnNavigateBack((s) => s.sound = 'returnDetails');
|
||||
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
useStickyDataAttr(headerRef, sentinelRef, ref);
|
||||
|
|
@ -190,7 +195,7 @@ function RouteComponent ()
|
|||
onFocus={scrollIntoViewHandler({ block: 'center' })}
|
||||
onSelect={(id, focus) =>
|
||||
{
|
||||
Router.navigate({ to: '/store/details/emulator/$id', params: { id } });
|
||||
router.navigate({ to: '/store/details/emulator/$id', params: { id } });
|
||||
}}
|
||||
emulators={recommendedEmulators} />}
|
||||
|
||||
|
|
@ -206,7 +211,7 @@ function RouteComponent ()
|
|||
</div>
|
||||
<GamesSection ref={intersct} showSources onSelect={(id, focus) =>
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { id: id.id, source: id.source } });
|
||||
router.navigate({ to: '/game/$source/$id', params: { id: id.id, source: id.source } });
|
||||
}} onFocus={scrollIntoViewHandler({ block: 'center', inline: 'nearest' })} games={recommendedGames} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import
|
|||
import
|
||||
{
|
||||
createFileRoute,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import
|
||||
|
|
@ -37,7 +38,6 @@ import Shortcuts from "../components/Shortcuts";
|
|||
import { PlatformsList } from "../components/PlatformsList";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
|
||||
import z from "zod";
|
||||
import { Router } from "..";
|
||||
import CollectionList from "../components/CollectionList";
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import { mobileCheck, useDragScroll } from "../scripts/utils";
|
||||
|
|
@ -45,6 +45,7 @@ import { AnimatedBackgroundContext } from "../scripts/contexts";
|
|||
import Carousel from "../components/Carousel";
|
||||
import { closeMutation } from "@queries/system";
|
||||
import { gameQuery } from "../scripts/queries/romm";
|
||||
import { oneShot } from "../scripts/audio/audio";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: ConsoleHomeUI,
|
||||
|
|
@ -90,9 +91,10 @@ function HomeListError (data: { focused: boolean; })
|
|||
|
||||
function ShowAllGamesCard ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const handleNavigate = () =>
|
||||
{
|
||||
Router.navigate({ to: '/games', viewTransition: { types: ['zoom-in'] } });
|
||||
router.navigate({ to: '/games', viewTransition: { types: ['zoom-in'] } });
|
||||
};
|
||||
const { ref } = useFocusable({ focusKey: 'all-games-btn', onEnterPress: handleNavigate });
|
||||
return <div ref={ref} onClick={handleNavigate} className="flex focusable focusable-primary justify-center items-center bg-base-300/80 rounded-3xl font-semibold w-(--game-card-width) h-(--game-card-height) focusable-hover cursor-pointer">All Games</div>;
|
||||
|
|
@ -102,6 +104,7 @@ function HomeList (data: {
|
|||
selectedFilter: string;
|
||||
})
|
||||
{
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const [initFocus, setInitFocus] = useState(false);
|
||||
const bg = useContext(AnimatedBackgroundContext);
|
||||
|
|
@ -124,7 +127,7 @@ function HomeList (data: {
|
|||
|
||||
function handleGameSelect (id: FrontEndId, source: string | null, sourceId: string | null)
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } });
|
||||
router.navigate({ to: '/game/$source/$id', params: { id: String(sourceId ?? id.id), source: source ?? id.source } });
|
||||
};
|
||||
|
||||
let activeList: JSX.Element;
|
||||
|
|
@ -213,9 +216,11 @@ function HomeList (data: {
|
|||
|
||||
function MainMenu ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: `main-menu`,
|
||||
trackChildren: true,
|
||||
focusBoundaryDirections: ['up', 'down']
|
||||
});
|
||||
return (
|
||||
<ul
|
||||
|
|
@ -226,13 +231,13 @@ function MainMenu ()
|
|||
>
|
||||
<FocusContext.Provider value={focusKey}>
|
||||
<CircleIcon
|
||||
action={() => Router.navigate({ to: "/games" })}
|
||||
action={() => router.navigate({ to: "/games" })}
|
||||
icon={<Gamepad2 />}
|
||||
label="Home"
|
||||
type="secondary"
|
||||
/>
|
||||
<CircleIcon icon={<MessageSquare />} label="News" />
|
||||
<CircleIcon type="info" icon={<Store />} action={() => Router.navigate({ to: "/store/tab" })} label="Shop" />
|
||||
<CircleIcon type="info" icon={<Store />} action={() => router.navigate({ to: "/store/tab" })} label="Shop" />
|
||||
<CircleIcon icon={<Image />} label="Album" />
|
||||
<CircleIcon
|
||||
icon={<Gamepad2 />}
|
||||
|
|
@ -241,7 +246,7 @@ function MainMenu ()
|
|||
<CircleIcon
|
||||
action={() =>
|
||||
{
|
||||
Router.navigate({ to: '/settings/accounts' });
|
||||
router.navigate({ to: '/settings/accounts' });
|
||||
}}
|
||||
icon={<Settings />}
|
||||
label="Settings"
|
||||
|
|
@ -259,11 +264,16 @@ function CircleIcon (data: {
|
|||
icon?: JSX.Element;
|
||||
})
|
||||
{
|
||||
const handleAction = () =>
|
||||
{
|
||||
data.action?.();
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: `navigation-icon-${data.label}`,
|
||||
onEnterPress: data.action,
|
||||
focusKey: `menu-navigation-icon-${data.label}`,
|
||||
onEnterPress: handleAction,
|
||||
});
|
||||
useShortcuts(focusKey, () => [{ label: data.label, action: (e) => data.action?.(), button: GamePadButtonCode.A }]);
|
||||
useShortcuts(focusKey, () => [{ label: data.label, action: handleAction, button: GamePadButtonCode.A }]);
|
||||
const typeClasses = {
|
||||
secondary: "bg-secondary text-secondary-content",
|
||||
accent: "bg-accent text-accent-content",
|
||||
|
|
@ -273,7 +283,8 @@ function CircleIcon (data: {
|
|||
return (
|
||||
<li
|
||||
ref={ref}
|
||||
onClick={data.action}
|
||||
data-sound-category={"menu"}
|
||||
onClick={handleAction}
|
||||
className={twMerge(
|
||||
`portrait:sm:size-12 sm:w-14 sm:h-10 menu-icon text-base-300 md:w-20 md:h-20 rounded-full flex items-center justify-center drop-shadow-lg cursor-pointer transition-all focusable focusable-primary focused:drop-shadow-2xl focused:animate-scale focusable-hover bg-base-content border-6 md:border-12 border-base-content focused:border-0 hover:border-0 z-1 active:border-0 active:bg-base-300 active:text-base-content active:transition-none`, typeClasses[data.type ?? 'none'])}
|
||||
>
|
||||
|
|
@ -287,7 +298,7 @@ export default function ConsoleHomeUI ()
|
|||
const { filter } = Route.useSearch();
|
||||
|
||||
const close = useMutation(closeMutation);
|
||||
|
||||
const router = useRouter();
|
||||
const { ref, focusKey } = useFocusable({
|
||||
forceFocus: true,
|
||||
autoRestoreFocus: false,
|
||||
|
|
@ -296,7 +307,7 @@ export default function ConsoleHomeUI ()
|
|||
preferredChildFocusKey: `home-list`,
|
||||
});
|
||||
|
||||
const setFilter = (filter: string) => Router.navigate({ to: '/', search: { filter }, viewTransition: false, replace: true });
|
||||
const setFilter = (filter: string) => router.navigate({ to: '/', search: { filter }, viewTransition: false, replace: true });
|
||||
|
||||
const { shortcuts } = useShortcutContext();
|
||||
const headerButtons: HeaderButton[] = [];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||
import DotsLoading from '../components/backgrounds/dots';
|
||||
import { Router } from '..';
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
|
||||
|
|
@ -16,9 +15,10 @@ export const Route = createFileRoute('/launcher/$source/$id')({
|
|||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const router = useRouter();
|
||||
function HandleGoBack ()
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id }, replace: true });
|
||||
router.navigate({ to: '/game/$source/$id', viewTransition: { types: ['zoom-out'] }, params: { source, id }, replace: true });
|
||||
}
|
||||
|
||||
const { source, id } = Route.useParams();
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ function RouteComponent ()
|
|||
{
|
||||
if (focus)
|
||||
{
|
||||
focusSelf();
|
||||
focusSelf({ instant: true });
|
||||
}
|
||||
}, [focus]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||
import { OptionSpace } from '../../components/options/OptionSpace';
|
||||
import { OptionInput } from '../../components/options/OptionInput';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
|
@ -19,7 +19,6 @@ import Carousel from '@/mainview/components/Carousel';
|
|||
import { FOCUS_KEYS } from '@/mainview/scripts/types';
|
||||
import { scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from '@/mainview/scripts/utils';
|
||||
import { SettingsOption } from '@/mainview/components/options/SettingsOption';
|
||||
import { Router } from '@/mainview';
|
||||
|
||||
export const Route = createFileRoute('/settings/emulators')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -76,7 +75,7 @@ function NewEmulatorPath (data: { addOverride: (emulator: string) => void; isAdd
|
|||
const handleCloseContext = () =>
|
||||
{
|
||||
setNewEmulatorTypeOpen(false);
|
||||
setFocus('emulator');
|
||||
setFocus('emulator', { instant: true });
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -123,7 +122,7 @@ function EmulatorPath (data: { id: string; })
|
|||
const handleCloseSearch = () =>
|
||||
{
|
||||
setIsSearching(false);
|
||||
setFocus(`search-${data.id}`);
|
||||
setFocus(`search-${data.id}`, { instant: true });
|
||||
};
|
||||
|
||||
const handleSelectPath = (path: string) =>
|
||||
|
|
@ -192,6 +191,7 @@ function EmulatorBadge (data: {
|
|||
addOverride: (emulator: string) => void;
|
||||
} & FocusParams)
|
||||
{
|
||||
const router = useRouter();
|
||||
const { focusKey, ref, focused } = useFocusable({
|
||||
focusKey: FOCUS_KEYS.EMULATOR_CARD(data.emulator.name),
|
||||
onFocus (l, p, details) { data.onFocus?.(focusKey, ref.current, details); }
|
||||
|
|
@ -212,12 +212,12 @@ function EmulatorBadge (data: {
|
|||
label: "Visit Store",
|
||||
action ()
|
||||
{
|
||||
Router.navigate({ to: '/store/details/emulator/$id', params: { id: data.emulator.name } });
|
||||
router.navigate({ to: '/store/details/emulator/$id', params: { id: data.emulator.name } });
|
||||
},
|
||||
});
|
||||
}
|
||||
return shortcuts;
|
||||
}, [data.addOverride]);
|
||||
}, [data.addOverride, router]);
|
||||
|
||||
|
||||
let statusIcon = <SearchAlert className={data.emulator.isCritical ? 'text-warning' : 'text-base-content/40'} />;
|
||||
|
|
@ -255,7 +255,7 @@ function EmulatorBadge (data: {
|
|||
case 'store':
|
||||
icon = <Store />;
|
||||
className = "hover:bg-base-content hover:text-base-100 cursor-pointer bg-accent text-accent-content";
|
||||
action = () => { Router.navigate({ to: '/store/details/emulator/$id', params: { id: data.emulator.name } }); };
|
||||
action = () => { router.navigate({ to: '/store/details/emulator/$id', params: { id: data.emulator.name } }); };
|
||||
break;
|
||||
case 'embedded':
|
||||
icon = <Plug />;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ function RouteComponent ()
|
|||
<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='soundEffects' label="Sounds" type='checkbox'></LocalOption>
|
||||
</FocusContext>
|
||||
</ul>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import
|
|||
Outlet,
|
||||
createFileRoute,
|
||||
useMatch,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { ViewTransitionOptions } from "@tanstack/router-core";
|
||||
import classNames from "classnames";
|
||||
|
|
@ -21,20 +22,24 @@ import
|
|||
MonitorCog,
|
||||
Puzzle,
|
||||
} from "lucide-react";
|
||||
import { JSX, useEffect } from "react";
|
||||
import { JSX } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import z from "zod";
|
||||
import { SettingsSchema } from "../../../shared/constants";
|
||||
import { Router } from "../..";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import Shortcuts from "@/mainview/components/Shortcuts";
|
||||
import { HandleGoBack } from "@/mainview/scripts/utils";
|
||||
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
|
||||
export const Route = createFileRoute("/settings")({
|
||||
component: SettingsUI,
|
||||
validateSearch: z.object({
|
||||
focus: z.keyof(SettingsSchema).optional()
|
||||
})
|
||||
}),
|
||||
staticData: {
|
||||
enterSound: 'openSettings'
|
||||
}
|
||||
});
|
||||
|
||||
function MenuItem (data: {
|
||||
|
|
@ -48,17 +53,18 @@ function MenuItem (data: {
|
|||
label: string;
|
||||
})
|
||||
{
|
||||
const router = useRouter();
|
||||
const acitve = !!useMatch({ from: data.route as any, shouldThrow: false });;
|
||||
const handleNonFocusSelect = () =>
|
||||
{
|
||||
if (data.return)
|
||||
{
|
||||
HandleGoBack();
|
||||
HandleGoBack(router);
|
||||
} else if (!acitve)
|
||||
{
|
||||
Router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
}
|
||||
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref, focusSelf } = useFocusable({
|
||||
focusKey: `menu-item-${data.route}`,
|
||||
|
|
@ -67,7 +73,7 @@ function MenuItem (data: {
|
|||
{
|
||||
if (data.focusSelect && !acitve)
|
||||
{
|
||||
Router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
router.navigate({ to: data.route, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
}
|
||||
(ref.current as HTMLElement).scrollIntoView({ inline: 'center' });
|
||||
},
|
||||
|
|
@ -81,6 +87,7 @@ function MenuItem (data: {
|
|||
<li
|
||||
ref={ref}
|
||||
key={data.route}
|
||||
data-sound-category={"menu"}
|
||||
onClick={data.focusSelect ? focusSelf : handleNonFocusSelect}
|
||||
onFocus={focusSelf}
|
||||
className={twMerge("flex group-focusable cursor-pointer", data.className)}
|
||||
|
|
@ -167,17 +174,13 @@ function SettingsMenu (data: {})
|
|||
|
||||
export function SettingsUI ()
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
focusKey: "settings-page-layout",
|
||||
preferredChildFocusKey: 'settings-menu'
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
focusSelf();
|
||||
}, []);
|
||||
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
|
||||
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: () => HandleGoBack(router) }], [router]);
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
return (
|
||||
|
|
@ -196,6 +199,7 @@ export function SettingsUI ()
|
|||
<Shortcuts shortcuts={shortcuts} />
|
||||
</div>
|
||||
</div>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</FocusContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import
|
|||
useFocusable,
|
||||
FocusContext,
|
||||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
import { Router } from "@/mainview";
|
||||
import Shortcuts from "@/mainview/components/Shortcuts";
|
||||
import { AnimatedBackground } from "@/mainview/components/AnimatedBackground";
|
||||
import { systemApi } from "@/mainview/scripts/clientApi";
|
||||
|
|
@ -18,7 +17,7 @@ import Screenshots from "@/mainview/components/Screenshots";
|
|||
import { StickyHeaderUI } from "@/mainview/components/Header";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { EmulatorsSection } from "@/mainview/components/store/EmulatorsSection";
|
||||
import { HandleGoBack, scrollIntoViewHandler, useJobStatus } from "@/mainview/scripts/utils";
|
||||
import { HandleGoBack, scrollIntoViewHandler, useJobStatus, useOnNavigateBack } from "@/mainview/scripts/utils";
|
||||
import toast from "react-hot-toast";
|
||||
import { getErrorMessage } from "react-error-boundary";
|
||||
import { emulatorStatusIcons } from "@/mainview/components/store/StoreEmulatorCard";
|
||||
|
|
@ -27,6 +26,7 @@ import { GamesSection } from "@/mainview/components/store/GamesSection";
|
|||
import { deleteBiosMutation, downloadBiosMutation, installEmulatorMutation, storeEmulatorDeleteMutation, storeEmulatorDetailsQuery, storeEmulatorsRecommendedQuery } from "@queries/store";
|
||||
import { gamesRecommendedBasedOnEmulatorQuery } from "@queries/romm";
|
||||
import FocusTooltip from "@/mainview/components/FocusTooltip";
|
||||
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
||||
|
||||
export const Route = createFileRoute('/store/details/emulator/$id')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -35,6 +35,10 @@ export const Route = createFileRoute('/store/details/emulator/$id')({
|
|||
ctx.context.queryClient.prefetchQuery(storeEmulatorDetailsQuery(ctx.params.id));
|
||||
ctx.context.queryClient.prefetchQuery(storeEmulatorsRecommendedQuery(ctx.params.id));
|
||||
ctx.context.queryClient.prefetchQuery(gamesRecommendedBasedOnEmulatorQuery(ctx.params.id));
|
||||
},
|
||||
staticData: {
|
||||
enterSound: "openDetails",
|
||||
goBackSound: "returnDetails"
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -288,7 +292,7 @@ function Description (data: { emulator?: FrontEndEmulatorDetailed; })
|
|||
export function RouteComponent ()
|
||||
{
|
||||
const { id } = Route.useParams();
|
||||
|
||||
const router = useRouter();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
focusKey: `GAME_DETAIL_${id}`,
|
||||
trackChildren: true,
|
||||
|
|
@ -301,22 +305,16 @@ export function RouteComponent ()
|
|||
|
||||
useShortcuts(focusKey, () => [{
|
||||
label: "Return",
|
||||
action: HandleGoBack,
|
||||
action: () => HandleGoBack(router),
|
||||
button: GamePadButtonCode.B
|
||||
}]);
|
||||
}], [router]);
|
||||
|
||||
const installMutation = useMutation({
|
||||
...installEmulatorMutation(id), onSuccess: (data, variables, onMutateResult, context) => context.client.refetchQueries(storeEmulatorDetailsQuery(id)),
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
focusSelf();
|
||||
}, []);
|
||||
|
||||
const { shortcuts } = useShortcutContext();
|
||||
|
||||
|
||||
const stats: StatEntry[] = [];
|
||||
if (emulator)
|
||||
{
|
||||
|
|
@ -341,6 +339,7 @@ export function RouteComponent ()
|
|||
|
||||
return (
|
||||
<AnimatedBackground ref={ref} className="" scrolling>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
<FocusContext.Provider value={focusKey}>
|
||||
<StickyHeaderUI ref={ref} />
|
||||
<div className="flex flex-col z-10">
|
||||
|
|
@ -370,7 +369,7 @@ export function RouteComponent ()
|
|||
onFocus={scrollIntoViewHandler({ block: 'center' })}
|
||||
onSelect={(id, focus) =>
|
||||
{
|
||||
Router.navigate({
|
||||
router.navigate({
|
||||
to: '/store/details/emulator/$id', params: { id }
|
||||
});
|
||||
}}
|
||||
|
|
@ -386,7 +385,7 @@ export function RouteComponent ()
|
|||
</div>
|
||||
<GamesSection showSources={true} onFocus={scrollIntoViewHandler({ behavior: 'smooth', block: 'center' })} onSelect={(id) =>
|
||||
{
|
||||
Router.navigate({
|
||||
router.navigate({
|
||||
to: '/game/$source/$id', params: { id: id.id, source: id.source }
|
||||
});
|
||||
}} games={recommendedGames} /></div>}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Router } from '@/mainview';
|
||||
import { AutoFocus } from '@/mainview/components/AutoFocus';
|
||||
import { FilterUI } from '@/mainview/components/Filters';
|
||||
import { HeaderUI } from '@/mainview/components/Header';
|
||||
import Shortcuts from '@/mainview/components/Shortcuts';
|
||||
|
|
@ -6,19 +6,21 @@ import { StoreContext } from '@/mainview/scripts/contexts';
|
|||
import { gameQuery } from '@/mainview/scripts/queries/romm';
|
||||
import { storeEmulatorDetailsQuery } from '@/mainview/scripts/queries/store';
|
||||
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||
import { mobileCheck, useStickyDataAttr } from '@/mainview/scripts/utils';
|
||||
import { HandleGoBack, mobileCheck, useStickyDataAttr } from '@/mainview/scripts/utils';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useMatchRoute } from '@tanstack/react-router';
|
||||
import { useMatchRoute, useRouter } from '@tanstack/react-router';
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||
import { zodValidator } from '@tanstack/zod-adapter';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import z from 'zod';
|
||||
|
||||
export const Route = createFileRoute('/store/tab')({
|
||||
component: RouteComponent,
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
|
||||
validateSearch: zodValidator(z.object({ focus: z.string().optional() })),
|
||||
staticData: {
|
||||
enterSound: 'openStore'
|
||||
}
|
||||
});
|
||||
|
||||
function useIsSettings (subPath: string)
|
||||
|
|
@ -33,6 +35,7 @@ function useIsSettings (subPath: string)
|
|||
|
||||
function TopArea (data: { filters: Record<string, FilterOption>; })
|
||||
{
|
||||
const router = useRouter();
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: 'top-area',
|
||||
preferredChildFocusKey: `store-tabs`,
|
||||
|
|
@ -44,13 +47,13 @@ function TopArea (data: { filters: Record<string, FilterOption>; })
|
|||
|
||||
useShortcuts("STORE_ROOT", () => [{
|
||||
label: "Return",
|
||||
action: () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-out'] } }),
|
||||
action: () => HandleGoBack(router),
|
||||
button: GamePadButtonCode.B
|
||||
}], []);
|
||||
}], [router]);
|
||||
|
||||
const handleNavigate = (s: string) =>
|
||||
{
|
||||
Router.navigate({ to: `/store/tab/${s === 'home' ? '' : s}`, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
router.navigate({ to: `/store/tab/${s === 'home' ? '' : s}`, viewTransition: { types: ['slide-up'] }, replace: true });
|
||||
};
|
||||
|
||||
return <div ref={ref}>
|
||||
|
|
@ -76,6 +79,7 @@ function StoreOutlet ()
|
|||
function RouteComponent ()
|
||||
{
|
||||
// Root spatial nav container
|
||||
const router = useRouter();
|
||||
const { ref, focusKey, focusSelf } = useFocusable({
|
||||
focusKey: "STORE_ROOT",
|
||||
preferredChildFocusKey: 'top-area',
|
||||
|
|
@ -93,25 +97,16 @@ function RouteComponent ()
|
|||
const { shortcuts } = useShortcutContext();
|
||||
const { focus } = Route.useSearch();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (!focus)
|
||||
{
|
||||
focusSelf();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDetails = (type: string, source: string, id: string, focus: string) =>
|
||||
{
|
||||
if (type === 'emulator')
|
||||
{
|
||||
Router.navigate({ to: '/store/details/emulator/$id', params: { id } });
|
||||
router.navigate({ to: '/store/details/emulator/$id', params: { id } });
|
||||
}
|
||||
else if (type === 'game')
|
||||
{
|
||||
Router.navigate({ to: '/game/$source/$id', params: { source: source, id: id } });
|
||||
router.navigate({ to: '/game/$source/$id', params: { source: source, id: id } });
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const handlePrefetch = (type: string, source: string, id: string) =>
|
||||
|
|
@ -150,5 +145,6 @@ function RouteComponent ()
|
|||
</div>
|
||||
</FocusContext.Provider>
|
||||
</StoreContext>
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</div >;
|
||||
}
|
||||
|
|
|
|||
70
src/mainview/scripts/audio/audio.ts
Normal file
70
src/mainview/scripts/audio/audio.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { Howl } from 'howler';
|
||||
import sounds from '../../assets/sounds.ogg';
|
||||
import soundSprites from '../../assets/sounds.json';
|
||||
import { getLocalSetting } from '../utils';
|
||||
|
||||
const timingMap = new Map<string, Date>();
|
||||
|
||||
const sound = new Howl({
|
||||
src: [sounds],
|
||||
sprite: soundSprites.sprite as any,
|
||||
volume: 0.5,
|
||||
});
|
||||
import.meta.hot?.dispose(() => { sound.unload(); });
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface StaticDataRouteOption
|
||||
{
|
||||
enterSound?: keyof typeof soundMap | null;
|
||||
goBackSound?: keyof typeof soundMap | null;
|
||||
}
|
||||
}
|
||||
|
||||
const volumeVariation = 0.05;
|
||||
const rateVariation = 0.01;
|
||||
|
||||
export const soundMap = {
|
||||
openDetails: { key: 'Classic UI SFX - Chords #1' },
|
||||
returnGeneric: { key: 'Classic UI SFX - Short - Low #2' },
|
||||
returnDetails: { key: 'Classic UI SFX - Short - Low #5' },
|
||||
openGeneric: { key: 'Classic UI SFX - Short - High #9' },
|
||||
select: { key: 'Classic UI SFX - Short - High #5', rateVariation, volumeVariation },
|
||||
selectAlt: { key: "Classic UI SFX - Short - High #6", rateVariation, volumeVariation },
|
||||
selectMenu: { key: 'Classic UI SFX - Short - High #7', rateVariation, volumeVariation },
|
||||
selectFilter: { key: 'Classic UI SFX - Short - High #3', volumeVariation },
|
||||
closeContext: { key: 'Classic UI SFX - Short - High #19' },
|
||||
openContext: { key: 'Classic UI SFX - Short - High #22' },
|
||||
openStore: { key: 'Classic UI SFX - Chords #16' },
|
||||
openSettings: { key: 'Classic UI SFX - Short - High #8' },
|
||||
click: { key: "UI_Single_Set 16_03", rateVariation, volumeVariation },
|
||||
clickAlt: { key: "UI_Single_Set 16_01", rateVariation, volumeVariation },
|
||||
invalidNavigation: { key: "Classic UI SFX - Short - Low #6", rateVariation, volumeVariation },
|
||||
} satisfies Record<string, { key: keyof typeof soundSprites.sprite, rateVariation?: number; volumeVariation?: number; }>;
|
||||
|
||||
function sinRanom ()
|
||||
{
|
||||
return Math.sin(new Date().getMilliseconds() / 1000 * Math.PI);
|
||||
}
|
||||
|
||||
function cosRandom ()
|
||||
{
|
||||
return Math.sin(new Date().getMilliseconds() / 1000 * Math.PI);
|
||||
}
|
||||
|
||||
function random ()
|
||||
{
|
||||
return Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
export function oneShot (id: keyof typeof soundMap)
|
||||
{
|
||||
const currentDate = timingMap.get(id);
|
||||
if (!getLocalSetting('soundEffects')) return;
|
||||
if (currentDate && new Date().getTime() - currentDate.getTime() <= 100) return;
|
||||
const soundValue = soundMap[id] as { key: keyof typeof soundSprites.sprite, rateVariation?: number; volumeVariation?: number; };
|
||||
const instanceId = sound.play(soundValue.key);
|
||||
sound.volume(sound.volume() + random() * (soundValue.volumeVariation ?? 0), instanceId);
|
||||
sound.rate(1 + random() * (soundValue.rateVariation ?? 0), instanceId);
|
||||
timingMap.set(id, new Date());
|
||||
}
|
||||
|
||||
90
src/mainview/scripts/audio/audioCallbacks.ts
Normal file
90
src/mainview/scripts/audio/audioCallbacks.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { Router } from "@/mainview";
|
||||
import { oneShot, soundMap } from "./audio";
|
||||
export default function load ()
|
||||
{
|
||||
let lastLocationPath: string | undefined;
|
||||
const unsub = Router.history.subscribe((op) =>
|
||||
{
|
||||
if (op.action.type === 'PUSH')
|
||||
{
|
||||
lastLocationPath = op.location.pathname;
|
||||
|
||||
const routes = Router.matchRoutes(op.location.pathname);
|
||||
const soundRoute = routes.find(r => r.staticData.enterSound !== undefined);
|
||||
if (soundRoute)
|
||||
{
|
||||
if (soundRoute.staticData.enterSound) oneShot(soundRoute.staticData.enterSound);
|
||||
} else
|
||||
{
|
||||
oneShot("openGeneric");
|
||||
}
|
||||
|
||||
} else if (op.action.type === 'BACK')
|
||||
{
|
||||
if (lastLocationPath)
|
||||
{
|
||||
const soundRoutes = Router.matchRoutes(lastLocationPath);
|
||||
const soundRoute = soundRoutes.find(r => r.staticData.goBackSound !== undefined);
|
||||
if (soundRoute)
|
||||
{
|
||||
if (soundRoute.staticData.goBackSound) oneShot(soundRoute.staticData.goBackSound);
|
||||
} else
|
||||
{
|
||||
oneShot("returnGeneric");
|
||||
}
|
||||
} else
|
||||
{
|
||||
oneShot("returnGeneric");
|
||||
}
|
||||
|
||||
lastLocationPath = op.location.state.key;
|
||||
}
|
||||
});
|
||||
|
||||
let focusChangeDebounced: undefined | NodeJS.Timeout;
|
||||
|
||||
const focuschangedHandler = (e: CustomEvent<FocusEventDetails>) =>
|
||||
{
|
||||
clearTimeout(focusChangeDebounced);
|
||||
if (!e.detail.focusKeyChanged) return;
|
||||
|
||||
if (e.detail.nativeEvent || e.detail.event)
|
||||
{
|
||||
let sound: keyof typeof soundMap;
|
||||
if (e.detail.node && e.detail.node.matches('[data-sound-category="menu"]'))
|
||||
{
|
||||
sound = 'selectMenu';
|
||||
|
||||
} else if (e.detail.node && e.detail.node.matches('[data-sound-category="filter"]'))
|
||||
{
|
||||
sound = "selectFilter";
|
||||
}
|
||||
else if (e.detail.node && e.detail.node.matches('[data-sound-category="emulator"]'))
|
||||
{
|
||||
sound = "selectAlt";
|
||||
}
|
||||
else if (!e.detail.node || !e.detail.node.matches('[data-sound-disable="focus"]'))
|
||||
{
|
||||
sound = e.detail.sound as any ?? 'select';
|
||||
}
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (e.detail.nativeEvent || e.detail.event)
|
||||
{
|
||||
oneShot(sound);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('focuschanged', focuschangedHandler as any);
|
||||
|
||||
return {
|
||||
cleanup: () =>
|
||||
{
|
||||
unsub();
|
||||
window.removeEventListener('focuschanged', focuschangedHandler as any);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { getCurrentFocusKey, navigateByDirection } from "@noriginmedia/norigin-s
|
|||
import { GetFocusedElement } from "./spatialNavigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { mobileCheck } from "./utils";
|
||||
import { oneShot } from "./audio/audio";
|
||||
|
||||
let loopStarted = false;
|
||||
let isTouching = false;
|
||||
|
|
@ -104,7 +105,10 @@ function throttleNav (key: string, dir: string, event: Event)
|
|||
const speed = Math.max(maxSpeed - (maxSpeed - minSpeed) * (acceleration / 6), minSpeed);
|
||||
if ((currentDate.getTime() - (lastTime ?? 0) > speed))
|
||||
{
|
||||
const currentFocusKey = getCurrentFocusKey();
|
||||
navigateByDirection(dir, { event });
|
||||
if (currentFocusKey === getCurrentFocusKey())
|
||||
oneShot('invalidNavigation');
|
||||
throttleMap.set(key, currentDate.getTime());
|
||||
throttleAcceleration.set(key, acceleration + 1);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import
|
||||
{
|
||||
FocusDetails,
|
||||
getCurrentFocusKey,
|
||||
init,
|
||||
SpatialNavigation,
|
||||
|
|
@ -9,7 +8,7 @@ import
|
|||
UseFocusableResult,
|
||||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { RefObject, useEffect, useState } from "react";
|
||||
import { focusQueue, Router } from "..";
|
||||
import { focusQueue } from "../App";
|
||||
|
||||
init({
|
||||
shouldFocusDOMNode: false,
|
||||
|
|
@ -97,13 +96,21 @@ SpatialNavigation.updateLayout = (focusKey) =>
|
|||
SpatialNavigation.setFocus = (newFocusKey, focusDetails) =>
|
||||
{
|
||||
setFocus(newFocusKey, focusDetails);
|
||||
dispatchFocusedEvent(new CustomEvent<FocusDetails>('focuschanged', { bubbles: true, detail: focusDetails }));
|
||||
};
|
||||
|
||||
SpatialNavigation.setCurrentFocusedKey = (newFocusKey, focusDetails) =>
|
||||
{
|
||||
const details: FocusEventDetails = {
|
||||
...focusDetails,
|
||||
focusKey: newFocusKey,
|
||||
focusKeyChanged: newFocusKey !== getCurrentFocusKey(),
|
||||
node: GetFocusedElement(newFocusKey)
|
||||
};
|
||||
setCurrentFocusedKey(newFocusKey, focusDetails);
|
||||
window.dispatchEvent(new CustomEvent<FocusDetails>('focuschanged', { bubbles: true, detail: focusDetails }));
|
||||
window.dispatchEvent(new CustomEvent<FocusEventDetails>('focuschanged', {
|
||||
bubbles: true,
|
||||
detail: details
|
||||
}));
|
||||
};
|
||||
|
||||
SpatialNavigation.updateFocusable = (key, data) =>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { RefObject, useEffect, useRef, useState } from "react";
|
|||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { jobsApi } from "./clientApi";
|
||||
import { JobsAPIType } from "@/bun/api/rpc";
|
||||
import { Router } from "..";
|
||||
import { AnyRouter, Router, useRouter } from "@tanstack/react-router";
|
||||
import { soundMap } from "./audio/audio";
|
||||
|
||||
export type ScrollSaveParams = {
|
||||
id: string;
|
||||
|
|
@ -59,6 +60,13 @@ export function mobileCheck ()
|
|||
return check;
|
||||
};
|
||||
|
||||
export function getLocalSetting<TKey extends keyof LocalSettingsType> (key: TKey)
|
||||
{
|
||||
const localValueRaw = localStorage.getItem(key);
|
||||
if (!localValueRaw) return LocalSettingsSchema.shape[key].parse(undefined);
|
||||
return LocalSettingsSchema.shape[key].parse(JSON.parse(localValueRaw));
|
||||
}
|
||||
|
||||
export function useLocalSetting<TKey extends keyof LocalSettingsType> (key: TKey)
|
||||
{
|
||||
const [localValue] = useLocalStorage(key, LocalSettingsSchema.shape[key].parse(undefined), { deserializer: (value) => LocalSettingsSchema.shape[key].parse(JSON.parse(value)) });
|
||||
|
|
@ -218,7 +226,7 @@ export function scrollIntoViewHandler (params?: ScrollIntoViewOptions)
|
|||
return (focusKey: string, node: HTMLElement, details: any) =>
|
||||
{
|
||||
if (details.nativeEvent instanceof PointerEvent) return;
|
||||
node.scrollIntoView({ ...params, behavior: details.instant ? 'instant' : 'smooth' });
|
||||
node.scrollIntoView({ ...params, behavior: details.instant || !details.event ? 'instant' : 'smooth' });
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -315,13 +323,37 @@ export function useJobStatus<const JOB extends keyof JobsAPIType['~Routes']['api
|
|||
return { data, state, error, wsRef: ref };
|
||||
}
|
||||
|
||||
export function HandleGoBack ()
|
||||
export function HandleGoBack (router: AnyRouter)
|
||||
{
|
||||
if (Router.history.canGoBack())
|
||||
if (router.history.canGoBack())
|
||||
{
|
||||
Router.history.back();
|
||||
router.history.back();
|
||||
} else
|
||||
{
|
||||
Router.navigate({ to: '/', viewTransition: { types: ['zoom-out'] } });
|
||||
router.navigate({ to: '/', viewTransition: { types: ['zoom-out'] } });
|
||||
}
|
||||
}
|
||||
|
||||
export function useOnNavigateBack (callback: (state: { sound?: keyof typeof soundMap; } & Record<string, any>) => void)
|
||||
{
|
||||
const router = useRouter();
|
||||
const prevIndex = useRef(router.history.location.state.__TSR_index);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const unsub = router.history.subscribe(() =>
|
||||
{
|
||||
const currentIndex = router.history.location.state.__TSR_index;
|
||||
const isBack = currentIndex < prevIndex.current;
|
||||
|
||||
if (isBack)
|
||||
{
|
||||
callback(router.history.location.state);
|
||||
}
|
||||
|
||||
prevIndex.current = currentIndex;
|
||||
});
|
||||
|
||||
return unsub;
|
||||
}, [router]);
|
||||
}
|
||||
19
src/mainview/types.d.ts
vendored
19
src/mainview/types.d.ts
vendored
|
|
@ -16,6 +16,25 @@ declare global
|
|||
"save-scroll"?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
module "@noriginmedia/norigin-spatial-navigation" {
|
||||
declare interface FocusDetails
|
||||
{
|
||||
instant?: boolean;
|
||||
sound?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare interface FocusEventDetails
|
||||
{
|
||||
focusKey: string;
|
||||
instant?: boolean;
|
||||
sound?: string;
|
||||
nativeEvent?: any;
|
||||
event?: Event;
|
||||
node: HTMLElement | undefined;
|
||||
focusKeyChanged: boolean;
|
||||
}
|
||||
|
||||
declare interface FocusParams
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ export const SettingsSchema = z.object({
|
|||
export const LocalSettingsSchema = z.object({
|
||||
backgroundBlur: z.stringbool().or(z.boolean()).default(true),
|
||||
backgroundAnimation: z.stringbool().or(z.boolean()).default(true),
|
||||
theme: z.enum(['dark', 'light', 'auto']).default('auto')
|
||||
theme: z.enum(['dark', 'light', 'auto']).default('auto'),
|
||||
soundEffects: z.boolean().default(true)
|
||||
});
|
||||
|
||||
export const GameListFilterSchema = z.object({
|
||||
|
|
|
|||
BIN
src/sounds/Classic UI SFX - Chords #1.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #1.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #10.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #10.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #11.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #11.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #12.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #12.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #13.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #13.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #14.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #14.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #15.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #15.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #16.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #16.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #17.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #17.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #18.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #18.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #19.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #19.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #2.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #2.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #20.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #20.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #3.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #3.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #4.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #4.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #5.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #5.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #6.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #6.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #7.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #7.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #8.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #8.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #9.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #9.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #1.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #1.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #10.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #10.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #11.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #11.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #12.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #12.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #13.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #13.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #14.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #14.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #15.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #15.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #16.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #16.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #17.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #17.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #18.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #18.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #19.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #19.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #2.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #2.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #20.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #20.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #21.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #21.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #22.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #22.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #23.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #23.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #24.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #24.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #25.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #25.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #3.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #3.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #4.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #4.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #5.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #5.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #6.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #6.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #7.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #7.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #8.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #8.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #9.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #9.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - Low #1.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - Low #1.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - Low #10.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - Low #10.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - Low #11.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - Low #11.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - Low #12.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - Low #12.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - Low #13.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - Low #13.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
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