feat: Added QR login
fix: Fixed webview for windows builds
This commit is contained in:
parent
01b91aa48c
commit
4739b89933
26 changed files with 545 additions and 83 deletions
27
bun.lock
27
bun.lock
|
|
@ -9,7 +9,6 @@
|
||||||
"@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",
|
||||||
"@rcompat/webview": "^0.18.0",
|
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"conf": "^15.0.2",
|
"conf": "^15.0.2",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
|
@ -26,6 +25,7 @@
|
||||||
"tough-cookie": "^6.0.0",
|
"tough-cookie": "^6.0.0",
|
||||||
"tough-cookie-file-store": "^3.3.0",
|
"tough-cookie-file-store": "^3.3.0",
|
||||||
"unzip-stream": "^0.3.4",
|
"unzip-stream": "^0.3.4",
|
||||||
|
"webview-bun": "^2.4.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-error-boundary": "^6.1.0",
|
"react-error-boundary": "^6.1.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
|
"react-qr-code": "^2.0.18",
|
||||||
"sass-embedded": "^1.97.3",
|
"sass-embedded": "^1.97.3",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
|
|
@ -321,16 +322,6 @@
|
||||||
|
|
||||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
||||||
|
|
||||||
"@rcompat/assert": ["@rcompat/assert@0.6.0", "", { "dependencies": { "@rcompat/is": "^0.4.0", "@rcompat/type": "^0.9.0" } }, "sha512-V8YrttJqBNsLo9DQkXGpPt09LqfUGvwA30q8Tf+uukEhE6Nraw4jM4Nq0c5yKQF0IWv1eSoPUJyCO4W4neS5IA=="],
|
|
||||||
|
|
||||||
"@rcompat/dict": ["@rcompat/dict@0.3.1", "", { "dependencies": { "@rcompat/assert": "^0.6.0", "@rcompat/is": "^0.4.2" } }, "sha512-eWZ4ACk0DpT8PS+umVlp/TmFfWAD0yqkGxfvvtfL/9fqPEh1bcCFtGMySCwmTGx/FU8sPnxwnSiZGZmN36gTBQ=="],
|
|
||||||
|
|
||||||
"@rcompat/is": ["@rcompat/is@0.4.3", "", {}, "sha512-IRTVOUhgmRjnlEyZ76wmxPNS46TnUTp7m54mbNgMFDdTTNpjnmpuWw0DcBSUuh4p66fqP/7t9M5tmMUiYAFVzQ=="],
|
|
||||||
|
|
||||||
"@rcompat/type": ["@rcompat/type@0.9.0", "", {}, "sha512-oMGchVrm9K98rXigdfHY98P223iHKfmCxH2GfD+vcwHdWqC3YyuJxlfhd6AeLFNkuBz+0G27GD5qm9g03IPIrA=="],
|
|
||||||
|
|
||||||
"@rcompat/webview": ["@rcompat/webview@0.18.0", "", { "dependencies": { "@rcompat/dict": "^0.3.0" } }, "sha512-bJaDPFPSgXg4dhKVbohDgUZZcP+wsO49RRDaWj01L4klxyiMa6EUCv7RIoOgYnwNt+WRDZ0dJbH8AuSmhHdcPA=="],
|
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="],
|
||||||
|
|
@ -955,6 +946,8 @@
|
||||||
|
|
||||||
"lodash.ismatch": ["lodash.ismatch@4.4.0", "", {}, "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g=="],
|
"lodash.ismatch": ["lodash.ismatch@4.4.0", "", {}, "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g=="],
|
||||||
|
|
||||||
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
|
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
|
||||||
|
|
@ -1019,6 +1012,8 @@
|
||||||
|
|
||||||
"oauth4webapi": ["oauth4webapi@2.17.0", "", {}, "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ=="],
|
"oauth4webapi": ["oauth4webapi@2.17.0", "", {}, "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||||
|
|
||||||
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
||||||
|
|
@ -1093,8 +1088,12 @@
|
||||||
|
|
||||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||||
|
|
||||||
|
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||||
|
|
||||||
"q": ["q@1.5.1", "", {}, "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="],
|
"q": ["q@1.5.1", "", {}, "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="],
|
||||||
|
|
||||||
|
"qr.js": ["qr.js@0.0.0", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
"quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="],
|
"quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="],
|
||||||
|
|
@ -1109,6 +1108,10 @@
|
||||||
|
|
||||||
"react-hot-toast": ["react-hot-toast@2.6.0", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg=="],
|
"react-hot-toast": ["react-hot-toast@2.6.0", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg=="],
|
||||||
|
|
||||||
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
|
"react-qr-code": ["react-qr-code@2.0.18", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg=="],
|
||||||
|
|
||||||
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||||
|
|
||||||
"read-pkg": ["read-pkg@3.0.0", "", { "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", "path-type": "^3.0.0" } }, "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA=="],
|
"read-pkg": ["read-pkg@3.0.0", "", { "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", "path-type": "^3.0.0" } }, "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA=="],
|
||||||
|
|
@ -1351,6 +1354,8 @@
|
||||||
|
|
||||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||||
|
|
||||||
|
"webview-bun": ["webview-bun@2.4.0", "", {}, "sha512-0+ugnQlcUHmuW+iLeb+Lzb8rGUJh7WEdXvNsuvaVEXT3EagK380XdD7heVJu0Ek/mNxMY3G2JM142YRQ1hDUGQ=="],
|
||||||
|
|
||||||
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
||||||
|
|
||||||
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@
|
||||||
"@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",
|
||||||
"@rcompat/webview": "^0.18.0",
|
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"conf": "^15.0.2",
|
"conf": "^15.0.2",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
|
@ -61,6 +60,7 @@
|
||||||
"tough-cookie": "^6.0.0",
|
"tough-cookie": "^6.0.0",
|
||||||
"tough-cookie-file-store": "^3.3.0",
|
"tough-cookie-file-store": "^3.3.0",
|
||||||
"unzip-stream": "^0.3.4",
|
"unzip-stream": "^0.3.4",
|
||||||
|
"webview-bun": "^2.4.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -99,6 +99,7 @@
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-error-boundary": "^6.1.0",
|
"react-error-boundary": "^6.1.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
|
"react-qr-code": "^2.0.18",
|
||||||
"sass-embedded": "^1.97.3",
|
"sass-embedded": "^1.97.3",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ await ensureDir("build");
|
||||||
// Copy app dir
|
// Copy app dir
|
||||||
await fs.cp(`${APP_DIR}/.`, path.join(APPDIR, `usr`, 'share'), { recursive: true });
|
await fs.cp(`${APP_DIR}/.`, path.join(APPDIR, `usr`, 'share'), { recursive: true });
|
||||||
await fs.rename(path.join(APPDIR, `usr`, 'share', BINARY_NAME), path.join(APPDIR, `usr`, 'bin', BINARY_NAME));
|
await fs.rename(path.join(APPDIR, `usr`, 'share', BINARY_NAME), path.join(APPDIR, `usr`, 'bin', BINARY_NAME));
|
||||||
|
await fs.rename(path.join(APPDIR, `usr`, 'share', `libwebview-${process.arch}.so`), path.join(APPDIR, `usr`, 'lib', `libwebview-${process.arch}.so`));
|
||||||
|
|
||||||
await fs.writeFile(path.join(APPDIR, `${APP_ID}.desktop`), `[Desktop Entry]
|
await fs.writeFile(path.join(APPDIR, `${APP_ID}.desktop`), `[Desktop Entry]
|
||||||
Version=${pkg.version}
|
Version=${pkg.version}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,17 @@ if (process.env.TARGET)
|
||||||
compileOption.target = process.env.TARGET as any;
|
compileOption.target = process.env.TARGET as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let webviewLib = "libwebview.dll";
|
||||||
|
if (process.platform === 'linux' && system.arch === 'x64')
|
||||||
|
webviewLib = "libwebview-x64.so";
|
||||||
|
if (process.platform === 'linux' && system.arch === 'arm64')
|
||||||
|
webviewLib = "libwebview-arm64.so";
|
||||||
|
if (process.platform === 'darwin')
|
||||||
|
webviewLib = "libwebview-arm64.dylib";
|
||||||
|
|
||||||
|
if (process.env.APPIMAGE === "true")
|
||||||
|
webviewLib = `./usr/lib/${webviewLib}`;
|
||||||
|
|
||||||
await Bun.build({
|
await Bun.build({
|
||||||
entrypoints: ["./src/bun/index.ts", `./src/bun/webview/${system.platform}.ts`],
|
entrypoints: ["./src/bun/index.ts", `./src/bun/webview/${system.platform}.ts`],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
|
|
@ -26,7 +37,8 @@ await Bun.build({
|
||||||
outdir: buildSubDir,
|
outdir: buildSubDir,
|
||||||
root: './src/bun',
|
root: './src/bun',
|
||||||
define: {
|
define: {
|
||||||
"process.env.IS_BINARY": "true"
|
"process.env.IS_BINARY": "true",
|
||||||
|
"process.env.WEBVIEW_PATH": `./${webviewLib}`,
|
||||||
},
|
},
|
||||||
minify: process.env.NODE_ENV !== 'development',
|
minify: process.env.NODE_ENV !== 'development',
|
||||||
sourcemap: process.env.NODE_ENV === 'development' ? 'inline' : "linked",
|
sourcemap: process.env.NODE_ENV === 'development' ? 'inline' : "linked",
|
||||||
|
|
@ -55,6 +67,7 @@ await Bun.build({
|
||||||
await fs.cp('./dist', `${buildSubDir}/dist`, { recursive: true });
|
await fs.cp('./dist', `${buildSubDir}/dist`, { recursive: true });
|
||||||
await fs.cp('./drizzle', `${buildSubDir}/drizzle`, { recursive: true });
|
await fs.cp('./drizzle', `${buildSubDir}/drizzle`, { recursive: true });
|
||||||
await fs.cp(`./vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, `${buildSubDir}/vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, { recursive: true });
|
await fs.cp(`./vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, `${buildSubDir}/vendors/es-de/emulators.${system.platform}.${system.arch}.sqlite`, { recursive: true });
|
||||||
|
await fs.cp(path.join(`node_modules/webview-bun/build/`, webviewLib), path.join(buildSubDir, webviewLib));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import EventEmitter from "node:events";
|
||||||
import { ErrorLike } from "bun";
|
import { ErrorLike } from "bun";
|
||||||
import { appPath, getErrorMessage } from "../utils";
|
import { appPath, getErrorMessage } from "../utils";
|
||||||
import { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
|
import { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
|
||||||
|
import { ensureDir } from "fs-extra";
|
||||||
|
|
||||||
export const config = new Conf<SettingsType>({
|
export const config = new Conf<SettingsType>({
|
||||||
projectName: projectPackage.name,
|
projectName: projectPackage.name,
|
||||||
|
|
@ -48,11 +49,9 @@ console.log("App Directory is ", process.env.APPDIR);
|
||||||
const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json'));
|
const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json'));
|
||||||
console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath);
|
console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath);
|
||||||
export const jar = new CookieJar(fileCookieStore);
|
export const jar = new CookieJar(fileCookieStore);
|
||||||
await fs.mkdir(config.get('downloadPath'), { recursive: true });
|
|
||||||
let sqlite: Database;
|
let sqlite: Database;
|
||||||
export let db: DrizzleSqliteDODatabase<typeof schema>;
|
export let db: DrizzleSqliteDODatabase<typeof schema>;
|
||||||
await reloadDatabase();
|
await reloadDatabase();
|
||||||
migrate(db!, { migrationsFolder: appPath("./drizzle") });
|
|
||||||
const emulatorsSqlite = new Database(appPath(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`), { readonly: true });
|
const emulatorsSqlite = new Database(appPath(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`), { readonly: true });
|
||||||
export const emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema });
|
export const emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema });
|
||||||
export const taskQueue = new TaskQueue();
|
export const taskQueue = new TaskQueue();
|
||||||
|
|
@ -73,7 +72,7 @@ events.addListener('activegameexit', ({ error }) =>
|
||||||
events.emit('notification', { message: getErrorMessage(error), type: 'error' });
|
events.emit('notification', { message: getErrorMessage(error), type: 'error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log("Logging In to Romm");
|
config.onDidChange('downloadPath', () => reloadDatabase());
|
||||||
|
|
||||||
export async function cleanup ()
|
export async function cleanup ()
|
||||||
{
|
{
|
||||||
|
|
@ -85,8 +84,10 @@ export async function cleanup ()
|
||||||
|
|
||||||
export async function reloadDatabase ()
|
export async function reloadDatabase ()
|
||||||
{
|
{
|
||||||
|
await ensureDir(config.get('downloadPath'));
|
||||||
sqlite = new Database(path.join(config.get('downloadPath'), 'db.sqlite'), { create: true, readwrite: true });
|
sqlite = new Database(path.join(config.get('downloadPath'), 'db.sqlite'), { create: true, readwrite: true });
|
||||||
db = drizzle(sqlite, { schema });
|
db = drizzle(sqlite, { schema });
|
||||||
|
migrate(db!, { migrationsFolder: appPath("./drizzle") });
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppEventMap
|
interface AppEventMap
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,47 @@
|
||||||
import Elysia, { status } from "elysia";
|
import Elysia, { sse, status } from "elysia";
|
||||||
import { config, db, jar } from "./app";
|
import { config, jar, taskQueue } from "./app";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { client } from "@clients/romm/client.gen";
|
import { client } from "@clients/romm/client.gen";
|
||||||
import { loginApiLoginPost } from "@clients/romm";
|
import { loginApiLoginPost, logoutApiLogoutPost } from "@clients/romm";
|
||||||
import secrets from '../api/secrets';
|
import secrets from '../api/secrets';
|
||||||
|
import { LoginJob } from "./jobs/login-job";
|
||||||
|
|
||||||
export default new Elysia()
|
export default new Elysia()
|
||||||
.post('/login', async ({ body: { host, username, password } }) =>
|
.post('/login/remote/start', async () =>
|
||||||
{
|
{
|
||||||
if (config.has('rommAddress') && config.has('rommUser'))
|
if (taskQueue.hasActiveOfType(LoginJob))
|
||||||
{
|
{
|
||||||
await logout();
|
return status("Conflict", "Login Already Active");
|
||||||
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);
|
const job = new LoginJob();
|
||||||
config.set('rommUser', username);
|
taskQueue.enqueue("login", job);
|
||||||
|
return status("OK");
|
||||||
|
})
|
||||||
|
.get('/login/remote/status', async function* ()
|
||||||
|
{
|
||||||
|
const job = taskQueue.findJob("login");
|
||||||
|
if (job)
|
||||||
|
{
|
||||||
|
const loginJob = job.job as LoginJob;
|
||||||
|
yield sse({ data: { endsAt: loginJob.endsAt, url: loginJob.url } });
|
||||||
|
await taskQueue.waitForJob('login');
|
||||||
|
yield sse({ data: {} });
|
||||||
|
}
|
||||||
|
|
||||||
await secrets.set({ service: 'gameflow', name: 'romm', value: password });
|
yield sse({ data: {} });
|
||||||
await login();
|
})
|
||||||
|
.post('/login/remote/cancel', async () =>
|
||||||
return status(200);
|
{
|
||||||
}, { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) })
|
const job = taskQueue.findJob("login");
|
||||||
|
if (job)
|
||||||
|
{
|
||||||
|
job.abort("cancel");
|
||||||
|
await taskQueue.waitForJob('login');
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
})
|
||||||
|
.post('/login', async ({ body }) => tryLoginAndSave(body), { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) })
|
||||||
.get('/login', async () =>
|
.get('/login', async () =>
|
||||||
{
|
{
|
||||||
const credentials = await secrets.get({ service: 'gameflow', name: 'romm' });
|
const credentials = await secrets.get({ service: 'gameflow', name: 'romm' });
|
||||||
|
|
@ -54,6 +69,31 @@ async function updateClient ()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function tryLoginAndSave ({ host, username, password }: { host: string, username: string, password: string; })
|
||||||
|
{
|
||||||
|
if (config.has('rommAddress') && config.has('rommUser'))
|
||||||
|
{
|
||||||
|
await logout();
|
||||||
|
const oldRommAddress = config.get('rommAddress');
|
||||||
|
if (oldRommAddress)
|
||||||
|
{
|
||||||
|
const cookies = await jar.getCookies(oldRommAddress);
|
||||||
|
await Promise.all(cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await login({ rommAddress: host, rommUser: username, rommPassword: password });
|
||||||
|
if (response?.code === 200)
|
||||||
|
{
|
||||||
|
config.set('rommAddress', host);
|
||||||
|
config.set('rommUser', username);
|
||||||
|
|
||||||
|
await secrets.set({ service: 'gameflow', name: 'romm', value: password });
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
export async function logout ()
|
export async function logout ()
|
||||||
{
|
{
|
||||||
if (!config.has('rommAddress'))
|
if (!config.has('rommAddress'))
|
||||||
|
|
@ -66,11 +106,12 @@ export async function logout ()
|
||||||
console.log("Logging Out of ROMM");
|
console.log("Logging Out of ROMM");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await loginApiLoginPost({
|
await logoutApiLogoutPost({
|
||||||
baseUrl: rommAddress, headers: {
|
baseUrl: rommAddress, headers: {
|
||||||
'cookie': await jar.getCookieString(rommAddress)
|
'cookie': await jar.getCookieString(rommAddress)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
await jar.store.removeCookie(new URL(rommAddress).host, null, "romm_session");
|
||||||
} catch (error)
|
} catch (error)
|
||||||
{
|
{
|
||||||
console.error("Failed to logout of ROMM ", error);
|
console.error("Failed to logout of ROMM ", error);
|
||||||
|
|
@ -78,20 +119,39 @@ export async function logout ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login ()
|
export async function login (data?: { rommAddress?: string, rommUser?: string, rommPassword?: string; })
|
||||||
{
|
{
|
||||||
if (!config.has('rommAddress') || !config.has('rommUser'))
|
const address = data?.rommAddress ?? config.get('rommAddress');
|
||||||
|
const user = data?.rommUser ?? config.get('rommUser');
|
||||||
|
const password = data?.rommPassword ?? await secrets.get({ service: 'gameflow', name: "romm" });
|
||||||
|
|
||||||
|
if (!address || !user)
|
||||||
{
|
{
|
||||||
return;
|
console.warn("Romm not setup");
|
||||||
|
return status(404);
|
||||||
}
|
}
|
||||||
const rommAddress = config.get('rommAddress');
|
const rommAddress = config.get('rommAddress');
|
||||||
const rommUser = config.get('rommUser');
|
const rommUser = config.get('rommUser');
|
||||||
if (rommAddress && rommUser)
|
if (rommAddress && rommUser)
|
||||||
{
|
{
|
||||||
console.log("Logging In to ROMM");
|
console.log("Logging In to ROMM");
|
||||||
const password = await secrets.get({ service: 'gameflow', name: "romm" });
|
if (password === null)
|
||||||
|
{
|
||||||
|
return status(404, "No Found Password");
|
||||||
|
}
|
||||||
|
|
||||||
const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` });
|
const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` });
|
||||||
|
if (loginResponse.response.status === 200)
|
||||||
|
{
|
||||||
loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress));
|
loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress));
|
||||||
await updateClient();
|
await updateClient();
|
||||||
|
return status(200, loginResponse.response.statusText);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
console.error("Could not Login to Romm: ", loginResponse.response.statusText);
|
||||||
|
return status(loginResponse.response.status, loginResponse.response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,6 @@ export default new Elysia()
|
||||||
}
|
}
|
||||||
|
|
||||||
return processImage(coverBlob.cover, query);
|
return processImage(coverBlob.cover, query);
|
||||||
/*return sharp(coverBlob.cover)
|
|
||||||
.resize({ width, height, withoutEnlargement: true })
|
|
||||||
.blur(blur)
|
|
||||||
.toBuffer();*/
|
|
||||||
}, {
|
}, {
|
||||||
params: z.object({ id: z.coerce.number() }),
|
params: z.object({ id: z.coerce.number() }),
|
||||||
query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() })
|
query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() })
|
||||||
|
|
@ -73,13 +69,6 @@ export default new Elysia()
|
||||||
{
|
{
|
||||||
const rommAdress = config.get('rommAddress');
|
const rommAdress = config.get('rommAddress');
|
||||||
return processImage(`${rommAdress}/${path}`, query);
|
return processImage(`${rommAdress}/${path}`, query);
|
||||||
|
|
||||||
/*
|
|
||||||
const rommFetch = await fetch(`${rommAdress}/${path}`);
|
|
||||||
return sharp(await rommFetch.arrayBuffer())
|
|
||||||
.resize({ width, height, withoutEnlargement: true })
|
|
||||||
.blur(blur)
|
|
||||||
.toBuffer();*/
|
|
||||||
}
|
}
|
||||||
return status('Not Found');
|
return status('Not Found');
|
||||||
}, { query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() }) })
|
}, { query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() }) })
|
||||||
|
|
@ -94,8 +83,6 @@ export default new Elysia()
|
||||||
}
|
}
|
||||||
|
|
||||||
return processImage(screenshot.content, query);
|
return processImage(screenshot.content, query);
|
||||||
//return sharp(screenshot.content).resize({ width, height, withoutEnlargement: true }).blur(blur).toBuffer();
|
|
||||||
//return screenshot.content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return status(404);
|
return status(404);
|
||||||
|
|
|
||||||
61
src/bun/api/jobs/login-job.ts
Normal file
61
src/bun/api/jobs/login-job.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import Elysia, { status } from "elysia";
|
||||||
|
import { IJob, JobContext } from "../task-queue";
|
||||||
|
import { LOGIN_PORT, SERVER_URL } from "@/shared/constants";
|
||||||
|
import { host, localIp } from "@/bun/utils/host";
|
||||||
|
import cors from "@elysiajs/cors";
|
||||||
|
import { tryLoginAndSave } from "../auth";
|
||||||
|
import z from "zod";
|
||||||
|
import { config } from "../app";
|
||||||
|
|
||||||
|
export class LoginJob implements IJob
|
||||||
|
{
|
||||||
|
endsAt: Date;
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
this.endsAt = new Date();
|
||||||
|
this.url = `http://${localIp}:${LOGIN_PORT}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start (context: JobContext): Promise<any>
|
||||||
|
{
|
||||||
|
const loginServer = new Elysia({ serve: { hostname: localIp, port: LOGIN_PORT } })
|
||||||
|
.use(cors())
|
||||||
|
.get(`/`, ({ headers }) => process.env.PUBLIC_ACCESS ? fetch(`${SERVER_URL(host)}/auth/qr/`, { headers: headers as any }) : Bun.file(`./dist/auth/qr/index.html`))
|
||||||
|
.get(`/*`, ({ path, headers }) => process.env.PUBLIC_ACCESS ? fetch(`${SERVER_URL(host)}/auth/qr/${path}`, { headers: headers as any }) : Bun.file(`./dist/${path}`))
|
||||||
|
.get('/status', () => ({ expires_at: this.endsAt, max_time: 300000 }))
|
||||||
|
.post('/cancel', () => context.abort("cancel"))
|
||||||
|
.get('/defaults', () => ({ host: config.get('rommAddress'), username: config.get('rommUser') ?? '' }))
|
||||||
|
.post(`/login`, async ({ body }) =>
|
||||||
|
{
|
||||||
|
const response = await tryLoginAndSave(body as any);
|
||||||
|
if (response?.code === 200)
|
||||||
|
{
|
||||||
|
context.abort("success");
|
||||||
|
return status("Accepted");
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
loginServer.listen({});
|
||||||
|
await new Promise((resolve, reject) =>
|
||||||
|
{
|
||||||
|
this.endsAt = new Date(new Date().getTime() + 300000);
|
||||||
|
context.abortSignal.addEventListener('abort', () => reject());
|
||||||
|
setTimeout(() => { reject('timeout'); }, 300000); // auto close after 5 minutes
|
||||||
|
});
|
||||||
|
} catch
|
||||||
|
{
|
||||||
|
} finally
|
||||||
|
{
|
||||||
|
await loginServer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { SettingsSchema } from "@shared/constants";
|
import { LOGIN_PORT, SettingsSchema } from "@shared/constants";
|
||||||
import Elysia, { status } from "elysia";
|
import Elysia, { status } from "elysia";
|
||||||
import { config, customEmulators, db, emulatorsDb, taskQueue } from "./app";
|
import { config, customEmulators, db, emulatorsDb, taskQueue } from "./app";
|
||||||
import * as appSchema from './schema/app';
|
import * as appSchema from './schema/app';
|
||||||
|
|
@ -103,7 +103,7 @@ export const settings = new Elysia({ prefix: '/api/settings' })
|
||||||
const oldDownloadPath = config.get('downloadPath');
|
const oldDownloadPath = config.get('downloadPath');
|
||||||
if (!existsSync(oldDownloadPath))
|
if (!existsSync(oldDownloadPath))
|
||||||
{
|
{
|
||||||
return status("Not Found", "Old downlod path doesn't exist");
|
return status("Not Found", "Old download path doesn't exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isDirEmpty (dirname: string)
|
async function isDirEmpty (dirname: string)
|
||||||
|
|
@ -121,7 +121,7 @@ export const settings = new Elysia({ prefix: '/api/settings' })
|
||||||
|
|
||||||
if (existsSync(path) && !isDirEmpty(path))
|
if (existsSync(path) && !isDirEmpty(path))
|
||||||
{
|
{
|
||||||
return status("Conflict", "New location alaready exists and is not empty");
|
return status("Conflict", "New location already exists and is not empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
await move(oldDownloadPath, path);
|
await move(oldDownloadPath, path);
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ export interface IPublicJob
|
||||||
state?: string;
|
state?: string;
|
||||||
status: JobStatus;
|
status: JobStatus;
|
||||||
job: any;
|
job: any;
|
||||||
|
abort: (reason?: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JobContext implements IPublicJob
|
export class JobContext implements IPublicJob
|
||||||
|
|
@ -177,7 +178,7 @@ export class JobContext implements IPublicJob
|
||||||
} catch (error)
|
} catch (error)
|
||||||
{
|
{
|
||||||
console.error(error);
|
console.error(error);
|
||||||
this.events.emit('error', { id: this.m_id, error });
|
this.events.emit('error', { id: this.m_id, job: this.m_job, error });
|
||||||
this.error = error;
|
this.error = error;
|
||||||
} finally
|
} finally
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,9 @@ export default async function init (events: EventEmitter, forceBrowser: boolean)
|
||||||
|
|
||||||
async function runWebview (events: EventEmitter)
|
async function runWebview (events: EventEmitter)
|
||||||
{
|
{
|
||||||
const webviewWorker = new Worker(Bun.env.IS_BINARY ? `./webview/${os.platform()}.ts` : new URL(`./webview/${os.platform()}`, import.meta.url).href, {
|
const webviewWorker = new Worker(Bun.env.IS_BINARY ? new URL(`./webview/${os.platform()}`, import.meta.url).href : `./webview/${os.platform()}.ts`, {
|
||||||
smol: true,
|
smol: true,
|
||||||
|
ref: false
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise((resolve, reject) =>
|
||||||
|
|
@ -47,6 +48,7 @@ async function runWebview (events: EventEmitter)
|
||||||
events.on('exitapp', () =>
|
events.on('exitapp', () =>
|
||||||
{
|
{
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
webviewWorker.terminate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { networkInterfaces } from "node:os";
|
import { networkInterfaces } from "node:os";
|
||||||
|
|
||||||
const localIp = Object.values(networkInterfaces())
|
export const localIp = Object.values(networkInterfaces())
|
||||||
.flat()
|
.flat()
|
||||||
.find((iface) => iface?.family === 'IPv4' && !iface.internal)?.address || 'localhost';
|
.find((iface) => iface?.family === 'IPv4' && !iface.internal)?.address || 'localhost';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,15 @@ import { host } from "../utils/host";
|
||||||
|
|
||||||
export default function (webview: { navigate: (url: string) => void; run: () => void; destroy: () => void; })
|
export default function (webview: { navigate: (url: string) => void; run: () => void; destroy: () => void; })
|
||||||
{
|
{
|
||||||
self.addEventListener('message', (e) =>
|
self.onmessage = (e) =>
|
||||||
{
|
{
|
||||||
console.log("Terminate");
|
console.log("Terminate");
|
||||||
if (e.data === 'exit')
|
if (e.data === 'exit')
|
||||||
{
|
{
|
||||||
webview.destroy();
|
webview.destroy();
|
||||||
|
process.exit();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
webview.navigate(SERVER_URL(host));
|
webview.navigate(SERVER_URL(host));
|
||||||
webview.run();
|
webview.run();
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import Webview from "@rcompat/webview";
|
import { Webview } from 'webview-bun';
|
||||||
import platform from "@rcompat/webview/linux-x64";
|
|
||||||
import webviewWorkerBase from "./base";
|
import webviewWorkerBase from "./base";
|
||||||
|
|
||||||
if (process.env.FLATPAK_BUILD === "true")
|
if (process.env.FLATPAK_BUILD === "true")
|
||||||
|
|
@ -29,6 +28,6 @@ if (process.env.FLATPAK_BUILD === "true")
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
console.log("Launching Webview");
|
console.log("Launching Webview");
|
||||||
const webview = new Webview({ debug: import.meta.env.NODE_ENV === 'development', platform });
|
const webview = new Webview(import.meta.env.NODE_ENV === 'development');
|
||||||
webviewWorkerBase(webview);
|
webviewWorkerBase(webview);
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import Webview from "@rcompat/webview";
|
|
||||||
import platform from "@rcompat/webview/windows-x64";
|
import { Webview } from 'webview-bun';
|
||||||
import webviewWorkerBase from "./base";
|
import webviewWorkerBase from "./base";
|
||||||
|
|
||||||
console.log("Launching Webview");
|
const webview = new Webview(import.meta.env.NODE_ENV === 'development');
|
||||||
const webview = new Webview({ debug: import.meta.env.NODE_ENV === 'development', platform });
|
|
||||||
webviewWorkerBase(webview);
|
webviewWorkerBase(webview);
|
||||||
2
src/mainview/auth/qr/index.css
Normal file
2
src/mainview/auth/qr/index.css
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui";
|
||||||
249
src/mainview/auth/qr/index.html
Normal file
249
src/mainview/auth/qr/index.html
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Quick Login</title>
|
||||||
|
<link rel="stylesheet" href="./index.css" />
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
class="flex overflow-hidden relative items-center justify-center min-h-screen bg-base-300"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col items-center justify-center gap-4">
|
||||||
|
<h1 class="text-4xl font-semibold">Quick Login</h1>
|
||||||
|
<div
|
||||||
|
id="arc"
|
||||||
|
class="radial-progress"
|
||||||
|
aria-valuenow="70"
|
||||||
|
role="progressbar"
|
||||||
|
>
|
||||||
|
70%
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-dash alert-info" id="expiryNote">
|
||||||
|
Fetching session info…
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="loginForm"
|
||||||
|
class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4"
|
||||||
|
>
|
||||||
|
<label class="label" for="host">Host</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="host"
|
||||||
|
name="host"
|
||||||
|
placeholder="my-server.local"
|
||||||
|
autocomplete="off"
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label class="label" for="username">Username</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
placeholder="your-username"
|
||||||
|
autocomplete="username"
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label class="label" for="password">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
autocomplete="current-password"
|
||||||
|
class="input join-item"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button class="btn btn-primary mt-4" id="loginBtn" type="submit">
|
||||||
|
Login →
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick="handleCancel()"
|
||||||
|
class="btn btn-primary mt-4"
|
||||||
|
id="loginBtn"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="alert hidden" id="statusPill">
|
||||||
|
<div class="dot"></div>
|
||||||
|
<span id="statusText"></span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const _base = window.location.href.split("?")[0].replace(/\/[^\/]*$/, "");
|
||||||
|
const ENDPOINT = _base + "/login";
|
||||||
|
const STATUS_EP = _base + "/status";
|
||||||
|
const DEFAULTS_EP = _base + "/defaults";
|
||||||
|
const CANCEL_EP = _base + "/cancel";
|
||||||
|
|
||||||
|
const arc = document.getElementById("arc");
|
||||||
|
const expireBar = document.getElementById("expireBar");
|
||||||
|
const expiryNote = document.getElementById("expiryNote");
|
||||||
|
const loginForm = document.getElementById("loginForm");
|
||||||
|
const pill = document.getElementById("statusPill");
|
||||||
|
const statusText = document.getElementById("statusText");
|
||||||
|
|
||||||
|
let expiresAt = null;
|
||||||
|
let totalSpan = null;
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
fetch(CANCEL_EP, { method: "POST" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDefault() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(DEFAULTS_EP, { method: "GET" });
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById("host").defaultValue = data.host;
|
||||||
|
document.getElementById("username").defaultValue =
|
||||||
|
data.username ?? "";
|
||||||
|
} catch (e) {
|
||||||
|
showStatus("Could not fetch defaults", "err");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDefault();
|
||||||
|
|
||||||
|
async function fetchExpiry() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(STATUS_EP, { method: "GET" });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.expires_at) {
|
||||||
|
expiresAt = new Date(data.expires_at);
|
||||||
|
totalSpan = data.max_time;
|
||||||
|
renderExpiryNote();
|
||||||
|
} else {
|
||||||
|
expiryNote.textContent = "No expiry info from server";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
expiryNote.textContent = "Could not reach server";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExpiryNote() {
|
||||||
|
if (!expiresAt) return;
|
||||||
|
const fmt = expiresAt.toLocaleString(undefined, {
|
||||||
|
weekday: "short",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
expiryNote.textContent = `Active until ${fmt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchExpiry();
|
||||||
|
|
||||||
|
// Countdown timer
|
||||||
|
function updateTimer() {
|
||||||
|
const loginBtn = document.getElementById("loginBtn");
|
||||||
|
|
||||||
|
if (!expiresAt) {
|
||||||
|
arc.style.setProperty("--value", 70);
|
||||||
|
arc.innerHTML = "<small>loading…</small>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainMs = expiresAt - Date.now();
|
||||||
|
|
||||||
|
if (remainMs <= 0) {
|
||||||
|
arc.style.setProperty("--value", 0);
|
||||||
|
arc.innerHTML = "<small>Expired</small>";
|
||||||
|
loginBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pct = Math.min(remainMs / (totalSpan || remainMs), 1);
|
||||||
|
arc.style.setProperty("--value", pct * 100);
|
||||||
|
|
||||||
|
const totalSec = Math.ceil(remainMs / 1000);
|
||||||
|
const d = Math.floor(totalSec / 86400);
|
||||||
|
const h = Math.floor((totalSec % 86400) / 3600);
|
||||||
|
const m = Math.floor((totalSec % 3600) / 60);
|
||||||
|
const s = totalSec % 60;
|
||||||
|
|
||||||
|
let main, sub;
|
||||||
|
if (d > 0) {
|
||||||
|
main = `${d}d`;
|
||||||
|
sub = `${h}h left`;
|
||||||
|
} else if (h > 0) {
|
||||||
|
main = `${h}h`;
|
||||||
|
sub = `${m}m left`;
|
||||||
|
} else if (m > 0) {
|
||||||
|
main = `${m}:${String(s).padStart(2, "0")}`;
|
||||||
|
sub = "left";
|
||||||
|
} else {
|
||||||
|
main = `${s}s`;
|
||||||
|
sub = "left";
|
||||||
|
}
|
||||||
|
|
||||||
|
arc.innerHTML = `${main}<br><small>${sub}</small>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimer();
|
||||||
|
setInterval(updateTimer, 1000);
|
||||||
|
|
||||||
|
function showStatus(msg, type) {
|
||||||
|
pill.className = "alert " + type;
|
||||||
|
statusText.textContent = msg;
|
||||||
|
if (type === "ok") setTimeout(() => pill.classList.add("hidden"), 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login submit
|
||||||
|
loginForm.onsubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!e.target.host.value) {
|
||||||
|
showStatus("Host is required", "alert-error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!e.target.username.value) {
|
||||||
|
showStatus("Username is required", "alert-error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!e.target.password.value) {
|
||||||
|
showStatus("Password is required", "alert-error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById("loginBtn");
|
||||||
|
btn.textContent = "Logging in…";
|
||||||
|
btn.style.opacity = "0.75";
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
host: e.target.host.value,
|
||||||
|
username: e.target.username.value,
|
||||||
|
password: e.target.password.value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
showStatus("Logged in!", "alert-success");
|
||||||
|
setTimeout(() => window.close(), 1000);
|
||||||
|
} else {
|
||||||
|
showStatus(`Error ${res.status}: ${res.statusText}`, "alert-error");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showStatus("Could not reach server", "alert-error");
|
||||||
|
} finally {
|
||||||
|
btn.textContent = "Login →";
|
||||||
|
btn.style.opacity = "1";
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -71,7 +71,7 @@ export function AnimatedBackground (data: {
|
||||||
backgroundSize: '100%',
|
backgroundSize: '100%',
|
||||||
backgroundPositionY: 'bottom',
|
backgroundPositionY: 'bottom',
|
||||||
backgroundPositionX: 'center',
|
backgroundPositionX: 'center',
|
||||||
backgroundBlendMode: 'soft-light',
|
backgroundBlendMode: blur ? 'normal' : 'soft-light',
|
||||||
backgroundColor: "var(--color-base-100)",
|
backgroundColor: "var(--color-base-100)",
|
||||||
} : {}}
|
} : {}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ export function ContextDialog (data: {
|
||||||
<ContextDialogContext value={{ id: data.id, close: data.close }} >
|
<ContextDialogContext value={{ id: data.id, close: data.close }} >
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"bg-base-100/80 delay-200 rounded-4xl sm:p-4 md:p-6 sm:min-w-[80vw] md:min-w-[30vw] cursor-auto",
|
"bg-base-100/80 delay-200 rounded-4xl sm:p-4 md:p-6 sm:min-w-[80vw] md:min-w-[20vw] cursor-auto",
|
||||||
data.open ? "animate-scale-delayed" : "opacity-0",
|
data.open ? "animate-scale-delayed" : "opacity-0",
|
||||||
data.className)
|
data.className)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export function Button (data: {
|
||||||
classNames({
|
classNames({
|
||||||
"btn-accent": focused,
|
"btn-accent": focused,
|
||||||
}, data.className))}
|
}, data.className))}
|
||||||
type={data.type}
|
type={data.type ?? 'button'}
|
||||||
>
|
>
|
||||||
{data.children}
|
{data.children}
|
||||||
</button>;
|
</button>;
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ export function OptionSpace (data: {
|
||||||
data.label
|
data.label
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex grow justify-end-safe">
|
||||||
{data.children}
|
{data.children}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export const { useAppForm: useSettingsForm, useTypedAppFormContext: useSettingsF
|
||||||
function FormOption (data: { type: HTMLInputTypeAttribute, icon?: JSX.Element; label?: string | JSX.Element; placeholder?: string; })
|
function FormOption (data: { type: HTMLInputTypeAttribute, icon?: JSX.Element; label?: string | JSX.Element; placeholder?: string; })
|
||||||
{
|
{
|
||||||
const field = useFieldContext<string>();
|
const field = useFieldContext<string>();
|
||||||
return <OptionSpace label={<div className="flex gap-2">
|
return <OptionSpace label={<div className="flex flex-1 gap-2">
|
||||||
{data.label}
|
{data.label}
|
||||||
{field.getMeta().errors.length > 0 && <div className="badge badge-error">
|
{field.getMeta().errors.length > 0 && <div className="badge badge-error">
|
||||||
{field.state.meta.errors.map(e => e.message).join(',')}
|
{field.state.meta.errors.map(e => e.message).join(',')}
|
||||||
|
|
@ -32,7 +32,7 @@ function FormOption (data: { type: HTMLInputTypeAttribute, icon?: JSX.Element; l
|
||||||
type={data.type}
|
type={data.type}
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
onChange={e => field.handleChange(e.target.value)}
|
||||||
placeholder={data.placeholder}
|
placeholder={data.placeholder}
|
||||||
className={classNames({ "ring-4 ring-accent": field.getMeta().isDirty })}
|
className={classNames({ " flex-3 ring-4 ring-accent": field.getMeta().isDirty })}
|
||||||
/>
|
/>
|
||||||
</OptionSpace>;;
|
</OptionSpace>;;
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
FocusContext,
|
FocusContext,
|
||||||
|
setFocus,
|
||||||
useFocusable,
|
useFocusable,
|
||||||
} from "@noriginmedia/norigin-spatial-navigation";
|
} from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { useIsMutating, useMutation, useQuery } from "@tanstack/react-query";
|
import { useIsMutating, useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Key, Link, Lock, Save, Trash, User, X } from "lucide-react";
|
import { Key, Link, Lock, Save, ScanQrCode, Trash, User, X } from "lucide-react";
|
||||||
import
|
import
|
||||||
{
|
{
|
||||||
useEffect,
|
useEffect,
|
||||||
|
|
@ -23,11 +24,22 @@ import { OptionSpace } from "../../components/options/OptionSpace";
|
||||||
import { useSettingsForm, useSettingsFormContext } from "../../components/options/SettingsAppForm";
|
import { useSettingsForm, useSettingsFormContext } from "../../components/options/SettingsAppForm";
|
||||||
import { rommApi, settingsApi } from "../../scripts/clientApi";
|
import { rommApi, settingsApi } from "../../scripts/clientApi";
|
||||||
import { Button } from "../../components/options/Button";
|
import { Button } from "../../components/options/Button";
|
||||||
|
import { ContextDialog } from "@/mainview/components/ContextDialog";
|
||||||
|
import QRCode from "react-qr-code";
|
||||||
|
import { useAsyncGenerator } from "@/mainview/scripts/utils";
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings/accounts")({
|
export const Route = createFileRoute("/settings/accounts")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function LoginQR (data: { id: string, isOpen: boolean, cancel: () => void, url: string; endsAt: Date; })
|
||||||
|
{
|
||||||
|
return <ContextDialog id={data.id} open={data.isOpen} close={() => data.cancel()} className="flex flex-col justify-center items-center gap-2">
|
||||||
|
<QRCode value={data.url} />
|
||||||
|
<Button id="qr-login-cancel" focusClassName="btn-warning" type="button" onAction={() => data.cancel()}><X /> Cancel</Button>
|
||||||
|
</ContextDialog>;
|
||||||
|
}
|
||||||
|
|
||||||
function LoginControls (data: { hasPassword: boolean; })
|
function LoginControls (data: { hasPassword: boolean; })
|
||||||
{
|
{
|
||||||
const user = useQuery({
|
const user = useQuery({
|
||||||
|
|
@ -36,8 +48,27 @@ function LoginControls (data: { hasPassword: boolean; })
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
retry: 0
|
retry: 0
|
||||||
});
|
});
|
||||||
|
const { data: qrLoginStatusGen, refetch } = useQuery({
|
||||||
|
queryKey: ['login', 'qr'], queryFn: async () =>
|
||||||
|
{
|
||||||
|
const { data, error } = await rommApi.api.romm.login.remote.status.get();
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusValue = useAsyncGenerator(qrLoginStatusGen, [qrLoginStatusGen]);
|
||||||
|
const cancelQrMutation = useMutation({
|
||||||
|
mutationKey: ['login', 'qr', 'cancel'],
|
||||||
|
mutationFn: () => rommApi.api.romm.login.remote.cancel.post(),
|
||||||
|
onSuccess: () => refetch()
|
||||||
|
});
|
||||||
|
const requestQrLoginMutation = useMutation({
|
||||||
|
mutationKey: ['login', 'qr'],
|
||||||
|
mutationFn: () => rommApi.api.romm.login.remote.start.post(),
|
||||||
|
onSuccess: () => refetch()
|
||||||
|
});
|
||||||
const context = useSettingsFormContext({});
|
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: () => rommApi.api.romm.logout.post(),
|
mutationKey: ["romm", "auth", "logout"], mutationFn: () => rommApi.api.romm.logout.post(),
|
||||||
|
|
@ -52,6 +83,7 @@ function LoginControls (data: { hasPassword: boolean; })
|
||||||
{user.isSuccess && <>
|
{user.isSuccess && <>
|
||||||
<div className="badge badge-success badge-lg rounded-full gap-2"> <p className="sm:hidden md:inline">Logged In As:</p> <img className="size-6 rounded-full" src={`${RPC_URL(__HOST__)}/api/romm/assets/romm/assets/${user.data?.avatar_path}`} /><b>{user.data?.username}</b></div>
|
<div className="badge badge-success badge-lg rounded-full gap-2"> <p className="sm:hidden md:inline">Logged In As:</p> <img className="size-6 rounded-full" src={`${RPC_URL(__HOST__)}/api/romm/assets/romm/assets/${user.data?.avatar_path}`} /><b>{user.data?.username}</b></div>
|
||||||
</>}
|
</>}
|
||||||
|
<Button id="qr-login" type="button" onAction={() => requestQrLoginMutation.mutate()}><ScanQrCode /> </Button>
|
||||||
<Button id="can-submit" disabled={!context.state.canSubmit || !context.state.isDirty} type="submit" onAction={() => context.handleSubmit()} >
|
<Button id="can-submit" disabled={!context.state.canSubmit || !context.state.isDirty} type="submit" onAction={() => context.handleSubmit()} >
|
||||||
<Save /> Save
|
<Save /> Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -67,6 +99,11 @@ function LoginControls (data: { hasPassword: boolean; })
|
||||||
<Button id="cancel" disabled={context.state.isDefaultValue} type="reset" onAction={() => context.reset()}>
|
<Button id="cancel" disabled={context.state.isDefaultValue} type="reset" onAction={() => context.reset()}>
|
||||||
<X /> Cancel
|
<X /> Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
{statusValue?.data?.endsAt && <LoginQR id="qr-login-context" endsAt={statusValue.data.endsAt} isOpen={true} cancel={() =>
|
||||||
|
{
|
||||||
|
setFocus(`qr-login`);
|
||||||
|
cancelQrMutation.mutate();
|
||||||
|
}} url={statusValue?.data?.url ?? ''} />}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,9 +156,10 @@ function RouteComponent ()
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
const loginMutation = useMutation({
|
||||||
mutationKey: ["romm", "login"],
|
mutationKey: ["romm", "login"],
|
||||||
mutationFn: (data: z.infer<typeof dataSchema>) =>
|
mutationFn: async (data: z.infer<typeof dataSchema>) =>
|
||||||
{
|
{
|
||||||
return rommApi.api.romm.login.post({ username: data.username, password: data.password, host: data.hostname });
|
const { error } = await rommApi.api.romm.login.post({ username: data.username, password: data.password, host: data.hostname });
|
||||||
|
if (error) throw error;
|
||||||
},
|
},
|
||||||
onSuccess: (d, v, r, c) =>
|
onSuccess: (d, v, r, c) =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { doesFocusableExist, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
import { doesFocusableExist, getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { RefObject, useEffect } from "react";
|
import { RefObject, useEffect, useState } from "react";
|
||||||
|
|
||||||
export function selfFocusSmart (shouldFocus: boolean, focusSelf: () => void)
|
export function selfFocusSmart (shouldFocus: boolean, focusSelf: () => void)
|
||||||
{
|
{
|
||||||
|
|
@ -63,3 +63,40 @@ export function mobileCheck ()
|
||||||
(function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true; })(navigator.userAgent || navigator.vendor || window.opera);
|
(function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true; })(navigator.userAgent || navigator.vendor || window.opera);
|
||||||
return check;
|
return check;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function useAsyncGenerator<T> (
|
||||||
|
generator: AsyncGenerator<T> | undefined,
|
||||||
|
deps: any[]
|
||||||
|
)
|
||||||
|
{
|
||||||
|
const [value, setValue] = useState<T | null>(null);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if (!generator)
|
||||||
|
{
|
||||||
|
setValue(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const run = async () =>
|
||||||
|
{
|
||||||
|
for await (const v of generator)
|
||||||
|
{
|
||||||
|
if (cancelled) break;
|
||||||
|
setValue(v);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run();
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, deps);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { FocusDetails } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { JSX } from 'react';
|
import { JSX } from 'react';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
export const LOGIN_PORT = 5196;
|
||||||
export const SERVER_PORT = 5173;
|
export const SERVER_PORT = 5173;
|
||||||
export const SERVER_URL = (host: string) => `http://${host}:${SERVER_PORT}`;
|
export const SERVER_URL = (host: string) => `http://${host}:${SERVER_PORT}`;
|
||||||
export const WINDOW_PORT = 4656;
|
export const WINDOW_PORT = 4656;
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ export default defineConfig(() =>
|
||||||
minify: production,
|
minify: production,
|
||||||
sourcemap: production ? false : 'inline',
|
sourcemap: production ? false : 'inline',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: 'src/mainview/index.html',
|
||||||
|
login: 'src/mainview/auth/qr/index.html'
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
manualChunks: (id
|
manualChunks: (id
|
||||||
) =>
|
) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue