feat: Implemented audio effects
This commit is contained in:
parent
fe0ab3b498
commit
edbc390d14
125 changed files with 1137 additions and 217 deletions
3
.gitattributes
vendored
3
.gitattributes
vendored
|
|
@ -4,3 +4,6 @@
|
||||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
*.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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
56
bun.lock
56
bun.lock
|
|
@ -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=="],
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
24
scripts/generate-audio-sprites.ts
Normal file
24
scripts/generate-audio-sprites.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import audioSprite from 'audiosprite';
|
||||||
|
import { $, which } from 'bun';
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
var files = await Array.fromAsync(new Bun.Glob('*.{ogg,wav}').scan({ cwd: './src/sounds' }));
|
||||||
|
console.log("Loaded", files.join(","));
|
||||||
|
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
{
|
||||||
|
audioSprite(
|
||||||
|
files.map(f => path.join(path.resolve('./src/sounds'), f)),
|
||||||
|
{
|
||||||
|
output: path.resolve('./src/mainview/assets/sounds'),
|
||||||
|
path: path.resolve('./src/sounds'),
|
||||||
|
format: 'howler',
|
||||||
|
export: 'ogg'
|
||||||
|
}, async function (err, obj: any)
|
||||||
|
{
|
||||||
|
if (err) return console.error(err);
|
||||||
|
delete obj.urls;
|
||||||
|
Bun.file('./src/mainview/assets/sounds.json').write(JSON.stringify(obj, null, 2)).then(r => resolve(true));
|
||||||
|
});
|
||||||
|
});
|
||||||
48
src/mainview/App.tsx
Normal file
48
src/mainview/App.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
import { Router } from ".";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import audioCallbacks from "./scripts/audio/audioCallbacks";
|
||||||
|
import { client as rommClient } from "../clients/romm/client.gen";
|
||||||
|
import { RPC_URL } from "@/shared/constants";
|
||||||
|
|
||||||
|
export const focusQueue: string[] = [];
|
||||||
|
|
||||||
|
export default function App (data: { children: any; })
|
||||||
|
{
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
const focusMap = new Map<number, string>();
|
||||||
|
rommClient.setConfig({
|
||||||
|
baseUrl: `${RPC_URL(__HOST__)}/api/romm`,
|
||||||
|
credentials: "include",
|
||||||
|
mode: "cors",
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsub = Router.history.subscribe((op) =>
|
||||||
|
{
|
||||||
|
if (op.action.type === 'PUSH')
|
||||||
|
{
|
||||||
|
focusMap.set(op.location.state.__TSR_index - 1, getCurrentFocusKey());
|
||||||
|
} else if (op.action.type === 'BACK')
|
||||||
|
{
|
||||||
|
if (focusMap.has(op.location.state.__TSR_index))
|
||||||
|
{
|
||||||
|
focusQueue.pop();
|
||||||
|
focusQueue.push(focusMap.get(op.location.state.__TSR_index)!);
|
||||||
|
focusMap.delete(op.location.state.__TSR_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const audio = audioCallbacks();
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
unsub();
|
||||||
|
audio.cleanup();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>{data.children}</>;
|
||||||
|
}
|
||||||
304
src/mainview/assets/sounds.json
Normal file
304
src/mainview/assets/sounds.json
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
{
|
||||||
|
"sprite": {
|
||||||
|
"Classic UI SFX - Chords #1": [
|
||||||
|
0,
|
||||||
|
4005.215419501134
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #10": [
|
||||||
|
6000,
|
||||||
|
4005.215419501134
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #11": [
|
||||||
|
12000,
|
||||||
|
4005.215419501134
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #12": [
|
||||||
|
18000,
|
||||||
|
4005.215419501134
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #13": [
|
||||||
|
24000,
|
||||||
|
4005.215419501134
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #14": [
|
||||||
|
30000,
|
||||||
|
4005.215419501134
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #15": [
|
||||||
|
36000,
|
||||||
|
4005.215419501134
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #16": [
|
||||||
|
42000,
|
||||||
|
4005.215419501134
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #17": [
|
||||||
|
48000,
|
||||||
|
4005.215419501134
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #18": [
|
||||||
|
54000,
|
||||||
|
4005.215419501134
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #19": [
|
||||||
|
60000,
|
||||||
|
4005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #2": [
|
||||||
|
66000,
|
||||||
|
4005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #20": [
|
||||||
|
72000,
|
||||||
|
4005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #3": [
|
||||||
|
78000,
|
||||||
|
4005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #4": [
|
||||||
|
84000,
|
||||||
|
4005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #5": [
|
||||||
|
90000,
|
||||||
|
4005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #6": [
|
||||||
|
96000,
|
||||||
|
4005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #7": [
|
||||||
|
102000,
|
||||||
|
4005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #8": [
|
||||||
|
108000,
|
||||||
|
4005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Chords #9": [
|
||||||
|
114000,
|
||||||
|
4005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #1": [
|
||||||
|
120000,
|
||||||
|
2546.893424036284
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #10": [
|
||||||
|
124000,
|
||||||
|
2552.0861678004535
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #11": [
|
||||||
|
128000,
|
||||||
|
2927.0975056689394
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #12": [
|
||||||
|
132000,
|
||||||
|
2927.0975056689394
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #13": [
|
||||||
|
136000,
|
||||||
|
3000
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #14": [
|
||||||
|
140000,
|
||||||
|
2802.0861678004394
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #15": [
|
||||||
|
144000,
|
||||||
|
2723.9455782312803
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #16": [
|
||||||
|
148000,
|
||||||
|
2927.0975056689394
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #17": [
|
||||||
|
152000,
|
||||||
|
2880.226757369627
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #18": [
|
||||||
|
156000,
|
||||||
|
2359.387755102034
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #19": [
|
||||||
|
160000,
|
||||||
|
3052.0861678004394
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #2": [
|
||||||
|
165000,
|
||||||
|
2843.7641723355964
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #20": [
|
||||||
|
169000,
|
||||||
|
2015.6462585034092
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #21": [
|
||||||
|
173000,
|
||||||
|
2005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #22": [
|
||||||
|
177000,
|
||||||
|
2489.5918367346894
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #23": [
|
||||||
|
181000,
|
||||||
|
2458.3446712018144
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #24": [
|
||||||
|
185000,
|
||||||
|
2093.7641723355964
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #25": [
|
||||||
|
189000,
|
||||||
|
2005.215419501127
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #3": [
|
||||||
|
193000,
|
||||||
|
2864.6031746031613
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #4": [
|
||||||
|
197000,
|
||||||
|
3031.2698412698464
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #5": [
|
||||||
|
202000,
|
||||||
|
2598.9795918367236
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #6": [
|
||||||
|
206000,
|
||||||
|
2427.0975056689394
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #7": [
|
||||||
|
210000,
|
||||||
|
2468.752834467125
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #8": [
|
||||||
|
214000,
|
||||||
|
2916.666666666657
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - High #9": [
|
||||||
|
218000,
|
||||||
|
2250
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #1": [
|
||||||
|
222000,
|
||||||
|
2010.4308390022538
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #10": [
|
||||||
|
226000,
|
||||||
|
3020.8390022675644
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #11": [
|
||||||
|
231000,
|
||||||
|
2458.3446712018144
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #12": [
|
||||||
|
235000,
|
||||||
|
2901.0430839002197
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #13": [
|
||||||
|
239000,
|
||||||
|
2843.7641723355964
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #14": [
|
||||||
|
243000,
|
||||||
|
3135.4195011337824
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #15": [
|
||||||
|
248000,
|
||||||
|
2703.1292517006877
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #16": [
|
||||||
|
252000,
|
||||||
|
2875.011337868472
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #17": [
|
||||||
|
256000,
|
||||||
|
2927.0975056689394
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #18": [
|
||||||
|
260000,
|
||||||
|
3057.2789115646515
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #19": [
|
||||||
|
265000,
|
||||||
|
2473.9455782312803
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #2": [
|
||||||
|
269000,
|
||||||
|
2583.3333333333144
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #20": [
|
||||||
|
273000,
|
||||||
|
2515.646258503409
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #21": [
|
||||||
|
277000,
|
||||||
|
2604.172335600879
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #22": [
|
||||||
|
281000,
|
||||||
|
3031.2698412698182
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #23": [
|
||||||
|
286000,
|
||||||
|
2937.50566893425
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #24": [
|
||||||
|
290000,
|
||||||
|
2609.387755102034
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #25": [
|
||||||
|
294000,
|
||||||
|
2625.0113378685
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #3": [
|
||||||
|
298000,
|
||||||
|
2828.140589569159
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #4": [
|
||||||
|
302000,
|
||||||
|
2614.6031746031895
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #5": [
|
||||||
|
306000,
|
||||||
|
3161.4739229024735
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #6": [
|
||||||
|
311000,
|
||||||
|
2333.3333333333144
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #7": [
|
||||||
|
315000,
|
||||||
|
2536.4625850340303
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #8": [
|
||||||
|
319000,
|
||||||
|
2630.2267573695985
|
||||||
|
],
|
||||||
|
"Classic UI SFX - Short - Low #9": [
|
||||||
|
323000,
|
||||||
|
2697.936507936504
|
||||||
|
],
|
||||||
|
"UI_Single_Set 16_01": [
|
||||||
|
327000,
|
||||||
|
309.5918367346826
|
||||||
|
],
|
||||||
|
"UI_Single_Set 16_02": [
|
||||||
|
329000,
|
||||||
|
309.5918367346826
|
||||||
|
],
|
||||||
|
"UI_Single_Set 16_03": [
|
||||||
|
331000,
|
||||||
|
309.5918367346826
|
||||||
|
],
|
||||||
|
"UI_TwoNote_Set 15_01": [
|
||||||
|
333000,
|
||||||
|
335.2380952380827
|
||||||
|
],
|
||||||
|
"UI_TwoNote_Set 15_02": [
|
||||||
|
335000,
|
||||||
|
309.5918367346826
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/mainview/assets/sounds.ogg
(Stored with Git LFS)
Normal file
BIN
src/mainview/assets/sounds.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
||||||
import { doesFocusableExist, FocusDetails, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
import { 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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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) =>
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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) => ({
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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 = () =>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function GamesSection (data: {
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (focused)
|
if (focused)
|
||||||
focusSelf();
|
focusSelf({ instant: true });
|
||||||
}, [!!data.games]);
|
}, [!!data.games]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 = "/";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
<App>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider router={Router} />
|
<RouterProvider router={Router} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</App>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>()({
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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[] = [];
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ function RouteComponent ()
|
||||||
{
|
{
|
||||||
if (focus)
|
if (focus)
|
||||||
{
|
{
|
||||||
focusSelf();
|
focusSelf({ instant: true });
|
||||||
}
|
}
|
||||||
}, [focus]);
|
}, [focus]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||||
import { OptionSpace } from '../../components/options/OptionSpace';
|
import { 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 />;
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>}
|
||||||
|
|
|
||||||
|
|
@ -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 >;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
src/mainview/scripts/audio/audio.ts
Normal file
70
src/mainview/scripts/audio/audio.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Howl } from 'howler';
|
||||||
|
import sounds from '../../assets/sounds.ogg';
|
||||||
|
import soundSprites from '../../assets/sounds.json';
|
||||||
|
import { getLocalSetting } from '../utils';
|
||||||
|
|
||||||
|
const timingMap = new Map<string, Date>();
|
||||||
|
|
||||||
|
const sound = new Howl({
|
||||||
|
src: [sounds],
|
||||||
|
sprite: soundSprites.sprite as any,
|
||||||
|
volume: 0.5,
|
||||||
|
});
|
||||||
|
import.meta.hot?.dispose(() => { sound.unload(); });
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface StaticDataRouteOption
|
||||||
|
{
|
||||||
|
enterSound?: keyof typeof soundMap | null;
|
||||||
|
goBackSound?: keyof typeof soundMap | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeVariation = 0.05;
|
||||||
|
const rateVariation = 0.01;
|
||||||
|
|
||||||
|
export const soundMap = {
|
||||||
|
openDetails: { key: 'Classic UI SFX - Chords #1' },
|
||||||
|
returnGeneric: { key: 'Classic UI SFX - Short - Low #2' },
|
||||||
|
returnDetails: { key: 'Classic UI SFX - Short - Low #5' },
|
||||||
|
openGeneric: { key: 'Classic UI SFX - Short - High #9' },
|
||||||
|
select: { key: 'Classic UI SFX - Short - High #5', rateVariation, volumeVariation },
|
||||||
|
selectAlt: { key: "Classic UI SFX - Short - High #6", rateVariation, volumeVariation },
|
||||||
|
selectMenu: { key: 'Classic UI SFX - Short - High #7', rateVariation, volumeVariation },
|
||||||
|
selectFilter: { key: 'Classic UI SFX - Short - High #3', volumeVariation },
|
||||||
|
closeContext: { key: 'Classic UI SFX - Short - High #19' },
|
||||||
|
openContext: { key: 'Classic UI SFX - Short - High #22' },
|
||||||
|
openStore: { key: 'Classic UI SFX - Chords #16' },
|
||||||
|
openSettings: { key: 'Classic UI SFX - Short - High #8' },
|
||||||
|
click: { key: "UI_Single_Set 16_03", rateVariation, volumeVariation },
|
||||||
|
clickAlt: { key: "UI_Single_Set 16_01", rateVariation, volumeVariation },
|
||||||
|
invalidNavigation: { key: "Classic UI SFX - Short - Low #6", rateVariation, volumeVariation },
|
||||||
|
} satisfies Record<string, { key: keyof typeof soundSprites.sprite, rateVariation?: number; volumeVariation?: number; }>;
|
||||||
|
|
||||||
|
function sinRanom ()
|
||||||
|
{
|
||||||
|
return Math.sin(new Date().getMilliseconds() / 1000 * Math.PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cosRandom ()
|
||||||
|
{
|
||||||
|
return Math.sin(new Date().getMilliseconds() / 1000 * Math.PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
function random ()
|
||||||
|
{
|
||||||
|
return Math.random() * 2 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function oneShot (id: keyof typeof soundMap)
|
||||||
|
{
|
||||||
|
const currentDate = timingMap.get(id);
|
||||||
|
if (!getLocalSetting('soundEffects')) return;
|
||||||
|
if (currentDate && new Date().getTime() - currentDate.getTime() <= 100) return;
|
||||||
|
const soundValue = soundMap[id] as { key: keyof typeof soundSprites.sprite, rateVariation?: number; volumeVariation?: number; };
|
||||||
|
const instanceId = sound.play(soundValue.key);
|
||||||
|
sound.volume(sound.volume() + random() * (soundValue.volumeVariation ?? 0), instanceId);
|
||||||
|
sound.rate(1 + random() * (soundValue.rateVariation ?? 0), instanceId);
|
||||||
|
timingMap.set(id, new Date());
|
||||||
|
}
|
||||||
|
|
||||||
90
src/mainview/scripts/audio/audioCallbacks.ts
Normal file
90
src/mainview/scripts/audio/audioCallbacks.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { Router } from "@/mainview";
|
||||||
|
import { oneShot, soundMap } from "./audio";
|
||||||
|
export default function load ()
|
||||||
|
{
|
||||||
|
let lastLocationPath: string | undefined;
|
||||||
|
const unsub = Router.history.subscribe((op) =>
|
||||||
|
{
|
||||||
|
if (op.action.type === 'PUSH')
|
||||||
|
{
|
||||||
|
lastLocationPath = op.location.pathname;
|
||||||
|
|
||||||
|
const routes = Router.matchRoutes(op.location.pathname);
|
||||||
|
const soundRoute = routes.find(r => r.staticData.enterSound !== undefined);
|
||||||
|
if (soundRoute)
|
||||||
|
{
|
||||||
|
if (soundRoute.staticData.enterSound) oneShot(soundRoute.staticData.enterSound);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
oneShot("openGeneric");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (op.action.type === 'BACK')
|
||||||
|
{
|
||||||
|
if (lastLocationPath)
|
||||||
|
{
|
||||||
|
const soundRoutes = Router.matchRoutes(lastLocationPath);
|
||||||
|
const soundRoute = soundRoutes.find(r => r.staticData.goBackSound !== undefined);
|
||||||
|
if (soundRoute)
|
||||||
|
{
|
||||||
|
if (soundRoute.staticData.goBackSound) oneShot(soundRoute.staticData.goBackSound);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
oneShot("returnGeneric");
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
oneShot("returnGeneric");
|
||||||
|
}
|
||||||
|
|
||||||
|
lastLocationPath = op.location.state.key;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let focusChangeDebounced: undefined | NodeJS.Timeout;
|
||||||
|
|
||||||
|
const focuschangedHandler = (e: CustomEvent<FocusEventDetails>) =>
|
||||||
|
{
|
||||||
|
clearTimeout(focusChangeDebounced);
|
||||||
|
if (!e.detail.focusKeyChanged) return;
|
||||||
|
|
||||||
|
if (e.detail.nativeEvent || e.detail.event)
|
||||||
|
{
|
||||||
|
let sound: keyof typeof soundMap;
|
||||||
|
if (e.detail.node && e.detail.node.matches('[data-sound-category="menu"]'))
|
||||||
|
{
|
||||||
|
sound = 'selectMenu';
|
||||||
|
|
||||||
|
} else if (e.detail.node && e.detail.node.matches('[data-sound-category="filter"]'))
|
||||||
|
{
|
||||||
|
sound = "selectFilter";
|
||||||
|
}
|
||||||
|
else if (e.detail.node && e.detail.node.matches('[data-sound-category="emulator"]'))
|
||||||
|
{
|
||||||
|
sound = "selectAlt";
|
||||||
|
}
|
||||||
|
else if (!e.detail.node || !e.detail.node.matches('[data-sound-disable="focus"]'))
|
||||||
|
{
|
||||||
|
sound = e.detail.sound as any ?? 'select';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() =>
|
||||||
|
{
|
||||||
|
if (e.detail.nativeEvent || e.detail.event)
|
||||||
|
{
|
||||||
|
oneShot(sound);
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('focuschanged', focuschangedHandler as any);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cleanup: () =>
|
||||||
|
{
|
||||||
|
unsub();
|
||||||
|
window.removeEventListener('focuschanged', focuschangedHandler as any);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { getCurrentFocusKey, navigateByDirection } from "@noriginmedia/norigin-s
|
||||||
import { GetFocusedElement } from "./spatialNavigation";
|
import { 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;
|
||||||
|
|
|
||||||
|
|
@ -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) =>
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
}
|
||||||
19
src/mainview/types.d.ts
vendored
19
src/mainview/types.d.ts
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
src/sounds/Classic UI SFX - Chords #1.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #10.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #10.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #11.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #11.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #12.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #12.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #13.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #13.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #14.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #14.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #15.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #15.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #16.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #16.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #17.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #17.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #18.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #18.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #19.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #19.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #2.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #2.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #20.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #20.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #3.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #3.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #4.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #4.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #5.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #5.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #6.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #6.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #7.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #7.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #8.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #8.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Chords #9.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Chords #9.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #1.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #1.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #10.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #10.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #11.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #11.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #12.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #12.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #13.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #13.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #14.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #14.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #15.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #15.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #16.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #16.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #17.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #17.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #18.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #18.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #19.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #19.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #2.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #2.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #20.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #20.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #21.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #21.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #22.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #22.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #23.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #23.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #24.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #24.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #25.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #25.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #3.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #3.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #4.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #4.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #5.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #5.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #6.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #6.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #7.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #7.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #8.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #8.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - High #9.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - High #9.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - Low #1.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - Low #1.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - Low #10.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - Low #10.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - Low #11.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - Low #11.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - Low #12.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - Low #12.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/sounds/Classic UI SFX - Short - Low #13.wav
(Stored with Git LFS)
Normal file
BIN
src/sounds/Classic UI SFX - Short - Low #13.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue