feat: move to secure OS credential storage so that you never get logged out again
This commit is contained in:
parent
d6e0a8350a
commit
ef08fa6114
15 changed files with 493 additions and 276 deletions
36
.vscode/launch.json
vendored
36
.vscode/launch.json
vendored
|
|
@ -4,35 +4,29 @@
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "bun",
|
"name": "Attach to Edge",
|
||||||
"internalConsoleOptions": "neverOpen",
|
"port": 9222,
|
||||||
"request": "launch",
|
"request": "attach",
|
||||||
"name": "Debug File",
|
"type": "msedge",
|
||||||
"program": "./src/bun/index.ts",
|
"webRoot": "${workspaceFolder}/src",
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
"stopOnEntry": false,
|
|
||||||
"watchMode": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "bun",
|
|
||||||
"internalConsoleOptions": "neverOpen",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Run File",
|
|
||||||
"program": "./src/bun/index.ts",
|
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
"noDebug": true,
|
|
||||||
"watchMode": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "bun",
|
"type": "bun",
|
||||||
"internalConsoleOptions": "neverOpen",
|
"internalConsoleOptions": "neverOpen",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"name": "Attach Bun",
|
"name": "Attach Bun",
|
||||||
"url": "ws://127.0.0.1:9229/nvnfnsqez8s",
|
"url": "ws://127.0.0.1:9229/7lt63qegtr8",
|
||||||
"localRoot": "${workspaceFolder}",
|
"localRoot": "${workspaceFolder}",
|
||||||
"stopOnEntry": false
|
"stopOnEntry": false,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Attach Debug App",
|
||||||
|
"configurations": ["Attach Bun", "Attach to Edge"],
|
||||||
|
"stopAll": true,
|
||||||
|
"preLaunchTask": "bun: dev"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ Focused on building a simple user experience and intuitive UI.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> This app is actively in development, it doesn't have most of its critical features implemented yet.
|
> This app is actively in development, it doesn't have most of its critical features implemented yet.
|
||||||
|
> It will have an opinionated design and will be used as an experiment in discovering a good UX.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
|
||||||
50
bun.lock
50
bun.lock
|
|
@ -9,16 +9,20 @@
|
||||||
"@elysiajs/cors": "^1.4.1",
|
"@elysiajs/cors": "^1.4.1",
|
||||||
"@elysiajs/eden": "^1.4.6",
|
"@elysiajs/eden": "^1.4.6",
|
||||||
"@elysiajs/static": "^1.4.7",
|
"@elysiajs/static": "^1.4.7",
|
||||||
|
"@hackolade/keytar": "^7.9.0-7",
|
||||||
"@rcompat/webview": "^0.18.0",
|
"@rcompat/webview": "^0.18.0",
|
||||||
"conf": "^15.0.2",
|
"conf": "^15.0.2",
|
||||||
"elysia": "^1.4.22",
|
"elysia": "^1.4.22",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
|
"tough-cookie": "^6.0.0",
|
||||||
|
"tough-cookie-file-store": "^3.3.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "^0.91.0",
|
"@hey-api/openapi-ts": "^0.91.0",
|
||||||
"@noriginmedia/norigin-spatial-navigation": "^2.3.0",
|
"@noriginmedia/norigin-spatial-navigation": "^2.3.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@tanstack/react-form": "^1.28.0",
|
||||||
"@tanstack/react-query": "^5.90.20",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
"@tanstack/react-query-devtools": "^5.91.3",
|
"@tanstack/react-query-devtools": "^5.91.3",
|
||||||
"@tanstack/react-router": "^1.157.16",
|
"@tanstack/react-router": "^1.157.16",
|
||||||
|
|
@ -159,6 +163,18 @@
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||||
|
|
||||||
|
"@hackolade/keytar": ["@hackolade/keytar@7.9.0-7", "", { "dependencies": { "@hackolade/keytar-darwin-arm64": "7.9.0-7", "@hackolade/keytar-darwin-x64": "7.9.0-7", "@hackolade/keytar-linux-arm64": "7.9.0-7", "@hackolade/keytar-linux-x64": "7.9.0-7", "@hackolade/keytar-win32-x64": "7.9.0-7" } }, "sha512-1U4Wfo3dbP63Dcl+SZyHgy3Q+sOdKzvZjuEu01BxDq4A/gtB/1e3Q9HouWUY37xdcTRSG1rD4iYirbQ79RN2iQ=="],
|
||||||
|
|
||||||
|
"@hackolade/keytar-darwin-arm64": ["@hackolade/keytar-darwin-arm64@7.9.0-7", "", {}, "sha512-A4rE3nAnjtJ0JaKfXoYLGDHky0JbIK2AqyZUcpSTIfVo28rBhqbfY8JPqEfgwibInKO1vjDUeizlhSOP0Q5RQA=="],
|
||||||
|
|
||||||
|
"@hackolade/keytar-darwin-x64": ["@hackolade/keytar-darwin-x64@7.9.0-7", "", {}, "sha512-xc/B1MrTD9cfxArBVto2dL9d9noRHgT/ZDZfBlfCEfb2pmbfg83WipDKxuu0nvL2Pzs2Ob26sBbFAxQE5djWIQ=="],
|
||||||
|
|
||||||
|
"@hackolade/keytar-linux-arm64": ["@hackolade/keytar-linux-arm64@7.9.0-7", "", {}, "sha512-G99cXS3li/mnW3qtncLAsDNPpx6Jqut1HnRgJVnO1RNotGdGU6EDcTog4pPHy7TVSwsc3QZ3Jay5wfJ0meXnSQ=="],
|
||||||
|
|
||||||
|
"@hackolade/keytar-linux-x64": ["@hackolade/keytar-linux-x64@7.9.0-7", "", {}, "sha512-Zx2e4aSbt3Ti0727GQMohlMqBOc7KElXOpZeW+F822U67CMckulJO0D4jcRKov5Kg3eO0nqCVT0GAnvLX6xtXw=="],
|
||||||
|
|
||||||
|
"@hackolade/keytar-win32-x64": ["@hackolade/keytar-win32-x64@7.9.0-7", "", {}, "sha512-tsSNLp5N8w7c3cauMxOpO5/ZVnEQ5TRDiAwZAQxI1TRJ3zerS7GmDbUhriZPU22d1qp4q9Fd+nQFhOe4NvJ8Lg=="],
|
||||||
|
|
||||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.6.0", "", { "dependencies": { "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-O6TRP3g1188gdpZC9yio64KkvWD97QpLjaCFnwTgykTZtzQXZbkyw+lYPVfZ7Ee+m7eLau1/jEJmQcUY9G0sLw=="],
|
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.6.0", "", { "dependencies": { "@hey-api/types": "0.1.3", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-O6TRP3g1188gdpZC9yio64KkvWD97QpLjaCFnwTgykTZtzQXZbkyw+lYPVfZ7Ee+m7eLau1/jEJmQcUY9G0sLw=="],
|
||||||
|
|
||||||
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-gRbyyTjzpFVNmbD+Upn3w4dWV+bCXGJbff3A7leDO/tfNxSz1xIb6Ad/5UKtvEW9kDt/2Uyc3XkFZ6rpafvbfQ=="],
|
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-gRbyyTjzpFVNmbD+Upn3w4dWV+bCXGJbff3A7leDO/tfNxSz1xIb6Ad/5UKtvEW9kDt/2Uyc3XkFZ6rpafvbfQ=="],
|
||||||
|
|
@ -179,6 +195,8 @@
|
||||||
|
|
||||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
|
||||||
|
|
||||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
@ -289,12 +307,20 @@
|
||||||
|
|
||||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
||||||
|
|
||||||
|
"@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="],
|
||||||
|
|
||||||
|
"@tanstack/form-core": ["@tanstack/form-core@1.28.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.7.7" } }, "sha512-MX3YveB6SKHAJ2yUwp+Ca/PCguub8bVEnLcLUbFLwdkSRMkP0lMGdaZl+F0JuEgZw56c6iFoRyfILhS7OQpydA=="],
|
||||||
|
|
||||||
"@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="],
|
"@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="],
|
||||||
|
|
||||||
|
"@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="],
|
||||||
|
|
||||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
||||||
|
|
||||||
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.93.0", "", {}, "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg=="],
|
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.93.0", "", {}, "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg=="],
|
||||||
|
|
||||||
|
"@tanstack/react-form": ["@tanstack/react-form@1.28.0", "", { "dependencies": { "@tanstack/form-core": "1.28.0", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-ibLcf5QkTogV0Ly944CuqGxWTpHyreNA4Cy8Wtky7zE9wtE3HVapQt4/hUuXo51zihfTkv5URiXpoTSKF5Xosg=="],
|
||||||
|
|
||||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="],
|
"@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="],
|
||||||
|
|
||||||
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.3", "", { "dependencies": { "@tanstack/query-devtools": "5.93.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.20", "react": "^18 || ^19" } }, "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA=="],
|
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.3", "", { "dependencies": { "@tanstack/query-devtools": "5.93.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.20", "react": "^18 || ^19" } }, "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA=="],
|
||||||
|
|
@ -319,7 +345,7 @@
|
||||||
|
|
||||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.154.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-61bGx32tMKuEpVRseu2sh1KQe8CfB7793Mch/kyQt0EP3tD7X0sXmimCl3truRiDGUtI0CaSoQV1NPjAII1RBA=="],
|
"@tanstack/router-utils": ["@tanstack/router-utils@1.154.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-61bGx32tMKuEpVRseu2sh1KQe8CfB7793Mch/kyQt0EP3tD7X0sXmimCl3truRiDGUtI0CaSoQV1NPjAII1RBA=="],
|
||||||
|
|
||||||
"@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
|
"@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="],
|
||||||
|
|
||||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.154.7", "", {}, "sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg=="],
|
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.154.7", "", {}, "sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg=="],
|
||||||
|
|
||||||
|
|
@ -391,6 +417,8 @@
|
||||||
|
|
||||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||||
|
|
||||||
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
@ -729,6 +757,8 @@
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
@ -753,6 +783,8 @@
|
||||||
|
|
||||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
||||||
|
|
||||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
"tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
|
"tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
|
||||||
|
|
@ -761,10 +793,18 @@
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"tldts": ["tldts@7.0.23", "", { "dependencies": { "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw=="],
|
||||||
|
|
||||||
|
"tldts-core": ["tldts-core@7.0.23", "", {}, "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ=="],
|
||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||||
|
|
||||||
|
"tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
|
||||||
|
|
||||||
|
"tough-cookie-file-store": ["tough-cookie-file-store@3.3.0", "", { "dependencies": { "tough-cookie": "^6.0.0" } }, "sha512-FbO/cOi/jp4wweo8soVNG/ZjDsgpBZWqaxWwu7gRKvsjg/Qt44kStp87VLfJnin749DlTbZDYvV1wuSr5jly2g=="],
|
||||||
|
|
||||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
@ -831,6 +871,10 @@
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
|
||||||
|
|
||||||
|
"@tanstack/router-core/@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
|
||||||
|
|
||||||
"@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
@ -853,8 +897,12 @@
|
||||||
|
|
||||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
|
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
"svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||||
|
|
||||||
|
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
export default {
|
|
||||||
app: {
|
|
||||||
name: "gameflow-deck",
|
|
||||||
identifier: "simeonradivoev.gameflow-deck.app",
|
|
||||||
version: "0.0.1",
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
// Vite builds to dist/, we copy from there
|
|
||||||
copy: {
|
|
||||||
"dist/index.html": "views/mainview/index.html",
|
|
||||||
"dist/assets": "views/mainview/assets",
|
|
||||||
},
|
|
||||||
mac: {
|
|
||||||
bundleCEF: false,
|
|
||||||
},
|
|
||||||
linux: {
|
|
||||||
bundleCEF: true,
|
|
||||||
},
|
|
||||||
win: {
|
|
||||||
bundleCEF: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"description": "Game Launcher",
|
"description": "Game Launcher",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run build:dev && NODE_ENV=development bun run --inspect=127.0.0.1:9229 --watch ./src/bun/index.ts",
|
"dev": "NODE_ENV=development bun run build && WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS='--remote-debugging-port=9222' bun run --inspect=127.0.0.1:9229 --watch ./src/bun/index.ts",
|
||||||
"dev:hmr": "PUBLIC_ACCESS=true conc -k 'bun run hmr' 'bun run dev'",
|
"dev:hmr": "PUBLIC_ACCESS=true conc -k 'bun run hmr' 'bun run dev'",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:pro": "NODE_ENV=production bun run build",
|
"build:pro": "NODE_ENV=production bun run build",
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"package:auto-prod": "bun run build:pro && NODE_ENV=production bun run ./scripts/package-bun.ts",
|
"package:auto-prod": "bun run build:pro && NODE_ENV=production bun run ./scripts/package-bun.ts",
|
||||||
"package:linux": "bun run build && TARGET=bun-linux-x64 bun run ./scripts/package-bun.ts",
|
"package:linux": "bun run build && TARGET=bun-linux-x64 bun run ./scripts/package-bun.ts",
|
||||||
"openapi-ts": "bun run ./scripts/romm/openapi-ts.ts",
|
"openapi-ts": "bun run ./scripts/romm/openapi-ts.ts",
|
||||||
"run:build-action": "act --artifact-server-path artifacts --env ACTIONS_RUNTIME_TOKEN=foo -W .github/workflows/build.yml",
|
"run:build-action": "act workflow_dispatch --artifact-server-path artifacts --env ACTIONS_RUNTIME_TOKEN=foo -W .github/workflows/build.yml",
|
||||||
"hmr": "vite --port 5173"
|
"hmr": "vite --port 5173"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -22,16 +22,20 @@
|
||||||
"@elysiajs/cors": "^1.4.1",
|
"@elysiajs/cors": "^1.4.1",
|
||||||
"@elysiajs/eden": "^1.4.6",
|
"@elysiajs/eden": "^1.4.6",
|
||||||
"@elysiajs/static": "^1.4.7",
|
"@elysiajs/static": "^1.4.7",
|
||||||
|
"@hackolade/keytar": "^7.9.0-7",
|
||||||
"@rcompat/webview": "^0.18.0",
|
"@rcompat/webview": "^0.18.0",
|
||||||
"conf": "^15.0.2",
|
"conf": "^15.0.2",
|
||||||
"elysia": "^1.4.22",
|
"elysia": "^1.4.22",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
|
"tough-cookie": "^6.0.0",
|
||||||
|
"tough-cookie-file-store": "^3.3.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "^0.91.0",
|
"@hey-api/openapi-ts": "^0.91.0",
|
||||||
"@noriginmedia/norigin-spatial-navigation": "^2.3.0",
|
"@noriginmedia/norigin-spatial-navigation": "^2.3.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@tanstack/react-form": "^1.28.0",
|
||||||
"@tanstack/react-query": "^5.90.20",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
"@tanstack/react-query-devtools": "^5.91.3",
|
"@tanstack/react-query-devtools": "^5.91.3",
|
||||||
"@tanstack/react-router": "^1.157.16",
|
"@tanstack/react-router": "^1.157.16",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,97 @@
|
||||||
|
import z from "zod";
|
||||||
import { config } from "./settings";
|
import { config } from "./settings";
|
||||||
import Elysia from "elysia";
|
import Elysia, { status } from "elysia";
|
||||||
|
import keytar from '@hackolade/keytar';
|
||||||
|
import { loginApiLoginPost } from "../../clients/romm";
|
||||||
|
import { CookieJar } from 'tough-cookie';
|
||||||
|
import FileCookieStore from 'tough-cookie-file-store';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json'));
|
||||||
|
console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath);
|
||||||
|
const jar = new CookieJar(fileCookieStore);
|
||||||
|
await login();
|
||||||
|
|
||||||
|
export async function logout ()
|
||||||
|
{
|
||||||
|
if (!config.has('rommAddress'))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rommAddress = config.get('rommAddress');
|
||||||
|
if (rommAddress)
|
||||||
|
{
|
||||||
|
console.log("Logging Out of ROMM");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await loginApiLoginPost({
|
||||||
|
baseUrl: rommAddress, headers: {
|
||||||
|
'cookie': await jar.getCookieString(rommAddress)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error)
|
||||||
|
{
|
||||||
|
console.error("Failed to logout of ROMM ", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login ()
|
||||||
|
{
|
||||||
|
if (!config.has('rommAddress') || !config.has('rommUser'))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rommAddress = config.get('rommAddress');
|
||||||
|
const rommUser = config.get('rommUser');
|
||||||
|
if (rommAddress && rommUser)
|
||||||
|
{
|
||||||
|
console.log("Logging In to ROMM");
|
||||||
|
const password = await keytar.getPassword('romm', 'gameflow');
|
||||||
|
const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` });
|
||||||
|
loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const romm = new Elysia({ prefix: "/romm" })
|
export const romm = new Elysia({ prefix: "/romm" })
|
||||||
|
.post('/login', async ({ body: { host, username, password } }) =>
|
||||||
|
{
|
||||||
|
if (config.has('rommAddress') && config.has('rommUser'))
|
||||||
|
{
|
||||||
|
await logout();
|
||||||
|
const oldRommAddress = config.get('rommAddress');
|
||||||
|
if (oldRommAddress)
|
||||||
|
{
|
||||||
|
const cookies = await jar.getCookies(oldRommAddress);
|
||||||
|
cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.set('rommAddress', host);
|
||||||
|
config.set('rommUser', username);
|
||||||
|
|
||||||
|
await keytar.setPassword('romm', 'gameflow', password);
|
||||||
|
await login();
|
||||||
|
|
||||||
|
return status(200);
|
||||||
|
}, { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) })
|
||||||
|
.get('/login', async () =>
|
||||||
|
{
|
||||||
|
const credentials = await keytar.getPassword('romm', 'gameflow');
|
||||||
|
return { hasPassword: !!credentials };
|
||||||
|
}, { response: z.object({ hasPassword: z.boolean() }) })
|
||||||
|
.post('/logout', async () =>
|
||||||
|
{
|
||||||
|
await keytar.deletePassword('romm', 'gameflow');
|
||||||
|
await logout();
|
||||||
|
const rommAddress = config.get('rommAddress');
|
||||||
|
if (rommAddress)
|
||||||
|
{
|
||||||
|
const cookies = await jar.getCookies(rommAddress);
|
||||||
|
cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key));
|
||||||
|
}
|
||||||
|
return status(200);
|
||||||
|
})
|
||||||
.all("/*", async ({ request, params, set }) =>
|
.all("/*", async ({ request, params, set }) =>
|
||||||
{
|
{
|
||||||
if (!config.has('rommAddress') && !config.get('rommAddress'))
|
if (!config.has('rommAddress') && !config.get('rommAddress'))
|
||||||
|
|
@ -20,14 +110,28 @@ export const romm = new Elysia({ prefix: "/romm" })
|
||||||
const headers = new Headers(request.headers);
|
const headers = new Headers(request.headers);
|
||||||
headers.delete('host');
|
headers.delete('host');
|
||||||
headers.set("accept-encoding", "identity");
|
headers.set("accept-encoding", "identity");
|
||||||
|
headers.set('cookie', await jar.getCookieString(rommUrl.href));
|
||||||
|
|
||||||
const rommResponse = await fetch(url, {
|
let rommResponse = await fetch(url, {
|
||||||
method: request.method,
|
method: request.method,
|
||||||
headers,
|
headers,
|
||||||
body: await request.arrayBuffer(),
|
body: await request.arrayBuffer(),
|
||||||
redirect: 'manual', // avoid ROMM redirects
|
redirect: 'manual', // avoid ROMM redirects
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (rommResponse.status === 403 && config.has('rommUser'))
|
||||||
|
{
|
||||||
|
await login();
|
||||||
|
headers.set('cookie', await jar.getCookieString(rommUrl.href));
|
||||||
|
rommResponse = await fetch(url, {
|
||||||
|
method: request.method,
|
||||||
|
headers,
|
||||||
|
body: await request.arrayBuffer(),
|
||||||
|
redirect: 'manual', // avoid ROMM redirects
|
||||||
|
});
|
||||||
|
}*/
|
||||||
|
|
||||||
set.status = rommResponse.status;
|
set.status = rommResponse.status;
|
||||||
rommResponse.headers.forEach((value, key) =>
|
rommResponse.headers.forEach((value, key) =>
|
||||||
{
|
{
|
||||||
|
|
@ -35,4 +139,4 @@ export const romm = new Elysia({ prefix: "/romm" })
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Response(rommResponse.body, { status: rommResponse.status });
|
return new Response(rommResponse.body, { status: rommResponse.status });
|
||||||
});
|
}).on('stop', logout);
|
||||||
|
|
@ -11,10 +11,10 @@ if (!Bun.env.PUBLIC_ACCESS)
|
||||||
bunServer = RunBunServer();
|
bunServer = RunBunServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanup ()
|
async function cleanup ()
|
||||||
{
|
{
|
||||||
bunServer?.stop();
|
bunServer?.stop();
|
||||||
api.apiServer.stop();
|
await api.apiServer.stop();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ try
|
||||||
});
|
});
|
||||||
webviewWorker.addEventListener('error', console.error);
|
webviewWorker.addEventListener('error', console.error);
|
||||||
await new Promise(resolve => webviewWorker.addEventListener('close', resolve));
|
await new Promise(resolve => webviewWorker.addEventListener('close', resolve));
|
||||||
cleanup();
|
await cleanup();
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { SERVER_PORT } from "../shared/constants";
|
import { SERVER_PORT } from "../shared/constants";
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { host } from "./utils";
|
import { host } from "./utils";
|
||||||
|
import appInfo from '../../package.json';
|
||||||
|
|
||||||
export function RunBunServer ()
|
export function RunBunServer ()
|
||||||
{
|
{
|
||||||
|
|
@ -12,6 +13,19 @@ export function RunBunServer ()
|
||||||
"/": Bun.file("./dist/index.html"),
|
"/": Bun.file("./dist/index.html"),
|
||||||
// Serve a file by lazily loading it into memory
|
// Serve a file by lazily loading it into memory
|
||||||
"/favicon.ico": Bun.file("./dist/favicon.ico"),
|
"/favicon.ico": Bun.file("./dist/favicon.ico"),
|
||||||
|
"/.well-known/appspecific/com.chrome.devtools.json": new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
name: appInfo.name,
|
||||||
|
version: appInfo.version,
|
||||||
|
debuggable: true,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
fetch: async (req) =>
|
fetch: async (req) =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
49
src/mainview/components/options/OptionInput.tsx
Normal file
49
src/mainview/components/options/OptionInput.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { ChangeEventHandler, FocusEventHandler, HTMLInputTypeAttribute, JSX, useRef } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { useOptionContext } from "./OptionSpace";
|
||||||
|
|
||||||
|
export function OptionInput (data: {
|
||||||
|
name: string;
|
||||||
|
type: HTMLInputTypeAttribute;
|
||||||
|
className?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
value?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||||
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const option = useOptionContext({
|
||||||
|
onOptionEnterPress ()
|
||||||
|
{
|
||||||
|
inputRef.current?.focus();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className="flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent">
|
||||||
|
<span className={twMerge("text-base-content/80", classNames({
|
||||||
|
"text-primary-content": option.focused
|
||||||
|
}))}>{data.icon}</span>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
id={data.name}
|
||||||
|
name={data.name}
|
||||||
|
value={data.value}
|
||||||
|
defaultValue={data.defaultValue}
|
||||||
|
type={data.type}
|
||||||
|
onFocus={() => option.focus()}
|
||||||
|
placeholder={data.placeholder}
|
||||||
|
onChange={data.onChange}
|
||||||
|
onBlur={data.onBlur}
|
||||||
|
className={twMerge(
|
||||||
|
"input grow rounded-full ring-primary-content focus:ring-3",
|
||||||
|
data.className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
src/mainview/components/options/OptionSpace.tsx
Normal file
89
src/mainview/components/options/OptionSpace.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { FocusContext, FocusDetails, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { createContext, JSX, useContext, useEffect, useMemo } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export const OptionContext = createContext(
|
||||||
|
{} as {
|
||||||
|
focused: boolean;
|
||||||
|
focus: (focusDetails?: FocusDetails | undefined) => void;
|
||||||
|
eventTarget: EventTarget;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export function useOptionContext (params?: { onOptionEnterPress?: () => void; })
|
||||||
|
{
|
||||||
|
const context = useContext(OptionContext);
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if (params?.onOptionEnterPress)
|
||||||
|
{
|
||||||
|
context.eventTarget.addEventListener(
|
||||||
|
"onEnterPress",
|
||||||
|
params.onOptionEnterPress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
if (params?.onOptionEnterPress)
|
||||||
|
{
|
||||||
|
context.eventTarget.removeEventListener(
|
||||||
|
"onEnterPress",
|
||||||
|
params.onOptionEnterPress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [context.eventTarget]);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OptionSpace (data: {
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
focusable?: boolean;
|
||||||
|
children: JSX.Element;
|
||||||
|
label?: string | JSX.Element;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
const eventTarget = useMemo(() => new EventTarget(), []);
|
||||||
|
const { ref, focused, focusSelf, focusKey, hasFocusedChild } = useFocusable({
|
||||||
|
focusKey: data.id,
|
||||||
|
focusable: data.focusable !== false,
|
||||||
|
trackChildren: true,
|
||||||
|
onEnterPress ()
|
||||||
|
{
|
||||||
|
eventTarget.dispatchEvent(new CustomEvent("onEnterPress"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<FocusContext value={focusKey}>
|
||||||
|
<OptionContext value={{ focused, focus: focusSelf, eventTarget }}>
|
||||||
|
<li
|
||||||
|
ref={ref}
|
||||||
|
className={twMerge("flex sm:p-2 md:p-4 pl-8! rounded-full bg-base-content/1", classNames(
|
||||||
|
{
|
||||||
|
"text-primary-content bg-primary ": focused || hasFocusedChild,
|
||||||
|
}),
|
||||||
|
data.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="label flex-1 md:text-lg pr-4">
|
||||||
|
{typeof data.label === "string" ? (
|
||||||
|
<label
|
||||||
|
className={classNames({
|
||||||
|
"text-primary-content font-semibold": focused,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{data.label}
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
data.label
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{data.children}
|
||||||
|
</li>
|
||||||
|
</OptionContext>
|
||||||
|
</FocusContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/mainview/components/options/SettingsAppForm.tsx
Normal file
38
src/mainview/components/options/SettingsAppForm.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
|
||||||
|
import { HTMLInputTypeAttribute, JSX } from "react";
|
||||||
|
import { OptionInput } from "./OptionInput";
|
||||||
|
import { OptionSpace } from "./OptionSpace";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { TriangleAlert } from "lucide-react";
|
||||||
|
|
||||||
|
// export useFieldContext for use in your custom components
|
||||||
|
export const { fieldContext, formContext, useFieldContext } =
|
||||||
|
createFormHookContexts();
|
||||||
|
|
||||||
|
export const { useAppForm: useSettingsForm, useTypedAppFormContext: useSettingsFormContext } = createFormHook({
|
||||||
|
fieldContext,
|
||||||
|
formContext,
|
||||||
|
fieldComponents: { FormOption },
|
||||||
|
formComponents: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
function FormOption (data: { type: HTMLInputTypeAttribute, icon?: JSX.Element; label?: string | JSX.Element; placeholder?: string; })
|
||||||
|
{
|
||||||
|
const field = useFieldContext<string>();
|
||||||
|
return <OptionSpace label={<div className="flex gap-2">
|
||||||
|
{data.label}
|
||||||
|
{field.getMeta().errors.length > 0 && <div className="badge badge-error">
|
||||||
|
{field.state.meta.errors.map(e => e.message).join(',')}
|
||||||
|
</div>}
|
||||||
|
</div>}>
|
||||||
|
<OptionInput
|
||||||
|
icon={data.icon}
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
type={data.type}
|
||||||
|
onChange={e => field.handleChange(e.target.value)}
|
||||||
|
placeholder={data.placeholder}
|
||||||
|
className={classNames({ "ring-4 ring-accent": field.getMeta().isDirty })}
|
||||||
|
/>
|
||||||
|
</OptionSpace>;;
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ function RootComponent ()
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden">
|
<div className="w-screen h-screen overflow-hidden">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
{import.meta.env.DEV && false &&
|
{import.meta.env.DEV &&
|
||||||
<>
|
<>
|
||||||
<TanStackRouterDevtools position="top-left" />
|
<TanStackRouterDevtools position="top-left" />
|
||||||
<ReactQueryDevtools buttonPosition="top-right" />
|
<ReactQueryDevtools buttonPosition="top-right" />
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ import { SaveSource } from "../scripts/spatialNavigation";
|
||||||
import LoadingCardList from "../components/LoadingCardList";
|
import LoadingCardList from "../components/LoadingCardList";
|
||||||
import { AutoFocus } from "../components/AutoFocus";
|
import { AutoFocus } from "../components/AutoFocus";
|
||||||
import SaveScroll from "../components/SaveScroll";
|
import SaveScroll from "../components/SaveScroll";
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import Shortcuts from "../components/Shortcuts";
|
import Shortcuts from "../components/Shortcuts";
|
||||||
|
|
||||||
|
|
@ -157,6 +157,15 @@ function CollectionList (data: { id: string, setBackground: (url: string) => voi
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HomeListError (data: { focused: boolean; })
|
||||||
|
{
|
||||||
|
const error = useErrorBoundary();
|
||||||
|
return <div className="flex justify-center items-center h-(--game-card-height)"><div role="alert" className={twMerge("alert alert-error", classNames({ "alert-outline": !data.focused }))}>
|
||||||
|
<OctagonAlert />
|
||||||
|
<span>{(error.error as any).detail}</span>
|
||||||
|
</div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
function HomeList (data: {
|
function HomeList (data: {
|
||||||
selectedFilter: keyof typeof filters;
|
selectedFilter: keyof typeof filters;
|
||||||
})
|
})
|
||||||
|
|
@ -176,14 +185,9 @@ function HomeList (data: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
<div ref={ref} className="flex overflow-x-scroll no-scrollbar pb-3 mb-1">
|
<div ref={ref} className="flex overflow-x-scroll no-scrollbar pb-3 mb-1 justify-center-safe">
|
||||||
<div className="flex px-16">
|
<div className="flex px-16">
|
||||||
<ErrorBoundary fallback={
|
<ErrorBoundary fallback={<HomeListError focused={focused} />}>
|
||||||
<div role="alert" className="alert alert-error alert-outline">
|
|
||||||
<OctagonAlert />
|
|
||||||
<span>Error! Task failed successfully.</span>
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<Suspense key={data.selectedFilter} fallback={<LoadingCardList placeholderCount={8} />}>
|
<Suspense key={data.selectedFilter} fallback={<LoadingCardList placeholderCount={8} />}>
|
||||||
{lists[data.selectedFilter]}
|
{lists[data.selectedFilter]}
|
||||||
<SaveScroll id={`card-list-${data.selectedFilter}`} ref={ref} />
|
<SaveScroll id={`card-list-${data.selectedFilter}`} ref={ref} />
|
||||||
|
|
|
||||||
|
|
@ -1,171 +1,39 @@
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
FocusContext,
|
FocusContext,
|
||||||
FocusDetails,
|
|
||||||
useFocusable,
|
useFocusable,
|
||||||
} from "@noriginmedia/norigin-spatial-navigation";
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { QueriesResults, useIsMutating, useMutation, useQuery, UseQueryResult } from "@tanstack/react-query";
|
import { useIsMutating, useMutation, useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||||
import { createFileRoute, useSearch } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { DoorOpen, Key, Link, Lock, User } from "lucide-react";
|
import { Cross, Delete, Key, Link, Lock, Save, Trash, User, X } from "lucide-react";
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
ChangeEventHandler,
|
|
||||||
createContext,
|
|
||||||
FocusEventHandler,
|
|
||||||
HTMLInputTypeAttribute,
|
HTMLInputTypeAttribute,
|
||||||
JSX,
|
JSX,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { client } from "../..";
|
import { client } from "../..";
|
||||||
import { SettingsType } from "../../../shared/constants";
|
import { RPC_URL, SettingsType } from "../../../shared/constants";
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
getCurrentUserApiUsersMeGetOptions,
|
getCurrentUserApiUsersMeGetOptions,
|
||||||
loginApiLoginPostMutation,
|
|
||||||
logoutApiLogoutPostMutation,
|
|
||||||
statsApiStatsGetOptions,
|
statsApiStatsGetOptions,
|
||||||
} from "../../../clients/romm/@tanstack/react-query.gen";
|
} from "../../../clients/romm/@tanstack/react-query.gen";
|
||||||
import { useToasters } from "../../contexts/ToasterContext";
|
|
||||||
import { UserSchema } from "../../../clients/romm";
|
import { UserSchema } from "../../../clients/romm";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import z from "zod";
|
||||||
|
import { OptionSpace } from "../../components/options/OptionSpace";
|
||||||
|
import { OptionInput } from "../../components/options/OptionInput";
|
||||||
|
import { useSettingsForm, useSettingsFormContext } from "../../components/options/SettingsAppForm";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings/accounts")({
|
export const Route = createFileRoute("/settings/accounts")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
const OptionContext = createContext(
|
|
||||||
{} as {
|
|
||||||
focused: boolean;
|
|
||||||
focus: (focusDetails?: FocusDetails | undefined) => void;
|
|
||||||
eventTarget: EventTarget;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function useOptionContext (params?: { onOptionEnterPress?: () => void; })
|
|
||||||
{
|
|
||||||
const context = useContext(OptionContext);
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
if (params?.onOptionEnterPress)
|
|
||||||
{
|
|
||||||
context.eventTarget.addEventListener(
|
|
||||||
"onEnterPress",
|
|
||||||
params.onOptionEnterPress,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () =>
|
|
||||||
{
|
|
||||||
if (params?.onOptionEnterPress)
|
|
||||||
{
|
|
||||||
context.eventTarget.removeEventListener(
|
|
||||||
"onEnterPress",
|
|
||||||
params.onOptionEnterPress,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [context.eventTarget]);
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
function OptionSpace (data: {
|
|
||||||
id?: string;
|
|
||||||
className?: string;
|
|
||||||
focusable?: boolean;
|
|
||||||
children: JSX.Element;
|
|
||||||
label?: string | JSX.Element;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
const eventTarget = useMemo(() => new EventTarget(), []);
|
|
||||||
const { ref, focused, focusSelf, focusKey, hasFocusedChild } = useFocusable({
|
|
||||||
focusKey: data.id,
|
|
||||||
focusable: data.focusable !== false,
|
|
||||||
trackChildren: true,
|
|
||||||
onEnterPress ()
|
|
||||||
{
|
|
||||||
eventTarget.dispatchEvent(new CustomEvent("onEnterPress"));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (<FocusContext value={focusKey}>
|
|
||||||
<OptionContext value={{ focused, focus: focusSelf, eventTarget }}>
|
|
||||||
<li
|
|
||||||
ref={ref}
|
|
||||||
className={twMerge("flex sm:p-2 md:p-4 pl-8! rounded-full bg-base-content/1", classNames(
|
|
||||||
{
|
|
||||||
"text-primary-content bg-primary ": focused || hasFocusedChild,
|
|
||||||
}),
|
|
||||||
data.className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{typeof data.label === "string" ? (
|
|
||||||
<label
|
|
||||||
className={classNames("label flex-1 md:text-lg pr-4", {
|
|
||||||
"text-primary-content font-semibold": focused,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{data.label}
|
|
||||||
</label>
|
|
||||||
) : (
|
|
||||||
data.label
|
|
||||||
)}
|
|
||||||
{data.children}
|
|
||||||
</li>
|
|
||||||
</OptionContext>
|
|
||||||
</FocusContext>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function OptionInput (data: {
|
|
||||||
name: string;
|
|
||||||
type: HTMLInputTypeAttribute;
|
|
||||||
className?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
icon?: JSX.Element;
|
|
||||||
value?: string;
|
|
||||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
|
||||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
|
||||||
})
|
|
||||||
{
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const option = useOptionContext({
|
|
||||||
onOptionEnterPress ()
|
|
||||||
{
|
|
||||||
inputRef.current?.focus();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label className="flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent">
|
|
||||||
<span className={twMerge("text-base-content/80", classNames({
|
|
||||||
"text-primary-content": option.focused
|
|
||||||
}))}>{data.icon}</span>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
id={data.name}
|
|
||||||
name={data.name}
|
|
||||||
value={data.value}
|
|
||||||
type={data.type}
|
|
||||||
onFocus={() => option.focus()}
|
|
||||||
placeholder={data.placeholder}
|
|
||||||
onChange={data.onChange}
|
|
||||||
onBlur={data.onBlur}
|
|
||||||
className={classNames(
|
|
||||||
"input grow rounded-full ring-primary-content focus:ring-3",
|
|
||||||
data.className,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeysWithValueAssignableTo<T, Value> = {
|
type KeysWithValueAssignableTo<T, Value> = {
|
||||||
[K in keyof T]: Exclude<T[K], undefined> extends Value ? K : never;
|
[K in keyof T]: Exclude<T[K], undefined> extends Value ? K : never;
|
||||||
}[keyof T];
|
}[keyof T];
|
||||||
|
|
@ -185,7 +53,7 @@ function Option (data: {
|
||||||
queryKey: ["setting", data.id],
|
queryKey: ["setting", data.id],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
{
|
{
|
||||||
const value = (await client.api.settings({ id: data.id! }).get()).data?.value;
|
const value = await client.api.settings({ id: data.id! }).get().then(d => d.data?.value);
|
||||||
if (!dirty)
|
if (!dirty)
|
||||||
{
|
{
|
||||||
setLocalValue(String(value));
|
setLocalValue(String(value));
|
||||||
|
|
@ -227,72 +95,109 @@ function Option (data: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Button (data: { children?: any, disabled?: boolean, type: "reset" | "button" | "submit" | undefined; } & InteractParams & FocusParams)
|
function Button (data: { children?: any, className?: string, disabled?: boolean, type: "reset" | "button" | "submit" | undefined; } & InteractParams & FocusParams)
|
||||||
{
|
{
|
||||||
const { ref, focused } = useFocusable({
|
const { ref, focused } = useFocusable({
|
||||||
focusKey: data.type,
|
focusKey: data.type,
|
||||||
onEnterPress: data.onAction,
|
onEnterPress: data.onAction,
|
||||||
onFocus: data.onFocus
|
onFocus: data.onFocus,
|
||||||
|
focusable: !data.disabled
|
||||||
});
|
});
|
||||||
return <button
|
return <button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={data.onAction}
|
onClick={data.onAction}
|
||||||
disabled={data.disabled}
|
disabled={data.disabled}
|
||||||
className={classNames("btn rounded-full focus:bg-base-content focus:text-base-300 md:text-lg", {
|
className={twMerge("btn rounded-full focus:bg-base-content focus:text-base-300 md:text-lg", classNames({
|
||||||
"btn-accent": focused
|
"btn-accent": focused
|
||||||
})}
|
}, data.className))}
|
||||||
type={data.type}
|
type={data.type}
|
||||||
>
|
>
|
||||||
{data.children}
|
{data.children}
|
||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoginControls (data: { user: UseQueryResult<UserSchema | null, Error>; })
|
function LoginControls (data: { hasPassword: boolean; })
|
||||||
{
|
{
|
||||||
|
const user = useQuery({
|
||||||
|
...getCurrentUserApiUsersMeGetOptions(),
|
||||||
|
queryKey: ['romm', 'auth', "login"],
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 0
|
||||||
|
});
|
||||||
|
const context = useSettingsFormContext({});
|
||||||
|
context.state.canSubmit;
|
||||||
const isMutatingRomm = useIsMutating({ mutationKey: ["romm", "auth"] }) > 0;
|
const isMutatingRomm = useIsMutating({ mutationKey: ["romm", "auth"] }) > 0;
|
||||||
const logoutMutation = useMutation({
|
const logoutMutation = useMutation({
|
||||||
mutationKey: ["romm", "auth", "logout"], mutationFn: () => window.cookieStore.delete({ name: "romm_session" }),
|
mutationKey: ["romm", "auth", "logout"], mutationFn: () => client.api.romm.logout.post(),
|
||||||
onSuccess: async (d, v, r, c) =>
|
onSuccess: async (d, v, r, c) =>
|
||||||
{
|
{
|
||||||
c.client.invalidateQueries({ queryKey: ["romm", "auth"] });
|
c.client.invalidateQueries({ queryKey: ["romm", "auth"] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return <div className="flex gap-2 items-center">
|
return <div className="flex gap-2 items-center">
|
||||||
{data.user.isError && <div className="badge badge-error gap-2 tooltip" data-tip={(data.user.error as any)?.detail ?? ''}>
|
{user.isError && <div className="badge badge-error gap-2 tooltip" data-tip={(user.error as any)?.detail ?? ''}>
|
||||||
<Lock className="size-4" /></div>}
|
<Lock className="size-4" /></div>}
|
||||||
{data.user.isSuccess && <div className="badge badge-success badge-lg rounded-full gap-2">Logged In As: <b>{data.user.data?.username}</b></div>}
|
{user.isSuccess && <>
|
||||||
<Button disabled={isMutatingRomm} type="submit" >
|
<div className="badge badge-success badge-lg rounded-full gap-2"> Logged In As: <img className="size-6 rounded-full" src={`${RPC_URL(__HOST__)}/api/romm/assets/romm/assets/${user.data?.avatar_path}`} /><b>{user.data?.username}</b></div>
|
||||||
<Lock /> Login
|
</>}
|
||||||
|
<Button disabled={!context.state.canSubmit || !context.state.isDirty} type="submit" onAction={() => context.handleSubmit()} >
|
||||||
|
<Save /> Save
|
||||||
</Button>
|
</Button>
|
||||||
|
{data.hasPassword &&
|
||||||
<Button onAction={() =>
|
<Button onAction={() =>
|
||||||
{
|
{
|
||||||
toast("Logout", { id: 'romm-logout-noti' });
|
toast("Logout", { id: 'romm-logout-noti' });
|
||||||
logoutMutation.mutate();
|
logoutMutation.mutate();
|
||||||
}} disabled={isMutatingRomm} type="button" >
|
}} disabled={isMutatingRomm} type="button" >
|
||||||
<DoorOpen /> Logout
|
<Trash /> Forget
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<Button disabled={context.state.isDefaultValue} type="reset" onAction={() => context.reset()}>
|
||||||
|
<X /> Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dataSchema = z.object({ hostname: z.url(), username: z.string(), password: z.string() });
|
||||||
|
|
||||||
function RouteComponent ()
|
function RouteComponent ()
|
||||||
{
|
{
|
||||||
const { focus } = Route.useSearch();
|
const { focus } = Route.useSearch();
|
||||||
const { ref, focusKey, focusSelf } = useFocusable({
|
const { ref, focusKey, focusSelf } = useFocusable({
|
||||||
preferredChildFocusKey: focus
|
preferredChildFocusKey: focus
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: hasPassword } = useQuery({ queryKey: ['romm', 'auth', 'passLength'], queryFn: () => client.api.romm.login.get().then(d => d.data?.hasPassword as boolean) });
|
||||||
|
const { data: hostname } = useQuery({ queryKey: ['romm', 'auth', 'hostname'], queryFn: () => client.api.settings({ id: 'rommAddress' }).get().then(d => d.data?.value as string) });
|
||||||
|
const { data: username } = useQuery({ queryKey: ['romm', 'auth', 'username'], queryFn: () => client.api.settings({ id: 'rommUser' }).get().then(d => d.data?.value as string) });
|
||||||
|
|
||||||
|
|
||||||
|
const loginForm = useSettingsForm({
|
||||||
|
defaultValues: {
|
||||||
|
hostname: hostname ?? '',
|
||||||
|
username: username ?? '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) =>
|
||||||
|
{
|
||||||
|
await toast.promise(loginMutation.mutateAsync(value), {
|
||||||
|
loading: "Logging In",
|
||||||
|
success: "Logged In",
|
||||||
|
error: e => e?.detail ?? "Error Logging In",
|
||||||
|
});
|
||||||
|
loginForm.reset();
|
||||||
|
},
|
||||||
|
validators: {
|
||||||
|
onChange: dataSchema
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const rommOnline = useQuery({
|
const rommOnline = useQuery({
|
||||||
...statsApiStatsGetOptions(),
|
...statsApiStatsGetOptions(),
|
||||||
refetchInterval: 30000,
|
refetchInterval: 30000,
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = useQuery({
|
|
||||||
...getCurrentUserApiUsersMeGetOptions(),
|
|
||||||
queryKey: ['romm', 'auth', "login"],
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
retry: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (focus)
|
if (focus)
|
||||||
|
|
@ -303,7 +208,10 @@ function RouteComponent ()
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
const loginMutation = useMutation({
|
||||||
mutationKey: ["romm", "login"],
|
mutationKey: ["romm", "login"],
|
||||||
...loginApiLoginPostMutation(),
|
mutationFn: (data: z.infer<typeof dataSchema>) =>
|
||||||
|
{
|
||||||
|
return client.api.romm.login.post({ username: data.username, password: data.password, host: data.hostname });
|
||||||
|
},
|
||||||
onSuccess: (d, v, r, c) =>
|
onSuccess: (d, v, r, c) =>
|
||||||
{
|
{
|
||||||
c.client.invalidateQueries({ queryKey: ['romm', 'auth'] });
|
c.client.invalidateQueries({ queryKey: ['romm', 'auth'] });
|
||||||
|
|
@ -331,54 +239,40 @@ function RouteComponent ()
|
||||||
<h3>Romm</h3>
|
<h3>Romm</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Option
|
<loginForm.AppForm>
|
||||||
id="rommAddress"
|
|
||||||
type="text"
|
|
||||||
icon={
|
|
||||||
<div className="indicator">
|
|
||||||
<span
|
|
||||||
className={classNames("indicator-item status", indicator)}
|
|
||||||
></span>
|
|
||||||
<Link />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
label="Romm Address"
|
|
||||||
/>
|
|
||||||
<form
|
<form
|
||||||
className="flex flex-col gap-2"
|
className="flex flex-col gap-2"
|
||||||
onSubmit={(e) =>
|
onSubmit={(e) =>
|
||||||
{
|
{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = new FormData(e.currentTarget);
|
e.stopPropagation();
|
||||||
toast.promise(loginMutation.mutateAsync({
|
loginForm.handleSubmit();
|
||||||
auth: `${data.get("username")}:${data.get("password")}`,
|
}}
|
||||||
}), {
|
onReset={e =>
|
||||||
loading: "Logging In",
|
{
|
||||||
success: "Logged In",
|
e.preventDefault();
|
||||||
error: e => e?.detail ?? "Error Logging In",
|
e.stopPropagation();
|
||||||
});
|
loginForm.reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<OptionSpace label="User">
|
<loginForm.AppField name="hostname" children={(field) =>
|
||||||
<OptionInput
|
<field.FormOption label="Romm Address" icon={<div className="indicator">
|
||||||
icon={<User />}
|
<span
|
||||||
name="username"
|
className={classNames("indicator-item status", indicator)}
|
||||||
type="text"
|
></span>
|
||||||
placeholder="Username"
|
<Link />
|
||||||
/>
|
</div>
|
||||||
</OptionSpace>
|
} type='url' />} />
|
||||||
<OptionSpace label="Password">
|
<loginForm.AppField name="username" children={(field) =>
|
||||||
<OptionInput
|
<field.FormOption label={"Romm Username"} icon={<User />} type="text" />} />
|
||||||
icon={<Key />}
|
<loginForm.AppField name="password" children={(field) =>
|
||||||
name="password"
|
<field.FormOption label={"Romm Password"} icon={<Key />} type="password" placeholder={hasPassword ? '*****' : "Password"} />} />
|
||||||
type="password"
|
<loginForm.Subscribe children={(form) =>
|
||||||
placeholder="Password"
|
|
||||||
/>
|
|
||||||
</OptionSpace>
|
|
||||||
<OptionSpace className="justify-end">
|
<OptionSpace className="justify-end">
|
||||||
<LoginControls user={user} />
|
<LoginControls hasPassword={hasPassword === true} />
|
||||||
</OptionSpace>
|
</OptionSpace>} />
|
||||||
</form>
|
</form>
|
||||||
|
</loginForm.AppForm>
|
||||||
</ul>
|
</ul>
|
||||||
</FocusContext.Provider>
|
</FocusContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export type GameMeta = z.infer<typeof GameMetaSchema>;
|
||||||
|
|
||||||
export const SettingsSchema = z.object({
|
export const SettingsSchema = z.object({
|
||||||
rommAddress: z.url().optional(),
|
rommAddress: z.url().optional(),
|
||||||
|
rommUser: z.string().default('admin').optional(),
|
||||||
disableBlur: z.boolean().default(false),
|
disableBlur: z.boolean().default(false),
|
||||||
windowSize: z.object({ width: z.number(), height: z.number() }).default({ width: 1280, height: 800 }),
|
windowSize: z.object({ width: z.number(), height: z.number() }).default({ width: 1280, height: 800 }),
|
||||||
windowPosition: z.object({ x: z.number(), y: z.number() }).optional(),
|
windowPosition: z.object({ x: z.number(), y: z.number() }).optional(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue