feat: Implemented audio effects

This commit is contained in:
Simeon Radivoev 2026-04-01 21:20:34 +03:00
parent fe0ab3b498
commit edbc390d14
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
125 changed files with 1137 additions and 217 deletions

3
.gitattributes vendored
View file

@ -4,3 +4,6 @@
*.gif filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text *.webp filter=lfs diff=lfs merge=lfs -text
*.svg 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

View file

@ -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 - [elysia](https://elysiajs.com/) for the APIs
- [webview](https://github.com/webview/webview) for launching existing system webviews instead of full browser if possible. - [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 - [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)

View file

@ -50,6 +50,7 @@
"@tanstack/router-plugin": "^1.157.16", "@tanstack/router-plugin": "^1.157.16",
"@tanstack/zod-adapter": "^1.162.4", "@tanstack/zod-adapter": "^1.162.4",
"@types/adm-zip": "^0.5.8", "@types/adm-zip": "^0.5.8",
"@types/audiosprite": "^0.7.3",
"@types/bun": "latest", "@types/bun": "latest",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/howler": "^2.2.12", "@types/howler": "^2.2.12",
@ -63,6 +64,7 @@
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"app-builder-bin": "^5.0.0-alpha.13", "app-builder-bin": "^5.0.0-alpha.13",
"audiosprite": "^0.7.2",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"concurrently": "^9.2.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/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__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=="], "@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=="], "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=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="], "atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="],
"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=="], "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=="], "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=="], "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=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
@ -840,6 +848,8 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "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=="], "daisyui": ["daisyui@5.5.14", "", {}, "sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg=="],
"dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="], "dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="],
@ -954,6 +964,8 @@
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], "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-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-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=="], "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=="], "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=="], "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=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
@ -1104,6 +1118,8 @@
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "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=="], "jackspeak": ["jackspeak@2.3.6", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ=="],
"jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="],
@ -1278,12 +1294,16 @@
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="], "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=="], "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=="], "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=="], "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-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
@ -1308,6 +1328,8 @@
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "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-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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": ["undici@7.21.0", "", {}, "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], "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=="], "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": ["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=="], "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=="], "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
@ -1752,6 +1784,8 @@
"@jimp/wasm-webp/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@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/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=="], "@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=="], "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/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=="], "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=="], "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=="], "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=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "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=="], "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/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=="], "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=="], "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-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-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=="], "@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=="], "@node-minify/terser/terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
@ -1938,8 +1982,6 @@
"get-pkg-repo/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], "get-pkg-repo/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"meow/read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "meow/read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
@ -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=="], "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/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=="], "dotgitignore/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],

View file

@ -40,7 +40,8 @@
"version:generate": "standard-version --sign", "version:generate": "standard-version --sign",
"package:Linux": "bun run build:prod:appimage", "package:Linux": "bun run build:prod:appimage",
"package:Windows": "bun run build:prod", "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": { "dependencies": {
"7zip-bin": "^5.2.0", "7zip-bin": "^5.2.0",
@ -88,6 +89,7 @@
"@tanstack/router-plugin": "^1.157.16", "@tanstack/router-plugin": "^1.157.16",
"@tanstack/zod-adapter": "^1.162.4", "@tanstack/zod-adapter": "^1.162.4",
"@types/adm-zip": "^0.5.8", "@types/adm-zip": "^0.5.8",
"@types/audiosprite": "^0.7.3",
"@types/bun": "latest", "@types/bun": "latest",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/howler": "^2.2.12", "@types/howler": "^2.2.12",
@ -101,6 +103,7 @@
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"app-builder-bin": "^5.0.0-alpha.13", "app-builder-bin": "^5.0.0-alpha.13",
"audiosprite": "^0.7.2",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",

View 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
View 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}</>;
}

View 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

Binary file not shown.

View file

@ -1,5 +1,5 @@
import { doesFocusableExist, FocusDetails, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation"; import { doesFocusableExist, FocusDetails, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
import { useEffect } from "react"; import { useEffect, useLayoutEffect } from "react";
export function AutoFocus (data: { export function AutoFocus (data: {
parentKey?: string; parentKey?: string;
@ -8,7 +8,7 @@ export function AutoFocus (data: {
delay?: number; delay?: number;
}) })
{ {
useEffect(() => useLayoutEffect(() =>
{ {
let delayTimeout: number | undefined; let delayTimeout: number | undefined;

View file

@ -3,6 +3,7 @@ import classNames from "classnames";
import { JSX } from "react"; import { JSX } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import useActiveControl from "../scripts/gamepads"; import useActiveControl from "../scripts/gamepads";
import { oneShot } from "../scripts/audio/audio";
export function GameCardSkeleton () export function GameCardSkeleton ()
{ {
@ -38,10 +39,15 @@ export interface GameCardParams
export default function CardElement (data: GameCardParams & InteractParams) export default function CardElement (data: GameCardParams & InteractParams)
{ {
const handleAction = () =>
{
data.onAction?.();
oneShot('click');
};
const { ref, focused, focusSelf } = useFocusable({ const { ref, focused, focusSelf } = useFocusable({
focusKey: data.focusKey, focusKey: data.focusKey,
onFocus: (l, p, details) => data.onFocus?.(data.id, ref.current as any, details), onFocus: (l, p, details) => data.onFocus?.(data.id, ref.current as any, details),
onEnterPress: () => data.onAction?.(), onEnterPress: handleAction,
onBlur: () => data.onBlur?.(data.id), onBlur: () => data.onBlur?.(data.id),
}); });
const { isPointer } = useActiveControl(); const { isPointer } = useActiveControl();
@ -57,11 +63,10 @@ export default function CardElement (data: GameCardParams & InteractParams)
scrollSnapAlign: isPointer ? "center" : "none" scrollSnapAlign: isPointer ? "center" : "none"
}} }}
onFocus={focusSelf} onFocus={focusSelf}
onDoubleClick={e => data.onAction?.(e.nativeEvent)}
onClick={() => onClick={() =>
{ {
focusSelf(); focusSelf();
data.onAction?.(); handleAction();
}} }}
className={twMerge( 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", "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",

View file

@ -3,7 +3,7 @@ import { useSuspenseQuery } from "@tanstack/react-query";
import { CardList, GameMetaExtra } from "./CardList"; import { CardList, GameMetaExtra } from "./CardList";
import { GameCardFocusHandler } from "./CardElement"; import { GameCardFocusHandler } from "./CardElement";
import { getCollectionsQuery } from "@queries/romm"; import { getCollectionsQuery } from "@queries/romm";
import { Router } from ".."; import { useRouter } from "@tanstack/react-router";
export default function CollectionList (data: { export default function CollectionList (data: {
id: string, id: string,
@ -14,12 +14,13 @@ export default function CollectionList (data: {
saveChildFocus?: 'session' | 'local'; saveChildFocus?: 'session' | 'local';
}) })
{ {
const router = useRouter();
const { data: collections } = useSuspenseQuery(getCollectionsQuery); const { data: collections } = useSuspenseQuery(getCollectionsQuery);
const handleDefaultSelect = (gameId: string) => const handleDefaultSelect = (gameId: string) =>
{ {
const [source, id] = gameId.split('@'); const [source, id] = gameId.split('@');
Router.navigate({ router.navigate({
to: `/collection/$source/$id`, to: `/collection/$source/$id`,
params: { source, id }, params: { source, id },
search: { countHint: collections.find(c => c.id.id === id && c.id.source === source)?.game_count } search: { countHint: collections.find(c => c.id.id === id && c.id.source === source)?.game_count }

View file

@ -1,18 +1,18 @@
import { AnimatedBackground } from './AnimatedBackground'; import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; import { StickyHeaderUI } from './Header';
import { HeaderUI, StickyHeaderUI } from './Header';
import { GameList } from './GameList'; import { GameList } from './GameList';
import { Search, Settings2 } from 'lucide-react'; import { Search, Settings2 } from 'lucide-react';
import { JSX, Suspense, useEffect } from 'react'; import { JSX, Suspense } from 'react';
import Shortcuts from './Shortcuts'; import Shortcuts from './Shortcuts';
import { AutoFocus } from './AutoFocus'; import { AutoFocus } from './AutoFocus';
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts'; import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
import { GameListFilterType } from '@/shared/constants'; import { GameListFilterType } from '@/shared/constants';
import { GameCardFocusHandler } from './CardElement'; import { GameCardFocusHandler } from './CardElement';
import { HandleGoBack, useStickyDataAttr } from '../scripts/utils'; import { HandleGoBack } from '../scripts/utils';
import LoadingCardList from './LoadingCardList'; import LoadingCardList from './LoadingCardList';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { gameQuery } from '../scripts/queries/romm'; import { gameQuery } from '../scripts/queries/romm';
import { useRouter } from '@tanstack/react-router';
export interface CollectionsDetailParams export interface CollectionsDetailParams
{ {
@ -29,6 +29,7 @@ export interface CollectionsDetailParams
export function CollectionsDetail (data: CollectionsDetailParams) export function CollectionsDetail (data: CollectionsDetailParams)
{ {
const router = useRouter();
const builtData = useQuery({ const builtData = useQuery({
queryKey: ['filter', data.id], queryFn: async () => queryKey: ['filter', data.id], queryFn: async () =>
{ {
@ -42,7 +43,7 @@ export function CollectionsDetail (data: CollectionsDetailParams)
preferredChildFocusKey: `${focusKey}-list` preferredChildFocusKey: `${focusKey}-list`
}); });
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]); useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: () => HandleGoBack(router) }], [router]);
const { shortcuts } = useShortcutContext(); const { shortcuts } = useShortcutContext();
const handleScroll: GameCardFocusHandler = (cardId, node, details) => const handleScroll: GameCardFocusHandler = (cardId, node, details) =>

View file

@ -6,6 +6,7 @@ import { X } from "lucide-react";
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts"; import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
import { ContextDialogContext } from "../scripts/contexts"; import { ContextDialogContext } from "../scripts/contexts";
import { FOCUS_KEYS } from "../scripts/types"; import { FOCUS_KEYS } from "../scripts/types";
import { oneShot } from "../scripts/audio/audio";
export function ContextList (data: { export function ContextList (data: {
options?: DialogEntry[]; options?: DialogEntry[];
@ -34,6 +35,7 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class
{ {
if (data.disabled === true) return; if (data.disabled === true) return;
data.action?.({ close: context.close, focus: focusSelf }); data.action?.({ close: context.close, focus: focusSelf });
oneShot('click');
}; };
const { ref, focusSelf, focusKey } = useFocusable({ const { ref, focusSelf, focusKey } = useFocusable({
focusKey: FOCUS_KEYS.CONTEXT_DIALOG_OPTION(context.id, data.id), focusKey: FOCUS_KEYS.CONTEXT_DIALOG_OPTION(context.id, data.id),
@ -57,6 +59,7 @@ export function OptionElement (data: DialogEntry & { onFocus?: () => void; class
onClick={handleAction} onClick={handleAction}
data-selected={data.selected} data-selected={data.selected}
aria-disabled={data.disabled} aria-disabled={data.disabled}
data-sound-category={"menu"}
className={ className={
twMerge("flex cursor-pointer sm:text-sm md:text-base group-focusable scroll-m-4")}> twMerge("flex cursor-pointer sm:text-sm md:text-base group-focusable scroll-m-4")}>
<FocusContext value={focusKey}> <FocusContext value={focusKey}>
@ -100,10 +103,10 @@ export function useContextDialog (id: string, data: { content?: JSX.Element; cla
data.onClose?.(); data.onClose?.();
if (newSourceFocusKey) if (newSourceFocusKey)
{ {
setFocus(newSourceFocusKey); setFocus(newSourceFocusKey, { instant: true });
} else if (sourceFocusKey) } else if (sourceFocusKey)
{ {
setFocus(sourceFocusKey); setFocus(sourceFocusKey, { instant: true });
} }
} }
@ -137,12 +140,14 @@ export function ContextDialog (data: {
const handleClose = () => const handleClose = () =>
{ {
data.close(false); data.close(false);
oneShot('closeContext');
}; };
useEffect(() => useEffect(() =>
{ {
if (data.open) if (data.open)
{ {
focusSelf(); focusSelf({ instant: true });
oneShot('openContext');
} }
}, [data.open]); }, [data.open]);

View file

@ -1,20 +1,20 @@
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { Home, TriangleAlert } from "lucide-react"; import { Home, TriangleAlert } from "lucide-react";
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts"; import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
import { Router } from "..";
import Shortcuts from "./Shortcuts"; import Shortcuts from "./Shortcuts";
import { Button } from "./options/Button"; import { Button } from "./options/Button";
import { useEffect } from "react"; import { useEffect } from "react";
import { ErrorComponentProps } from "@tanstack/react-router"; import { ErrorComponentProps, useRouter } from "@tanstack/react-router";
export default function Error (data: ErrorComponentProps) export default function Error (data: ErrorComponentProps)
{ {
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "not-found" }); 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 }]); useShortcuts(focusKey, () => [{ label: "Return Home", button: GamePadButtonCode.B, action: handleReturn }]);
const { shortcuts } = useShortcutContext(); 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"> return <div ref={ref} className="absolute flex flex-col justify-center items-center w-full h-full gap-4">
<FocusContext value={focusKey}> <FocusContext value={focusKey}>

View file

@ -8,6 +8,7 @@ import SvgIcon from "./SvgIcon";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { useEffect } from "react"; import { useEffect } from "react";
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts"; import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
import { oneShot } from "../scripts/audio/audio";
function FilterCat ( function FilterCat (
data: { data: {
@ -19,7 +20,10 @@ function FilterCat (
{ {
const { ref, focusSelf } = useFocusable({ const { ref, focusSelf } = useFocusable({
focusKey: data.id, 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 onEnterPress: data.onAction
}); });
@ -27,7 +31,8 @@ function FilterCat (
<li <li
aria-selected={data.active} aria-selected={data.active}
ref={ref} 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"} 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>} {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) if (!data.options[newFilter].selected)
{ {
data.setSelected(newFilter); data.setSelected(newFilter);
oneShot('selectFilter');
} else
{
oneShot('invalidNavigation');
} }
}, },
button: GamePadButtonCode.R1 button: GamePadButtonCode.R1
@ -80,7 +89,13 @@ export function FilterUI (data: {
const selectedFilterIndex = Math.max(0, filterIndex - 1,); const selectedFilterIndex = Math.max(0, filterIndex - 1,);
const newFilter = filterKeys[selectedFilterIndex]; const newFilter = filterKeys[selectedFilterIndex];
if (!data.options[newFilter].selected) if (!data.options[newFilter].selected)
{
data.setSelected(newFilter); data.setSelected(newFilter);
oneShot('selectFilter');
} else
{
oneShot('invalidNavigation');
}
}, },
button: GamePadButtonCode.L1 button: GamePadButtonCode.L1
}], [data.options]); }], [data.options]);
@ -90,7 +105,7 @@ export function FilterUI (data: {
{ {
if (hasFocusedChild) if (hasFocusedChild)
{ {
setFocus(`${data.id}-${defaultFocus}`); setFocus(`${data.id}-${defaultFocus}`, { instant: true });
} }
}, [hasFocusedChild, defaultFocus, data.id]); }, [hasFocusedChild, defaultFocus, data.id]);

View file

@ -1,15 +1,16 @@
import { RPC_URL } from "@/shared/constants"; import { RPC_URL } from "@/shared/constants";
import CardElement from "./CardElement"; import CardElement from "./CardElement";
import { Router } from "..";
import { FileQuestion, HardDrive, Store } from "lucide-react"; import { FileQuestion, HardDrive, Store } from "lucide-react";
import { JSX } from "react"; import { JSX } from "react";
import { FOCUS_KEYS } from "../scripts/types"; 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) 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) 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}`); const platformUrl = new URL(`${RPC_URL(__HOST__)}${data.game.path_platform_cover}`);

View file

@ -14,7 +14,6 @@ import
Bell, Bell,
Bluetooth, Bluetooth,
Clock, Clock,
Plug,
Settings, Settings,
Wifi, Wifi,
WifiHigh, WifiHigh,
@ -23,17 +22,15 @@ import
} from "lucide-react"; } from "lucide-react";
import { RoundButton } from "./RoundButton"; import { RoundButton } from "./RoundButton";
import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { RPC_URL, SystemInfoType } from "../../shared/constants";
import { JSX, RefObject, useContext, useEffect, useRef, useState } from "react"; import { JSX, RefObject, useContext, useEffect, useRef, useState } from "react";
import { systemApi } from "../scripts/clientApi";
import { Router } from "..";
import { useStickyDataAttr } from "../scripts/utils"; import { useStickyDataAttr } from "../scripts/utils";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { TwitchIcon } from "../scripts/brandIcons"; 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 { twitchLoginVerificationQuery } from "../scripts/queries/settings";
import { da } from "zod/v4/locales";
import { SystemInfoContext } from "../scripts/contexts"; import { SystemInfoContext } from "../scripts/contexts";
import { useRouter } from "@tanstack/react-router";
import { oneShot } from "../scripts/audio/audio";
function HeaderAvatar (data: { function HeaderAvatar (data: {
id: string; id: string;
@ -206,19 +203,23 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; })
placeholderData: keepPreviousData 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[] = []; const accounts: HeaderAccount[] = [];
if (data.accounts) accounts.push(...data.accounts); if (data.accounts) accounts.push(...data.accounts);
const router = useRouter();
if (rommUser.data?.hasLogin || rommUser.isError) if (rommUser.data?.hasLogin || rommUser.isError)
{ {
accounts.push({ accounts.push({
id: 'romm', preview: `https://romm.app/_ipx/q_80/images/blocks/logos/romm.svg`, 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", className: rommUser.data?.hasLogin && !rommUser.isError ? undefined : "border-error",
type: 'secondary' type: 'secondary'
}); });
@ -228,15 +229,11 @@ export function HeaderAccounts (data: { accounts?: HeaderAccount[]; })
{ {
accounts.push({ accounts.push({
id: 'twitch', preview: TwitchIcon, id: 'twitch', preview: TwitchIcon,
action: () =>
{
Router.navigate({ to: '/settings/accounts', search: { focus: 'rommAddress' } });
},
type: 'secondary' 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 {accounts?.map(a => <HeaderAvatar
key={`header-avatar-${a.id}`} key={`header-avatar-${a.id}`}
id={`account-${a.id}`} id={`account-${a.id}`}
@ -285,9 +282,10 @@ interface HeaderUIParams
export function HeaderUI (data: HeaderUIParams) export function HeaderUI (data: HeaderUIParams)
{ {
const { ref, focusKey } = useFocusable({ focusKey: "header-elements", focusable: data.focusable, preferredChildFocusKey: data.preferredChildFocusKey }); const { ref, focusKey } = useFocusable({ focusKey: "header-elements", focusable: data.focusable, preferredChildFocusKey: data.preferredChildFocusKey });
const router = useRouter();
const goToSettings = () => const goToSettings = () =>
{ {
Router.navigate({ to: '/settings/accounts' }); router.navigate({ to: '/settings/accounts' });
}; };
return ( return (
<FocusContext.Provider value={focusKey}> <FocusContext.Provider value={focusKey}>
@ -296,7 +294,7 @@ export function HeaderUI (data: HeaderUIParams)
className="flex items-center justify-between text-base-content" className="flex items-center justify-between text-base-content"
style={{ viewTimelineName: 'header' }} style={{ viewTimelineName: 'header' }}
> >
<HeaderAccounts accounts={data.accounts} /> <HeaderAccounts key={"header-accounts"} accounts={data.accounts} />
{data.title} {data.title}
<HeaderStatusBar key={"header-status-bar"} buttonElements={data.buttonElements} buttons={[...data.buttons ?? [], { icon: <Settings />, id: "settings", action: goToSettings, external: true }]} /> <HeaderStatusBar key={"header-status-bar"} buttonElements={data.buttonElements} buttons={[...data.buttons ?? [], { icon: <Settings />, id: "settings", action: goToSettings, external: true }]} />
</header> </header>

View file

@ -13,6 +13,7 @@ export default function LoadMoreButton (data: { isFetching: boolean; lastId?: Fr
}; };
const { ref, focusKey, focused } = useFocusable({ const { ref, focusKey, focused } = useFocusable({
focusable: !data.isFetching,
focusKey: 'load-more-btn', focusKey: 'load-more-btn',
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details), onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details),
onEnterPress: handleAction onEnterPress: handleAction

View file

@ -1,19 +1,20 @@
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { Home, TriangleAlert } from "lucide-react"; import { Home, TriangleAlert } from "lucide-react";
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts"; import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
import { Router } from "..";
import Shortcuts from "./Shortcuts"; import Shortcuts from "./Shortcuts";
import { Button } from "./options/Button"; import { Button } from "./options/Button";
import { useEffect } from "react"; import { useEffect } from "react";
import { useRouter } from "@tanstack/react-router";
export default function NotFound () export default function NotFound ()
{ {
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "not-found" }); 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 }]); useShortcuts(focusKey, () => [{ label: "Return Home", button: GamePadButtonCode.B, action: handleReturn }]);
const { shortcuts } = useShortcutContext(); 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"> return <div ref={ref} className="absolute flex flex-col justify-center items-center w-full h-full gap-4">
<FocusContext value={focusKey}> <FocusContext value={focusKey}>

View file

@ -83,7 +83,7 @@ export default function Screenshots (data: { screenshots?: string[]; className?:
const closest = findClosestElementToCenter(scrollRef.current); const closest = findClosestElementToCenter(scrollRef.current);
if (!closest) return; if (!closest) return;
const closestIndex = Array.from(scrollRef.current.children).indexOf(closest); const closestIndex = Array.from(scrollRef.current.children).indexOf(closest);
setFocus(`screenshot-${closestIndex}`); setFocus(`screenshot-${closestIndex}`, { instant: true });
} }
}, [focused, hasFocusedChild, scrollRef.current]); }, [focused, hasFocusedChild, scrollRef.current]);

View file

@ -9,8 +9,7 @@ import MainActions from "./MainActions";
import ActionButton from "./ActionButton"; import ActionButton from "./ActionButton";
import { useLocalStorage } from "usehooks-ts"; import { useLocalStorage } from "usehooks-ts";
import FocusTooltip from "../FocusTooltip"; import FocusTooltip from "../FocusTooltip";
import { Router } from "@/mainview"; import { useBlocker, useRouter } from "@tanstack/react-router";
import { useBlocker } from "@tanstack/react-router";
function AchievementsInfo (data: { game: FrontEndGameTypeDetailed; } & InteractParams) function AchievementsInfo (data: { game: FrontEndGameTypeDetailed; } & InteractParams)
{ {
@ -35,11 +34,12 @@ export default function ActionButtons (data: { game?: FrontEndGameTypeDetailed,
const [, setDetailsSection] = useLocalStorage('details-section', 'screenshots'); const [, setDetailsSection] = useLocalStorage('details-section', 'screenshots');
const { ref, focusKey, hasFocusedChild } = useFocusable({ focusKey: 'actions', forceFocus: true, trackChildren: true, preferredChildFocusKey: 'mainAction' }); const { ref, focusKey, hasFocusedChild } = useFocusable({ focusKey: 'actions', forceFocus: true, trackChildren: true, preferredChildFocusKey: 'mainAction' });
const router = useRouter();
const deleteMutation = useMutation({ const deleteMutation = useMutation({
...deleteGameMutation({ id: data.id, source: data.source }), ...deleteGameMutation({ id: data.id, source: data.source }),
onSuccess: (d, v, r, ctx) => 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) onError (error)
{ {

View file

@ -1,4 +1,3 @@
import { Router } from "@/mainview";
import { rommApi } from "@/mainview/scripts/clientApi"; import { rommApi } from "@/mainview/scripts/clientApi";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { JSX, useEffect, useRef, useState } from "react"; 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 { Clock, Download, EllipsisVertical, Import, PackageOpen, Play, TriangleAlert } from "lucide-react";
import { gameInvalidationQuery, installMutation, playMutation } from "@/mainview/scripts/queries/romm"; import { gameInvalidationQuery, installMutation, playMutation } from "@/mainview/scripts/queries/romm";
import ActionButton from "./ActionButton"; import ActionButton from "./ActionButton";
import { useRouter } from "@tanstack/react-router";
export default function MainActions (data: { game?: FrontEndGameTypeDetailed, source: string, id: string; }) export default function MainActions (data: { game?: FrontEndGameTypeDetailed, source: string, id: string; })
{ {
const installMut = useMutation(installMutation(data.source, data.id)); const installMut = useMutation(installMutation(data.source, data.id));
const router = useRouter();
const playMut = useMutation({ const playMut = useMutation({
...playMutation, onError (error) ...playMutation, onError (error)
{ {
@ -20,7 +21,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
}, },
onSuccess (data, { source, id }, onMutateResult, context) 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); const ws = useRef<{ send: (data: string) => void; }>(undefined);
@ -58,10 +59,10 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
{ {
if (localId) 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 } 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') } else if (e.data.status === 'error')
@ -78,7 +79,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
sub.close(); sub.close();
ws.current = undefined; ws.current = undefined;
}; };
}, [data.source, data.id]); }, [data.source, data.id, router]);
let progressIcon: JSX.Element | undefined = undefined; let progressIcon: JSX.Element | undefined = undefined;
switch (status) switch (status)
@ -107,7 +108,7 @@ export default function MainActions (data: { game?: FrontEndGameTypeDetailed, so
if (cmd.emulator === 'EMULATORJS') if (cmd.emulator === 'EMULATORJS')
{ {
const params = new URLSearchParams(cmd.command); 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 } else
{ {
playMut.mutate({ source: data.source, id: data.id, command_id: cmd.id }); 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') if (status === 'missing-emulator')
{ {
Router.navigate({ to: '/settings/directories' }); router.navigate({ to: '/settings/directories' });
} }
}} }}
id="mainAction"> id="mainAction">

View file

@ -7,6 +7,7 @@ import
import classNames from "classnames"; import classNames from "classnames";
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts"; import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
import { CSSProperties } from "react"; import { CSSProperties } from "react";
import { oneShot } from "@/mainview/scripts/audio/audio";
export type ButtonStyle = 'base' | 'accent' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'; export type ButtonStyle = 'base' | 'accent' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error';
@ -35,9 +36,14 @@ export function Button (data: {
tooltipType?: "base" | "accent" | "error"; tooltipType?: "base" | "accent" | "error";
} & InteractParams & FocusParams) } & InteractParams & FocusParams)
{ {
const handleAction = (e?: any) =>
{
data.onAction?.(e);
oneShot('click');
};
const { ref, focused, focusKey } = useFocusable({ const { ref, focused, focusKey } = useFocusable({
focusKey: data.id, focusKey: data.id,
onEnterPress: data.onAction, onEnterPress: () => handleAction(),
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details), onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details),
focusable: !data.disabled focusable: !data.disabled
}); });
@ -49,7 +55,7 @@ export function Button (data: {
return <button return <button
ref={ref} ref={ref}
onClick={e => data.onAction?.(e.nativeEvent)} onClick={handleAction}
disabled={data.disabled} disabled={data.disabled}
data-tooltip={data.tooltip} data-tooltip={data.tooltip}
data-tooltip_type={data.tooltipType} data-tooltip_type={data.tooltipType}

View file

@ -4,6 +4,7 @@ import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { ContextDialog, ContextList, DialogEntry } from "../ContextDialog"; import { ContextDialog, ContextList, DialogEntry } from "../ContextDialog";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { FOCUS_KEYS } from "@/mainview/scripts/types"; import { FOCUS_KEYS } from "@/mainview/scripts/types";
import { oneShot } from "@/mainview/scripts/audio/audio";
export function OptionDropdown (data: { export function OptionDropdown (data: {
name: string; name: string;
@ -23,6 +24,7 @@ export function OptionDropdown (data: {
const handlePress = () => const handlePress = () =>
{ {
setOpen(true); setOpen(true);
oneShot('click');
}; };
const handleClose = () => setOpen(false); const handleClose = () => setOpen(false);
const { ref } = useFocusable({ 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")}> <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>} {!!data.icon && <span className={"text-base-content/80 is-focused:text-primary-content"}>{data.icon}</span>}
<button onClick={() => <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>
{
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>
</label> </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}> {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) => ({ <ContextList options={data.values.map((v, i) => ({

View file

@ -4,6 +4,7 @@ import { useOptionContext } from "./OptionSpace";
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { systemApi } from "../../scripts/clientApi"; import { systemApi } from "../../scripts/clientApi";
import { CheckIcon, X } from "lucide-react"; import { CheckIcon, X } from "lucide-react";
import { oneShot } from "@/mainview/scripts/audio/audio";
export function OptionInput (data: { export function OptionInput (data: {
name: string; name: string;
@ -27,6 +28,7 @@ export function OptionInput (data: {
{ {
inputRef.current?.focus(); inputRef.current?.focus();
} }
oneShot('click');
}; };
const { ref } = useFocusable({ const { ref } = useFocusable({
focusKey: data.name, onEnterPress: handlePress focusKey: data.name, onEnterPress: handlePress
@ -79,12 +81,14 @@ export function OptionInput (data: {
name={data.name} name={data.name}
checked={Boolean(data.value)} checked={Boolean(data.value)}
type={data.type} type={data.type}
onClick={() => { oneShot("click"); }}
autoComplete={data.autocomplete} autoComplete={data.autocomplete}
onFocus={handleFocus} onFocus={handleFocus}
placeholder={data.placeholder} placeholder={data.placeholder}
onChange={e => data.onChange?.(e.target.checked)} onChange={e => data.onChange?.(e.target.checked)}
onBlur={data.onBlur} onBlur={data.onBlur}
className={twMerge( className={twMerge(
"active:bg-base-content rounded-full",
data.className data.className
)} )}
/> />

View file

@ -80,7 +80,7 @@ export function PathSettingsOptionBase (data: PathSettingsOptionParams & {
const handleCloseSeatch = () => const handleCloseSeatch = () =>
{ {
setIsBrowsing(false); setIsBrowsing(false);
setFocus(`${data.id}-browse`); setFocus(`${data.id}-browse`, { instant: true });
}; };
const handleInputBlur = () => const handleInputBlur = () =>

View file

@ -8,12 +8,12 @@ import { ChevronRight, Joystick } from "lucide-react";
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts"; import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
import { scrollIntoNearestParent, useDragScroll } from "@/mainview/scripts/utils"; import { scrollIntoNearestParent, useDragScroll } from "@/mainview/scripts/utils";
import FocusDots from "../FocusDots"; import FocusDots from "../FocusDots";
import { Router } from "@/mainview";
import { StoreEmulatorCard } from "./StoreEmulatorCard"; import { StoreEmulatorCard } from "./StoreEmulatorCard";
import { FOCUS_KEYS } from "@/mainview/scripts/types"; import { FOCUS_KEYS } from "@/mainview/scripts/types";
import Carousel from "../Carousel"; 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({ const { ref, focusKey } = useFocusable({
focusKey: data.id, focusKey: data.id,
@ -39,6 +39,7 @@ export function EmulatorsSection (data: {
header?: any; header?: any;
} & FocusParams) } & FocusParams)
{ {
const router = useRouter();
const { ref, focusKey } = useFocusable({ const { ref, focusKey } = useFocusable({
focusKey: FOCUS_KEYS.EMULATOR_SECTION(data.id), focusKey: FOCUS_KEYS.EMULATOR_SECTION(data.id),
trackChildren: true, trackChildren: true,
@ -68,7 +69,7 @@ export function EmulatorsSection (data: {
scrollIntoNearestParent(node, { behavior: details.instant ? 'instant' : 'smooth' }); scrollIntoNearestParent(node, { behavior: details.instant ? 'instant' : 'smooth' });
}} /> }} />
)) ?? Array.from({ length: 8 }).map((_, i) => <div key={i} className="skeleton h-38 w-full rounded-4xl" />)} )) ?? 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> </Carousel>
</section> </section>

View file

@ -30,7 +30,7 @@ export function GamesSection (data: {
useEffect(() => useEffect(() =>
{ {
if (focused) if (focused)
focusSelf(); focusSelf({ instant: true });
}, [!!data.games]); }, [!!data.games]);
return ( return (

View file

@ -9,6 +9,7 @@ import { ChevronRight, CircleQuestionMark, SearchAlert } from "lucide-react";
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts"; import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
import { RPC_URL } from "@/shared/constants"; import { RPC_URL } from "@/shared/constants";
import { FOCUS_KEYS } from "@/mainview/scripts/types"; import { FOCUS_KEYS } from "@/mainview/scripts/types";
import { oneShot } from "@/mainview/scripts/audio/audio";
// ── Single missing-emulator card ─────────────────────────────────────────── // ── Single missing-emulator card ───────────────────────────────────────────
interface MissingCardProps interface MissingCardProps
@ -19,7 +20,11 @@ interface MissingCardProps
function MissingCard ({ emulator: em, onSelect }: 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({ const { ref, focusKey } = useFocusable({
focusKey: FOCUS_KEYS.MISSING_CARD(em.name), focusKey: FOCUS_KEYS.MISSING_CARD(em.name),

View file

@ -9,6 +9,7 @@ import { BadgeCheck, ChevronRight, EllipsisVertical, FileQuestion, IceCream2, Pa
import { FOCUS_KEYS } from "@/mainview/scripts/types"; import { FOCUS_KEYS } from "@/mainview/scripts/types";
import { FlatpackIcon } from "@/mainview/scripts/brandIcons"; import { FlatpackIcon } from "@/mainview/scripts/brandIcons";
import { JSX } from "react"; import { JSX } from "react";
import { oneShot } from "@/mainview/scripts/audio/audio";
export const emulatorStatusIcons: Record<string, JSX.Element> = { export const emulatorStatusIcons: Record<string, JSX.Element> = {
store: <Store />, store: <Store />,
@ -26,7 +27,11 @@ export function StoreEmulatorCard (data: {
className?: string; className?: string;
}) })
{ {
const handleSelect = () => data.onSelect?.(data.emulator.name, focusKey); const handleSelect = () =>
{
data.onSelect?.(data.emulator.name, focusKey);
oneShot('click');
};
const { ref, focusKey } = useFocusable({ const { ref, focusKey } = useFocusable({
focusKey: FOCUS_KEYS.EMULATOR_CARD(data.id), focusKey: FOCUS_KEYS.EMULATOR_CARD(data.id),
@ -45,6 +50,7 @@ export function StoreEmulatorCard (data: {
ref={ref} ref={ref}
role="button" role="button"
tabIndex={0} tabIndex={0}
data-sound-category="emulator"
data-installed={data.emulator.validSources.some(s => s.exists)} data-installed={data.emulator.validSources.some(s => s.exists)}
onClick={isTouch ? handleSelect : undefined} onClick={isTouch ? handleSelect : undefined}
className={twMerge("relative focusable focusable-info bg-base-100 rounded-4xl transition-shadow focused:not-control-mouse:animate-scale-small shadow-lg border border-base-content/10 active:ring-4 active:ring-base-content active:transition-none", data.className)} 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>; </div>;
})} })}
{isMouse && <> {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> </div>

View file

@ -464,7 +464,7 @@ const assets = new Set<string>([
]); ]);
// Store basePath resolved from Vite config // Store basePath resolved from Vite config
const BASE_PATH = "./"; const BASE_PATH = "/";
/** /**

View file

@ -9,15 +9,13 @@ import
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { routeTree } from "./gen/routeTree.gen"; import { routeTree } from "./gen/routeTree.gen";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RPC_URL } from "../shared/constants";
import "./scripts/gamepads"; import "./scripts/gamepads";
import "./scripts/windowEvents"; import "./scripts/windowEvents";
import { client as rommClient } from "../clients/romm/client.gen";
import "./scripts/spatialNavigation"; import "./scripts/spatialNavigation";
import NotFound from "./components/NotFound"; import NotFound from "./components/NotFound";
import Error from "./components/Error"; import Error from "./components/Error";
import serviceWorker from './scripts/serviceWorker?worker&url'; import serviceWorker from './scripts/serviceWorker?worker&url';
import { getCurrentFocusKey, setFocus } from "@noriginmedia/norigin-spatial-navigation"; import App from "./App";
if ('serviceWorker' in navigator) if ('serviceWorker' in navigator)
{ {
@ -26,12 +24,6 @@ if ('serviceWorker' in navigator)
const hashHistory = createHashHistory({}); const hashHistory = createHashHistory({});
rommClient.setConfig({
baseUrl: `${RPC_URL(__HOST__)}/api/romm`,
credentials: "include",
mode: "cors",
});
const queryClient = new QueryClient(); const queryClient = new QueryClient();
export interface RouterContext 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 // Register things for typesafety
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
interface Register interface Register
@ -100,9 +73,11 @@ if (!rootElement.innerHTML)
const root = createRoot(rootElement); const root = createRoot(rootElement);
root.render( root.render(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <App>
<RouterProvider router={Router} /> <QueryClientProvider client={queryClient}>
</QueryClientProvider> <RouterProvider router={Router} />
</QueryClientProvider>
</App>
</StrictMode>, </StrictMode>,
); );
} }

View file

@ -4,10 +4,7 @@ import Notifications from "../components/Notifications";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { mobileCheck, useLocalSetting } from "../scripts/utils"; import { mobileCheck, useLocalSetting } from "../scripts/utils";
import useActiveControl from "../scripts/gamepads"; import useActiveControl from "../scripts/gamepads";
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { SystemInfoContext } from "../scripts/contexts";
import { SystemInfoType } from "@/shared/constants";
import { systemApi } from "../scripts/clientApi";
import AppCommunication from "../components/AppCommunication"; import AppCommunication from "../components/AppCommunication";
export const Route = createRootRouteWithContext<RouterContext>()({ export const Route = createRootRouteWithContext<RouterContext>()({

View file

@ -1,9 +1,8 @@
import { RPC_URL, SERVER_URL } from '@/shared/constants'; 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 { zodValidator } from '@tanstack/zod-adapter';
import z from 'zod'; import z from 'zod';
import { RefObject, useEffect, useRef, useState } from 'react'; import { RefObject, useEffect, useRef, useState } from 'react';
import { Router } from '..';
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation'; import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
import { ButtonStyle } from '../components/options/Button'; import { ButtonStyle } from '../components/options/Button';
import { DoorOpen, RefreshCw, Undo } from 'lucide-react'; import { DoorOpen, RefreshCw, Undo } from 'lucide-react';
@ -57,7 +56,7 @@ function Overlay (data: {
{ {
if (data.open) if (data.open)
{ {
focusSelf(); focusSelf({ instant: true });
} }
}, [data.open]); }, [data.open]);
@ -122,6 +121,7 @@ function Frame (data: { ref: RefObject<HTMLIFrameElement | null>; })
function RouteComponent () function RouteComponent ()
{ {
const router = useRouter();
const { ref, focusSelf, focusKey } = useFocusable({ const { ref, focusSelf, focusKey } = useFocusable({
focusKey: 'emulatorjs', focusKey: 'emulatorjs',
preferredChildFocusKey: 'frame', preferredChildFocusKey: 'frame',
@ -133,7 +133,7 @@ function RouteComponent ()
function HandleGoBack () 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 => useEventListener('message', e =>
@ -173,7 +173,7 @@ function RouteComponent ()
}; };
useEffect(() => setPaused(overlayOpen), [overlayOpen]); useEffect(() => setPaused(overlayOpen), [overlayOpen]);
const { shortcuts } = useShortcutContext(); const { shortcuts } = useShortcutContext();
useEffect(() => { if (!overlayOpen) focusSelf(); }, [overlayOpen]); useEffect(() => { if (!overlayOpen) focusSelf({ instant: true }); }, [overlayOpen]);
function handleClose () function handleClose ()
{ {
setOverlayOpen(false); setOverlayOpen(false);

View file

@ -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 { RPC_URL } from "@shared/constants";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { FocusContext, setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation"; import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { Calendar, Clock, Folder, Gamepad2, Image, Info, Store, TriangleAlert, Trophy } from "lucide-react"; import { Calendar, Folder, Gamepad2, Image, Info, TriangleAlert, Trophy } from "lucide-react";
import { HeaderUI } from "../../components/Header"; import { HeaderUI } from "../../components/Header";
import { AnimatedBackground } from "../../components/AnimatedBackground"; import { AnimatedBackground } from "../../components/AnimatedBackground";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Router } from "../..";
import Shortcuts from "../../components/Shortcuts"; import Shortcuts from "../../components/Shortcuts";
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts"; import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
import Screenshots from "@/mainview/components/Screenshots"; 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 { FilterUI } from "@/mainview/components/Filters";
import StatList, { StatEntry } from "@/mainview/components/StatList"; import StatList, { StatEntry } from "@/mainview/components/StatList";
import { useIntersectionObserver, useLocalStorage } from "usehooks-ts"; import { useIntersectionObserver, useLocalStorage } from "usehooks-ts";
@ -21,7 +20,7 @@ import Achievements from "@/mainview/components/game/Achievements";
import { GameDetailsContext } from "@/mainview/scripts/contexts"; import { GameDetailsContext } from "@/mainview/scripts/contexts";
import { gameQuery, gamesRecommendedBasedOnGameQuery } from "@queries/romm"; import { gameQuery, gamesRecommendedBasedOnGameQuery } from "@queries/romm";
import { GamesSection } from "@/mainview/components/store/GamesSection"; 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"; import { AutoFocus } from "@/mainview/components/AutoFocus";
export const Route = createFileRoute("/game/$source/$id")({ export const Route = createFileRoute("/game/$source/$id")({
@ -31,7 +30,11 @@ export const Route = createFileRoute("/game/$source/$id")({
}, },
component: RouteComponent, component: RouteComponent,
errorComponent: Error, 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 () function useDetailsSection ()
@ -45,10 +48,6 @@ function Error (data: ErrorComponentProps)
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]); useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
const { shortcuts } = useShortcutContext(); const { shortcuts } = useShortcutContext();
useEffect(() =>
{
focusSelf();
}, []);
return <AnimatedBackground ref={ref} backgroundKey="game-details"> return <AnimatedBackground ref={ref} backgroundKey="game-details">
<div className="relative z-10 h-full"> <div className="relative z-10 h-full">
@ -68,6 +67,7 @@ function Error (data: ErrorComponentProps)
</div> </div>
</FocusContext> </FocusContext>
</div> </div>
<AutoFocus force focus={focusSelf} />
</AnimatedBackground>; </AnimatedBackground>;
} }
@ -139,10 +139,10 @@ function Divider (data: { rootFocusKey: string; showShortcuts: boolean; game: Fr
function RouteComponent () function RouteComponent ()
{ {
const router = useRouter();
const [recommendedGamesVisible, setRecommendedGamesVisible] = useState(false); const [recommendedGamesVisible, setRecommendedGamesVisible] = useState(false);
const { source, id } = Route.useParams(); const { source, id } = Route.useParams();
const { data } = useQuery(gameQuery(source, id)); const { data } = useQuery(gameQuery(source, id));
const { focus } = Route.useSearch();
const [, setUpdate] = useState(0); const [, setUpdate] = useState(0);
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details", forceFocus: true }); const { ref, focusKey, focusSelf } = useFocusable({ focusKey: "game-details", preferredChildFocusKey: "main-details", forceFocus: true });
const headerRef = useRef(null); const headerRef = useRef(null);
@ -150,7 +150,12 @@ function RouteComponent ()
const backgroundImage = data ? new URL(`${RPC_URL(__HOST__)}${data.path_cover}`) : undefined; 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 }); 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(); const { shortcuts } = useShortcutContext();
useStickyDataAttr(headerRef, sentinelRef, ref); useStickyDataAttr(headerRef, sentinelRef, ref);
@ -190,7 +195,7 @@ function RouteComponent ()
onFocus={scrollIntoViewHandler({ block: 'center' })} onFocus={scrollIntoViewHandler({ block: 'center' })}
onSelect={(id, focus) => onSelect={(id, focus) =>
{ {
Router.navigate({ to: '/store/details/emulator/$id', params: { id } }); router.navigate({ to: '/store/details/emulator/$id', params: { id } });
}} }}
emulators={recommendedEmulators} />} emulators={recommendedEmulators} />}
@ -206,7 +211,7 @@ function RouteComponent ()
</div> </div>
<GamesSection ref={intersct} showSources onSelect={(id, focus) => <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} /> }} onFocus={scrollIntoViewHandler({ block: 'center', inline: 'nearest' })} games={recommendedGames} />
</div> </div>
</div> </div>

View file

@ -14,6 +14,7 @@ import
import import
{ {
createFileRoute, createFileRoute,
useRouter,
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import import
@ -37,7 +38,6 @@ import Shortcuts from "../components/Shortcuts";
import { PlatformsList } from "../components/PlatformsList"; import { PlatformsList } from "../components/PlatformsList";
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts"; import { GamePadButtonCode, useShortcutContext, useShortcuts } from "../scripts/shortcuts";
import z from "zod"; import z from "zod";
import { Router } from "..";
import CollectionList from "../components/CollectionList"; import CollectionList from "../components/CollectionList";
import { zodValidator } from '@tanstack/zod-adapter'; import { zodValidator } from '@tanstack/zod-adapter';
import { mobileCheck, useDragScroll } from "../scripts/utils"; import { mobileCheck, useDragScroll } from "../scripts/utils";
@ -45,6 +45,7 @@ import { AnimatedBackgroundContext } from "../scripts/contexts";
import Carousel from "../components/Carousel"; import Carousel from "../components/Carousel";
import { closeMutation } from "@queries/system"; import { closeMutation } from "@queries/system";
import { gameQuery } from "../scripts/queries/romm"; import { gameQuery } from "../scripts/queries/romm";
import { oneShot } from "../scripts/audio/audio";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: ConsoleHomeUI, component: ConsoleHomeUI,
@ -90,9 +91,10 @@ function HomeListError (data: { focused: boolean; })
function ShowAllGamesCard () function ShowAllGamesCard ()
{ {
const router = useRouter();
const handleNavigate = () => 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 }); 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>; 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; selectedFilter: string;
}) })
{ {
const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [initFocus, setInitFocus] = useState(false); const [initFocus, setInitFocus] = useState(false);
const bg = useContext(AnimatedBackgroundContext); const bg = useContext(AnimatedBackgroundContext);
@ -124,7 +127,7 @@ function HomeList (data: {
function handleGameSelect (id: FrontEndId, source: string | null, sourceId: string | null) 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; let activeList: JSX.Element;
@ -213,9 +216,11 @@ function HomeList (data: {
function MainMenu () function MainMenu ()
{ {
const router = useRouter();
const { ref, focusKey } = useFocusable({ const { ref, focusKey } = useFocusable({
focusKey: `main-menu`, focusKey: `main-menu`,
trackChildren: true, trackChildren: true,
focusBoundaryDirections: ['up', 'down']
}); });
return ( return (
<ul <ul
@ -226,13 +231,13 @@ function MainMenu ()
> >
<FocusContext.Provider value={focusKey}> <FocusContext.Provider value={focusKey}>
<CircleIcon <CircleIcon
action={() => Router.navigate({ to: "/games" })} action={() => router.navigate({ to: "/games" })}
icon={<Gamepad2 />} icon={<Gamepad2 />}
label="Home" label="Home"
type="secondary" type="secondary"
/> />
<CircleIcon icon={<MessageSquare />} label="News" /> <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={<Image />} label="Album" />
<CircleIcon <CircleIcon
icon={<Gamepad2 />} icon={<Gamepad2 />}
@ -241,7 +246,7 @@ function MainMenu ()
<CircleIcon <CircleIcon
action={() => action={() =>
{ {
Router.navigate({ to: '/settings/accounts' }); router.navigate({ to: '/settings/accounts' });
}} }}
icon={<Settings />} icon={<Settings />}
label="Settings" label="Settings"
@ -259,11 +264,16 @@ function CircleIcon (data: {
icon?: JSX.Element; icon?: JSX.Element;
}) })
{ {
const handleAction = () =>
{
data.action?.();
oneShot('click');
};
const { ref, focusKey } = useFocusable({ const { ref, focusKey } = useFocusable({
focusKey: `navigation-icon-${data.label}`, focusKey: `menu-navigation-icon-${data.label}`,
onEnterPress: data.action, 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 = { const typeClasses = {
secondary: "bg-secondary text-secondary-content", secondary: "bg-secondary text-secondary-content",
accent: "bg-accent text-accent-content", accent: "bg-accent text-accent-content",
@ -273,7 +283,8 @@ function CircleIcon (data: {
return ( return (
<li <li
ref={ref} ref={ref}
onClick={data.action} data-sound-category={"menu"}
onClick={handleAction}
className={twMerge( className={twMerge(
`portrait:sm:size-12 sm:w-14 sm:h-10 menu-icon text-base-300 md:w-20 md:h-20 rounded-full flex items-center justify-center drop-shadow-lg cursor-pointer transition-all 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'])} `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 { filter } = Route.useSearch();
const close = useMutation(closeMutation); const close = useMutation(closeMutation);
const router = useRouter();
const { ref, focusKey } = useFocusable({ const { ref, focusKey } = useFocusable({
forceFocus: true, forceFocus: true,
autoRestoreFocus: false, autoRestoreFocus: false,
@ -296,7 +307,7 @@ export default function ConsoleHomeUI ()
preferredChildFocusKey: `home-list`, 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 { shortcuts } = useShortcutContext();
const headerButtons: HeaderButton[] = []; const headerButtons: HeaderButton[] = [];

View file

@ -1,7 +1,6 @@
import { AnimatedBackground } from '@/mainview/components/AnimatedBackground'; 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 DotsLoading from '../components/backgrounds/dots';
import { Router } from '..';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts'; import { GamePadButtonCode, useShortcutContext, useShortcuts } from '../scripts/shortcuts';
@ -16,9 +15,10 @@ export const Route = createFileRoute('/launcher/$source/$id')({
function RouteComponent () function RouteComponent ()
{ {
const router = useRouter();
function HandleGoBack () 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(); const { source, id } = Route.useParams();

View file

@ -171,7 +171,7 @@ function RouteComponent ()
{ {
if (focus) if (focus)
{ {
focusSelf(); focusSelf({ instant: true });
} }
}, [focus]); }, [focus]);

View file

@ -1,4 +1,4 @@
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute, useRouter } from '@tanstack/react-router';
import { OptionSpace } from '../../components/options/OptionSpace'; import { OptionSpace } from '../../components/options/OptionSpace';
import { OptionInput } from '../../components/options/OptionInput'; import { OptionInput } from '../../components/options/OptionInput';
import { useMutation, useQuery } from '@tanstack/react-query'; 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 { FOCUS_KEYS } from '@/mainview/scripts/types';
import { scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from '@/mainview/scripts/utils'; import { scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from '@/mainview/scripts/utils';
import { SettingsOption } from '@/mainview/components/options/SettingsOption'; import { SettingsOption } from '@/mainview/components/options/SettingsOption';
import { Router } from '@/mainview';
export const Route = createFileRoute('/settings/emulators')({ export const Route = createFileRoute('/settings/emulators')({
component: RouteComponent, component: RouteComponent,
@ -76,7 +75,7 @@ function NewEmulatorPath (data: { addOverride: (emulator: string) => void; isAdd
const handleCloseContext = () => const handleCloseContext = () =>
{ {
setNewEmulatorTypeOpen(false); setNewEmulatorTypeOpen(false);
setFocus('emulator'); setFocus('emulator', { instant: true });
}; };
@ -123,7 +122,7 @@ function EmulatorPath (data: { id: string; })
const handleCloseSearch = () => const handleCloseSearch = () =>
{ {
setIsSearching(false); setIsSearching(false);
setFocus(`search-${data.id}`); setFocus(`search-${data.id}`, { instant: true });
}; };
const handleSelectPath = (path: string) => const handleSelectPath = (path: string) =>
@ -192,6 +191,7 @@ function EmulatorBadge (data: {
addOverride: (emulator: string) => void; addOverride: (emulator: string) => void;
} & FocusParams) } & FocusParams)
{ {
const router = useRouter();
const { focusKey, ref, focused } = useFocusable({ const { focusKey, ref, focused } = useFocusable({
focusKey: FOCUS_KEYS.EMULATOR_CARD(data.emulator.name), focusKey: FOCUS_KEYS.EMULATOR_CARD(data.emulator.name),
onFocus (l, p, details) { data.onFocus?.(focusKey, ref.current, details); } onFocus (l, p, details) { data.onFocus?.(focusKey, ref.current, details); }
@ -212,12 +212,12 @@ function EmulatorBadge (data: {
label: "Visit Store", label: "Visit Store",
action () 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; return shortcuts;
}, [data.addOverride]); }, [data.addOverride, router]);
let statusIcon = <SearchAlert className={data.emulator.isCritical ? 'text-warning' : 'text-base-content/40'} />; let statusIcon = <SearchAlert className={data.emulator.isCritical ? 'text-warning' : 'text-base-content/40'} />;
@ -255,7 +255,7 @@ function EmulatorBadge (data: {
case 'store': case 'store':
icon = <Store />; icon = <Store />;
className = "hover:bg-base-content hover:text-base-100 cursor-pointer bg-accent text-accent-content"; 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; break;
case 'embedded': case 'embedded':
icon = <Plug />; icon = <Plug />;

View file

@ -19,6 +19,7 @@ function RouteComponent ()
<LocalOption id="backgroundBlur" label="Background Blur" type='checkbox'></LocalOption> <LocalOption id="backgroundBlur" label="Background Blur" type='checkbox'></LocalOption>
<LocalOption id="backgroundAnimation" label="Background Animation" type='checkbox'></LocalOption> <LocalOption id="backgroundAnimation" label="Background Animation" type='checkbox'></LocalOption>
<LocalOption id="theme" label="Theme" type='dropdown' values={['dark', 'light', 'auto']}></LocalOption> <LocalOption id="theme" label="Theme" type='dropdown' values={['dark', 'light', 'auto']}></LocalOption>
<LocalOption id='soundEffects' label="Sounds" type='checkbox'></LocalOption>
</FocusContext> </FocusContext>
</ul>; </ul>;
} }

View file

@ -8,6 +8,7 @@ import
Outlet, Outlet,
createFileRoute, createFileRoute,
useMatch, useMatch,
useRouter,
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { ViewTransitionOptions } from "@tanstack/router-core"; import { ViewTransitionOptions } from "@tanstack/router-core";
import classNames from "classnames"; import classNames from "classnames";
@ -21,20 +22,24 @@ import
MonitorCog, MonitorCog,
Puzzle, Puzzle,
} from "lucide-react"; } from "lucide-react";
import { JSX, useEffect } from "react"; import { JSX } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import z from "zod"; import z from "zod";
import { SettingsSchema } from "../../../shared/constants"; import { SettingsSchema } from "../../../shared/constants";
import { Router } from "../..";
import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts"; import { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
import Shortcuts from "@/mainview/components/Shortcuts"; import Shortcuts from "@/mainview/components/Shortcuts";
import { HandleGoBack } from "@/mainview/scripts/utils"; import { HandleGoBack } from "@/mainview/scripts/utils";
import { AutoFocus } from "@/mainview/components/AutoFocus";
import { oneShot } from "@/mainview/scripts/audio/audio";
export const Route = createFileRoute("/settings")({ export const Route = createFileRoute("/settings")({
component: SettingsUI, component: SettingsUI,
validateSearch: z.object({ validateSearch: z.object({
focus: z.keyof(SettingsSchema).optional() focus: z.keyof(SettingsSchema).optional()
}) }),
staticData: {
enterSound: 'openSettings'
}
}); });
function MenuItem (data: { function MenuItem (data: {
@ -48,17 +53,18 @@ function MenuItem (data: {
label: string; label: string;
}) })
{ {
const router = useRouter();
const acitve = !!useMatch({ from: data.route as any, shouldThrow: false });; const acitve = !!useMatch({ from: data.route as any, shouldThrow: false });;
const handleNonFocusSelect = () => const handleNonFocusSelect = () =>
{ {
if (data.return) if (data.return)
{ {
HandleGoBack(); HandleGoBack(router);
} else if (!acitve) } 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({ const { ref, focusSelf } = useFocusable({
focusKey: `menu-item-${data.route}`, focusKey: `menu-item-${data.route}`,
@ -67,7 +73,7 @@ function MenuItem (data: {
{ {
if (data.focusSelect && !acitve) 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' }); (ref.current as HTMLElement).scrollIntoView({ inline: 'center' });
}, },
@ -81,6 +87,7 @@ function MenuItem (data: {
<li <li
ref={ref} ref={ref}
key={data.route} key={data.route}
data-sound-category={"menu"}
onClick={data.focusSelect ? focusSelf : handleNonFocusSelect} onClick={data.focusSelect ? focusSelf : handleNonFocusSelect}
onFocus={focusSelf} onFocus={focusSelf}
className={twMerge("flex group-focusable cursor-pointer", data.className)} className={twMerge("flex group-focusable cursor-pointer", data.className)}
@ -167,17 +174,13 @@ function SettingsMenu (data: {})
export function SettingsUI () export function SettingsUI ()
{ {
const router = useRouter();
const { ref, focusKey, focusSelf } = useFocusable({ const { ref, focusKey, focusSelf } = useFocusable({
focusKey: "settings-page-layout", focusKey: "settings-page-layout",
preferredChildFocusKey: 'settings-menu' preferredChildFocusKey: 'settings-menu'
}); });
useEffect(() => useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: () => HandleGoBack(router) }], [router]);
{
focusSelf();
}, []);
useShortcuts(focusKey, () => [{ label: "Back", button: GamePadButtonCode.B, action: HandleGoBack }]);
const { shortcuts } = useShortcutContext(); const { shortcuts } = useShortcutContext();
return ( return (
@ -196,6 +199,7 @@ export function SettingsUI ()
<Shortcuts shortcuts={shortcuts} /> <Shortcuts shortcuts={shortcuts} />
</div> </div>
</div> </div>
<AutoFocus focus={focusSelf} />
</FocusContext.Provider> </FocusContext.Provider>
); );
} }

View file

@ -4,9 +4,8 @@ import
useFocusable, useFocusable,
FocusContext, FocusContext,
} from "@noriginmedia/norigin-spatial-navigation"; } 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 { GamePadButtonCode, useShortcutContext, useShortcuts } from "@/mainview/scripts/shortcuts";
import { Router } from "@/mainview";
import Shortcuts from "@/mainview/components/Shortcuts"; import Shortcuts from "@/mainview/components/Shortcuts";
import { AnimatedBackground } from "@/mainview/components/AnimatedBackground"; import { AnimatedBackground } from "@/mainview/components/AnimatedBackground";
import { systemApi } from "@/mainview/scripts/clientApi"; import { systemApi } from "@/mainview/scripts/clientApi";
@ -18,7 +17,7 @@ import Screenshots from "@/mainview/components/Screenshots";
import { StickyHeaderUI } from "@/mainview/components/Header"; import { StickyHeaderUI } from "@/mainview/components/Header";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { EmulatorsSection } from "@/mainview/components/store/EmulatorsSection"; 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 toast from "react-hot-toast";
import { getErrorMessage } from "react-error-boundary"; import { getErrorMessage } from "react-error-boundary";
import { emulatorStatusIcons } from "@/mainview/components/store/StoreEmulatorCard"; 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 { deleteBiosMutation, downloadBiosMutation, installEmulatorMutation, storeEmulatorDeleteMutation, storeEmulatorDetailsQuery, storeEmulatorsRecommendedQuery } from "@queries/store";
import { gamesRecommendedBasedOnEmulatorQuery } from "@queries/romm"; import { gamesRecommendedBasedOnEmulatorQuery } from "@queries/romm";
import FocusTooltip from "@/mainview/components/FocusTooltip"; import FocusTooltip from "@/mainview/components/FocusTooltip";
import { AutoFocus } from "@/mainview/components/AutoFocus";
export const Route = createFileRoute('/store/details/emulator/$id')({ export const Route = createFileRoute('/store/details/emulator/$id')({
component: RouteComponent, 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(storeEmulatorDetailsQuery(ctx.params.id));
ctx.context.queryClient.prefetchQuery(storeEmulatorsRecommendedQuery(ctx.params.id)); ctx.context.queryClient.prefetchQuery(storeEmulatorsRecommendedQuery(ctx.params.id));
ctx.context.queryClient.prefetchQuery(gamesRecommendedBasedOnEmulatorQuery(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 () export function RouteComponent ()
{ {
const { id } = Route.useParams(); const { id } = Route.useParams();
const router = useRouter();
const { ref, focusKey, focusSelf } = useFocusable({ const { ref, focusKey, focusSelf } = useFocusable({
focusKey: `GAME_DETAIL_${id}`, focusKey: `GAME_DETAIL_${id}`,
trackChildren: true, trackChildren: true,
@ -301,22 +305,16 @@ export function RouteComponent ()
useShortcuts(focusKey, () => [{ useShortcuts(focusKey, () => [{
label: "Return", label: "Return",
action: HandleGoBack, action: () => HandleGoBack(router),
button: GamePadButtonCode.B button: GamePadButtonCode.B
}]); }], [router]);
const installMutation = useMutation({ const installMutation = useMutation({
...installEmulatorMutation(id), onSuccess: (data, variables, onMutateResult, context) => context.client.refetchQueries(storeEmulatorDetailsQuery(id)), ...installEmulatorMutation(id), onSuccess: (data, variables, onMutateResult, context) => context.client.refetchQueries(storeEmulatorDetailsQuery(id)),
}); });
useEffect(() =>
{
focusSelf();
}, []);
const { shortcuts } = useShortcutContext(); const { shortcuts } = useShortcutContext();
const stats: StatEntry[] = []; const stats: StatEntry[] = [];
if (emulator) if (emulator)
{ {
@ -341,6 +339,7 @@ export function RouteComponent ()
return ( return (
<AnimatedBackground ref={ref} className="" scrolling> <AnimatedBackground ref={ref} className="" scrolling>
<AutoFocus focus={focusSelf} />
<FocusContext.Provider value={focusKey}> <FocusContext.Provider value={focusKey}>
<StickyHeaderUI ref={ref} /> <StickyHeaderUI ref={ref} />
<div className="flex flex-col z-10"> <div className="flex flex-col z-10">
@ -370,7 +369,7 @@ export function RouteComponent ()
onFocus={scrollIntoViewHandler({ block: 'center' })} onFocus={scrollIntoViewHandler({ block: 'center' })}
onSelect={(id, focus) => onSelect={(id, focus) =>
{ {
Router.navigate({ router.navigate({
to: '/store/details/emulator/$id', params: { id } to: '/store/details/emulator/$id', params: { id }
}); });
}} }}
@ -386,7 +385,7 @@ export function RouteComponent ()
</div> </div>
<GamesSection showSources={true} onFocus={scrollIntoViewHandler({ behavior: 'smooth', block: 'center' })} onSelect={(id) => <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 } to: '/game/$source/$id', params: { id: id.id, source: id.source }
}); });
}} games={recommendedGames} /></div>} }} games={recommendedGames} /></div>}

View file

@ -1,4 +1,4 @@
import { Router } from '@/mainview'; import { AutoFocus } from '@/mainview/components/AutoFocus';
import { FilterUI } from '@/mainview/components/Filters'; import { FilterUI } from '@/mainview/components/Filters';
import { HeaderUI } from '@/mainview/components/Header'; import { HeaderUI } from '@/mainview/components/Header';
import Shortcuts from '@/mainview/components/Shortcuts'; import Shortcuts from '@/mainview/components/Shortcuts';
@ -6,19 +6,21 @@ import { StoreContext } from '@/mainview/scripts/contexts';
import { gameQuery } from '@/mainview/scripts/queries/romm'; import { gameQuery } from '@/mainview/scripts/queries/romm';
import { storeEmulatorDetailsQuery } from '@/mainview/scripts/queries/store'; import { storeEmulatorDetailsQuery } from '@/mainview/scripts/queries/store';
import { GamePadButtonCode, useShortcutContext, useShortcuts } from '@/mainview/scripts/shortcuts'; 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 { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
import { useQueryClient } from '@tanstack/react-query'; 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 { createFileRoute, Outlet } from '@tanstack/react-router';
import { zodValidator } from '@tanstack/zod-adapter'; import { zodValidator } from '@tanstack/zod-adapter';
import { Settings } from 'lucide-react'; import { useRef } from 'react';
import { useEffect, useRef } from 'react';
import z from 'zod'; import z from 'zod';
export const Route = createFileRoute('/store/tab')({ export const Route = createFileRoute('/store/tab')({
component: RouteComponent, 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) function useIsSettings (subPath: string)
@ -33,6 +35,7 @@ function useIsSettings (subPath: string)
function TopArea (data: { filters: Record<string, FilterOption>; }) function TopArea (data: { filters: Record<string, FilterOption>; })
{ {
const router = useRouter();
const { ref, focusKey } = useFocusable({ const { ref, focusKey } = useFocusable({
focusKey: 'top-area', focusKey: 'top-area',
preferredChildFocusKey: `store-tabs`, preferredChildFocusKey: `store-tabs`,
@ -44,13 +47,13 @@ function TopArea (data: { filters: Record<string, FilterOption>; })
useShortcuts("STORE_ROOT", () => [{ useShortcuts("STORE_ROOT", () => [{
label: "Return", label: "Return",
action: () => Router.navigate({ to: '/', viewTransition: { types: ['zoom-out'] } }), action: () => HandleGoBack(router),
button: GamePadButtonCode.B button: GamePadButtonCode.B
}], []); }], [router]);
const handleNavigate = (s: string) => 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}> return <div ref={ref}>
@ -76,6 +79,7 @@ function StoreOutlet ()
function RouteComponent () function RouteComponent ()
{ {
// Root spatial nav container // Root spatial nav container
const router = useRouter();
const { ref, focusKey, focusSelf } = useFocusable({ const { ref, focusKey, focusSelf } = useFocusable({
focusKey: "STORE_ROOT", focusKey: "STORE_ROOT",
preferredChildFocusKey: 'top-area', preferredChildFocusKey: 'top-area',
@ -93,25 +97,16 @@ function RouteComponent ()
const { shortcuts } = useShortcutContext(); const { shortcuts } = useShortcutContext();
const { focus } = Route.useSearch(); const { focus } = Route.useSearch();
useEffect(() =>
{
if (!focus)
{
focusSelf();
}
}, []);
const handleDetails = (type: string, source: string, id: string, focus: string) => const handleDetails = (type: string, source: string, id: string, focus: string) =>
{ {
if (type === 'emulator') 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') 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) => const handlePrefetch = (type: string, source: string, id: string) =>
@ -150,5 +145,6 @@ function RouteComponent ()
</div> </div>
</FocusContext.Provider> </FocusContext.Provider>
</StoreContext> </StoreContext>
<AutoFocus focus={focusSelf} />
</div >; </div >;
} }

View 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());
}

View 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);
}
};
}

View file

@ -2,6 +2,7 @@ import { getCurrentFocusKey, navigateByDirection } from "@noriginmedia/norigin-s
import { GetFocusedElement } from "./spatialNavigation"; import { GetFocusedElement } from "./spatialNavigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { mobileCheck } from "./utils"; import { mobileCheck } from "./utils";
import { oneShot } from "./audio/audio";
let loopStarted = false; let loopStarted = false;
let isTouching = 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); const speed = Math.max(maxSpeed - (maxSpeed - minSpeed) * (acceleration / 6), minSpeed);
if ((currentDate.getTime() - (lastTime ?? 0) > speed)) if ((currentDate.getTime() - (lastTime ?? 0) > speed))
{ {
const currentFocusKey = getCurrentFocusKey();
navigateByDirection(dir, { event }); navigateByDirection(dir, { event });
if (currentFocusKey === getCurrentFocusKey())
oneShot('invalidNavigation');
throttleMap.set(key, currentDate.getTime()); throttleMap.set(key, currentDate.getTime());
throttleAcceleration.set(key, acceleration + 1); throttleAcceleration.set(key, acceleration + 1);
return true; return true;

View file

@ -1,6 +1,5 @@
import import
{ {
FocusDetails,
getCurrentFocusKey, getCurrentFocusKey,
init, init,
SpatialNavigation, SpatialNavigation,
@ -9,7 +8,7 @@ import
UseFocusableResult, UseFocusableResult,
} from "@noriginmedia/norigin-spatial-navigation"; } from "@noriginmedia/norigin-spatial-navigation";
import { RefObject, useEffect, useState } from "react"; import { RefObject, useEffect, useState } from "react";
import { focusQueue, Router } from ".."; import { focusQueue } from "../App";
init({ init({
shouldFocusDOMNode: false, shouldFocusDOMNode: false,
@ -97,13 +96,21 @@ SpatialNavigation.updateLayout = (focusKey) =>
SpatialNavigation.setFocus = (newFocusKey, focusDetails) => SpatialNavigation.setFocus = (newFocusKey, focusDetails) =>
{ {
setFocus(newFocusKey, focusDetails); setFocus(newFocusKey, focusDetails);
dispatchFocusedEvent(new CustomEvent<FocusDetails>('focuschanged', { bubbles: true, detail: focusDetails }));
}; };
SpatialNavigation.setCurrentFocusedKey = (newFocusKey, focusDetails) => SpatialNavigation.setCurrentFocusedKey = (newFocusKey, focusDetails) =>
{ {
const details: FocusEventDetails = {
...focusDetails,
focusKey: newFocusKey,
focusKeyChanged: newFocusKey !== getCurrentFocusKey(),
node: GetFocusedElement(newFocusKey)
};
setCurrentFocusedKey(newFocusKey, focusDetails); 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) => SpatialNavigation.updateFocusable = (key, data) =>

View file

@ -3,7 +3,8 @@ import { RefObject, useEffect, useRef, useState } from "react";
import { useLocalStorage } from "usehooks-ts"; import { useLocalStorage } from "usehooks-ts";
import { jobsApi } from "./clientApi"; import { jobsApi } from "./clientApi";
import { JobsAPIType } from "@/bun/api/rpc"; 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 = { export type ScrollSaveParams = {
id: string; id: string;
@ -59,6 +60,13 @@ export function mobileCheck ()
return check; 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) 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)) }); 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) => return (focusKey: string, node: HTMLElement, details: any) =>
{ {
if (details.nativeEvent instanceof PointerEvent) return; 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 }; 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 } 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]);
}

View file

@ -16,6 +16,25 @@ declare global
"save-scroll"?: boolean; "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 declare interface FocusParams

View file

@ -41,7 +41,8 @@ export const SettingsSchema = z.object({
export const LocalSettingsSchema = z.object({ export const LocalSettingsSchema = z.object({
backgroundBlur: z.stringbool().or(z.boolean()).default(true), backgroundBlur: z.stringbool().or(z.boolean()).default(true),
backgroundAnimation: 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({ export const GameListFilterSchema = z.object({

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more