v1.2.0 (#33)
@@ -91,7 +91,10 @@ jobs:
|
|||||||
- uses: tauri-apps/tauri-action@v0
|
- uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
with:
|
with:
|
||||||
|
uploadPlainBinary: true
|
||||||
tagName: v__VERSION__
|
tagName: v__VERSION__
|
||||||
releaseName: 'Emerald v__VERSION__'
|
releaseName: 'Emerald v__VERSION__'
|
||||||
releaseBody: |
|
releaseBody: |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "emerald-legacy-launcher",
|
"name": "emerald-legacy-launcher",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -15,8 +15,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tauri-apps/plugin-process": "~2.3.1",
|
||||||
|
"@tauri-apps/plugin-updater": "~2.10.1",
|
||||||
"framer-motion": "^12.36.0",
|
"framer-motion": "^12.36.0",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -28,6 +31,7 @@
|
|||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/pako": "^2.0.4",
|
"@types/pako": "^2.0.4",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@types/three": "^0.183.1",
|
"@types/three": "^0.183.1",
|
||||||
|
|||||||
@@ -14,12 +14,21 @@ importers:
|
|||||||
'@tauri-apps/plugin-opener':
|
'@tauri-apps/plugin-opener':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.5.3
|
version: 2.5.3
|
||||||
|
'@tauri-apps/plugin-process':
|
||||||
|
specifier: ~2.3.1
|
||||||
|
version: 2.3.1
|
||||||
|
'@tauri-apps/plugin-updater':
|
||||||
|
specifier: ~2.10.1
|
||||||
|
version: 2.10.1
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.36.0
|
specifier: ^12.36.0
|
||||||
version: 12.38.0([email protected]([email protected]))([email protected])
|
version: 12.38.0([email protected]([email protected]))([email protected])
|
||||||
pako:
|
pako:
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
|
qrcode:
|
||||||
|
specifier: ^1.5.4
|
||||||
|
version: 1.5.4
|
||||||
react:
|
react:
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.2.4
|
version: 19.2.4
|
||||||
@@ -41,13 +50,16 @@ importers:
|
|||||||
version: 4.2.1
|
version: 4.2.1
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.2.1
|
specifier: ^4.2.1
|
||||||
version: 4.2.1([email protected]([email protected])([email protected]))
|
version: 4.2.1([email protected](@types/[email protected])([email protected])([email protected]))
|
||||||
'@tauri-apps/cli':
|
'@tauri-apps/cli':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.10.1
|
version: 2.10.1
|
||||||
'@types/pako':
|
'@types/pako':
|
||||||
specifier: ^2.0.4
|
specifier: ^2.0.4
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
|
'@types/qrcode':
|
||||||
|
specifier: ^1.5.6
|
||||||
|
version: 1.5.6
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.1.8
|
specifier: ^19.1.8
|
||||||
version: 19.2.14
|
version: 19.2.14
|
||||||
@@ -59,7 +71,7 @@ importers:
|
|||||||
version: 0.183.1
|
version: 0.183.1
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.6.0
|
specifier: ^4.6.0
|
||||||
version: 4.7.0([email protected]([email protected])([email protected]))
|
version: 4.7.0([email protected](@types/[email protected])([email protected])([email protected]))
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.27
|
specifier: ^10.4.27
|
||||||
version: 10.4.27([email protected])
|
version: 10.4.27([email protected])
|
||||||
@@ -80,7 +92,7 @@ importers:
|
|||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^7.0.4
|
specifier: ^7.0.4
|
||||||
version: 7.3.1([email protected])([email protected])
|
version: 7.3.1(@types/[email protected])([email protected])([email protected])
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -647,6 +659,12 @@ packages:
|
|||||||
'@tauri-apps/[email protected]':
|
'@tauri-apps/[email protected]':
|
||||||
resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==}
|
resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==}
|
||||||
|
|
||||||
|
'@tauri-apps/[email protected]':
|
||||||
|
resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
|
||||||
|
|
||||||
|
'@tauri-apps/[email protected]':
|
||||||
|
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
|
||||||
|
|
||||||
'@tweenjs/[email protected]':
|
'@tweenjs/[email protected]':
|
||||||
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
||||||
|
|
||||||
@@ -680,9 +698,15 @@ packages:
|
|||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==}
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
|
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -718,6 +742,14 @@ packages:
|
|||||||
'@webgpu/[email protected]':
|
'@webgpu/[email protected]':
|
||||||
resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==}
|
resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==}
|
resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
@@ -738,6 +770,10 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==}
|
resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==}
|
||||||
|
|
||||||
@@ -756,6 +792,16 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||||
|
|
||||||
@@ -774,6 +820,10 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
||||||
|
|
||||||
@@ -788,9 +838,15 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==}
|
resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -826,6 +882,10 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
||||||
|
|
||||||
@@ -852,6 +912,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
@@ -876,6 +940,10 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
|
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
||||||
|
|
||||||
@@ -970,6 +1038,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
|
resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
|
|
||||||
@@ -1131,12 +1203,28 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -1144,6 +1232,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||||
|
|
||||||
@@ -1154,6 +1246,11 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1185,6 +1282,13 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
|
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
@@ -1197,6 +1301,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1204,9 +1311,17 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
|
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
|
||||||
|
|
||||||
@@ -1249,6 +1364,9 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||||
|
|
||||||
@@ -1319,9 +1437,27 @@ packages:
|
|||||||
yaml:
|
yaml:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||||
|
|
||||||
@@ -1686,12 +1822,12 @@ snapshots:
|
|||||||
postcss: 8.5.8
|
postcss: 8.5.8
|
||||||
tailwindcss: 4.2.1
|
tailwindcss: 4.2.1
|
||||||
|
|
||||||
'@tailwindcss/[email protected]([email protected]([email protected])([email protected]))':
|
'@tailwindcss/[email protected]([email protected](@types/[email protected])([email protected])([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.2.1
|
'@tailwindcss/node': 4.2.1
|
||||||
'@tailwindcss/oxide': 4.2.1
|
'@tailwindcss/oxide': 4.2.1
|
||||||
tailwindcss: 4.2.1
|
tailwindcss: 4.2.1
|
||||||
vite: 7.3.1([email protected])([email protected])
|
vite: 7.3.1(@types/[email protected])([email protected])([email protected])
|
||||||
|
|
||||||
'@tauri-apps/[email protected]': {}
|
'@tauri-apps/[email protected]': {}
|
||||||
|
|
||||||
@@ -1748,6 +1884,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.10.1
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
|
'@tauri-apps/[email protected]':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
|
'@tauri-apps/[email protected]':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
'@tweenjs/[email protected]': {}
|
'@tweenjs/[email protected]': {}
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
@@ -1791,8 +1935,16 @@ snapshots:
|
|||||||
|
|
||||||
'@types/[email protected]': {}
|
'@types/[email protected]': {}
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.24.6
|
||||||
|
|
||||||
'@types/[email protected]': {}
|
'@types/[email protected]': {}
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.8.0
|
||||||
|
|
||||||
'@types/[email protected](@types/[email protected])':
|
'@types/[email protected](@types/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
@@ -1821,7 +1973,7 @@ snapshots:
|
|||||||
|
|
||||||
'@ungap/[email protected]': {}
|
'@ungap/[email protected]': {}
|
||||||
|
|
||||||
'@vitejs/[email protected]([email protected]([email protected])([email protected]))':
|
'@vitejs/[email protected]([email protected](@types/[email protected])([email protected])([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/[email protected])
|
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/[email protected])
|
||||||
@@ -1829,12 +1981,18 @@ snapshots:
|
|||||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.17.0
|
react-refresh: 0.17.0
|
||||||
vite: 7.3.1([email protected])([email protected])
|
vite: 7.3.1(@types/[email protected])([email protected])([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@webgpu/[email protected]': {}
|
'@webgpu/[email protected]': {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
|
||||||
[email protected]([email protected]):
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.28.1
|
browserslist: 4.28.1
|
||||||
@@ -1856,6 +2014,8 @@ snapshots:
|
|||||||
node-releases: 2.0.36
|
node-releases: 2.0.36
|
||||||
update-browserslist-db: 1.2.3([email protected])
|
update-browserslist-db: 1.2.3([email protected])
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -1868,6 +2028,18 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 6.2.0
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -1878,6 +2050,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
@@ -1890,8 +2064,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -1940,6 +2118,11 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
locate-path: 5.0.0
|
||||||
|
path-exists: 4.0.0
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]([email protected]([email protected]))([email protected]):
|
[email protected]([email protected]([email protected]))([email protected]):
|
||||||
@@ -1956,6 +2139,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
@@ -1995,6 +2180,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -2056,6 +2243,10 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.31.1
|
lightningcss-win32-arm64-msvc: 1.31.1
|
||||||
lightningcss-win32-x64-msvc: 1.31.1
|
lightningcss-win32-x64-msvc: 1.31.1
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
p-locate: 4.1.0
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
@@ -2426,6 +2617,16 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
p-try: 2.2.0
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
p-limit: 2.3.0
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
@@ -2438,10 +2639,14 @@ snapshots:
|
|||||||
is-decimal: 2.0.1
|
is-decimal: 2.0.1
|
||||||
is-hexadecimal: 2.0.1
|
is-hexadecimal: 2.0.1
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
@@ -2452,6 +2657,12 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
dijkstrajs: 1.0.3
|
||||||
|
pngjs: 5.0.0
|
||||||
|
yargs: 15.4.1
|
||||||
|
|
||||||
[email protected]([email protected]):
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
@@ -2513,6 +2724,10 @@ snapshots:
|
|||||||
mdast-util-to-markdown: 2.1.2
|
mdast-util-to-markdown: 2.1.2
|
||||||
unified: 11.0.5
|
unified: 11.0.5
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@@ -2548,15 +2763,27 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities-html4: 2.1.0
|
character-entities-html4: 2.1.0
|
||||||
character-entities-legacy: 3.0.0
|
character-entities-legacy: 3.0.0
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
style-to-object: 1.0.14
|
style-to-object: 1.0.14
|
||||||
@@ -2593,6 +2820,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
@@ -2642,7 +2871,7 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
[email protected]([email protected])([email protected]):
|
[email protected](@types/[email protected])([email protected])([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.4
|
esbuild: 0.27.4
|
||||||
fdir: 6.5.0([email protected])
|
fdir: 6.5.0([email protected])
|
||||||
@@ -2651,10 +2880,40 @@ snapshots:
|
|||||||
rollup: 4.59.0
|
rollup: 4.59.0
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/node': 25.8.0
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
lightningcss: 1.31.1
|
lightningcss: 1.31.1
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
camelcase: 5.3.1
|
||||||
|
decamelize: 1.2.0
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
cliui: 6.0.0
|
||||||
|
decamelize: 1.2.0
|
||||||
|
find-up: 4.1.0
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
require-directory: 2.1.1
|
||||||
|
require-main-filename: 2.0.0
|
||||||
|
set-blocking: 2.0.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
which-module: 2.0.1
|
||||||
|
y18n: 4.0.3
|
||||||
|
yargs-parser: 18.1.3
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 893 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 273 B |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 715 B |
|
After Width: | Height: | Size: 400 B |
|
After Width: | Height: | Size: 148 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 225 B |
|
After Width: | Height: | Size: 210 B |
|
After Width: | Height: | Size: 210 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 744 B |
@@ -47,6 +47,15 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ascii"
|
name = "ascii"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -773,6 +782,12 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -783,6 +798,17 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.20"
|
version = "0.99.20"
|
||||||
@@ -988,13 +1014,14 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "emerald-legacy-launcher"
|
name = "emerald-legacy-launcher"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"http 1.4.0",
|
||||||
"image 0.24.9",
|
"image 0.24.9",
|
||||||
"libc",
|
"libc",
|
||||||
"percent-encoding",
|
"rand 0.8.5",
|
||||||
"reqwest 0.11.27",
|
"reqwest 0.11.27",
|
||||||
"rfd",
|
"rfd",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1005,8 +1032,12 @@ dependencies = [
|
|||||||
"tauri-plugin-drpc",
|
"tauri-plugin-drpc",
|
||||||
"tauri-plugin-gamepad",
|
"tauri-plugin-gamepad",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-process",
|
||||||
|
"tauri-plugin-updater",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1153,6 +1184,16 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d5b2eef6fafbf69f877e55509ce5b11a760690ac9700a2921be067aa6afaef6"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -1899,6 +1940,21 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.27.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||||
|
dependencies = [
|
||||||
|
"http 1.4.0",
|
||||||
|
"hyper 1.9.0",
|
||||||
|
"hyper-util",
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-tls"
|
name = "hyper-tls"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -2524,6 +2580,12 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minisign-verify"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -2775,6 +2837,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -2801,6 +2864,18 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-osa-kit"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"objc2",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-quartz-core"
|
name = "objc2-quartz-core"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -2917,6 +2992,20 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "osakit"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||||
|
dependencies = [
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"objc2-osa-kit",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -3675,15 +3764,20 @@ dependencies = [
|
|||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.9.0",
|
"hyper 1.9.0",
|
||||||
|
"hyper-rustls",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-platform-verifier",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sync_wrapper 1.0.2",
|
"sync_wrapper 1.0.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
@@ -3719,6 +3813,20 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpcdiscord"
|
name = "rpcdiscord"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -3760,6 +3868,32 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-native-certs"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||||
|
dependencies = [
|
||||||
|
"openssl-probe",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pemfile"
|
name = "rustls-pemfile"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -3769,6 +3903,53 @@ dependencies = [
|
|||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-platform-verifier"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation 0.10.1",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"jni",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"rustls",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"rustls-platform-verifier-android",
|
||||||
|
"rustls-webpki",
|
||||||
|
"security-framework",
|
||||||
|
"security-framework-sys",
|
||||||
|
"webpki-root-certs",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-platform-verifier-android"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -4111,6 +4292,17 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -4309,6 +4501,12 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swift-rs"
|
name = "swift-rs"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -4451,6 +4649,17 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tar"
|
||||||
|
version = "0.4.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
|
||||||
|
dependencies = [
|
||||||
|
"filetime",
|
||||||
|
"libc",
|
||||||
|
"xattr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "target-lexicon"
|
name = "target-lexicon"
|
||||||
version = "0.12.16"
|
version = "0.12.16"
|
||||||
@@ -4643,6 +4852,49 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-process"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
|
||||||
|
dependencies = [
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-updater"
|
||||||
|
version = "2.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"dirs",
|
||||||
|
"flate2",
|
||||||
|
"futures-util",
|
||||||
|
"http 1.4.0",
|
||||||
|
"infer",
|
||||||
|
"log",
|
||||||
|
"minisign-verify",
|
||||||
|
"osakit",
|
||||||
|
"percent-encoding",
|
||||||
|
"reqwest 0.13.2",
|
||||||
|
"rustls",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tar",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.10.1"
|
version = "2.10.1"
|
||||||
@@ -4907,6 +5159,30 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tungstenite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -5130,6 +5406,26 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http 1.4.0",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"native-tls",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"url",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -5212,6 +5508,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -5610,6 +5912,15 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-root-certs"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.2"
|
version = "0.38.2"
|
||||||
@@ -6409,6 +6720,16 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xattr"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -6534,6 +6855,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -6567,6 +6894,18 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"crc32fast",
|
||||||
|
"indexmap 2.13.1",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "emerald-legacy-launcher"
|
name = "emerald-legacy-launcher"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
description = "A FOSS, cross-platform launcher for Minecraft Legacy Edition"
|
description = "A FOSS, cross-platform launcher for Minecraft Legacy Edition"
|
||||||
authors = ["Emerald Team"]
|
authors = ["Emerald Team"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -24,8 +24,15 @@ tokio = { version = "1", features = ["full"] }
|
|||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
tokio-util = { version = "0.7.18", features = ["rt"] }
|
tokio-util = { version = "0.7.18", features = ["rt"] }
|
||||||
base64 = "0.21"
|
base64 = "0.21"
|
||||||
|
tokio-tungstenite = { version = "0.21", default-features = false, features = ["connect", "native-tls"] }
|
||||||
|
url = "2"
|
||||||
|
rand = "0.8"
|
||||||
|
http = "1"
|
||||||
rfd = "0.15"
|
rfd = "0.15"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
image = "0.24"
|
image = "0.24"
|
||||||
percent-encoding = "2.3"
|
|
||||||
steam_shortcuts_util = "1.1.8"
|
steam_shortcuts_util = "1.1.8"
|
||||||
|
tauri-plugin-process = "2"
|
||||||
|
|
||||||
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
|
|||||||
@@ -18,13 +18,23 @@
|
|||||||
"opener:default",
|
"opener:default",
|
||||||
"gamepad:default",
|
"gamepad:default",
|
||||||
"drpc:default",
|
"drpc:default",
|
||||||
|
"updater:default",
|
||||||
{
|
{
|
||||||
"identifier": "opener:allow-open-path",
|
"identifier": "opener:allow-open-path",
|
||||||
"allow": [{ "path": "**" }]
|
"allow": [
|
||||||
|
{
|
||||||
|
"path": "**"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identifier": "opener:allow-reveal-item-in-dir",
|
"identifier": "opener:allow-reveal-item-in-dir",
|
||||||
"allow": [{ "path": "**" }]
|
"allow": [
|
||||||
|
{
|
||||||
|
"path": "**"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"process:default"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"identifier": "desktop-capability",
|
||||||
|
"platforms": [
|
||||||
|
"macOS",
|
||||||
|
"windows",
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"updater:default",
|
||||||
|
"process:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Default permissions","local":true,"windows":["main"],"permissions":["core:default","core:app:default","core:event:default","core:image:default","core:menu:default","core:path:default","core:resources:default","core:tray:default","core:webview:default","core:window:default","opener:default","gamepad:default","drpc:default",{"identifier":"opener:allow-open-path","allow":[{"path":"**"}]},{"identifier":"opener:allow-reveal-item-in-dir","allow":[{"path":"**"}]}]}}
|
{"default":{"identifier":"default","description":"Default permissions","local":true,"windows":["main"],"permissions":["core:default","core:app:default","core:event:default","core:image:default","core:menu:default","core:path:default","core:resources:default","core:tray:default","core:webview:default","core:window:default","opener:default","gamepad:default","drpc:default","updater:default",{"identifier":"opener:allow-open-path","allow":[{"path":"**"}]},{"identifier":"opener:allow-reveal-item-in-dir","allow":[{"path":"**"}]},"process:default"]},"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["updater:default","process:default"],"platforms":["macOS","windows","linux"]}}
|
||||||
@@ -2443,6 +2443,90 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "opener:deny-reveal-item-in-dir",
|
"const": "opener:deny-reveal-item-in-dir",
|
||||||
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
|
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:default",
|
||||||
|
"markdownDescription": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the exit command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:allow-exit",
|
||||||
|
"markdownDescription": "Enables the exit command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the restart command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:allow-restart",
|
||||||
|
"markdownDescription": "Enables the restart command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the exit command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:deny-exit",
|
||||||
|
"markdownDescription": "Denies the exit command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the restart command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:deny-restart",
|
||||||
|
"markdownDescription": "Denies the restart command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:default",
|
||||||
|
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the check command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-check",
|
||||||
|
"markdownDescription": "Enables the check command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the download command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-download",
|
||||||
|
"markdownDescription": "Enables the download command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the download_and_install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-download-and-install",
|
||||||
|
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-install",
|
||||||
|
"markdownDescription": "Enables the install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the check command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-check",
|
||||||
|
"markdownDescription": "Denies the check command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the download command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-download",
|
||||||
|
"markdownDescription": "Denies the download command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the download_and_install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-download-and-install",
|
||||||
|
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-install",
|
||||||
|
"markdownDescription": "Denies the install command without any pre-configured scope."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2443,6 +2443,90 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "opener:deny-reveal-item-in-dir",
|
"const": "opener:deny-reveal-item-in-dir",
|
||||||
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
|
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:default",
|
||||||
|
"markdownDescription": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the exit command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:allow-exit",
|
||||||
|
"markdownDescription": "Enables the exit command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the restart command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:allow-restart",
|
||||||
|
"markdownDescription": "Enables the restart command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the exit command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:deny-exit",
|
||||||
|
"markdownDescription": "Denies the exit command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the restart command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "process:deny-restart",
|
||||||
|
"markdownDescription": "Denies the restart command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:default",
|
||||||
|
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the check command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-check",
|
||||||
|
"markdownDescription": "Enables the check command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the download command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-download",
|
||||||
|
"markdownDescription": "Enables the download command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the download_and_install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-download-and-install",
|
||||||
|
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:allow-install",
|
||||||
|
"markdownDescription": "Enables the install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the check command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-check",
|
||||||
|
"markdownDescription": "Denies the check command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the download command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-download",
|
||||||
|
"markdownDescription": "Denies the download command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the download_and_install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-download-and-install",
|
||||||
|
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the install command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "updater:deny-install",
|
||||||
|
"markdownDescription": "Denies the install command without any pre-configured scope."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::net::UdpSocket;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use steam_shortcuts_util::{Shortcut, parse_shortcuts, shortcuts_to_bytes};
|
use steam_shortcuts_util::{Shortcut, parse_shortcuts, shortcuts_to_bytes};
|
||||||
@@ -8,9 +9,10 @@ use steam_shortcuts_util::{Shortcut, parse_shortcuts, shortcuts_to_bytes};
|
|||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
|
|
||||||
use tauri::{AppHandle, Emitter, State, Manager};
|
use tauri::{AppHandle, Emitter, State, Manager};
|
||||||
use futures_util::StreamExt;
|
use futures_util::{StreamExt, SinkExt};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -60,6 +62,7 @@ pub struct AppConfig {
|
|||||||
pub music_vol: Option<u32>,
|
pub music_vol: Option<u32>,
|
||||||
pub sfx_vol: Option<u32>,
|
pub sfx_vol: Option<u32>,
|
||||||
pub legacy_mode: Option<bool>,
|
pub legacy_mode: Option<bool>,
|
||||||
|
pub mangohud_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
@@ -87,9 +90,20 @@ pub struct ScreenshotInfo {
|
|||||||
pub date: u64,
|
pub date: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct HttpResponse {
|
||||||
|
status: u16,
|
||||||
|
body: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct DownloadState { pub token: Arc<Mutex<Option<CancellationToken>>> }
|
pub struct DownloadState { pub token: Arc<Mutex<Option<CancellationToken>>> }
|
||||||
pub struct GameState { pub child: Arc<Mutex<Option<tokio::process::Child>>> }
|
pub struct GameState { pub child: Arc<Mutex<Option<tokio::process::Child>>> }
|
||||||
|
|
||||||
|
pub struct ProxyGuard {
|
||||||
|
pub cancel_token: Arc<Mutex<Option<CancellationToken>>>,
|
||||||
|
pub local_port: Arc<Mutex<Option<u16>>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
#[derive(Serialize, Clone, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -251,6 +265,7 @@ fn load_config(app: AppHandle) -> AppConfig {
|
|||||||
music_vol: Some(50),
|
music_vol: Some(50),
|
||||||
sfx_vol: Some(100),
|
sfx_vol: Some(100),
|
||||||
legacy_mode: Some(false),
|
legacy_mode: Some(false),
|
||||||
|
mangohud_enabled: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +364,29 @@ fn read_binary_file(path: String) -> Result<Vec<u8>, String> {
|
|||||||
fs::read(path).map_err(|e| e.to_string())
|
fs::read(path).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn read_screenshot_as_data_url(path: String) -> Result<String, String> {
|
||||||
|
let data = fs::read(&path).map_err(|e| e.to_string())?;
|
||||||
|
let mime = if path.to_lowercase().ends_with(".png") {
|
||||||
|
"image/png"
|
||||||
|
} else if path.to_lowercase().ends_with(".jpg") || path.to_lowercase().ends_with(".jpeg") {
|
||||||
|
"image/jpeg"
|
||||||
|
} else if path.to_lowercase().ends_with(".gif") {
|
||||||
|
"image/gif"
|
||||||
|
} else if path.to_lowercase().ends_with(".webp") {
|
||||||
|
"image/webp"
|
||||||
|
} else if data.len() > 4 && data[..4] == [0x89, 0x50, 0x4E, 0x47] {
|
||||||
|
"image/png"
|
||||||
|
} else if data.len() > 2 && data[..2] == [0xFF, 0xD8] {
|
||||||
|
"image/jpeg"
|
||||||
|
} else {
|
||||||
|
"image/png"
|
||||||
|
};
|
||||||
|
use base64::Engine;
|
||||||
|
let b64 = base64::engine::general_purpose::STANDARD.encode(&data);
|
||||||
|
Ok(format!("data:{};base64,{}", mime, b64))
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_available_runners(app: AppHandle) -> Vec<Runner> {
|
fn get_available_runners(app: AppHandle) -> Vec<Runner> {
|
||||||
let mut runners = Vec::new();
|
let mut runners = Vec::new();
|
||||||
@@ -836,13 +874,8 @@ async fn download_and_install(app: AppHandle, state: State<'_, DownloadState>, u
|
|||||||
{ *state.token.lock().await = None; }
|
{ *state.token.lock().await = None; }
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let unzip_check = Command::new("unzip").arg("-v").status();
|
let status = Command::new("bsdtar")
|
||||||
if unzip_check.is_err() || !unzip_check.unwrap().success() {
|
.args(["-xf", zip_path.to_str().unwrap(), "-C", instance_dir.to_str().unwrap()])
|
||||||
return Err("The 'unzip' command was not found. Please install it (e.g., 'sudo apt install unzip') to continue.".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = Command::new("unzip")
|
|
||||||
.args(["-o", "-q", zip_path.to_str().unwrap(), "-d", instance_dir.to_str().unwrap()])
|
|
||||||
.status()
|
.status()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
@@ -900,63 +933,78 @@ async fn download_and_install(app: AppHandle, state: State<'_, DownloadState>, u
|
|||||||
Ok("Success".into())
|
Ok("Success".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_server_list(instance_dir: &PathBuf, servers: Vec<McServer>) {
|
#[repr(C, packed)]
|
||||||
let servers_db = instance_dir.join("servers.db");
|
struct LanBroadcastPacket {
|
||||||
let mut all_servers = Vec::new();
|
magic: u32,
|
||||||
if let Ok(content) = fs::read(&servers_db) {
|
net_version: u16,
|
||||||
if content.len() >= 12 && &content[0..4] == b"MCSV" {
|
game_port: u16,
|
||||||
let count = u32::from_le_bytes(content[8..12].try_into().unwrap_or([0; 4]));
|
host_name: [u16; 32],
|
||||||
let mut pos = 12;
|
player_count: u8,
|
||||||
for _ in 0..count {
|
max_players: u8,
|
||||||
if pos + 2 > content.len() { break; }
|
game_host_settings: u32,
|
||||||
let ip_len = u16::from_le_bytes(content[pos..pos+2].try_into().unwrap_or([0; 2])) as usize;
|
texture_pack_parent_id: u32,
|
||||||
pos += 2;
|
sub_texture_pack_id: u8,
|
||||||
if pos + ip_len > content.len() { break; }
|
is_joinable: u8,
|
||||||
let ip = String::from_utf8_lossy(&content[pos..pos+ip_len]).to_string();
|
}
|
||||||
pos += ip_len;
|
|
||||||
if pos + 2 > content.len() { break; }
|
|
||||||
let port = u16::from_le_bytes(content[pos..pos+2].try_into().unwrap_or([0; 2]));
|
|
||||||
pos += 2;
|
|
||||||
if pos + 2 > content.len() { break; }
|
|
||||||
let name_len = u16::from_le_bytes(content[pos..pos+2].try_into().unwrap_or([0; 2])) as usize;
|
|
||||||
pos += 2;
|
|
||||||
if pos + name_len > content.len() { break; }
|
|
||||||
let name = String::from_utf8_lossy(&content[pos..pos+name_len]).to_string();
|
|
||||||
pos += name_len;
|
|
||||||
|
|
||||||
all_servers.push(McServer { name, ip, port });
|
struct LanServicesGuard(CancellationToken);
|
||||||
|
impl Drop for LanServicesGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.cancel();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_lan_broadcast(servers: &[(McServer, u16)], cancel: CancellationToken) {
|
||||||
|
for (server, broadcast_port) in servers {
|
||||||
|
let cancel = cancel.clone();
|
||||||
|
let name = server.name.clone();
|
||||||
|
let port = *broadcast_port;
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let socket = match UdpSocket::bind("0.0.0.0:0") {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[Emerald] LAN broadcast socket bind failed: {e}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = socket.set_broadcast(true) {
|
||||||
|
eprintln!("[Emerald] LAN broadcast set_broadcast failed: {e}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for s in servers {
|
let mut host_name = [0u16; 32];
|
||||||
all_servers.push(s);
|
for (i, c) in name.encode_utf16().take(31).enumerate() {
|
||||||
|
host_name[i] = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut unique_servers = Vec::new();
|
let packet = LanBroadcastPacket {
|
||||||
let mut seen: std::collections::HashSet<(String, u16)> = std::collections::HashSet::new();
|
magic: 0x4D434C4E,
|
||||||
for s in all_servers {
|
net_version: 170,
|
||||||
let key = (s.ip.clone(), s.port);
|
game_port: port,
|
||||||
if seen.insert(key) {
|
host_name,
|
||||||
unique_servers.push(s);
|
player_count: 0,
|
||||||
}
|
max_players: 8,
|
||||||
}
|
game_host_settings: 0,
|
||||||
|
texture_pack_parent_id: 0,
|
||||||
|
sub_texture_pack_id: 0,
|
||||||
|
is_joinable: 1,
|
||||||
|
};
|
||||||
|
|
||||||
let mut file_content = Vec::new();
|
let packet_bytes = unsafe {
|
||||||
file_content.extend_from_slice(b"MCSV");
|
std::slice::from_raw_parts(
|
||||||
file_content.extend_from_slice(&1u32.to_le_bytes());
|
&packet as *const LanBroadcastPacket as *const u8,
|
||||||
file_content.extend_from_slice(&(unique_servers.len() as u32).to_le_bytes());
|
std::mem::size_of::<LanBroadcastPacket>(),
|
||||||
for server in unique_servers {
|
)
|
||||||
let ip_bytes = server.ip.as_bytes();
|
};
|
||||||
let name_bytes = server.name.as_bytes();
|
|
||||||
file_content.extend_from_slice(&(ip_bytes.len() as u16).to_le_bytes());
|
while !cancel.is_cancelled() {
|
||||||
file_content.extend_from_slice(ip_bytes);
|
if let Err(e) = socket.send_to(packet_bytes, "255.255.255.255:25566") {
|
||||||
file_content.extend_from_slice(&server.port.to_le_bytes());
|
eprintln!("[Emerald] LAN broadcast send failed: {e}");
|
||||||
file_content.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
|
}
|
||||||
file_content.extend_from_slice(name_bytes);
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let _ = fs::create_dir_all(instance_dir);
|
|
||||||
let _ = fs::write(&servers_db, file_content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform_dlc_sync(app: &AppHandle, instance_dir: &PathBuf) -> Result<(), String> {
|
fn perform_dlc_sync(app: &AppHandle, instance_dir: &PathBuf) -> Result<(), String> {
|
||||||
@@ -1127,8 +1175,8 @@ async fn workshop_install(app: AppHandle, request: WorkshopInstallRequest) -> Re
|
|||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let status = Command::new("unzip")
|
let status = Command::new("bsdtar")
|
||||||
.args(["-o", "-q", zip_tmp.to_str().unwrap(), "-d", dest_dir.to_str().unwrap()])
|
.args(["-xf", zip_tmp.to_str().unwrap(), "-C", dest_dir.to_str().unwrap()])
|
||||||
.status()
|
.status()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
@@ -1308,7 +1356,17 @@ async fn launch_game(app: AppHandle, state: State<'_, GameState>, instance_id: S
|
|||||||
perform_instance_sync(&app, &instance_id).await?;
|
perform_instance_sync(&app, &instance_id).await?;
|
||||||
let working_dir = get_instance_working_dir(&app, &instance_id);
|
let working_dir = get_instance_working_dir(&app, &instance_id);
|
||||||
let config = load_config(app.clone());
|
let config = load_config(app.clone());
|
||||||
ensure_server_list(&working_dir, servers);
|
let _lan_services: Option<LanServicesGuard> = if !servers.is_empty() {
|
||||||
|
let cancel = CancellationToken::new();
|
||||||
|
let servers_with_ports: Vec<(McServer, u16)> = servers
|
||||||
|
.iter()
|
||||||
|
.map(|s| (s.clone(), s.port))
|
||||||
|
.collect();
|
||||||
|
start_lan_broadcast(&servers_with_ports, cancel.clone());
|
||||||
|
Some(LanServicesGuard(cancel))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let game_exe = working_dir.join("Minecraft.Client.exe");
|
let game_exe = working_dir.join("Minecraft.Client.exe");
|
||||||
if !game_exe.exists() {
|
if !game_exe.exists() {
|
||||||
return Err("Game executable not found in instance folder.".into());
|
return Err("Game executable not found in instance folder.".into());
|
||||||
@@ -1319,23 +1377,49 @@ async fn launch_game(app: AppHandle, state: State<'_, GameState>, instance_id: S
|
|||||||
if let Some(runner_id) = config.linux_runner {
|
if let Some(runner_id) = config.linux_runner {
|
||||||
let runners = get_available_runners(app.clone());
|
let runners = get_available_runners(app.clone());
|
||||||
if let Some(runner) = runners.into_iter().find(|r| r.id == runner_id) {
|
if let Some(runner) = runners.into_iter().find(|r| r.id == runner_id) {
|
||||||
let mut cmd = if runner.r#type == "proton" {
|
let is_proton = runner.r#type == "proton";
|
||||||
let proton_exe = PathBuf::from(&runner.path).join("proton");
|
let program = if is_proton {
|
||||||
let mut c = tokio::process::Command::new(proton_exe);
|
PathBuf::from(&runner.path).join("proton").to_string_lossy().to_string()
|
||||||
let compat_data = working_dir.join("proton_prefix");
|
|
||||||
fs::create_dir_all(&compat_data).map_err(|e| e.to_string())?;
|
|
||||||
if std::env::var("STEAM_COMPAT_CLIENT_INSTALL_PATH").is_err() {
|
|
||||||
c.env("STEAM_COMPAT_CLIENT_INSTALL_PATH", "");
|
|
||||||
}
|
|
||||||
c.env("STEAM_COMPAT_DATA_PATH", compat_data.to_str().unwrap());
|
|
||||||
if std::env::var("SteamAppId").is_err() {
|
|
||||||
c.env("SteamAppId", "480");
|
|
||||||
}
|
|
||||||
c.arg("run");
|
|
||||||
c
|
|
||||||
} else {
|
} else {
|
||||||
tokio::process::Command::new(runner.path)
|
runner.path.clone()
|
||||||
};
|
};
|
||||||
|
let mut args: Vec<String> = Vec::new();
|
||||||
|
if is_proton {
|
||||||
|
args.push("run".to_string());
|
||||||
|
}
|
||||||
|
let compat_data = if is_proton {
|
||||||
|
let cd = working_dir.join("proton_prefix");
|
||||||
|
fs::create_dir_all(&cd).map_err(|e| e.to_string())?;
|
||||||
|
Some(cd)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mangohud = config.mangohud_enabled.unwrap_or(false);
|
||||||
|
let (prog, extra_args): (&str, &[&str]) = if mangohud {
|
||||||
|
("mangohud", &[&program])
|
||||||
|
} else {
|
||||||
|
(&program, &[])
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmd = tokio::process::Command::new(prog);
|
||||||
|
for a in extra_args {
|
||||||
|
cmd.arg(a);
|
||||||
|
}
|
||||||
|
for a in &args {
|
||||||
|
cmd.arg(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_proton {
|
||||||
|
let cd = compat_data.as_ref().unwrap();
|
||||||
|
if std::env::var("STEAM_COMPAT_CLIENT_INSTALL_PATH").is_err() {
|
||||||
|
cmd.env("STEAM_COMPAT_CLIENT_INSTALL_PATH", "");
|
||||||
|
}
|
||||||
|
cmd.env("STEAM_COMPAT_DATA_PATH", cd.to_str().unwrap());
|
||||||
|
if std::env::var("SteamAppId").is_err() {
|
||||||
|
cmd.env("SteamAppId", "480");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
@@ -1809,41 +1893,573 @@ async fn save_global_skin_pck(app: AppHandle, pck_data: Vec<u8>) -> Result<(), S
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn http_proxy_request(method: String, url: String, body: Option<String>, headers: std::collections::HashMap<String, String>) -> Result<HttpResponse, String> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut req = match method.to_uppercase().as_str() {
|
||||||
|
"GET" => client.get(&url),
|
||||||
|
"POST" => client.post(&url),
|
||||||
|
"PUT" => client.put(&url),
|
||||||
|
"DELETE" => client.delete(&url),
|
||||||
|
_ => return Err(format!("Unsupported method: {}", method)),
|
||||||
|
};
|
||||||
|
for (k, v) in headers {
|
||||||
|
req = req.header(k, v);
|
||||||
|
}
|
||||||
|
if let Some(b) = body {
|
||||||
|
req = req.body(b);
|
||||||
|
}
|
||||||
|
let res = req.send().await.map_err(|e| e.to_string())?;
|
||||||
|
let status = res.status().as_u16();
|
||||||
|
let text = res.text().await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(HttpResponse {
|
||||||
|
status,
|
||||||
|
body: text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct P2pEndpoint {
|
||||||
|
pub ip: String,
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ws_base_url(api_base_url: &str) -> String {
|
||||||
|
if api_base_url.starts_with("https") {
|
||||||
|
api_base_url.replace("https", "wss")
|
||||||
|
} else {
|
||||||
|
api_base_url.replace("http", "ws")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stun_discover_impl() -> Result<P2pEndpoint, String> {
|
||||||
|
let socket = tokio::net::UdpSocket::bind("0.0.0.0:0").await.map_err(|e| e.to_string())?;
|
||||||
|
let stun_addr = tokio::net::lookup_host("stun.l.google.com:19302").await
|
||||||
|
.map_err(|e| format!("STUN DNS lookup failed: {}", e))?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| "STUN DNS returned no addresses".to_string())?;
|
||||||
|
|
||||||
|
let magic_cookie: u32 = 0x2112A442;
|
||||||
|
let mut trans_id = [0u8; 12];
|
||||||
|
rand::Rng::fill(&mut rand::thread_rng(), &mut trans_id);
|
||||||
|
|
||||||
|
let mut req = Vec::with_capacity(20);
|
||||||
|
req.extend_from_slice(&0x0001u16.to_be_bytes());
|
||||||
|
req.extend_from_slice(&0x0000u16.to_be_bytes());
|
||||||
|
req.extend_from_slice(&magic_cookie.to_be_bytes());
|
||||||
|
req.extend_from_slice(&trans_id);
|
||||||
|
|
||||||
|
socket.send_to(&req, stun_addr).await.map_err(|e| format!("STUN send: {}", e))?;
|
||||||
|
|
||||||
|
let mut buf = [0u8; 512];
|
||||||
|
tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
socket.recv_from(&mut buf)
|
||||||
|
).await.map_err(|_| "STUN request timed out (5s)".to_string())?
|
||||||
|
.map_err(|e| format!("STUN recv: {}", e))?;
|
||||||
|
|
||||||
|
let msg_type = u16::from_be_bytes([buf[0], buf[1]]);
|
||||||
|
let rcvd_cookie = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
|
||||||
|
|
||||||
|
if msg_type != 0x0101 || rcvd_cookie != magic_cookie {
|
||||||
|
return Err(format!("Invalid STUN response (type=0x{:04X}, cookie=0x{:08X})", msg_type, rcvd_cookie));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pos = 20;
|
||||||
|
while pos + 4 <= buf.len() {
|
||||||
|
let attr_type = u16::from_be_bytes([buf[pos], buf[pos+1]]);
|
||||||
|
let attr_len = u16::from_be_bytes([buf[pos+2], buf[pos+3]]) as usize;
|
||||||
|
pos += 4;
|
||||||
|
if attr_type == 0x0020 && attr_len >= 8 && pos + 8 <= buf.len() {
|
||||||
|
let _family = buf[pos+1];
|
||||||
|
let xport = u16::from_be_bytes([buf[pos+2], buf[pos+3]]);
|
||||||
|
let port = xport ^ (magic_cookie >> 16) as u16;
|
||||||
|
let ip_bytes = [
|
||||||
|
buf[pos+4] ^ (magic_cookie >> 24) as u8,
|
||||||
|
buf[pos+5] ^ (magic_cookie >> 16) as u8,
|
||||||
|
buf[pos+6] ^ (magic_cookie >> 8) as u8,
|
||||||
|
buf[pos+7] ^ magic_cookie as u8,
|
||||||
|
];
|
||||||
|
return Ok(P2pEndpoint {
|
||||||
|
ip: format!("{}.{}.{}.{}", ip_bytes[0], ip_bytes[1], ip_bytes[2], ip_bytes[3]),
|
||||||
|
port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pos += attr_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("No XOR-MAPPED-ADDRESS in STUN response".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn stun_discover() -> Result<P2pEndpoint, String> {
|
||||||
|
stun_discover_impl().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_relay_proxy(
|
||||||
|
proxy_state: &ProxyGuard,
|
||||||
|
ws_url: &str,
|
||||||
|
auth_token: &str,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
) -> Result<u16, String> {
|
||||||
|
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||||
|
|
||||||
|
eprintln!("[Emerald] Joiner relay: connecting WS...");
|
||||||
|
let mut request = ws_url
|
||||||
|
.into_client_request()
|
||||||
|
.map_err(|e| format!("Failed to build WS request: {}", e))?;
|
||||||
|
request.headers_mut().insert(
|
||||||
|
http::header::AUTHORIZATION,
|
||||||
|
format!("Bearer {}", auth_token)
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "Invalid auth header value".to_string())?,
|
||||||
|
);
|
||||||
|
request.headers_mut().insert(
|
||||||
|
http::header::USER_AGENT,
|
||||||
|
"MCLCE-LceLive/1.0"
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "Invalid UA header value".to_string())?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (ws_stream, _) = tokio_tungstenite::connect_async(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Relay WS connect failed: {}", e))?;
|
||||||
|
eprintln!("[Emerald] Joiner relay: WS connected");
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:0")
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Bind failed: {}", e))?;
|
||||||
|
let local_port = listener.local_addr().map_err(|e| e.to_string())?.port();
|
||||||
|
eprintln!("[Emerald] Joiner relay: bound on 0.0.0.0:{}", local_port);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut port = proxy_state.local_port.lock().await;
|
||||||
|
*port = Some(local_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
eprintln!("[Emerald] Joiner relay: waiting for TCP accept on port {}...", local_port);
|
||||||
|
let (tcp_stream, _) = tokio::select! {
|
||||||
|
result = listener.accept() => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: TCP accepted");
|
||||||
|
result.map_err(|e| format!("Accept failed: {}", e)).unwrap()
|
||||||
|
},
|
||||||
|
_ = cancel.cancelled() => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: cancelled before accept");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!("[Emerald] Joiner relay: starting forwarders");
|
||||||
|
let (tcp_read, tcp_write) = tcp_stream.into_split();
|
||||||
|
let (ws_write, ws_read) = ws_stream.split();
|
||||||
|
|
||||||
|
let cancel_ws = cancel.clone();
|
||||||
|
let forward_tcp = tokio::spawn(async move {
|
||||||
|
let mut ws_write = ws_write;
|
||||||
|
let mut tcp_read = tcp_read;
|
||||||
|
let mut buf = [0u8; 65536];
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
result = tcp_read.read(&mut buf) => {
|
||||||
|
match result {
|
||||||
|
Ok(0) => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: TCP→WS EOF");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: TCP→WS read error: {e}");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Ok(n) => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: TCP→WS forwarding {} bytes", n);
|
||||||
|
if ws_write.send(tokio_tungstenite::tungstenite::Message::Binary(buf[..n].to_vec())).await.is_err() {
|
||||||
|
eprintln!("[Emerald] Joiner relay: TCP→WS send error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = cancel_ws.cancelled() => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: TCP→WS cancelled");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let cancel_tcp = cancel.clone();
|
||||||
|
let forward_ws = tokio::spawn(async move {
|
||||||
|
let ws_read = ws_read;
|
||||||
|
let mut tcp_write = tcp_write;
|
||||||
|
tokio::pin!(ws_read);
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
result = ws_read.next() => {
|
||||||
|
match result {
|
||||||
|
Some(Ok(tokio_tungstenite::tungstenite::Message::Binary(data))) => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: WS→TCP forwarding {} bytes", data.len());
|
||||||
|
if tcp_write.write_all(&data).await.is_err() {
|
||||||
|
eprintln!("[Emerald] Joiner relay: WS→TCP write error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: WS→TCP close frame");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: WS→TCP stream ended");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: WS→TCP error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = cancel_tcp.cancelled() => {
|
||||||
|
eprintln!("[Emerald] Joiner relay: WS→TCP cancelled");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = forward_tcp => eprintln!("[Emerald] Joiner relay: forward_tcp done"),
|
||||||
|
_ = forward_ws => eprintln!("[Emerald] Joiner relay: forward_ws done"),
|
||||||
|
_ = cancel.cancelled() => eprintln!("[Emerald] Joiner relay: cancelled"),
|
||||||
|
}
|
||||||
|
eprintln!("[Emerald] Joiner relay: relay task ended");
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(local_port)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_host_relay(
|
||||||
|
_proxy_state: &ProxyGuard,
|
||||||
|
ws_url: &str,
|
||||||
|
auth_token: &str,
|
||||||
|
game_port: u16,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||||
|
|
||||||
|
eprintln!("[Emerald] Host relay: connecting WS...");
|
||||||
|
let mut request = ws_url
|
||||||
|
.into_client_request()
|
||||||
|
.map_err(|e| format!("Failed to build WS request: {}", e))?;
|
||||||
|
request.headers_mut().insert(
|
||||||
|
http::header::AUTHORIZATION,
|
||||||
|
format!("Bearer {}", auth_token)
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "Invalid auth header value".to_string())?,
|
||||||
|
);
|
||||||
|
request.headers_mut().insert(
|
||||||
|
http::header::USER_AGENT,
|
||||||
|
"MCLCE-LceLive/1.0"
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "Invalid UA header value".to_string())?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (ws_stream, _) = tokio_tungstenite::connect_async(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Host relay WS connect failed: {}", e))?;
|
||||||
|
eprintln!("[Emerald] Host relay: WS connected");
|
||||||
|
|
||||||
|
eprintln!("[Emerald] Host relay: connecting to game 127.0.0.1:{}...", game_port);
|
||||||
|
let game_stream = loop {
|
||||||
|
match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", game_port)).await {
|
||||||
|
Ok(stream) => {
|
||||||
|
eprintln!("[Emerald] Host relay: connected to game");
|
||||||
|
break stream;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[Emerald] Host relay: game connect failed (retrying): {e}");
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => return Err("Host relay cancelled".into()),
|
||||||
|
_ = tokio::time::sleep(std::time::Duration::from_secs(2)) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!("[Emerald] Host relay: starting forwarders");
|
||||||
|
let (game_read, game_write) = game_stream.into_split();
|
||||||
|
let (ws_write, ws_read) = ws_stream.split();
|
||||||
|
|
||||||
|
let cancel_ws = cancel.clone();
|
||||||
|
let forward_game = tokio::spawn(async move {
|
||||||
|
let mut ws_write = ws_write;
|
||||||
|
let mut game_read = game_read;
|
||||||
|
let mut buf = [0u8; 65536];
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
result = game_read.read(&mut buf) => {
|
||||||
|
match result {
|
||||||
|
Ok(0) => {
|
||||||
|
eprintln!("[Emerald] Host relay: game→WS EOF");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[Emerald] Host relay: game→WS read error: {e}");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
Ok(n) => {
|
||||||
|
eprintln!("[Emerald] Host relay: game→WS forwarding {} bytes", n);
|
||||||
|
if ws_write.send(tokio_tungstenite::tungstenite::Message::Binary(buf[..n].to_vec())).await.is_err() {
|
||||||
|
eprintln!("[Emerald] Host relay: game→WS send error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = cancel_ws.cancelled() => {
|
||||||
|
eprintln!("[Emerald] Host relay: game→WS cancelled");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let cancel_ws2 = cancel.clone();
|
||||||
|
let forward_ws = tokio::spawn(async move {
|
||||||
|
let ws_read = ws_read;
|
||||||
|
let mut game_write = game_write;
|
||||||
|
tokio::pin!(ws_read);
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
result = ws_read.next() => {
|
||||||
|
match result {
|
||||||
|
Some(Ok(tokio_tungstenite::tungstenite::Message::Binary(data))) => {
|
||||||
|
eprintln!("[Emerald] Host relay: WS→game forwarding {} bytes", data.len());
|
||||||
|
if game_write.write_all(&data).await.is_err() {
|
||||||
|
eprintln!("[Emerald] Host relay: WS→game write error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => {
|
||||||
|
eprintln!("[Emerald] Host relay: WS→game close frame");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
eprintln!("[Emerald] Host relay: WS→game stream ended");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
eprintln!("[Emerald] Host relay: WS→game error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = cancel_ws2.cancelled() => {
|
||||||
|
eprintln!("[Emerald] Host relay: WS→game cancelled");
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = forward_game => eprintln!("[Emerald] Host relay: forward_game done"),
|
||||||
|
_ = forward_ws => eprintln!("[Emerald] Host relay: forward_ws done"),
|
||||||
|
_ = cancel.cancelled() => eprintln!("[Emerald] Host relay: cancelled"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_direct_proxy(
|
||||||
|
proxy_state: &ProxyGuard,
|
||||||
|
target_ip: &str,
|
||||||
|
target_port: u16,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
) -> Result<u16, String> {
|
||||||
|
let remote = tokio::net::TcpStream::connect(format!("{}:{}", target_ip, target_port))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Direct TCP connect failed: {}", e))?;
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:0")
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Bind failed: {}", e))?;
|
||||||
|
let local_port = listener.local_addr().map_err(|e| e.to_string())?.port();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut port = proxy_state.local_port.lock().await;
|
||||||
|
*port = Some(local_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (local_stream, _) = tokio::select! {
|
||||||
|
result = listener.accept() => result.map_err(|e| format!("Accept failed: {}", e))?,
|
||||||
|
_ = cancel.cancelled() => return Err("Proxy cancelled".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut a_read, mut a_write) = remote.into_split();
|
||||||
|
let (mut b_read, mut b_write) = local_stream.into_split();
|
||||||
|
let cancel_a = cancel.clone();
|
||||||
|
|
||||||
|
let task_a = tokio::spawn(async move {
|
||||||
|
let mut buf = [0u8; 65536];
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
result = a_read.read(&mut buf) => {
|
||||||
|
match result {
|
||||||
|
Ok(0) | Err(_) => break,
|
||||||
|
Ok(n) => {
|
||||||
|
if b_write.write_all(&buf[..n]).await.is_err() { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = cancel_a.cancelled() => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let cancel_b = cancel.clone();
|
||||||
|
let task_b = tokio::spawn(async move {
|
||||||
|
let mut buf = [0u8; 65536];
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
result = b_read.read(&mut buf) => {
|
||||||
|
match result {
|
||||||
|
Ok(0) | Err(_) => break,
|
||||||
|
Ok(n) => {
|
||||||
|
if a_write.write_all(&buf[..n]).await.is_err() { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = cancel_b.cancelled() => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = task_a => {},
|
||||||
|
_ = task_b => {},
|
||||||
|
_ = cancel.cancelled() => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(local_port)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn start_direct_proxy(
|
||||||
|
proxy_state: State<'_, ProxyGuard>,
|
||||||
|
target_ip: String,
|
||||||
|
target_port: u16,
|
||||||
|
) -> Result<u16, String> {
|
||||||
|
let cancel = CancellationToken::new();
|
||||||
|
{
|
||||||
|
let mut token = proxy_state.cancel_token.lock().await;
|
||||||
|
if let Some(old) = token.take() {
|
||||||
|
old.cancel();
|
||||||
|
}
|
||||||
|
*token = Some(cancel.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_port = run_direct_proxy(&proxy_state, &target_ip, target_port, cancel).await?;
|
||||||
|
Ok(local_port)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn start_relay_proxy(
|
||||||
|
proxy_state: State<'_, ProxyGuard>,
|
||||||
|
api_base_url: String,
|
||||||
|
access_token: String,
|
||||||
|
session_id: String,
|
||||||
|
) -> Result<u16, String> {
|
||||||
|
let ws_base = ws_base_url(&api_base_url);
|
||||||
|
let ws_url = format!("{}/api/relay/ws?sessionId={}&role=joiner", ws_base, session_id);
|
||||||
|
|
||||||
|
let cancel = CancellationToken::new();
|
||||||
|
{
|
||||||
|
let mut token = proxy_state.cancel_token.lock().await;
|
||||||
|
if let Some(old) = token.take() {
|
||||||
|
old.cancel();
|
||||||
|
}
|
||||||
|
*token = Some(cancel.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_port = run_relay_proxy(&proxy_state, &ws_url, &access_token, cancel).await?;
|
||||||
|
Ok(local_port)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn start_host_relay(
|
||||||
|
proxy_state: State<'_, ProxyGuard>,
|
||||||
|
api_base_url: String,
|
||||||
|
access_token: String,
|
||||||
|
session_id: String,
|
||||||
|
game_port: u16,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let ws_base = ws_base_url(&api_base_url);
|
||||||
|
let ws_url = format!("{}/api/relay/ws?sessionId={}&role=host", ws_base, session_id);
|
||||||
|
|
||||||
|
let cancel = CancellationToken::new();
|
||||||
|
{
|
||||||
|
let mut token = proxy_state.cancel_token.lock().await;
|
||||||
|
if let Some(old) = token.take() {
|
||||||
|
old.cancel();
|
||||||
|
}
|
||||||
|
*token = Some(cancel.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
run_host_relay(&proxy_state, &ws_url, &access_token, game_port, cancel).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn stop_proxy(proxy_state: State<'_, ProxyGuard>) -> Result<(), String> {
|
||||||
|
let mut token = proxy_state.cancel_token.lock().await;
|
||||||
|
if let Some(t) = token.take() {
|
||||||
|
t.cancel();
|
||||||
|
}
|
||||||
|
let mut port = proxy_state.local_port.lock().await;
|
||||||
|
*port = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn join_game(
|
||||||
|
app: AppHandle,
|
||||||
|
game_state: State<'_, GameState>,
|
||||||
|
_proxy_state: State<'_, ProxyGuard>,
|
||||||
|
_api_base_url: String,
|
||||||
|
_access_token: String,
|
||||||
|
host_ip: String,
|
||||||
|
host_port: u16,
|
||||||
|
_session_id: String,
|
||||||
|
instance_id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let server = McServer {
|
||||||
|
name: host_ip.clone(),
|
||||||
|
ip: host_ip,
|
||||||
|
port: host_port,
|
||||||
|
};
|
||||||
|
|
||||||
|
launch_game(app, game_state, instance_id, vec![server]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_instance_path(app: tauri::AppHandle, instance_id: String) -> String {
|
||||||
|
get_instance_working_dir(&app, &instance_id).to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.manage(DownloadState { token: Arc::new(Mutex::new(None)) })
|
.manage(DownloadState { token: Arc::new(Mutex::new(None)) })
|
||||||
.manage(GameState { child: Arc::new(Mutex::new(None)) })
|
.manage(GameState { child: Arc::new(Mutex::new(None)) })
|
||||||
|
.manage(ProxyGuard { cancel_token: Arc::new(Mutex::new(None)), local_port: Arc::new(Mutex::new(None)) })
|
||||||
.plugin(tauri_plugin_gamepad::init())
|
.plugin(tauri_plugin_gamepad::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_drpc::init())
|
.plugin(tauri_plugin_drpc::init())
|
||||||
.register_uri_scheme_protocol("screenshots", |_app, request| {
|
.invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, pick_folder, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, workshop_uninstall, workshop_list_installed, get_screenshots, delete_screenshot, open_screenshot_folder, save_global_skin_pck, check_game_update, check_macos_runtime_installed, check_macos_runtime_installed_fast, download_logo, pick_file, save_file_dialog, write_binary_file, read_binary_file, read_screenshot_as_data_url, add_to_steam, http_proxy_request, get_instance_path, stun_discover, start_direct_proxy, start_relay_proxy, start_host_relay, stop_proxy, join_game])
|
||||||
let uri = request.uri().path();
|
|
||||||
let decoded_path = percent_encoding::percent_decode_str(uri).decode_utf8_lossy();
|
|
||||||
let mut path_str = decoded_path.to_string();
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if path_str.starts_with('/') {
|
|
||||||
path_str = path_str[1..].to_string();
|
|
||||||
}
|
|
||||||
let path = std::path::Path::new(&path_str);
|
|
||||||
|
|
||||||
match std::fs::read(path) {
|
|
||||||
Ok(data) => {
|
|
||||||
tauri::http::Response::builder()
|
|
||||||
.header("Content-Type", "image/png")
|
|
||||||
.header("Access-Control-Allow-Origin", "*")
|
|
||||||
.body(data)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
tauri::http::Response::builder()
|
|
||||||
.status(404)
|
|
||||||
.body(Vec::new())
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, pick_folder, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, workshop_uninstall, workshop_list_installed, get_screenshots, delete_screenshot, open_screenshot_folder, save_global_skin_pck, check_game_update, check_macos_runtime_installed, check_macos_runtime_installed_fast, download_logo, pick_file, save_file_dialog, write_binary_file, read_binary_file, add_to_steam])
|
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
if args.len() > 1 && !args[1].starts_with('-') {
|
if args.len() > 1 && !args[1].starts_with('-') {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "LCE Emerald Launcher",
|
"productName": "LCE Emerald Launcher",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"identifier": "com.emerald.legacy",
|
"identifier": "com.emerald.legacy",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
@@ -26,17 +26,25 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM1NTk5MjQ4QjMxQjFCQgpSV1M3c1RHTEpKbFZBMEtxMnNUZ3F5V2FmQmQrREY5Z0pkbnJuUlBYL2d1cUtzRkJIeDFkUkR4RQo=",
|
||||||
|
"endpoints": [
|
||||||
|
"https://github.com/LCE-Hub/LCE-Emerald-Launcher/releases/latest/download/latest.json"
|
||||||
|
],
|
||||||
|
"windows": { "installMode": "passive" }
|
||||||
|
}
|
||||||
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
|
"createUpdaterArtifacts": true,
|
||||||
"linux": {
|
"linux": {
|
||||||
"appimage": {
|
"appimage": {
|
||||||
"bundleMediaFramework": true
|
"bundleMediaFramework": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resources": [
|
"resources": ["resources/DLC"],
|
||||||
"resources/DLC"
|
|
||||||
],
|
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/64x64.png",
|
"icons/64x64.png",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useUI } from '../../context/LauncherContext';
|
import { useUI } from "../../context/LauncherContext";
|
||||||
|
|
||||||
interface PanoramaProps {
|
interface PanoramaProps {
|
||||||
profile: string;
|
profile: string;
|
||||||
@@ -8,10 +8,9 @@ interface PanoramaProps {
|
|||||||
|
|
||||||
const PanoramaBackground = React.memo(({ profile, isDay }: PanoramaProps) => {
|
const PanoramaBackground = React.memo(({ profile, isDay }: PanoramaProps) => {
|
||||||
const { isWindowVisible } = useUI();
|
const { isWindowVisible } = useUI();
|
||||||
const PANORAMA_PROFILES = ['legacy_evolved', 'vanilla_tu19', '360revived', 'vanilla_tu24'];
|
|
||||||
const baseId = profile;
|
const baseId = profile;
|
||||||
const profileId = PANORAMA_PROFILES.includes(baseId) ? baseId : 'vanilla_tu19';
|
const profileId = baseId ? baseId : "vanilla_tu19";
|
||||||
const currentPanorama = `/panorama/${profileId}_Panorama_Background_${isDay ? 'Day' : 'Night'}.png`;
|
const currentPanorama = `/panorama/${profileId}_Panorama_Background_${isDay ? "Day" : "Night"}.png`;
|
||||||
const [bgWidth, setBgWidth] = useState<number | null>(null);
|
const [bgWidth, setBgWidth] = useState<number | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -30,10 +29,10 @@ const PanoramaBackground = React.memo(({ profile, isDay }: PanoramaProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
updateWidth();
|
updateWidth();
|
||||||
window.addEventListener('resize', updateWidth);
|
window.addEventListener("resize", updateWidth);
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
window.removeEventListener('resize', updateWidth);
|
window.removeEventListener("resize", updateWidth);
|
||||||
};
|
};
|
||||||
}, [currentPanorama]);
|
}, [currentPanorama]);
|
||||||
|
|
||||||
@@ -48,16 +47,19 @@ const PanoramaBackground = React.memo(({ profile, isDay }: PanoramaProps) => {
|
|||||||
`}</style>
|
`}</style>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div ref={containerRef} className="absolute inset-0 overflow-hidden pointer-events-none transition-opacity duration-500">
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="absolute inset-0 overflow-hidden pointer-events-none transition-opacity duration-500"
|
||||||
|
>
|
||||||
{isWindowVisible && (
|
{isWindowVisible && (
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 left-0 h-full will-change-transform"
|
className="absolute top-0 left-0 h-full will-change-transform"
|
||||||
style={{
|
style={{
|
||||||
width: bgWidth ? `calc(100vw + ${bgWidth}px)` : '200vw',
|
width: bgWidth ? `calc(100vw + ${bgWidth}px)` : "200vw",
|
||||||
backgroundImage: `url("${currentPanorama}")`,
|
backgroundImage: `url("${currentPanorama}")`,
|
||||||
backgroundSize: bgWidth ? `${bgWidth}px 100%` : 'auto 100%',
|
backgroundSize: bgWidth ? `${bgWidth}px 100%` : "auto 100%",
|
||||||
backgroundRepeat: 'repeat-x',
|
backgroundRepeat: "repeat-x",
|
||||||
animation: bgWidth ? 'panoramaLoop 140s linear infinite' : 'none'
|
animation: bgWidth ? "panoramaLoop 140s linear infinite" : "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { TauriService } from "../../services/TauriService";
|
||||||
|
|
||||||
|
interface ScreenshotImageProps {
|
||||||
|
path: string;
|
||||||
|
className?: string;
|
||||||
|
alt?: string;
|
||||||
|
loading?: "lazy" | "eager";
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
fallbackSrc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenshotImage({
|
||||||
|
path,
|
||||||
|
className,
|
||||||
|
alt,
|
||||||
|
loading,
|
||||||
|
style,
|
||||||
|
fallbackSrc,
|
||||||
|
}: ScreenshotImageProps) {
|
||||||
|
const [src, setSrc] = useState<string | undefined>(fallbackSrc);
|
||||||
|
const imgRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
setSrc(fallbackSrc);
|
||||||
|
TauriService.readScreenshotAsDataUrl(path)
|
||||||
|
.then((url) => {
|
||||||
|
if (!cancelled) setSrc(url);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled && fallbackSrc) setSrc(fallbackSrc);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [path, fallbackSrc]);
|
||||||
|
|
||||||
|
const handleError = () => {
|
||||||
|
if (fallbackSrc) setSrc(fallbackSrc);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
ref={imgRef}
|
||||||
|
src={src}
|
||||||
|
className={className}
|
||||||
|
alt={alt}
|
||||||
|
loading={loading}
|
||||||
|
style={style}
|
||||||
|
onError={handleError}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -256,6 +256,9 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo
|
|||||||
} else if (focusIndex === 4) {
|
} else if (focusIndex === 4) {
|
||||||
playPressSound();
|
playPressSound();
|
||||||
setActiveView('screenshots');
|
setActiveView('screenshots');
|
||||||
|
} else if (focusIndex === 5) {
|
||||||
|
playPressSound();
|
||||||
|
setActiveView('lcelive');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -346,6 +349,16 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo
|
|||||||
>
|
>
|
||||||
<img src="/images/Screenshots_Icon.png" alt="Screenshots" className="w-8 h-8 object-contain" style={{ imageRendering: 'pixelated' }} />
|
<img src="/images/Screenshots_Icon.png" alt="Screenshots" className="w-8 h-8 object-contain" style={{ imageRendering: 'pixelated' }} />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
data-focus="5" tabIndex={0}
|
||||||
|
onMouseEnter={() => isFocusedSection && setFocusIndex(5)}
|
||||||
|
onClick={() => { playPressSound(); setActiveView('lcelive'); }}
|
||||||
|
className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none transition-all ${isFocusedSection && focusIndex === 5 ? 'scale-110' : ''}`}
|
||||||
|
style={isFocusedSection && focusIndex === 5 ? { backgroundImage: "url('/images/Button_Square_Highlighted.png')" } : {}}
|
||||||
|
title="LCELive"
|
||||||
|
>
|
||||||
|
<img src="/images/friends.png" alt="LCELive" className="w-8 h-8 object-contain" style={{ imageRendering: 'pixelated' }} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { TauriService } from "../../services/TauriService";
|
||||||
|
import { lceLiveService, GameInvite } from "../../services/LceLiveService";
|
||||||
|
|
||||||
|
export default function ChooseInstanceModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
playPressSound,
|
||||||
|
playBackSound,
|
||||||
|
editions,
|
||||||
|
installs,
|
||||||
|
invite,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
playPressSound: (s?: string) => void;
|
||||||
|
playBackSound: (s?: string) => void;
|
||||||
|
editions: any[];
|
||||||
|
installs: string[];
|
||||||
|
invite: GameInvite | null;
|
||||||
|
}) {
|
||||||
|
const [selectedInstance, setSelectedInstance] = useState<string>("");
|
||||||
|
const [status, setStatus] = useState<string>("");
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
const [isJoining, setIsJoining] = useState(false);
|
||||||
|
const [focusIndex, setFocusIndex] = useState(0);
|
||||||
|
|
||||||
|
const validInstances = editions.filter((e: any) =>
|
||||||
|
installs.includes(e.instanceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setSelectedInstance("");
|
||||||
|
setStatus("");
|
||||||
|
setError("");
|
||||||
|
setIsJoining(false);
|
||||||
|
setFocusIndex(0);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && validInstances.length > 0 && !selectedInstance) {
|
||||||
|
setSelectedInstance(validInstances[0].instanceId);
|
||||||
|
}
|
||||||
|
}, [isOpen, validInstances, selectedInstance]);
|
||||||
|
|
||||||
|
const handleJoin = async () => {
|
||||||
|
if (!invite || !selectedInstance) return;
|
||||||
|
playPressSound();
|
||||||
|
setIsJoining(true);
|
||||||
|
setError("");
|
||||||
|
setStatus("Accepting invite...");
|
||||||
|
try {
|
||||||
|
const inviteData = await lceLiveService.acceptGameInvite(invite.inviteId);
|
||||||
|
const hostIp = inviteData.hostIp || (typeof invite.from !== 'string' && (invite as any).from?.hostIp);
|
||||||
|
const hostPort = inviteData.hostPort || invite.hostPort;
|
||||||
|
const sessionId = inviteData.signalingSessionId || invite.signalingSessionId || "";
|
||||||
|
|
||||||
|
if (sessionId) {
|
||||||
|
setStatus("Connecting via relay...");
|
||||||
|
const baseUrl = lceLiveService.apiBaseUrl;
|
||||||
|
const accessToken = lceLiveService.accessToken ?? "";
|
||||||
|
const port = await TauriService.startRelayProxy(baseUrl, accessToken, sessionId);
|
||||||
|
setStatus("Launching game...");
|
||||||
|
await TauriService.launchGame(selectedInstance, [
|
||||||
|
{ name: invite.hostName || "LCELive Game", ip: "127.0.0.1", port }
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setStatus("Launching game...");
|
||||||
|
await TauriService.stopProxy();
|
||||||
|
await TauriService.launchGame(selectedInstance, [
|
||||||
|
{ name: invite.hostName || "LCELive Game", ip: hostIp, port: hostPort }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.toString());
|
||||||
|
setStatus("");
|
||||||
|
setIsJoining(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
playBackSound();
|
||||||
|
if (isJoining) return;
|
||||||
|
onClose();
|
||||||
|
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
||||||
|
e.preventDefault();
|
||||||
|
const max = 1 + (validInstances.length > 0 ? 1 : 0) + 1;
|
||||||
|
setFocusIndex((prev) => (prev + 1) % max);
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
const max = 1 + (validInstances.length > 0 ? 1 : 0) + 1;
|
||||||
|
setFocusIndex((prev) => (prev - 1 + max) % max);
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
if (focusIndex === 0 && validInstances.length > 0) {
|
||||||
|
const currentIdx = validInstances.findIndex((i: any) => i.instanceId === selectedInstance);
|
||||||
|
const next = (currentIdx + 1) % validInstances.length;
|
||||||
|
setSelectedInstance(validInstances[next].instanceId);
|
||||||
|
playPressSound();
|
||||||
|
} else if (focusIndex === 1 && !isJoining) {
|
||||||
|
handleJoin();
|
||||||
|
} else if (focusIndex === (validInstances.length > 0 ? 2 : 1) && !isJoining) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKey);
|
||||||
|
return () => window.removeEventListener("keydown", handleKey);
|
||||||
|
}, [isOpen, focusIndex, selectedInstance, validInstances, isJoining]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const hostName = invite ? (typeof invite.from === 'string' ? invite.from : invite.from.displayName) : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 w-screen h-screen z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md outline-none border-none"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative w-[450px] p-6 flex flex-col items-center shadow-2xl"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/images/frame_background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isJoining ? (
|
||||||
|
<>
|
||||||
|
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-2 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
|
||||||
|
Join Game
|
||||||
|
</h2>
|
||||||
|
<p className="text-white text-sm mc-text-shadow mb-4 text-center">
|
||||||
|
Joining {hostName}'s game. Choose an instance:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{validInstances.length > 0 ? (
|
||||||
|
<div className="w-full mb-4 flex flex-col gap-2 max-h-[300px] overflow-y-auto"
|
||||||
|
style={{ scrollbarWidth: "thin", scrollbarColor: "#373737 transparent" }}>
|
||||||
|
{validInstances.map((inst: any) => {
|
||||||
|
const isSelected = selectedInstance === inst.instanceId;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={inst.instanceId}
|
||||||
|
onClick={() => { playPressSound(); setSelectedInstance(inst.instanceId); }}
|
||||||
|
onMouseEnter={() => setFocusIndex(0)}
|
||||||
|
className={`w-full px-4 py-3 cursor-pointer flex items-center gap-3 transition-all outline-none border-none ${isSelected ? "bg-white/15 border-l-4 border-[#FFFF55]" : "bg-black/20 hover:bg-black/30 border-l-4 border-transparent"}`}
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
>
|
||||||
|
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${isSelected ? "border-[#FFFF55]" : "border-gray-500"}`}>
|
||||||
|
{isSelected && <div className="w-2 h-2 rounded-full bg-[#FFFF55]" />}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-white text-lg font-bold mc-text-shadow">{inst.name}</span>
|
||||||
|
{inst.selectedBranch && (
|
||||||
|
<span className="text-gray-400 text-xs">{inst.selectedBranch}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-red-400 text-sm mc-text-shadow mb-4 text-center">
|
||||||
|
No installed instances available. Install a version first.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest mb-3">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-4 w-full justify-center">
|
||||||
|
<button
|
||||||
|
onMouseEnter={() => {
|
||||||
|
const cancelIdx = validInstances.length > 0 ? 2 : 1;
|
||||||
|
setFocusIndex(cancelIdx);
|
||||||
|
}}
|
||||||
|
onClick={() => { playBackSound(); onClose(); }}
|
||||||
|
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${(() => {
|
||||||
|
const cancelIdx = validInstances.length > 0 ? 2 : 1;
|
||||||
|
return focusIndex === cancelIdx ? "text-[#FFFF55]" : "text-white";
|
||||||
|
})()}`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: (() => {
|
||||||
|
const cancelIdx = validInstances.length > 0 ? 2 : 1;
|
||||||
|
return focusIndex === cancelIdx
|
||||||
|
? "url('/images/button_highlighted.png')"
|
||||||
|
: "url('/images/Button_Background.png')";
|
||||||
|
})(),
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{validInstances.length > 0 && (
|
||||||
|
<button
|
||||||
|
onMouseEnter={() => setFocusIndex(1)}
|
||||||
|
onClick={handleJoin}
|
||||||
|
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${focusIndex === 1 ? "text-[#FFFF55]" : "text-white"}`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: focusIndex === 1
|
||||||
|
? "url('/images/button_highlighted.png')"
|
||||||
|
: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
|
||||||
|
Joining Game
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col items-center gap-4 py-8">
|
||||||
|
<div className="w-12 h-12 border-4 border-[#FFFF55] border-t-transparent rounded-full animate-spin" />
|
||||||
|
<p className="text-white text-lg mc-text-shadow text-center">{status}</p>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest mb-3">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { TauriService } from "../../services/TauriService";
|
||||||
|
|
||||||
|
export default function SetUidModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
playPressSound,
|
||||||
|
playBackSound,
|
||||||
|
instances,
|
||||||
|
installedVersions,
|
||||||
|
targetInstanceId,
|
||||||
|
}: any) {
|
||||||
|
const [mode, setMode] = useState<"manual" | "copy">("manual");
|
||||||
|
const [uid, setUid] = useState("0xFF02F0C87E8AC1F2");
|
||||||
|
const [selectedInstance, setSelectedInstance] = useState("");
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [focusIndex, setFocusIndex] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setMode("manual");
|
||||||
|
setUid("0xFF02F0C87E8AC1F2");
|
||||||
|
setSelectedInstance("");
|
||||||
|
setError("");
|
||||||
|
setFocusIndex(0);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
} else if (targetInstanceId) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const targetPath = await TauriService.getInstancePath(targetInstanceId);
|
||||||
|
const data = await TauriService.readBinaryFile(`${targetPath}/uid.dat`);
|
||||||
|
const currentUid = new TextDecoder().decode(data);
|
||||||
|
if (currentUid.trim()) {
|
||||||
|
setUid(currentUid);
|
||||||
|
} else {
|
||||||
|
setUid("0xFF02F0C87E8AC1F2");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setUid("0xFF02F0C87E8AC1F2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, [isOpen, targetInstanceId]);
|
||||||
|
|
||||||
|
const validInstances = instances.filter((i: any) => installedVersions.includes(i.instanceId) && i.instanceId !== targetInstanceId);
|
||||||
|
const handleSave = async () => {
|
||||||
|
playPressSound("save_click.wav");
|
||||||
|
try {
|
||||||
|
let finalUid = uid;
|
||||||
|
if (mode === "copy") {
|
||||||
|
if (!selectedInstance) {
|
||||||
|
setError("Select an instance to copy from.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sourcePath = await TauriService.getInstancePath(selectedInstance);
|
||||||
|
try {
|
||||||
|
const sourceData = await TauriService.readBinaryFile(`${sourcePath}/uid.dat`);
|
||||||
|
finalUid = new TextDecoder().decode(sourceData);
|
||||||
|
} catch (e) {
|
||||||
|
setError("Source instance has no uid.dat or it could not be read.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!finalUid) {
|
||||||
|
setError("UID cannot be empty.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedUid = new TextEncoder().encode(finalUid);
|
||||||
|
const targetPath = await TauriService.getInstancePath(targetInstanceId);
|
||||||
|
await TauriService.writeBinaryFile(`${targetPath}/uid.dat`, encodedUid);
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
playBackSound("close_click.wav");
|
||||||
|
onClose();
|
||||||
|
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
||||||
|
e.preventDefault();
|
||||||
|
setFocusIndex((prev) => (prev + 1) % 5);
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
setFocusIndex((prev) => (prev - 1 + 5) % 5);
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
if (focusIndex === 0) {
|
||||||
|
playPressSound();
|
||||||
|
setMode("manual");
|
||||||
|
} else if (focusIndex === 1) {
|
||||||
|
playPressSound();
|
||||||
|
setMode("copy");
|
||||||
|
} else if (focusIndex === 2 && mode === "copy") {
|
||||||
|
playPressSound();
|
||||||
|
setIsDropdownOpen(!isDropdownOpen);
|
||||||
|
} else if (focusIndex === 3) {
|
||||||
|
playBackSound("close_click.wav");
|
||||||
|
onClose();
|
||||||
|
} else if (focusIndex === 4) {
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKey);
|
||||||
|
return () => window.removeEventListener("keydown", handleKey);
|
||||||
|
}, [isOpen, focusIndex, mode, uid, selectedInstance, isDropdownOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 w-screen h-screen z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md outline-none border-none"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative w-[400px] p-6 flex flex-col items-center shadow-2xl"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/images/frame_background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
|
||||||
|
Set UID
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="flex gap-4 mb-5 w-full justify-center">
|
||||||
|
<button
|
||||||
|
onMouseEnter={() => setFocusIndex(0)}
|
||||||
|
onClick={() => { playPressSound(); setMode("manual"); }}
|
||||||
|
className={`w-32 h-10 flex items-center justify-center text-sm mc-text-shadow transition-all outline-none border-none bg-transparent ${focusIndex === 0 ? "text-[#FFFF55]" : mode === "manual" ? "text-white" : "text-gray-400"}`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: focusIndex === 0 || mode === "manual"
|
||||||
|
? "url('/images/button_highlighted.png')"
|
||||||
|
: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Manual
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onMouseEnter={() => setFocusIndex(1)}
|
||||||
|
onClick={() => { playPressSound(); setMode("copy"); }}
|
||||||
|
className={`w-32 h-10 flex items-center justify-center text-sm mc-text-shadow transition-all outline-none border-none bg-transparent ${focusIndex === 1 ? "text-[#FFFF55]" : mode === "copy" ? "text-white" : "text-gray-400"}`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: focusIndex === 1 || mode === "copy"
|
||||||
|
? "url('/images/button_highlighted.png')"
|
||||||
|
: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 w-full min-h-[80px]">
|
||||||
|
{mode === "manual" ? (
|
||||||
|
<div className="flex flex-col gap-1 items-center w-full">
|
||||||
|
<label className="text-gray text-xs mc-text-shadow uppercase tracking-widest text-[#AAAAAA]">
|
||||||
|
Enter UID
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
value={uid}
|
||||||
|
onChange={(e) => setUid(e.target.value)}
|
||||||
|
onFocus={() => setFocusIndex(2)}
|
||||||
|
placeholder="0xFF02F0C87E8AC1F2"
|
||||||
|
className={`w-full h-10 px-3 bg-black/40 border-2 ${focusIndex === 2 ? 'border-white' : 'border-[#373737]'} text-white text-base outline-none font-['Mojangles'] text-center`}
|
||||||
|
style={{ imageRendering: "pixelated", filter: focusIndex === 2 ? 'brightness(1.2)' : 'none' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-1 items-center w-full relative">
|
||||||
|
<label className="text-gray text-xs mc-text-shadow uppercase tracking-widest text-[#AAAAAA]">
|
||||||
|
Select Installed Instance
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => { playPressSound(); setIsDropdownOpen(!isDropdownOpen); }}
|
||||||
|
onFocus={() => { setFocusIndex(2); setIsDropdownOpen(false); }}
|
||||||
|
tabIndex={0}
|
||||||
|
className={`w-full h-10 px-3 bg-black/40 border-2 ${focusIndex === 2 ? 'border-white' : 'border-[#373737]'} flex items-center justify-between text-white text-base outline-none font-['Mojangles'] cursor-pointer`}
|
||||||
|
style={{ imageRendering: "pixelated", filter: focusIndex === 2 ? 'brightness(1.2)' : 'none' }}
|
||||||
|
>
|
||||||
|
<span className="truncate">
|
||||||
|
{selectedInstance
|
||||||
|
? (() => {
|
||||||
|
const i = validInstances.find((inst: any) => inst.instanceId === selectedInstance);
|
||||||
|
return i ? `${i.name} ${i.selectedBranch ? `(${i.selectedBranch})` : ""}` : "-- Select an Instance --";
|
||||||
|
})()
|
||||||
|
: "-- Select an Instance --"}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs">▼</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDropdownOpen && validInstances.length > 0 && (
|
||||||
|
<div className="absolute top-[60px] left-0 w-full max-h-40 overflow-y-auto bg-black/90 border-2 border-[#373737] z-50 flex flex-col custom-scrollbar shadow-xl" style={{ imageRendering: "pixelated" }}>
|
||||||
|
{validInstances.map((i: any) => (
|
||||||
|
<div
|
||||||
|
key={i.instanceId}
|
||||||
|
onClick={() => {
|
||||||
|
playPressSound();
|
||||||
|
setSelectedInstance(i.instanceId);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className="px-3 py-2 text-white text-sm cursor-pointer hover:bg-white/20 transition-colors truncate font-['Mojangles']"
|
||||||
|
>
|
||||||
|
{i.name} {i.selectedBranch ? `(${i.selectedBranch})` : ""}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{validInstances.length === 0 && (
|
||||||
|
<p className="text-red-400 text-xs text-center mt-1">No other installed instances available.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest mt-2">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 mt-6 w-full justify-center">
|
||||||
|
<button
|
||||||
|
onMouseEnter={() => setFocusIndex(3)}
|
||||||
|
onClick={() => {
|
||||||
|
playBackSound("close_click.wav");
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${focusIndex === 3 ? "text-[#FFFF55]" : "text-white"}`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: focusIndex === 3
|
||||||
|
? "url('/images/button_highlighted.png')"
|
||||||
|
: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onMouseEnter={() => setFocusIndex(4)}
|
||||||
|
onClick={handleSave}
|
||||||
|
className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${focusIndex === 4 ? "text-[#FFFF55]" : "text-white"}`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: focusIndex === 4
|
||||||
|
? "url('/images/button_highlighted.png')"
|
||||||
|
: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -48,17 +48,37 @@ const HomeView = memo(function HomeView() {
|
|||||||
: isGameRunning
|
: isGameRunning
|
||||||
? stopGame
|
? stopGame
|
||||||
: isDownloading
|
: isDownloading
|
||||||
? () => { }
|
? () => {}
|
||||||
: isInstalled
|
: isInstalled
|
||||||
? handleLaunch
|
? handleLaunch
|
||||||
: () => toggleInstall(profile),
|
: () => toggleInstall(profile),
|
||||||
isDanger: isGameRunning,
|
isDanger: isGameRunning,
|
||||||
disabled: isDownloading,
|
disabled: isDownloading,
|
||||||
},
|
},
|
||||||
{ label: "Help & Options", action: () => setActiveView("settings"), disabled: false, id: "settings" },
|
{
|
||||||
{ label: "Versions", action: () => setActiveView("versions"), disabled: false, id: "versions" },
|
label: "Help & Options",
|
||||||
{ label: "Workshop", action: () => setActiveView("workshop"), disabled: false, id: "workshop" },
|
action: () => setActiveView("settings"),
|
||||||
{ label: "Developer Tools", action: () => setActiveView("devtools"), disabled: false, id: "devtools" },
|
disabled: false,
|
||||||
|
id: "settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Versions",
|
||||||
|
action: () => setActiveView("versions"),
|
||||||
|
disabled: false,
|
||||||
|
id: "versions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Workshop",
|
||||||
|
action: () => setActiveView("workshop"),
|
||||||
|
disabled: false,
|
||||||
|
id: "workshop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Developer Tools",
|
||||||
|
action: () => setActiveView("devtools"),
|
||||||
|
disabled: false,
|
||||||
|
id: "devtools",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
isDownloading,
|
isDownloading,
|
||||||
@@ -111,7 +131,9 @@ const HomeView = memo(function HomeView() {
|
|||||||
{buttons.map((btn: any, i: number) => (
|
{buttons.map((btn: any, i: number) => (
|
||||||
<div key={i} className="relative w-full group">
|
<div key={i} className="relative w-full group">
|
||||||
<button
|
<button
|
||||||
onMouseEnter={() => isFocusedSection && !btn.disabled && setMenuFocus(i)}
|
onMouseEnter={() =>
|
||||||
|
isFocusedSection && !btn.disabled && setMenuFocus(i)
|
||||||
|
}
|
||||||
onMouseLeave={() => setMenuFocus(null)}
|
onMouseLeave={() => setMenuFocus(null)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isFocusedSection && !btn.disabled) {
|
if (isFocusedSection && !btn.disabled) {
|
||||||
@@ -134,11 +156,16 @@ const HomeView = memo(function HomeView() {
|
|||||||
>
|
>
|
||||||
<div className="w-full h-full flex items-center justify-center relative">
|
<div className="w-full h-full flex items-center justify-center relative">
|
||||||
<span>{btn.label}</span>
|
<span>{btn.label}</span>
|
||||||
{btn.id === "versions" && Object.values(updatesAvailable || {}).some((v) => v) && (
|
{btn.id === "versions" &&
|
||||||
|
Object.values(updatesAvailable || {}).some((v) => v) && (
|
||||||
<img
|
<img
|
||||||
src="/images/Update_Icon.png"
|
src="/images/Update_Icon.png"
|
||||||
className="absolute right-4 w-6 h-6 object-contain"
|
className="absolute right-4 w-6 h-6 object-contain"
|
||||||
style={{ imageRendering: "pixelated", filter: "drop-shadow(0 0 2px rgba(255, 255, 0, 0.8)) sepia(100%) saturate(500%) hue-rotate(5deg) brightness(1.2)" }}
|
style={{
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
filter:
|
||||||
|
"drop-shadow(0 0 2px rgba(255, 255, 0, 0.8)) sepia(100%) saturate(500%) hue-rotate(5deg) brightness(1.2)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -150,7 +177,7 @@ const HomeView = memo(function HomeView() {
|
|||||||
<div className="pt-4 flex flex-col items-center w-full gap-3">
|
<div className="pt-4 flex flex-col items-center w-full gap-3">
|
||||||
<div className="flex gap-8">
|
<div className="flex gap-8">
|
||||||
<a
|
<a
|
||||||
href="https://discord.gg/NPMp9JhGYH"
|
href="https://discord.gg/cQVKhQXcCx"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ScreenshotService,
|
ScreenshotService,
|
||||||
ScreenshotInfo,
|
ScreenshotInfo,
|
||||||
} from "../../services/ScreenshotService";
|
} from "../../services/ScreenshotService";
|
||||||
|
import { ScreenshotImage } from "../common/ScreenshotImage";
|
||||||
const ScreenshotsView = memo(function ScreenshotsView() {
|
const ScreenshotsView = memo(function ScreenshotsView() {
|
||||||
const { setActiveView } = useUI();
|
const { setActiveView } = useUI();
|
||||||
const { playPressSound, playBackSound } = useAudio();
|
const { playPressSound, playBackSound } = useAudio();
|
||||||
@@ -207,15 +208,12 @@ const ScreenshotsView = memo(function ScreenshotsView() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full h-full relative overflow-hidden bg-black/50">
|
<div className="w-full h-full relative overflow-hidden bg-black/50">
|
||||||
<img
|
<ScreenshotImage
|
||||||
src={`screenshots://localhost/${ss.path.replace(/\\/g, "/")}`}
|
path={ss.path}
|
||||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
alt={ss.name}
|
alt={ss.name}
|
||||||
onError={(e) => {
|
fallbackSrc="/images/Folder_Icon.png"
|
||||||
(e.target as HTMLImageElement).src =
|
|
||||||
"/images/Folder_Icon.png";
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-60" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-60" />
|
||||||
|
|
||||||
@@ -288,13 +286,10 @@ const ScreenshotsView = memo(function ScreenshotsView() {
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="relative w-full aspect-video bg-black/60 overflow-hidden border border-[#444] rounded-sm">
|
<div className="relative w-full aspect-video bg-black/60 overflow-hidden border border-[#444] rounded-sm">
|
||||||
<img
|
<ScreenshotImage
|
||||||
src={`screenshots://localhost/${selectedScreenshot.path.replace(/\\/g, "/")}`}
|
path={selectedScreenshot.path}
|
||||||
className="w-full h-full object-contain"
|
className="w-full h-full object-contain"
|
||||||
onError={(e) => {
|
fallbackSrc="/images/Pack_Icon.png"
|
||||||
(e.target as HTMLImageElement).src =
|
|
||||||
"/images/Pack_Icon.png";
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-4 left-6 right-6 flex items-end justify-between pointer-events-none">
|
<div className="absolute bottom-4 left-6 right-6 flex items-end justify-between pointer-events-none">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
|||||||
@@ -2,16 +2,56 @@ import { useState, useEffect, useRef, useMemo, memo } from "react";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { TauriService, Runner } from "../../services/TauriService";
|
import { TauriService, Runner } from "../../services/TauriService";
|
||||||
import { usePlatform } from "../../hooks/usePlatform";
|
import { usePlatform } from "../../hooks/usePlatform";
|
||||||
import { useUI, useConfig, useAudio, useGame } from "../../context/LauncherContext";
|
import {
|
||||||
|
useUI,
|
||||||
|
useConfig,
|
||||||
|
useAudio,
|
||||||
|
useGame,
|
||||||
|
} from "../../context/LauncherContext";
|
||||||
|
|
||||||
const SettingsView = memo(function SettingsView() {
|
const SettingsView = memo(function SettingsView() {
|
||||||
const { setActiveView } = useUI();
|
const { setActiveView } = useUI();
|
||||||
const { vfxEnabled, setVfxEnabled, animationsEnabled, setAnimationsEnabled, musicVol: musicVolume, setMusicVol: setMusicVolume, sfxVol: sfxVolume, setSfxVol: setSfxVolume, layout, setLayout, linuxRunner, setLinuxRunner, perfBoost, setPerfBoost, rpcEnabled, setRpcEnabled, legacyMode, setLegacyMode } = useConfig();
|
const {
|
||||||
const { currentTrack, setCurrentTrack, tracks, playPressSound, playBackSound } = useAudio();
|
vfxEnabled,
|
||||||
const { isGameRunning, stopGame, isRunnerDownloading, runnerDownloadProgress, downloadRunner } = useGame();
|
setVfxEnabled,
|
||||||
|
animationsEnabled,
|
||||||
|
setAnimationsEnabled,
|
||||||
|
musicVol: musicVolume,
|
||||||
|
setMusicVol: setMusicVolume,
|
||||||
|
sfxVol: sfxVolume,
|
||||||
|
setSfxVol: setSfxVolume,
|
||||||
|
layout,
|
||||||
|
setLayout,
|
||||||
|
linuxRunner,
|
||||||
|
setLinuxRunner,
|
||||||
|
perfBoost,
|
||||||
|
setPerfBoost,
|
||||||
|
rpcEnabled,
|
||||||
|
setRpcEnabled,
|
||||||
|
legacyMode,
|
||||||
|
setLegacyMode,
|
||||||
|
mangohudEnabled,
|
||||||
|
setMangohudEnabled,
|
||||||
|
} = useConfig();
|
||||||
|
const {
|
||||||
|
currentTrack,
|
||||||
|
setCurrentTrack,
|
||||||
|
tracks,
|
||||||
|
playPressSound,
|
||||||
|
playBackSound,
|
||||||
|
} = useAudio();
|
||||||
|
const {
|
||||||
|
isGameRunning,
|
||||||
|
stopGame,
|
||||||
|
isRunnerDownloading,
|
||||||
|
runnerDownloadProgress,
|
||||||
|
downloadRunner,
|
||||||
|
} = useGame();
|
||||||
const { isLinux, isMac } = usePlatform();
|
const { isLinux, isMac } = usePlatform();
|
||||||
const [focusIndex, setFocusIndex] = useState<number | null>(null);
|
const [focusIndex, setFocusIndex] = useState<number | null>(null);
|
||||||
const [currentSubMenu, setCurrentSubMenu] = useState<"main" | "audio" | "video" | "controls" | "launcher">("main");
|
const [currentSubMenu, setCurrentSubMenu] = useState<
|
||||||
|
"main" | "audio" | "video" | "controls" | "launcher"
|
||||||
|
>("main");
|
||||||
const [runners, setRunners] = useState<Runner[]>([]);
|
const [runners, setRunners] = useState<Runner[]>([]);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -53,6 +93,11 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
setLegacyMode(!legacyMode);
|
setLegacyMode(!legacyMode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMangohudToggle = () => {
|
||||||
|
playPressSound();
|
||||||
|
setMangohudEnabled(!mangohudEnabled);
|
||||||
|
};
|
||||||
|
|
||||||
const handleRunnerToggle = () => {
|
const handleRunnerToggle = () => {
|
||||||
playPressSound();
|
playPressSound();
|
||||||
if (runners.length === 0) return;
|
if (runners.length === 0) return;
|
||||||
@@ -69,35 +114,37 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
const handleResetSetup = () => {
|
const handleResetSetup = () => {
|
||||||
playPressSound();
|
playPressSound();
|
||||||
|
|
||||||
// Create styled confirmation dialog
|
const dialog = document.createElement("div");
|
||||||
const dialog = document.createElement('div');
|
dialog.className =
|
||||||
dialog.className = 'fixed inset-0 bg-black/80 flex items-center justify-center z-50';
|
"fixed inset-0 bg-black/80 flex items-center justify-center z-50";
|
||||||
dialog.innerHTML = `
|
dialog.innerHTML = `
|
||||||
<div class="relative p-8 max-w-md mx-4" style="background-image: url('/images/frame_background.png'); background-size: 100% 100%; background-repeat: no-repeat; image-rendering: pixelated;">
|
<div class="w-[420px] p-4 flex flex-col items-center mc-options-bg">
|
||||||
<h3 class="text-2xl font-bold text-white mb-4 text-center" style="text-shadow: 2px 2px 0px rgba(0,0,0,0.8)">Reset Setup</h3>
|
<h3 class="text-2xl font-bold text-[#333333] mb-4 text-left w-full px-4 mc-text-shadow">Reset Setup</h3>
|
||||||
<p class="text-white mb-6 text-center">Are you sure you want to reset launcher setup?</p>
|
<p class="text-[#333333] mb-8 text-left w-full px-4">Are you sure you want to reset launcher setup?</p>
|
||||||
<div class="flex gap-4 justify-center">
|
<div class="flex flex-col gap-3 w-full px-4">
|
||||||
<button id="reset-yes" class="mc-sq-btn px-6 py-3 text-white hover:scale-105 active:scale-95 transition-transform">Yes</button>
|
<button id="reset-cancel" class="w-full h-10 flex items-center justify-center text-lg mc-text-shadow text-white hover:text-[#ffff00]" style="background-image: url('/images/Button_Background.png'); background-size: 100% 100%; image-rendering: pixelated; border: none; cursor: pointer;" onmouseenter="this.style.backgroundImage='url(/images/button_highlighted.png)'" onmouseleave="this.style.backgroundImage='url(/images/Button_Background.png)'">Cancel</button>
|
||||||
<button id="reset-no" class="mc-sq-btn px-6 py-3 text-white hover:scale-105 active:scale-95 transition-transform">No</button>
|
<button id="reset-ok" class="w-full h-10 flex items-center justify-center text-lg mc-text-shadow text-white hover:text-[#ffff00]" style="background-image: url('/images/Button_Background.png'); background-size: 100% 100%; image-rendering: pixelated; border: none; cursor: pointer;" onmouseenter="this.style.backgroundImage='url(/images/button_highlighted.png)'" onmouseleave="this.style.backgroundImage='url(/images/Button_Background.png)'">OK</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(dialog);
|
document.body.appendChild(dialog);
|
||||||
|
|
||||||
const handleYes = () => {
|
const handleOk = () => {
|
||||||
document.body.removeChild(dialog);
|
document.body.removeChild(dialog);
|
||||||
showSecondConfirmation();
|
showSecondConfirmation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNo = () => {
|
const handleCancel = () => {
|
||||||
document.body.removeChild(dialog);
|
document.body.removeChild(dialog);
|
||||||
};
|
};
|
||||||
|
|
||||||
dialog.querySelector('#reset-yes')?.addEventListener('click', handleYes);
|
dialog.querySelector("#reset-ok")?.addEventListener("click", handleOk);
|
||||||
dialog.querySelector('#reset-no')?.addEventListener('click', handleNo);
|
dialog
|
||||||
|
.querySelector("#reset-cancel")
|
||||||
|
?.addEventListener("click", handleCancel);
|
||||||
|
|
||||||
dialog.addEventListener('click', (e) => {
|
dialog.addEventListener("click", (e) => {
|
||||||
if (e.target === dialog) {
|
if (e.target === dialog) {
|
||||||
document.body.removeChild(dialog);
|
document.body.removeChild(dialog);
|
||||||
}
|
}
|
||||||
@@ -105,43 +152,48 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showSecondConfirmation = () => {
|
const showSecondConfirmation = () => {
|
||||||
const dialog = document.createElement('div');
|
const dialog = document.createElement("div");
|
||||||
dialog.className = 'fixed inset-0 bg-black/80 flex items-center justify-center z-50';
|
dialog.className =
|
||||||
|
"fixed inset-0 bg-black/80 flex items-center justify-center z-50";
|
||||||
dialog.innerHTML = `
|
dialog.innerHTML = `
|
||||||
<div class="relative p-8 max-w-md mx-4" style="background-image: url('/images/frame_background.png'); background-size: 100% 100%; background-repeat: no-repeat; image-rendering: pixelated;">
|
<div class="w-[420px] p-4 flex flex-col items-center mc-options-bg">
|
||||||
<h3 class="text-2xl font-bold text-yellow-400 mb-4 text-center" style="text-shadow: 2px 2px 0px rgba(0,0,0,0.8)">CONFIRM RESET</h3>
|
<h3 class="text-2xl font-bold text-[#333333] mb-2 text-left w-full px-4 mc-text-shadow">CONFIRM RESET</h3>
|
||||||
<div class="text-white mb-6 text-left">
|
<div class="text-[#333333] mb-6 text-left w-full px-4">
|
||||||
<p class="mb-2">⚠️ This will:</p>
|
<p class="mb-2">⚠️ This will:</p>
|
||||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
<ul class="list-none space-y-1 text-sm">
|
||||||
<li>Clear all launcher settings</li>
|
<li>Clear all launcher settings</li>
|
||||||
<li>Reset your username</li>
|
<li>Reset your username</li>
|
||||||
<li>Show setup screen again</li>
|
<li>Show setup screen again</li>
|
||||||
<li>Require reconfiguration</li>
|
<li>Require reconfiguration</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-3 text-yellow-400 font-bold">This action cannot be undone!</p>
|
<p class="mt-3 text-[#333333] font-bold">This action cannot be undone!</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4 justify-center">
|
<div class="flex flex-col gap-3 w-full px-4">
|
||||||
<button id="reset-final-yes" class="mc-sq-btn px-6 py-3 text-yellow-400 hover:scale-105 active:scale-95 transition-transform">YES, RESET</button>
|
<button id="reset-final-cancel" class="w-full h-10 flex items-center justify-center text-lg mc-text-shadow text-white hover:text-[#ffff00]" style="background-image: url('/images/Button_Background.png'); background-size: 100% 100%; image-rendering: pixelated; border: none; cursor: pointer;" onmouseenter="this.style.backgroundImage='url(/images/button_highlighted.png)'" onmouseleave="this.style.backgroundImage='url(/images/Button_Background.png)'">Cancel</button>
|
||||||
<button id="reset-final-no" class="mc-sq-btn px-6 py-3 text-white hover:scale-105 active:scale-95 transition-transform">Cancel</button>
|
<button id="reset-final-ok" class="w-full h-10 flex items-center justify-center text-lg mc-text-shadow text-white hover:text-[#ffff00]" style="background-image: url('/images/Button_Background.png'); background-size: 100% 100%; image-rendering: pixelated; border: none; cursor: pointer;" onmouseenter="this.style.backgroundImage='url(/images/button_highlighted.png)'" onmouseleave="this.style.backgroundImage='url(/images/Button_Background.png)'">OK</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.body.appendChild(dialog);
|
document.body.appendChild(dialog);
|
||||||
|
|
||||||
const handleFinalYes = () => {
|
const handleFinalOk = () => {
|
||||||
document.body.removeChild(dialog);
|
document.body.removeChild(dialog);
|
||||||
performReset();
|
performReset();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinalNo = () => {
|
const handleFinalCancel = () => {
|
||||||
document.body.removeChild(dialog);
|
document.body.removeChild(dialog);
|
||||||
};
|
};
|
||||||
|
|
||||||
dialog.querySelector('#reset-final-yes')?.addEventListener('click', handleFinalYes);
|
dialog
|
||||||
dialog.querySelector('#reset-final-no')?.addEventListener('click', handleFinalNo);
|
.querySelector("#reset-final-ok")
|
||||||
|
?.addEventListener("click", handleFinalOk);
|
||||||
|
dialog
|
||||||
|
.querySelector("#reset-final-cancel")
|
||||||
|
?.addEventListener("click", handleFinalCancel);
|
||||||
|
|
||||||
dialog.addEventListener('click', (e) => {
|
dialog.addEventListener("click", (e) => {
|
||||||
if (e.target === dialog) {
|
if (e.target === dialog) {
|
||||||
document.body.removeChild(dialog);
|
document.body.removeChild(dialog);
|
||||||
}
|
}
|
||||||
@@ -149,13 +201,10 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const performReset = () => {
|
const performReset = () => {
|
||||||
// Clear all localStorage data
|
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
|
||||||
// Set setup as not completed
|
localStorage.setItem("lce-setup-completed", "false");
|
||||||
localStorage.setItem('lce-setup-completed', 'false');
|
|
||||||
|
|
||||||
// Force reload to show setup screen
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -163,11 +212,9 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
if (tracks && tracks.length > 0) {
|
if (tracks && tracks.length > 0) {
|
||||||
const fullPath = tracks[currentTrack];
|
const fullPath = tracks[currentTrack];
|
||||||
if (fullPath) {
|
if (fullPath) {
|
||||||
trackName = fullPath
|
trackName =
|
||||||
.split("/")
|
fullPath.split("/").pop()?.replace(".ogg", "").replace(".wav", "") ||
|
||||||
.pop()
|
"Unknown";
|
||||||
?.replace(".ogg", "")
|
|
||||||
.replace(".wav", "") || "Unknown";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,25 +246,41 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
id: "audio_menu",
|
id: "audio_menu",
|
||||||
label: "Audio",
|
label: "Audio",
|
||||||
type: "button",
|
type: "button",
|
||||||
onClick: () => { playPressSound(); setCurrentSubMenu("audio"); setFocusIndex(0); },
|
onClick: () => {
|
||||||
|
playPressSound();
|
||||||
|
setCurrentSubMenu("audio");
|
||||||
|
setFocusIndex(0);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
id: "video_menu",
|
id: "video_menu",
|
||||||
label: "Video",
|
label: "Video",
|
||||||
type: "button",
|
type: "button",
|
||||||
onClick: () => { playPressSound(); setCurrentSubMenu("video"); setFocusIndex(0); },
|
onClick: () => {
|
||||||
|
playPressSound();
|
||||||
|
setCurrentSubMenu("video");
|
||||||
|
setFocusIndex(0);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
id: "controls_menu",
|
id: "controls_menu",
|
||||||
label: "Controls",
|
label: "Controls",
|
||||||
type: "button",
|
type: "button",
|
||||||
onClick: () => { playPressSound(); setCurrentSubMenu("controls"); setFocusIndex(0); },
|
onClick: () => {
|
||||||
|
playPressSound();
|
||||||
|
setCurrentSubMenu("controls");
|
||||||
|
setFocusIndex(0);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
id: "launcher_menu",
|
id: "launcher_menu",
|
||||||
label: "Launcher",
|
label: "Launcher",
|
||||||
type: "button",
|
type: "button",
|
||||||
onClick: () => { playPressSound(); setCurrentSubMenu("launcher"); setFocusIndex(0); },
|
onClick: () => {
|
||||||
|
playPressSound();
|
||||||
|
setCurrentSubMenu("launcher");
|
||||||
|
setFocusIndex(0);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else if (currentSubMenu === "audio") {
|
} else if (currentSubMenu === "audio") {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -288,6 +351,12 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
type: "button",
|
type: "button",
|
||||||
onClick: handleRunnerToggle,
|
onClick: handleRunnerToggle,
|
||||||
});
|
});
|
||||||
|
items.push({
|
||||||
|
id: "mangohud",
|
||||||
|
label: `MangoHud: ${mangohudEnabled ? "ON" : "OFF"}`,
|
||||||
|
type: "button",
|
||||||
|
onClick: handleMangohudToggle,
|
||||||
|
});
|
||||||
items.push({
|
items.push({
|
||||||
id: "download_runner",
|
id: "download_runner",
|
||||||
label: isRunnerDownloading
|
label: isRunnerDownloading
|
||||||
@@ -296,7 +365,10 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
type: "button",
|
type: "button",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (!isRunnerDownloading) {
|
if (!isRunnerDownloading) {
|
||||||
downloadRunner("GE-Proton9-25", "https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton9-25/GE-Proton9-25.tar.gz");
|
downloadRunner(
|
||||||
|
"GE-Proton9-25",
|
||||||
|
"https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton9-25/GE-Proton9-25.tar.gz",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
small: true,
|
small: true,
|
||||||
@@ -349,6 +421,7 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
animationsEnabled,
|
animationsEnabled,
|
||||||
layout,
|
layout,
|
||||||
isLinux,
|
isLinux,
|
||||||
|
mangohudEnabled,
|
||||||
selectedRunnerName,
|
selectedRunnerName,
|
||||||
isRunnerDownloading,
|
isRunnerDownloading,
|
||||||
runnerDownloadProgress,
|
runnerDownloadProgress,
|
||||||
@@ -363,6 +436,7 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
handleLayoutToggle,
|
handleLayoutToggle,
|
||||||
handleRunnerToggle,
|
handleRunnerToggle,
|
||||||
handlePerfToggle,
|
handlePerfToggle,
|
||||||
|
handleMangohudToggle,
|
||||||
handleResetSetup,
|
handleResetSetup,
|
||||||
stopGame,
|
stopGame,
|
||||||
downloadRunner,
|
downloadRunner,
|
||||||
@@ -435,11 +509,16 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
backgroundImage: "url('/images/Button_Background2.png')",
|
backgroundImage: "url('/images/Button_Background2.png')",
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
imageRendering: "pixelated" as const,
|
imageRendering: "pixelated" as const,
|
||||||
color: focusIndex === index ? "#FFFF55" : "white",
|
color: focusIndex === index ? "#ffff00" : "white",
|
||||||
});
|
});
|
||||||
|
|
||||||
const isToggleOption = (label: string): boolean => {
|
const isToggleOption = (label: string): boolean => {
|
||||||
return label.includes("ON") || label.includes("OFF") || label.includes("Enabled") || label.includes("Disabled");
|
return (
|
||||||
|
label.includes("ON") ||
|
||||||
|
label.includes("OFF") ||
|
||||||
|
label.includes("Enabled") ||
|
||||||
|
label.includes("Disabled")
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getToggleState = (label: string): boolean => {
|
const getToggleState = (label: string): boolean => {
|
||||||
@@ -454,10 +533,18 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||||
className="flex flex-col items-center w-full max-w-2xl outline-none"
|
className="flex flex-col items-center w-full max-w-3xl outline-none"
|
||||||
>
|
>
|
||||||
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[40%] max-w-[200px] text-center tracking-widest uppercase opacity-80 font-bold whitespace-nowrap px-4">
|
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[40%] max-w-[200px] text-center tracking-widest uppercase opacity-80 font-bold whitespace-nowrap px-4">
|
||||||
{currentSubMenu === "main" ? "Settings" : currentSubMenu === "audio" ? "Audio" : currentSubMenu === "video" ? "Video" : currentSubMenu === "controls" ? "Controls" : "Launcher"}
|
{currentSubMenu === "main"
|
||||||
|
? "Settings"
|
||||||
|
: currentSubMenu === "audio"
|
||||||
|
? "Audio"
|
||||||
|
: currentSubMenu === "video"
|
||||||
|
? "Video"
|
||||||
|
: currentSubMenu === "controls"
|
||||||
|
? "Controls"
|
||||||
|
: "Launcher"}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{currentSubMenu === "main" ? (
|
{currentSubMenu === "main" ? (
|
||||||
@@ -472,11 +559,11 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
data-index={index}
|
data-index={index}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onMouseEnter={() => setFocusIndex(index)}
|
onMouseEnter={() => setFocusIndex(index)}
|
||||||
className="relative w-[360px] h-10 flex items-center justify-center cursor-pointer transition-all outline-none border-none hover:text-[#FFFF55] shrink-0"
|
className="relative w-[360px] h-10 flex items-center justify-center cursor-pointer transition-all outline-none border-none hover:text-[#ffff00] shrink-0"
|
||||||
style={getSliderStyle(index)}
|
style={getSliderStyle(index)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`absolute z-10 text-xl mc-text-shadow pointer-events-none transition-colors tracking-widest ${focusIndex === index ? "text-[#FFFF55]" : "text-white"}`}
|
className={`absolute z-10 text-xl mc-text-shadow pointer-events-none transition-colors tracking-widest ${focusIndex === index ? "text-[#ffff00]" : "text-white"}`}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -505,14 +592,15 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
data-index={index}
|
data-index={index}
|
||||||
onMouseEnter={() => setFocusIndex(index)}
|
onMouseEnter={() => setFocusIndex(index)}
|
||||||
onClick={item.onClick}
|
onClick={item.onClick}
|
||||||
className={`w-[360px] h-10 flex items-center justify-center px-4 relative z-30 transition-colors outline-none border-none shrink-0 ${isRed
|
className={`w-[360px] h-10 flex items-center justify-center px-4 relative z-30 transition-colors outline-none border-none shrink-0 ${
|
||||||
|
isRed
|
||||||
? focusIndex === index
|
? focusIndex === index
|
||||||
? "text-red-400"
|
? "text-red-400"
|
||||||
: "text-red-200"
|
: "text-red-200"
|
||||||
: focusIndex === index
|
: focusIndex === index
|
||||||
? "text-[#FFFF55]"
|
? "text-[#ffff00]"
|
||||||
: "text-white"
|
: "text-white"
|
||||||
} ${isRed ? "hover:text-red-500" : "hover:text-[#FFFF55]"}`}
|
} ${isRed ? "hover:text-red-500" : "hover:text-[#ffff00]"}`}
|
||||||
style={getItemStyle(index)}
|
style={getItemStyle(index)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -525,15 +613,7 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="min-w-[420px] w-fit p-4 flex flex-col items-center mc-options-bg">
|
||||||
className="min-w-[420px] w-fit p-6 flex flex-col items-center"
|
|
||||||
style={{
|
|
||||||
backgroundImage: "url('/images/background.png')",
|
|
||||||
backgroundSize: "100% 100%",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
imageRendering: "pixelated",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="w-full space-y-3 flex flex-col items-center overflow-y-auto max-h-[50vh] py-2">
|
<div className="w-full space-y-3 flex flex-col items-center overflow-y-auto max-h-[50vh] py-2">
|
||||||
{settingsItems.map((item, index) => {
|
{settingsItems.map((item, index) => {
|
||||||
if (item.id === "back") return null;
|
if (item.id === "back") return null;
|
||||||
@@ -545,11 +625,11 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
data-index={index}
|
data-index={index}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onMouseEnter={() => setFocusIndex(index)}
|
onMouseEnter={() => setFocusIndex(index)}
|
||||||
className="relative w-[360px] h-10 flex items-center justify-center cursor-pointer transition-all outline-none border-none hover:text-[#FFFF55] shrink-0"
|
className="relative w-[360px] h-10 flex items-center justify-center cursor-pointer transition-all outline-none border-none hover:text-[#ffff00] shrink-0"
|
||||||
style={getSliderStyle(index)}
|
style={getSliderStyle(index)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`absolute z-10 text-xl pointer-events-none transition-colors tracking-widest ${focusIndex === index ? "text-[#FFFF55]" : item.id === "music" || item.id === "sfx" ? "text-white" : "text-[#2a2a2a]"}`}
|
className={`absolute z-10 text-xl pointer-events-none transition-colors tracking-widest ${focusIndex === index ? "text-[#ffff00]" : item.id === "music" || item.id === "sfx" ? "text-white" : "text-[#2a2a2a]"}`}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -560,7 +640,9 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
max="100"
|
max="100"
|
||||||
step="1"
|
step="1"
|
||||||
value={item.value}
|
value={item.value}
|
||||||
onChange={(e) => item.onChange(parseInt(e.target.value))}
|
onChange={(e) =>
|
||||||
|
item.onChange(parseInt(e.target.value))
|
||||||
|
}
|
||||||
onMouseUp={playPressSound}
|
onMouseUp={playPressSound}
|
||||||
className="mc-slider-custom w-[calc(100%+16px)] h-full opacity-100 cursor-pointer z-20 outline-none m-0"
|
className="mc-slider-custom w-[calc(100%+16px)] h-full opacity-100 cursor-pointer z-20 outline-none m-0"
|
||||||
/>
|
/>
|
||||||
@@ -580,21 +662,35 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
data-index={index}
|
data-index={index}
|
||||||
onMouseEnter={() => setFocusIndex(index)}
|
onMouseEnter={() => setFocusIndex(index)}
|
||||||
onClick={item.onClick}
|
onClick={item.onClick}
|
||||||
className={`w-[360px] h-10 flex items-center justify-between px-4 relative z-30 transition-all outline-none border-none shrink-0 rounded ${focusIndex === index ? "bg-black/10" : "bg-transparent"} ${isRed ? "text-red-600" : "text-[#2a2a2a]"} hover:bg-black/15`}
|
className={`w-[360px] h-10 flex items-center pl-1.5 pr-4 relative z-30 outline-none border-none shrink-0 rounded ${focusIndex === index ? "text-[#ffff00]" : isRed ? "text-red-600" : "text-[#333333]"}`}
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
className={`tracking-widest uppercase ${isSmall ? "text-xs" : item.label.length > 25 ? "text-base" : "text-lg"} truncate text-left flex-1`}
|
|
||||||
>
|
|
||||||
{isToggle ? item.label.split(":")[0] : item.label}
|
|
||||||
</span>
|
|
||||||
{isToggle && (
|
{isToggle && (
|
||||||
|
<div className="relative w-6 h-6 mr-3 shrink-0">
|
||||||
<img
|
<img
|
||||||
src={toggleState ? "/images/Toggle_Switch_On.png" : "/images/Toggle_Switch_Off.png"}
|
src={
|
||||||
alt={toggleState ? "ON" : "OFF"}
|
focusIndex === index
|
||||||
className="w-12 h-6 object-contain shrink-0 ml-2"
|
? "/images/checkbox_highlighted.png"
|
||||||
|
: "/images/checkbox.png"
|
||||||
|
}
|
||||||
|
alt="checkbox"
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
|
{toggleState && (
|
||||||
|
<img
|
||||||
|
src="/images/check.png"
|
||||||
|
alt="checked"
|
||||||
|
className="absolute inset-0 w-full h-full object-contain"
|
||||||
style={{ imageRendering: "pixelated" }}
|
style={{ imageRendering: "pixelated" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`tracking-widest text-xl mc-text-shadow ${isSmall ? "text-xs" : item.label.length > 25 ? "text-base" : "text-lg"} truncate text-left`}
|
||||||
|
>
|
||||||
|
{isToggle ? item.label.split(":")[0] : item.label}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -612,9 +708,10 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
data-index={backIndex}
|
data-index={backIndex}
|
||||||
onMouseEnter={() => setFocusIndex(backIndex)}
|
onMouseEnter={() => setFocusIndex(backIndex)}
|
||||||
onClick={backItem.onClick}
|
onClick={backItem.onClick}
|
||||||
className={`w-72 h-10 flex items-center justify-center transition-colors text-xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] mt-4 ${focusIndex === backIndex ? "text-[#FFFF55]" : "text-white"}`}
|
className={`w-40 h-10 flex items-center justify-center transition-colors text-xl mc-text-shadow outline-none border-none hover:text-[#ffff00] mt-4 ${focusIndex === backIndex ? "text-[#ffff00]" : "text-white"}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: focusIndex === backIndex
|
backgroundImage:
|
||||||
|
focusIndex === backIndex
|
||||||
? "url('/images/button_highlighted.png')"
|
? "url('/images/button_highlighted.png')"
|
||||||
: "url('/images/Button_Background.png')",
|
: "url('/images/Button_Background.png')",
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
|
|||||||
@@ -46,7 +46,14 @@ const DEFAULT_SKINS: SavedSkin[] = [
|
|||||||
},
|
},
|
||||||
{ id: "peter", name: "Peter", url: "/Skins/Peter.png", isSlim: false },
|
{ id: "peter", name: "Peter", url: "/Skins/Peter.png", isSlim: false },
|
||||||
{ id: "piebot", name: "piebot", url: "/Skins/piebot.png", isSlim: false },
|
{ id: "piebot", name: "piebot", url: "/Skins/piebot.png", isSlim: false },
|
||||||
|
{ id: "andipog", name: "Andi_Pog", url: "/Skins/andi.png", isSlim: false },
|
||||||
{ id: "sevenhundred", name: "700", url: "/Skins/700.png", isSlim: false },
|
{ id: "sevenhundred", name: "700", url: "/Skins/700.png", isSlim: false },
|
||||||
|
{
|
||||||
|
id: "prismachunk0",
|
||||||
|
name: "PrismaChunk0",
|
||||||
|
url: "/Skins/PrismaChunk0.png",
|
||||||
|
isSlim: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const SkinsView = memo(function SkinsView() {
|
const SkinsView = memo(function SkinsView() {
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { useState, useEffect, useRef, memo } from "react";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { TauriService } from "../../services/TauriService";
|
import { TauriService } from "../../services/TauriService";
|
||||||
import CustomTUModal from "../modals/CustomTUModal";
|
import CustomTUModal from "../modals/CustomTUModal";
|
||||||
|
import SetUidModal from "../modals/SetUidModal";
|
||||||
import {
|
import {
|
||||||
useUI,
|
useUI,
|
||||||
useConfig,
|
useConfig,
|
||||||
useAudio,
|
useAudio,
|
||||||
useGame,
|
useGame,
|
||||||
} from "../../context/LauncherContext";
|
} from "../../context/LauncherContext";
|
||||||
|
import { ScreenshotImage } from "../common/ScreenshotImage";
|
||||||
interface DeleteConfirmButtonProps {
|
interface DeleteConfirmButtonProps {
|
||||||
label: string;
|
label: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
@@ -26,7 +28,8 @@ const DeleteConfirmButton = memo(function DeleteConfirmButton({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
className={`w-24 h-10 flex items-center justify-center mc-text-shadow transition-colors ${isDanger ? "text-red-500" : "text-white"
|
className={`w-24 h-10 flex items-center justify-center mc-text-shadow transition-colors ${
|
||||||
|
isDanger ? "text-red-500" : "text-white"
|
||||||
} ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`}
|
} ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: isHovered
|
backgroundImage: isHovered
|
||||||
@@ -68,6 +71,8 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
const [focusIndex, setFocusIndex] = useState<number>(0);
|
const [focusIndex, setFocusIndex] = useState<number>(0);
|
||||||
const [focusBtn, setFocusBtn] = useState<number>(0);
|
const [focusBtn, setFocusBtn] = useState<number>(0);
|
||||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||||
|
const [isSetUidModalOpen, setIsSetUidModalOpen] = useState(false);
|
||||||
|
const [setUidTargetId, setSetUidTargetId] = useState("");
|
||||||
const [editingEdition, setEditingEdition] = useState<any>(null);
|
const [editingEdition, setEditingEdition] = useState<any>(null);
|
||||||
const [initialPath, setInitialPath] = useState<string>("");
|
const [initialPath, setInitialPath] = useState<string>("");
|
||||||
const [hoveredBtn, setHoveredBtn] = useState<{
|
const [hoveredBtn, setHoveredBtn] = useState<{
|
||||||
@@ -166,7 +171,7 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
toggleInstall,
|
toggleInstall,
|
||||||
handleCancelDownload,
|
handleCancelDownload,
|
||||||
addToSteam,
|
addToSteam,
|
||||||
isDayTime
|
isDayTime,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -208,7 +213,7 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: 20 }}
|
exit={{ opacity: 0, y: 20 }}
|
||||||
transition={{ duration: animationsEnabled ? 0.25 : 0 }}
|
transition={{ duration: animationsEnabled ? 0.25 : 0 }}
|
||||||
className="flex flex-col items-center w-full max-w-2xl outline-none"
|
className="flex flex-col items-center w-full max-w-5xl outline-none"
|
||||||
>
|
>
|
||||||
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 pb-2 w-[40%] max-w-[200px] text-center tracking-widest uppercase font-bold">
|
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 pb-2 w-[40%] max-w-[200px] text-center tracking-widest uppercase font-bold">
|
||||||
Versions
|
Versions
|
||||||
@@ -229,7 +234,9 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{editions.map((edition: any, i: number) => {
|
{editions.map((edition: any, i: number) => {
|
||||||
const isInstalled = installedVersions.includes(edition.instanceId);
|
const isInstalled = installedVersions.includes(
|
||||||
|
edition.instanceId,
|
||||||
|
);
|
||||||
const hasAnyInstall = installedVersions.length > 0;
|
const hasAnyInstall = installedVersions.length > 0;
|
||||||
const isSelected =
|
const isSelected =
|
||||||
hasAnyInstall && selectedProfile === edition.instanceId;
|
hasAnyInstall && selectedProfile === edition.instanceId;
|
||||||
@@ -242,8 +249,10 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
<div
|
<div
|
||||||
key={edition.id}
|
key={edition.id}
|
||||||
data-index={i}
|
data-index={i}
|
||||||
className={`w-[calc(100%-16px)] mx-2 flex items-center gap-3 p-2 rounded-sm ${isSelected && !isComingSoon ? "bg-[#404040]/50" : ""
|
className={`w-[calc(100%-16px)] mx-2 flex items-center gap-3 p-2 rounded-sm ${
|
||||||
} ${isFocused && !isComingSoon ? "ring-2 ring-white" : ""} ${isComingSoon ? "opacity-50 cursor-not-allowed" : ""
|
isSelected && !isComingSoon ? "bg-[#404040]/50" : ""
|
||||||
|
} ${isFocused && !isComingSoon ? "ring-2 ring-white" : ""} ${
|
||||||
|
isComingSoon ? "opacity-50 cursor-not-allowed" : ""
|
||||||
} relative ${openMenuId === edition.id ? "z-50" : "z-0"}`}
|
} relative ${openMenuId === edition.id ? "z-50" : "z-0"}`}
|
||||||
onMouseEnter={() => !isComingSoon && setFocusIndex(i)}
|
onMouseEnter={() => !isComingSoon && setFocusIndex(i)}
|
||||||
>
|
>
|
||||||
@@ -280,27 +289,33 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
!isComingSoon && handleEditionClick(edition, i)
|
!isComingSoon && handleEditionClick(edition, i)
|
||||||
}
|
}
|
||||||
className={`flex-1 text-left min-w-0 outline-none rounded cursor-pointer ${focusIndex === i && focusBtn === 0 && !isComingSoon
|
className={`flex-1 text-left min-w-0 outline-none rounded cursor-pointer ${
|
||||||
|
focusIndex === i && focusBtn === 0 && !isComingSoon
|
||||||
? "ring-2 ring-white"
|
? "ring-2 ring-white"
|
||||||
: ""
|
: ""
|
||||||
} ${isComingSoon ? "cursor-not-allowed" : ""}`}
|
} ${isComingSoon ? "cursor-not-allowed" : ""}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{edition.logo && (
|
{edition.logo &&
|
||||||
|
(edition.logo.startsWith("http") ||
|
||||||
|
edition.logo.startsWith("/images") ? (
|
||||||
<img
|
<img
|
||||||
src={
|
src={edition.logo}
|
||||||
edition.logo.startsWith("http") ||
|
|
||||||
edition.logo.startsWith("/images")
|
|
||||||
? edition.logo
|
|
||||||
: `screenshots://localhost/${edition.logo.replace(/\\/g, "/")}`
|
|
||||||
}
|
|
||||||
alt=""
|
alt=""
|
||||||
className="w-5 h-5 object-contain flex-shrink-0"
|
className="w-5 h-5 object-contain flex-shrink-0"
|
||||||
style={{ imageRendering: "pixelated" }}
|
style={{ imageRendering: "pixelated" }}
|
||||||
/>
|
/>
|
||||||
)}
|
) : (
|
||||||
|
<ScreenshotImage
|
||||||
|
path={edition.logo}
|
||||||
|
alt=""
|
||||||
|
className="w-5 h-5 object-contain flex-shrink-0"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<span
|
<span
|
||||||
className={`text-xl tracking-wide truncate ${isSelected ? "text-white" : "text-black"
|
className={`text-xl tracking-wide truncate ${
|
||||||
|
isSelected ? "text-white" : "text-black"
|
||||||
}`}
|
}`}
|
||||||
style={{ textShadow: "none" }}
|
style={{ textShadow: "none" }}
|
||||||
>
|
>
|
||||||
@@ -322,7 +337,8 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
className={`text-base font-medium leading-tight ${isSelected ? "text-[#DDDDDD]" : "text-[#666666]"
|
className={`text-base font-medium leading-tight ${
|
||||||
|
isSelected ? "text-[#DDDDDD]" : "text-[#666666]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{edition.desc}
|
{edition.desc}
|
||||||
@@ -340,12 +356,20 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
handleCancelDownload();
|
handleCancelDownload();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: "main" })}
|
onMouseEnter={() =>
|
||||||
|
setHoveredBtn({ row: i, btn: "main" })
|
||||||
|
}
|
||||||
onMouseLeave={() => setHoveredBtn(null)}
|
onMouseLeave={() => setHoveredBtn(null)}
|
||||||
className={`w-9 h-9 flex items-center justify-center ${(isDownloading || (!!downloadingId && !isInstalled)) ? "opacity-50" : ""
|
className={`w-9 h-9 flex items-center justify-center ${
|
||||||
|
isDownloading || (!!downloadingId && !isInstalled)
|
||||||
|
? "opacity-50"
|
||||||
|
: ""
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "main") || (focusIndex === i && focusBtn === 0)
|
backgroundImage:
|
||||||
|
(hoveredBtn?.row === i &&
|
||||||
|
hoveredBtn?.btn === "main") ||
|
||||||
|
(focusIndex === i && focusBtn === 0)
|
||||||
? "url('/images/Button_Square_Highlighted.png')"
|
? "url('/images/Button_Square_Highlighted.png')"
|
||||||
: "url('/images/Button_Square.png')",
|
: "url('/images/Button_Square.png')",
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
@@ -353,10 +377,19 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={isDownloading ? "/images/Trash_Bin_Icon.png" : "/images/Download_Icon.png"}
|
src={
|
||||||
|
isDownloading
|
||||||
|
? "/images/Trash_Bin_Icon.png"
|
||||||
|
: "/images/Download_Icon.png"
|
||||||
|
}
|
||||||
alt=""
|
alt=""
|
||||||
className="w-5 h-5 object-contain"
|
className="w-5 h-5 object-contain"
|
||||||
style={{ imageRendering: "pixelated", filter: isDownloading ? "hue-rotate(300deg)" : "none" }}
|
style={{
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
filter: isDownloading
|
||||||
|
? "hue-rotate(300deg)"
|
||||||
|
: "none",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
@@ -364,23 +397,39 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
playPressSound();
|
playPressSound();
|
||||||
setOpenMenuId(openMenuId === edition.id ? null : edition.id);
|
setOpenMenuId(
|
||||||
|
openMenuId === edition.id ? null : edition.id,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => setHoveredBtn({ row: i, btn: "menu" })}
|
onMouseEnter={() =>
|
||||||
|
setHoveredBtn({ row: i, btn: "menu" })
|
||||||
|
}
|
||||||
onMouseLeave={() => setHoveredBtn(null)}
|
onMouseLeave={() => setHoveredBtn(null)}
|
||||||
className="w-9 h-9 flex flex-col items-center justify-center gap-1 transition-colors relative"
|
className="w-9 h-9 flex flex-col items-center justify-center gap-1 transition-colors relative"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "menu") || (focusIndex === i && (focusBtn === 0 || focusBtn === 1))
|
backgroundImage:
|
||||||
|
(hoveredBtn?.row === i &&
|
||||||
|
hoveredBtn?.btn === "menu") ||
|
||||||
|
(focusIndex === i &&
|
||||||
|
(focusBtn === 0 || focusBtn === 1))
|
||||||
? "url('/images/Button_Square_Highlighted.png')"
|
? "url('/images/Button_Square_Highlighted.png')"
|
||||||
: "url('/images/Button_Square.png')",
|
: "url('/images/Button_Square.png')",
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
imageRendering: "pixelated",
|
imageRendering: "pixelated",
|
||||||
filter: updatesAvailable?.[edition.instanceId] ? "drop-shadow(0 0 4px rgba(255,255,0,0.8))" : "none"
|
filter: updatesAvailable?.[edition.instanceId]
|
||||||
|
? "drop-shadow(0 0 4px rgba(255,255,0,0.8))"
|
||||||
|
: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`} />
|
<div
|
||||||
<div className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`} />
|
className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`}
|
||||||
<div className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`} />
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-1.5 h-1.5 ${updatesAvailable?.[edition.instanceId] ? "bg-[#ffff55]" : "bg-white"}`}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -388,7 +437,7 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
<div
|
<div
|
||||||
className="absolute right-0 top-11 w-48 bg-[#1a1a1a] border-2 border-[#555] z-[100] shadow-2xl p-0.5 animate-in fade-in zoom-in duration-75"
|
className="absolute right-0 top-11 w-48 bg-[#1a1a1a] border-2 border-[#555] z-[100] shadow-2xl p-0.5 animate-in fade-in zoom-in duration-75"
|
||||||
style={{
|
style={{
|
||||||
imageRendering: "pixelated"
|
imageRendering: "pixelated",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{updatesAvailable?.[edition.instanceId] && (
|
{updatesAvailable?.[edition.instanceId] && (
|
||||||
@@ -401,11 +450,17 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-1.5 text-xs text-[#ffff55] hover:text-white hover:bg-[#ffff55]/20 flex items-center gap-2 group transition-colors mc-text-shadow font-bold border-b border-white/5 mb-1"
|
className="w-full text-left px-3 py-1.5 text-xs text-[#ffff55] hover:text-white hover:bg-[#ffff55]/20 flex items-center gap-2 group transition-colors mc-text-shadow font-bold border-b border-white/5 mb-1"
|
||||||
>
|
>
|
||||||
<img src="/images/Download_Icon.png" alt="" className="w-3 h-3 object-contain" style={{ imageRendering: "pixelated" }} />
|
<img
|
||||||
|
src="/images/Download_Icon.png"
|
||||||
|
alt=""
|
||||||
|
className="w-3 h-3 object-contain"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
Update Available!
|
Update Available!
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{Array.isArray(edition.branches) && edition.branches.length > 0 && (
|
{Array.isArray(edition.branches) &&
|
||||||
|
edition.branches.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -432,24 +487,72 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 text-xs text-[#dddddd] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
className="w-full text-left px-3 py-2 text-xs text-[#dddddd] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
||||||
>
|
>
|
||||||
<img src="/images/Folder_Icon.png" alt="" className="w-3.5 h-3.5 object-contain" style={{ imageRendering: "pixelated" }} />
|
<img
|
||||||
|
src="/images/Folder_Icon.png"
|
||||||
|
alt=""
|
||||||
|
className="w-3.5 h-3.5 object-contain"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
Open Folder
|
Open Folder
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
playPressSound();
|
playPressSound();
|
||||||
const PANORAMA_PROFILES = ['legacy_evolved', 'vanilla_tu19', '360revived', 'vanilla_tu24'];
|
const PANORAMA_PROFILES = [
|
||||||
const panoId = PANORAMA_PROFILES.includes(edition.id) ? edition.id : 'vanilla_tu19';
|
"legacy_evolved",
|
||||||
const panoramaUrl = `/panorama/${panoId}_Panorama_Background_${isDayTime ? 'Day' : 'Night'}.png`;
|
"vanilla_tu19",
|
||||||
addToSteam(edition.instanceId, edition.name, edition.titleImage, panoramaUrl);
|
"360revived",
|
||||||
|
"vanilla_tu24",
|
||||||
|
];
|
||||||
|
const panoId = PANORAMA_PROFILES.includes(
|
||||||
|
edition.id,
|
||||||
|
)
|
||||||
|
? edition.id
|
||||||
|
: "vanilla_tu19";
|
||||||
|
const panoramaUrl = `/panorama/${panoId}_Panorama_Background_${isDayTime ? "Day" : "Night"}.png`;
|
||||||
|
addToSteam(
|
||||||
|
edition.instanceId,
|
||||||
|
edition.name,
|
||||||
|
edition.titleImage,
|
||||||
|
panoramaUrl,
|
||||||
|
);
|
||||||
setOpenMenuId(null);
|
setOpenMenuId(null);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 text-xs text-[#dddddd] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
className="w-full text-left px-3 py-2 text-xs text-[#dddddd] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
||||||
>
|
>
|
||||||
<img src="/images/steam.png" alt="" className="w-3.5 h-3.5 object-contain invert brightness-0" style={{ imageRendering: "pixelated" }} />
|
<img
|
||||||
|
src="/images/steam.png"
|
||||||
|
alt=""
|
||||||
|
className="w-3.5 h-3.5 object-contain invert brightness-0"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
Add to Steam
|
Add to Steam
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
playPressSound();
|
||||||
|
setSetUidTargetId(edition.instanceId);
|
||||||
|
setIsSetUidModalOpen(true);
|
||||||
|
setOpenMenuId(null);
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 text-xs text-[#dddddd] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
className="w-3.5 h-3.5"
|
||||||
|
>
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||||
|
<circle cx="12" cy="7" r="4"></circle>
|
||||||
|
</svg>
|
||||||
|
Set UID
|
||||||
|
</button>
|
||||||
{isCustom ? (
|
{isCustom ? (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -461,7 +564,17 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 text-xs text-[#aaaaaa] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
className="w-full text-left px-3 py-2 text-xs text-[#aaaaaa] hover:text-white hover:bg-white/10 flex items-center gap-2 transition-colors mc-text-shadow"
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-3.5 h-3.5"><path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" /></svg>
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
className="w-3.5 h-3.5"
|
||||||
|
>
|
||||||
|
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||||
|
</svg>
|
||||||
Edit Custom
|
Edit Custom
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -479,7 +592,12 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
}}
|
}}
|
||||||
className="w-full text-left px-3 py-2 text-xs text-red-500 hover:text-red-400 hover:bg-red-500/10 flex items-center gap-2 transition-colors mc-text-shadow font-bold"
|
className="w-full text-left px-3 py-2 text-xs text-red-500 hover:text-red-400 hover:bg-red-500/10 flex items-center gap-2 transition-colors mc-text-shadow font-bold"
|
||||||
>
|
>
|
||||||
<img src="/images/Trash_Bin_Icon.png" alt="" className="w-3.5 h-3.5 object-contain" style={{ imageRendering: "pixelated" }} />
|
<img
|
||||||
|
src="/images/Trash_Bin_Icon.png"
|
||||||
|
alt=""
|
||||||
|
className="w-3.5 h-3.5 object-contain"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
{isCustom ? "Remove Custom" : "Uninstall"}
|
{isCustom ? "Remove Custom" : "Uninstall"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -598,6 +716,16 @@ const VersionsView = memo(function VersionsView() {
|
|||||||
initialPath={initialPath}
|
initialPath={initialPath}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SetUidModal
|
||||||
|
isOpen={isSetUidModalOpen}
|
||||||
|
onClose={() => setIsSetUidModalOpen(false)}
|
||||||
|
playPressSound={playPressSound}
|
||||||
|
playBackSound={playBackSound}
|
||||||
|
instances={editions}
|
||||||
|
installedVersions={installedVersions}
|
||||||
|
targetInstanceId={setUidTargetId}
|
||||||
|
/>
|
||||||
|
|
||||||
{deleteConfirmEdition && (
|
{deleteConfirmEdition && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||||||
configRaw.username, configRaw.theme, configRaw.layout, configRaw.vfxEnabled,
|
configRaw.username, configRaw.theme, configRaw.layout, configRaw.vfxEnabled,
|
||||||
configRaw.rpcEnabled, configRaw.musicVol, configRaw.sfxVol, configRaw.isDayTime,
|
configRaw.rpcEnabled, configRaw.musicVol, configRaw.sfxVol, configRaw.isDayTime,
|
||||||
configRaw.profile, configRaw.linuxRunner, configRaw.perfBoost, configRaw.customEditions,
|
configRaw.profile, configRaw.linuxRunner, configRaw.perfBoost, configRaw.customEditions,
|
||||||
configRaw.legacyMode, configRaw.animationsEnabled
|
configRaw.legacyMode, configRaw.animationsEnabled, configRaw.mangohudEnabled
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const game = useMemo(() => gameRaw, [
|
const game = useMemo(() => gameRaw, [
|
||||||
@@ -129,13 +129,15 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||||||
musicVol: config.musicVol,
|
musicVol: config.musicVol,
|
||||||
sfxVol: config.sfxVol,
|
sfxVol: config.sfxVol,
|
||||||
legacyMode: config.legacyMode,
|
legacyMode: config.legacyMode,
|
||||||
|
mangohudEnabled: config.mangohudEnabled,
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
config.username, skinSync.skinBase64, config.theme, config.linuxRunner,
|
config.username, skinSync.skinBase64, config.theme, config.linuxRunner,
|
||||||
config.perfBoost, config.customEditions, config.profile,
|
config.perfBoost, config.customEditions, config.profile,
|
||||||
config.vfxEnabled, config.animationsEnabled,
|
config.vfxEnabled, config.animationsEnabled,
|
||||||
config.rpcEnabled, config.musicVol, config.sfxVol, config.legacyMode, config.isLoaded
|
config.rpcEnabled, config.musicVol, config.sfxVol, config.legacyMode,
|
||||||
|
config.mangohudEnabled, config.isLoaded
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Mojangles';
|
font-family: "Mojangles";
|
||||||
src: url('/fonts/Mojangles.ttf') format('truetype');
|
src: url("/fonts/Mojangles.ttf") format("truetype");
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
color: #FFFF55;
|
color: #ffff55;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
.mc-setup-nav-btn {
|
.mc-setup-nav-btn {
|
||||||
border: 4px solid transparent;
|
border: 4px solid transparent;
|
||||||
border-image: url('/images/Button_Background.png') 4 stretch;
|
border-image: url("/images/Button_Background.png") 4 stretch;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
background: rgba(0,0,0,0.6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,26 +38,111 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mc-options-bg {
|
||||||
|
border-image: url("/images/options_background.png") 4 fill;
|
||||||
|
border-width: 12px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-lce-tab {
|
||||||
|
background: linear-gradient(180deg, #c0c0c0 0%, #a0a0a0 100%);
|
||||||
|
border: 2px solid #808080;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
box-shadow:
|
||||||
|
inset 2px 2px 0 #e0e0e0,
|
||||||
|
inset -2px 0 0 #808080,
|
||||||
|
0 -2px 0 #606060;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
position: relative;
|
||||||
|
transform: translateY(2px);
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-lce-tab.active {
|
||||||
|
background: linear-gradient(180deg, #d0d0d0 0%, #b0b0b0 100%);
|
||||||
|
transform: translateY(0);
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow:
|
||||||
|
inset 2px 2px 0 #f0f0f0,
|
||||||
|
inset -2px 0 0 #909090,
|
||||||
|
0 -3px 0 #707070;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-lce-content {
|
||||||
|
background: linear-gradient(180deg, #b0b0b0 0%, #909090 100%);
|
||||||
|
border: 3px solid #808080;
|
||||||
|
border-radius: 0 4px 4px 4px;
|
||||||
|
box-shadow:
|
||||||
|
inset 3px 3px 0 #d0d0d0,
|
||||||
|
inset -3px -3px 0 #606060;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-lce-content::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-lce-tab-bg {
|
||||||
|
border: 4px solid transparent;
|
||||||
|
border-image: url("/images/options_background.png") 4 fill;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
position: relative;
|
||||||
|
transform: translateY(4px);
|
||||||
|
z-index: 1;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
min-height: 40px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-lce-tab-bg.active {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-lce-content-bg {
|
||||||
|
border-image: url("/images/options_background.png") 4 fill;
|
||||||
|
border-width: 12px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
margin-top: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
cursor: url('/images/pointer.png') 16 16, auto;
|
cursor:
|
||||||
|
url("/images/pointer.png") 16 16,
|
||||||
|
auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
font-family: 'Mojangles', monospace;
|
font-family: "Mojangles", monospace;
|
||||||
-webkit-font-smoothing: none;
|
-webkit-font-smoothing: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mc-button {
|
.mc-button {
|
||||||
background-image: url('/images/button.png');
|
background-image: url("/images/button.png");
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mc-button:hover:not(:disabled) {
|
.mc-button:hover:not(:disabled) {
|
||||||
background-image: url('/images/button_highlighted.png');
|
background-image: url("/images/button_highlighted.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.mc-text-shadow {
|
.mc-text-shadow {
|
||||||
@@ -68,7 +153,6 @@ body {
|
|||||||
text-shadow: 1px 1px 0px rgba(63, 90, 63, 1);
|
text-shadow: 1px 1px 0px rgba(63, 90, 63, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@keyframes sga-burst {
|
@keyframes sga-burst {
|
||||||
0% {
|
0% {
|
||||||
transform: translate(-50%, -50%) scale(0.5);
|
transform: translate(-50%, -50%) scale(0.5);
|
||||||
@@ -76,7 +160,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: translate(calc(-50% + var(--vX)), calc(-50% + var(--vY))) scale(1.2) rotate(var(--rot));
|
transform: translate(calc(-50% + var(--vX)), calc(-50% + var(--vY)))
|
||||||
|
scale(1.2) rotate(var(--rot));
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,13 +176,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: url('/images/SliderHandlerBackground.png') center;
|
background: url("/images/SliderHandlerBackground.png") center;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: url('/images/Slider_Handle.png') no-repeat center;
|
background: url("/images/Slider_Handle.png") no-repeat center;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
@@ -112,13 +197,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-track {
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
background: url('/images/SliderHandlerBackground.png') center;
|
background: url("/images/SliderHandlerBackground.png") center;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
background: url('/images/Slider_Handle.png') no-repeat center;
|
background: url("/images/Slider_Handle.png") no-repeat center;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
@@ -136,14 +221,14 @@ body {
|
|||||||
.workshop-markdown h1,
|
.workshop-markdown h1,
|
||||||
.workshop-markdown h2,
|
.workshop-markdown h2,
|
||||||
.workshop-markdown h3 {
|
.workshop-markdown h3 {
|
||||||
color: #FFFF55;
|
color: #ffff55;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-markdown strong {
|
.workshop-markdown strong {
|
||||||
color: #FFFF55;
|
color: #ffff55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-markdown ul,
|
.workshop-markdown ul,
|
||||||
@@ -157,15 +242,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workshop-markdown a {
|
.workshop-markdown a {
|
||||||
color: #55FFFF;
|
color: #55ffff;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-markdown code {
|
.workshop-markdown code {
|
||||||
background: rgba(0,0,0,0.4);
|
background: rgba(0, 0, 0, 0.4);
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
color: #FF55FF;
|
color: #ff55ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-markdown table {
|
.workshop-markdown table {
|
||||||
@@ -173,14 +258,14 @@ body {
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: rgba(0,0,0,0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
border: 2px solid #555;
|
border: 2px solid #555;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-markdown th {
|
.workshop-markdown th {
|
||||||
background: rgba(0,0,0,0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
color: #FFFF55;
|
color: #ffff55;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -191,30 +276,30 @@ body {
|
|||||||
.workshop-markdown td {
|
.workshop-markdown td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
color: #E0E0E0;
|
color: #e0e0e0;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-markdown tr:nth-child(even) {
|
.workshop-markdown tr:nth-child(even) {
|
||||||
background: rgba(255,255,255,0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-markdown blockquote {
|
.workshop-markdown blockquote {
|
||||||
border-left: 4px solid #555555;
|
border-left: 4px solid #555555;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
color: #A0A0A0;
|
color: #a0a0a0;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
background: rgba(255,255,255,0.02);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-markdown del {
|
.workshop-markdown del {
|
||||||
color: #AA0000;
|
color: #aa0000;
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workshop-markdown input[type="checkbox"] {
|
.workshop-markdown input[type="checkbox"] {
|
||||||
accent-color: #FFFF55;
|
accent-color: #ffff55;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function useAppConfig() {
|
|||||||
const [linuxRunner, setLinuxRunner] = useState<string | undefined>();
|
const [linuxRunner, setLinuxRunner] = useState<string | undefined>();
|
||||||
const [perfBoost, setPerfBoost] = useState(false);
|
const [perfBoost, setPerfBoost] = useState(false);
|
||||||
const [customEditions, setCustomEditions] = useState<any[]>([]);
|
const [customEditions, setCustomEditions] = useState<any[]>([]);
|
||||||
|
const [mangohudEnabled, setMangohudEnabled] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
TauriService.loadConfig().then((config) => {
|
TauriService.loadConfig().then((config) => {
|
||||||
if (config.username) setUsername(config.username);
|
if (config.username) setUsername(config.username);
|
||||||
@@ -33,6 +34,7 @@ export function useAppConfig() {
|
|||||||
if (config.musicVol !== undefined && config.musicVol !== null) setMusicVol(config.musicVol);
|
if (config.musicVol !== undefined && config.musicVol !== null) setMusicVol(config.musicVol);
|
||||||
if (config.sfxVol !== undefined && config.sfxVol !== null) setSfxVol(config.sfxVol);
|
if (config.sfxVol !== undefined && config.sfxVol !== null) setSfxVol(config.sfxVol);
|
||||||
if (config.legacyMode !== undefined) setLegacyMode(config.legacyMode);
|
if (config.legacyMode !== undefined) setLegacyMode(config.legacyMode);
|
||||||
|
if (config.mangohudEnabled !== undefined) setMangohudEnabled(config.mangohudEnabled);
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -52,9 +54,10 @@ export function useAppConfig() {
|
|||||||
musicVol,
|
musicVol,
|
||||||
sfxVol,
|
sfxVol,
|
||||||
legacyMode,
|
legacyMode,
|
||||||
|
mangohudEnabled,
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, animationsEnabled, vfxEnabled, rpcEnabled, musicVol, sfxVol, legacyMode, isLoaded]);
|
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, animationsEnabled, vfxEnabled, rpcEnabled, musicVol, sfxVol, legacyMode, mangohudEnabled, isLoaded]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
username,
|
username,
|
||||||
@@ -88,5 +91,7 @@ export function useAppConfig() {
|
|||||||
isLoaded,
|
isLoaded,
|
||||||
hasCompletedSetup,
|
hasCompletedSetup,
|
||||||
setHasCompletedSetup,
|
setHasCompletedSetup,
|
||||||
|
mangohudEnabled,
|
||||||
|
setMangohudEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export function useDiscordRPC({
|
|||||||
devtools: "Developing for LCE",
|
devtools: "Developing for LCE",
|
||||||
skins: "Changing Skins",
|
skins: "Changing Skins",
|
||||||
workshop: "Browsing Workshop",
|
workshop: "Browsing Workshop",
|
||||||
|
lcelive: "Browsing Friends",
|
||||||
"pck-editor": "Editing a PCK file",
|
"pck-editor": "Editing a PCK file",
|
||||||
"options-editor": "Editing Options Files",
|
"options-editor": "Editing Options Files",
|
||||||
"arc-editor": "Editing an ARC file",
|
"arc-editor": "Editing an ARC file",
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ const BASE_EDITIONS = [
|
|||||||
url: "https://github.com/pieeebot/neoLegacy/releases/download/latest/neoLegacyWindows64.zip",
|
url: "https://github.com/pieeebot/neoLegacy/releases/download/latest/neoLegacyWindows64.zip",
|
||||||
titleImage: "/images/minecraft_title_neoLegacy.png",
|
titleImage: "/images/minecraft_title_neoLegacy.png",
|
||||||
supportsSlimSkins: true,
|
supportsSlimSkins: true,
|
||||||
logo: "/images/neoLegacy.png"
|
logo: "/images/neoLegacy.png",
|
||||||
|
panorama: "legacy_evolved",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "revelations",
|
id: "revelations",
|
||||||
@@ -33,6 +34,7 @@ const BASE_EDITIONS = [
|
|||||||
url: "https://github.com/itsRevela/LCE-Revelations/releases/download/Nightly/LCE-Revelations-Client-Win64.zip",
|
url: "https://github.com/itsRevela/LCE-Revelations/releases/download/Nightly/LCE-Revelations-Client-Win64.zip",
|
||||||
titleImage: "/images/minecraft_title_revelations.png",
|
titleImage: "/images/minecraft_title_revelations.png",
|
||||||
supportsSlimSkins: false,
|
supportsSlimSkins: false,
|
||||||
|
panorama: "vanilla_tu24",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "360revived",
|
id: "360revived",
|
||||||
@@ -41,7 +43,8 @@ const BASE_EDITIONS = [
|
|||||||
url: "https://github.com/BluTac10/360Revived/releases/download/nightly/LCEWindows64.zip",
|
url: "https://github.com/BluTac10/360Revived/releases/download/nightly/LCEWindows64.zip",
|
||||||
titleImage: "/images/minecraft_title_360revived.png",
|
titleImage: "/images/minecraft_title_360revived.png",
|
||||||
supportsSlimSkins: false,
|
supportsSlimSkins: false,
|
||||||
logo: "/images/360_revived.png"
|
logo: "/images/360_revived.png",
|
||||||
|
panorama: "360revived",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "legacy_nether_fork",
|
id: "legacy_nether_fork",
|
||||||
@@ -50,8 +53,9 @@ const BASE_EDITIONS = [
|
|||||||
url: "https://github.com/deadvoxelx/HellishEnds/releases/download/nightly/LCEWindows64.zip",
|
url: "https://github.com/deadvoxelx/HellishEnds/releases/download/nightly/LCEWindows64.zip",
|
||||||
titleImage: "/images/minecraft_title_hellishends.png",
|
titleImage: "/images/minecraft_title_hellishends.png",
|
||||||
supportsSlimSkins: false,
|
supportsSlimSkins: false,
|
||||||
logo: "/images/netherrack_0.png"
|
logo: "/images/netherrack_0.png",
|
||||||
}
|
panorama: "vanilla_tu19",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const PARTNERSHIP_SERVERS = [
|
const PARTNERSHIP_SERVERS = [
|
||||||
@@ -101,22 +105,28 @@ export function useGameManager({
|
|||||||
number | null
|
number | null
|
||||||
>(null);
|
>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [gameUpdateMessage, setGameUpdateMessage] = useState<string | null>(null);
|
const [gameUpdateMessage, setGameUpdateMessage] = useState<string | null>(
|
||||||
const [steamSuccessMessage, setSteamSuccessMessage] = useState<string | null>(null);
|
null,
|
||||||
|
);
|
||||||
|
const [steamSuccessMessage, setSteamSuccessMessage] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [dynamicUrls, setDynamicUrls] = useState<Record<string, string>>({});
|
const [dynamicUrls, setDynamicUrls] = useState<Record<string, string>>({});
|
||||||
const [branches, setBranches] = useState<Record<string, string[]>>({});
|
const [branches, setBranches] = useState<Record<string, string[]>>({});
|
||||||
const [selectedBranches, setSelectedBranches] = useState<Record<string, string>>({});
|
const [selectedBranches, setSelectedBranches] = useState<
|
||||||
|
Record<string, string>
|
||||||
|
>({});
|
||||||
const branchesFetched = useRef<Set<string>>(new Set());
|
const branchesFetched = useRef<Set<string>>(new Set());
|
||||||
const initialBranchesSet = useRef(false);
|
const initialBranchesSet = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialBranchesSet.current || !profile) return;
|
if (initialBranchesSet.current || !profile) return;
|
||||||
BASE_EDITIONS.forEach(e => {
|
BASE_EDITIONS.forEach((e) => {
|
||||||
if (profile.startsWith(e.id + "_")) {
|
if (profile.startsWith(e.id + "_")) {
|
||||||
const branch = profile.replace(e.id + "_", "");
|
const branch = profile.replace(e.id + "_", "");
|
||||||
setSelectedBranches(prev => ({ ...prev, [e.id]: branch }));
|
setSelectedBranches((prev) => ({ ...prev, [e.id]: branch }));
|
||||||
} else if (profile === e.id) {
|
} else if (profile === e.id) {
|
||||||
setSelectedBranches(prev => ({ ...prev, [e.id]: "Stable" }));
|
setSelectedBranches((prev) => ({ ...prev, [e.id]: "Stable" }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
initialBranchesSet.current = true;
|
initialBranchesSet.current = true;
|
||||||
@@ -125,12 +135,19 @@ export function useGameManager({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchLatestReleases() {
|
async function fetchLatestReleases() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://api.github.com/repos/pieeebot/neoLegacy/releases/latest");
|
const response = await fetch(
|
||||||
|
"https://api.github.com/repos/pieeebot/neoLegacy/releases/latest",
|
||||||
|
);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const asset = data.assets.find((a: any) => a.name === "neoLegacyWindows64.zip");
|
const asset = data.assets.find(
|
||||||
|
(a: any) => a.name === "neoLegacyWindows64.zip",
|
||||||
|
);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
setDynamicUrls(prev => ({ ...prev, legacy_evolved: asset.browser_download_url }));
|
setDynamicUrls((prev) => ({
|
||||||
|
...prev,
|
||||||
|
legacy_evolved: asset.browser_download_url,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -140,43 +157,54 @@ export function useGameManager({
|
|||||||
fetchLatestReleases();
|
fetchLatestReleases();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchBranchesForEdition = useCallback(async (editionId: string, url: string) => {
|
const fetchBranchesForEdition = useCallback(
|
||||||
|
async (editionId: string, url: string) => {
|
||||||
if (branchesFetched.current.has(editionId)) return;
|
if (branchesFetched.current.has(editionId)) return;
|
||||||
if (!url.includes("github.com")) return;
|
if (!url.includes("github.com")) return;
|
||||||
const parts = url.split("github.com/")[1].split("/");
|
const parts = url.split("github.com/")[1].split("/");
|
||||||
const owner = parts[0];
|
const owner = parts[0];
|
||||||
const repo = parts[1];
|
const repo = parts[1];
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`);
|
const response = await fetch(
|
||||||
|
`https://api.github.com/repos/${owner}/${repo}/releases`,
|
||||||
|
);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
let tags: string[] = data
|
let tags: string[] = data
|
||||||
.map((r: any) => r.tag_name)
|
.map((r: any) => r.tag_name)
|
||||||
.filter((t: string) => !t.toLowerCase().includes("server"));
|
.filter((t: string) => !t.toLowerCase().includes("server"));
|
||||||
|
|
||||||
const vTags = tags.filter(t => t.startsWith("v")).sort(compareVersions);
|
const vTags = tags
|
||||||
|
.filter((t) => t.startsWith("v"))
|
||||||
|
.sort(compareVersions);
|
||||||
const bestVTag = vTags[vTags.length - 1];
|
const bestVTag = vTags[vTags.length - 1];
|
||||||
if (!tags.includes("Stable")) tags.unshift("Stable");
|
if (!tags.includes("Stable")) tags.unshift("Stable");
|
||||||
if (bestVTag) {
|
if (bestVTag) {
|
||||||
setDynamicUrls(prev => ({ ...prev, [`${editionId}_Stable`]: bestVTag }));
|
setDynamicUrls((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${editionId}_Stable`]: bestVTag,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
setBranches(prev => ({ ...prev, [editionId]: tags }));
|
setBranches((prev) => ({ ...prev, [editionId]: tags }));
|
||||||
branchesFetched.current.add(editionId);
|
branchesFetched.current.add(editionId);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to fetch branches for ${editionId}:`, e);
|
console.error(`Failed to fetch branches for ${editionId}:`, e);
|
||||||
}
|
}
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
BASE_EDITIONS.forEach(e => fetchBranchesForEdition(e.id, e.url));
|
BASE_EDITIONS.forEach((e) => fetchBranchesForEdition(e.id, e.url));
|
||||||
}, [fetchBranchesForEdition]);
|
}, [fetchBranchesForEdition]);
|
||||||
|
|
||||||
const cycleBranch = useCallback((editionId: string) => {
|
const cycleBranch = useCallback(
|
||||||
|
(editionId: string) => {
|
||||||
const available = branches[editionId] || ["Stable"];
|
const available = branches[editionId] || ["Stable"];
|
||||||
if (available.length <= 1) return;
|
if (available.length <= 1) return;
|
||||||
setSelectedBranches(prev => {
|
setSelectedBranches((prev) => {
|
||||||
const current = prev[editionId] || available[0];
|
const current = prev[editionId] || available[0];
|
||||||
let currentIndex = available.indexOf(current);
|
let currentIndex = available.indexOf(current);
|
||||||
let nextIndex = currentIndex;
|
let nextIndex = currentIndex;
|
||||||
@@ -194,17 +222,23 @@ export function useGameManager({
|
|||||||
}
|
}
|
||||||
return { ...prev, [editionId]: nextBranch };
|
return { ...prev, [editionId]: nextBranch };
|
||||||
});
|
});
|
||||||
}, [branches, profile, setProfile]);
|
},
|
||||||
|
[branches, profile, setProfile],
|
||||||
|
);
|
||||||
|
|
||||||
const editions = useMemo(() => {
|
const editions = useMemo(() => {
|
||||||
return [...BASE_EDITIONS.map(e => {
|
return [
|
||||||
|
...BASE_EDITIONS.map((e) => {
|
||||||
const availableBranches = branches[e.id] || ["Stable"];
|
const availableBranches = branches[e.id] || ["Stable"];
|
||||||
const selectedBranch = selectedBranches[e.id] || availableBranches[0];
|
const selectedBranch = selectedBranches[e.id] || availableBranches[0];
|
||||||
let url = dynamicUrls[e.id] || e.url;
|
let url = dynamicUrls[e.id] || e.url;
|
||||||
const defaultBranchFromUrl = e.url.includes("/releases/download/")
|
const defaultBranchFromUrl = e.url.includes("/releases/download/")
|
||||||
? e.url.split("/releases/download/")[1].split("/")[0]
|
? e.url.split("/releases/download/")[1].split("/")[0]
|
||||||
: "nightly";
|
: "nightly";
|
||||||
const branchToUse = selectedBranch === "Stable" ? (dynamicUrls[`${e.id}_Stable`] || defaultBranchFromUrl) : selectedBranch;
|
const branchToUse =
|
||||||
|
selectedBranch === "Stable"
|
||||||
|
? dynamicUrls[`${e.id}_Stable`] || defaultBranchFromUrl
|
||||||
|
: selectedBranch;
|
||||||
if (e.url.includes("github.com")) {
|
if (e.url.includes("github.com")) {
|
||||||
const baseUrl = e.url.split("/releases/download/")[0];
|
const baseUrl = e.url.split("/releases/download/")[0];
|
||||||
const filename = e.url.split("/").pop();
|
const filename = e.url.split("/").pop();
|
||||||
@@ -216,9 +250,12 @@ export function useGameManager({
|
|||||||
url,
|
url,
|
||||||
branches: availableBranches,
|
branches: availableBranches,
|
||||||
selectedBranch,
|
selectedBranch,
|
||||||
instanceId: selectedBranch === "Stable" ? e.id : `${e.id}_${selectedBranch}`
|
instanceId:
|
||||||
|
selectedBranch === "Stable" ? e.id : `${e.id}_${selectedBranch}`,
|
||||||
};
|
};
|
||||||
}), ...customEditions.map(e => ({ ...e, instanceId: e.id }))];
|
}),
|
||||||
|
...customEditions.map((e) => ({ ...e, instanceId: e.id })),
|
||||||
|
];
|
||||||
}, [customEditions, dynamicUrls, branches, selectedBranches]);
|
}, [customEditions, dynamicUrls, branches, selectedBranches]);
|
||||||
|
|
||||||
const checkInstalls = useCallback(async () => {
|
const checkInstalls = useCallback(async () => {
|
||||||
@@ -231,19 +268,25 @@ export function useGameManager({
|
|||||||
setInstalls(results.filter((id): id is string => id !== null));
|
setInstalls(results.filter((id): id is string => id !== null));
|
||||||
}, [editions]);
|
}, [editions]);
|
||||||
|
|
||||||
const [updatesAvailable, setUpdatesAvailable] = useState<Record<string, boolean>>({});
|
const [updatesAvailable, setUpdatesAvailable] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
const checkForGameUpdates = useCallback(async () => {
|
const checkForGameUpdates = useCallback(async () => {
|
||||||
const checks = await Promise.all(
|
const checks = await Promise.all(
|
||||||
editions.map(async (edition) => {
|
editions.map(async (edition) => {
|
||||||
if (!installs.includes(edition.instanceId)) return [edition.instanceId, false] as const;
|
if (!installs.includes(edition.instanceId))
|
||||||
|
return [edition.instanceId, false] as const;
|
||||||
try {
|
try {
|
||||||
const isUpdate = await TauriService.checkGameUpdate(edition.instanceId, edition.url);
|
const isUpdate = await TauriService.checkGameUpdate(
|
||||||
|
edition.instanceId,
|
||||||
|
edition.url,
|
||||||
|
);
|
||||||
return [edition.instanceId, isUpdate] as const;
|
return [edition.instanceId, isUpdate] as const;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return [edition.instanceId, false] as const;
|
return [edition.instanceId, false] as const;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const newUpdates: Record<string, boolean> = {};
|
const newUpdates: Record<string, boolean> = {};
|
||||||
@@ -252,12 +295,16 @@ export function useGameManager({
|
|||||||
}
|
}
|
||||||
setUpdatesAvailable(newUpdates);
|
setUpdatesAvailable(newUpdates);
|
||||||
|
|
||||||
const updatedGames = editions.filter(e => newUpdates[e.id]);
|
const updatedGames = editions.filter((e) => newUpdates[e.id]);
|
||||||
if (updatedGames.length > 0) {
|
if (updatedGames.length > 0) {
|
||||||
if (updatedGames.length === 1) {
|
if (updatedGames.length === 1) {
|
||||||
setGameUpdateMessage(`An update is available for ${updatedGames[0].name}!`);
|
setGameUpdateMessage(
|
||||||
|
`An update is available for ${updatedGames[0].name}!`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setGameUpdateMessage(`Updates are available for ${updatedGames.length} versions!`);
|
setGameUpdateMessage(
|
||||||
|
`Updates are available for ${updatedGames.length} versions!`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setGameUpdateMessage(null);
|
setGameUpdateMessage(null);
|
||||||
@@ -378,7 +425,15 @@ export function useGameManager({
|
|||||||
}, [profile]);
|
}, [profile]);
|
||||||
|
|
||||||
const addCustomEdition = useCallback(
|
const addCustomEdition = useCallback(
|
||||||
(edition: { name: string; desc: string; url: string; path?: string; category?: string[]; logo?: string; id?: string }) => {
|
(edition: {
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
url: string;
|
||||||
|
path?: string;
|
||||||
|
category?: string[];
|
||||||
|
logo?: string;
|
||||||
|
id?: string;
|
||||||
|
}) => {
|
||||||
const id = edition.id || `custom_${Date.now()}`;
|
const id = edition.id || `custom_${Date.now()}`;
|
||||||
const newEdition = {
|
const newEdition = {
|
||||||
...edition,
|
...edition,
|
||||||
@@ -400,7 +455,10 @@ export function useGameManager({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const updateCustomEdition = useCallback(
|
const updateCustomEdition = useCallback(
|
||||||
(id: string, updated: { name: string; desc: string; url: string; path?: string }) => {
|
(
|
||||||
|
id: string,
|
||||||
|
updated: { name: string; desc: string; url: string; path?: string },
|
||||||
|
) => {
|
||||||
setCustomEditions(
|
setCustomEditions(
|
||||||
customEditions.map((e) => (e.id === id ? { ...e, ...updated } : e)),
|
customEditions.map((e) => (e.id === id ? { ...e, ...updated } : e)),
|
||||||
);
|
);
|
||||||
@@ -409,18 +467,27 @@ export function useGameManager({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const addToSteam = useCallback(
|
const addToSteam = useCallback(
|
||||||
async (id: string, name: string, titleImage: string, panoramaImage: string) => {
|
async (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
titleImage: string,
|
||||||
|
panoramaImage: string,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const titleBase64 = await imageUrlToBase64(titleImage);
|
const titleBase64 = await imageUrlToBase64(titleImage);
|
||||||
const panoramaBase64 = await imageUrlToBase64(panoramaImage);
|
const panoramaBase64 = await imageUrlToBase64(panoramaImage);
|
||||||
await TauriService.addToSteam(id, name, titleBase64, panoramaBase64);
|
await TauriService.addToSteam(id, name, titleBase64, panoramaBase64);
|
||||||
setSteamSuccessMessage(`Added ${name} to Steam! (Restart Steam to see it)`);
|
setSteamSuccessMessage(
|
||||||
|
`Added ${name} to Steam! (Restart Steam to see it)`,
|
||||||
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(typeof e === "string" ? e : e.message || "Failed to add to Steam");
|
setError(
|
||||||
|
typeof e === "string" ? e : e.message || "Failed to add to Steam",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setError, setSteamSuccessMessage]
|
[setError, setSteamSuccessMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { lceLiveService } from "../services/LceLiveService";
|
||||||
|
export function useLceLiveNotifications() {
|
||||||
|
const [friendRequestMessage, setFriendRequestMessage] = useState<string | null>(null);
|
||||||
|
const [gameInviteMessage, setGameInviteMessage] = useState<string | null>(null);
|
||||||
|
const seenRequests = useRef<Set<string>>(new Set());
|
||||||
|
const seenInvites = useRef<Set<string>>(new Set());
|
||||||
|
useEffect(() => {
|
||||||
|
let pollInterval: any;
|
||||||
|
const init = async () => {
|
||||||
|
if (lceLiveService.signedIn) {
|
||||||
|
try {
|
||||||
|
await lceLiveService.refreshSession();
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lceLiveService.signedIn) {
|
||||||
|
try {
|
||||||
|
const [reqs, invs] = await Promise.all([
|
||||||
|
lceLiveService.getPendingRequests(),
|
||||||
|
lceLiveService.getGameInvites()
|
||||||
|
]);
|
||||||
|
reqs.incoming.forEach((r: any) => seenRequests.current.add(r.accountId));
|
||||||
|
invs.filter((i: any) => i.status === "pending").forEach((i: any) => seenInvites.current.add(i.inviteId));
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
pollInterval = setInterval(async () => {
|
||||||
|
if (!lceLiveService.signedIn) return;
|
||||||
|
try {
|
||||||
|
const [reqs, invs] = await Promise.all([
|
||||||
|
lceLiveService.getPendingRequests(),
|
||||||
|
lceLiveService.getGameInvites()
|
||||||
|
]);
|
||||||
|
|
||||||
|
reqs.incoming.forEach((r: any) => {
|
||||||
|
if (!seenRequests.current.has(r.accountId)) {
|
||||||
|
seenRequests.current.add(r.accountId);
|
||||||
|
setFriendRequestMessage(`New request from ${r.displayName}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
invs.filter((i: any) => i.status === "pending").forEach((i: any) => {
|
||||||
|
if (!seenInvites.current.has(i.inviteId)) {
|
||||||
|
seenInvites.current.add(i.inviteId);
|
||||||
|
const fromName = typeof i.from === 'string' ? "Unknown" : i.from.displayName;
|
||||||
|
setGameInviteMessage(`${fromName} invited you to play!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) { }
|
||||||
|
}, 10000);
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
return () => {
|
||||||
|
if (pollInterval) clearInterval(pollInterval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
friendRequestMessage,
|
||||||
|
gameInviteMessage,
|
||||||
|
clearFriendRequestMessage: () => setFriendRequestMessage(null),
|
||||||
|
clearGameInviteMessage: () => setGameInviteMessage(null),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,49 +1,40 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
declare const __BUILD_DATE__: string;
|
import { check } from "@tauri-apps/plugin-updater";
|
||||||
const LATEST_URL = "https://api.github.com/repos/LCE-Hub/LCE-Emerald-Launcher/releases/latest";
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
const NIGHTLY_URL = "https://api.github.com/repos/LCE-Hub/LCE-Emerald-Launcher/releases/tags/nightly";
|
|
||||||
export function useUpdateCheck() {
|
export function useUpdateCheck() {
|
||||||
const [updateMessage, setUpdateMessage] = useState<string | null>(null);
|
const [updateMessage, setUpdateMessage] = useState<string | null>(null);
|
||||||
const [updateUrl, setUpdateUrl] = useState<string | null>(null);
|
const [updateUrl, _setUpdateUrl] = useState<string | null>(null);
|
||||||
const checkUpdates = useCallback(async () => {
|
const checkUpdates = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const buildDate = new Date(__BUILD_DATE__);
|
const update = await check();
|
||||||
let isNightly = false;
|
if (update) {
|
||||||
let latestData = null;
|
setUpdateMessage(`Downloading update: ${update.version}...`);
|
||||||
let latestDate = new Date(0);
|
let downloaded = 0;
|
||||||
|
let contentLength = 0;
|
||||||
|
await update.downloadAndInstall((event) => {
|
||||||
|
switch (event.event) {
|
||||||
|
case 'Started':
|
||||||
|
contentLength = event.data.contentLength || 0;
|
||||||
|
setUpdateMessage(`Starting download of ${update.version}...`);
|
||||||
|
break;
|
||||||
|
case 'Progress':
|
||||||
|
downloaded += event.data.chunkLength;
|
||||||
|
if (contentLength > 0) {
|
||||||
|
const percent = Math.round((downloaded / contentLength) * 100);
|
||||||
|
setUpdateMessage(`Downloading update: ${percent}%`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Finished':
|
||||||
|
setUpdateMessage(`Installing update...`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setUpdateMessage("Update installed. Restarting...");
|
||||||
try {
|
try {
|
||||||
const latestResponse = await fetch(LATEST_URL);
|
await relaunch();
|
||||||
if (latestResponse.ok) {
|
|
||||||
latestData = await latestResponse.json();
|
|
||||||
latestDate = new Date(latestData.published_at);
|
|
||||||
if (buildDate > latestDate) {
|
|
||||||
isNightly = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isNightly = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isNightly = true;
|
console.warn("Could not relaunch immediately:", e);
|
||||||
}
|
|
||||||
|
|
||||||
if (isNightly) {
|
|
||||||
const nightlyResponse = await fetch(NIGHTLY_URL);
|
|
||||||
if (!nightlyResponse.ok) return;
|
|
||||||
const nightlyData = await nightlyResponse.json();
|
|
||||||
let releaseDate = new Date(nightlyData.published_at || nightlyData.updated_at);
|
|
||||||
if (nightlyData.assets && nightlyData.assets.length > 0) {
|
|
||||||
const assetDate = new Date(nightlyData.assets[0].updated_at);
|
|
||||||
if (assetDate > releaseDate) releaseDate = assetDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (releaseDate > buildDate) {
|
|
||||||
setUpdateMessage(`A new Nightly build is available!`);
|
|
||||||
setUpdateUrl("https://github.com/LCE-Hub/LCE-Emerald-Launcher/releases/tag/nightly");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (latestDate > buildDate && latestData) {
|
|
||||||
setUpdateMessage(`Version ${latestData.tag_name} is now available!`);
|
|
||||||
setUpdateUrl(latestData.html_url || LATEST_URL);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ColEditorView from "../components/views/ColEditorView";
|
|||||||
import OptionsEditorView from "../components/views/OptionsEditorView";
|
import OptionsEditorView from "../components/views/OptionsEditorView";
|
||||||
import ScreenshotsView from "../components/views/ScreenshotsView";
|
import ScreenshotsView from "../components/views/ScreenshotsView";
|
||||||
import SwfView from "../components/views/SwfView";
|
import SwfView from "../components/views/SwfView";
|
||||||
|
import LceLiveView from "../components/views/LceLiveView";
|
||||||
import SkinViewer from "../components/common/SkinViewer";
|
import SkinViewer from "../components/common/SkinViewer";
|
||||||
import TeamModal from "../components/modals/TeamModal";
|
import TeamModal from "../components/modals/TeamModal";
|
||||||
import PanoramaBackground from "../components/common/PanoramaBackground";
|
import PanoramaBackground from "../components/common/PanoramaBackground";
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
useSkin,
|
useSkin,
|
||||||
} from "../context/LauncherContext";
|
} from "../context/LauncherContext";
|
||||||
import { TauriService } from "../services/TauriService";
|
import { TauriService } from "../services/TauriService";
|
||||||
|
import { useLceLiveNotifications } from "../hooks/useLceLiveNotifications";
|
||||||
import pkg from "../../package.json";
|
import pkg from "../../package.json";
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const {
|
const {
|
||||||
@@ -52,6 +54,12 @@ export default function App() {
|
|||||||
const audio = useAudio();
|
const audio = useAudio();
|
||||||
const game = useGame();
|
const game = useGame();
|
||||||
const { skinUrl, setSkinUrl, capeUrl } = useSkin();
|
const { skinUrl, setSkinUrl, capeUrl } = useSkin();
|
||||||
|
const {
|
||||||
|
friendRequestMessage,
|
||||||
|
gameInviteMessage,
|
||||||
|
clearFriendRequestMessage,
|
||||||
|
clearGameInviteMessage,
|
||||||
|
} = useLceLiveNotifications();
|
||||||
const [showSetup, setShowSetup] = useState(true);
|
const [showSetup, setShowSetup] = useState(true);
|
||||||
const [displayIsDay, setDisplayIsDay] = useState(config.isDayTime);
|
const [displayIsDay, setDisplayIsDay] = useState(config.isDayTime);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -64,7 +72,7 @@ export default function App() {
|
|||||||
const selectedVersionName = selectedEdition?.name || "";
|
const selectedVersionName = selectedEdition?.name || "";
|
||||||
const hasAnyInstall = game.installs.length > 0;
|
const hasAnyInstall = game.installs.length > 0;
|
||||||
const titleImage = hasAnyInstall
|
const titleImage = hasAnyInstall
|
||||||
? (selectedEdition?.titleImage || "/images/MenuTitle.png")
|
? selectedEdition?.titleImage || "/images/MenuTitle.png"
|
||||||
: "/images/MenuTitle.png";
|
: "/images/MenuTitle.png";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -124,7 +132,7 @@ export default function App() {
|
|||||||
{...backgroundFade}
|
{...backgroundFade}
|
||||||
>
|
>
|
||||||
<PanoramaBackground
|
<PanoramaBackground
|
||||||
profile={config.profile}
|
profile={selectedEdition.panorama}
|
||||||
isDay={displayIsDay}
|
isDay={displayIsDay}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -160,7 +168,10 @@ export default function App() {
|
|||||||
message={updateMessage}
|
message={updateMessage}
|
||||||
onClose={clearUpdateMessage}
|
onClose={clearUpdateMessage}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
TauriService.openUrl(updateUrl || "https://github.com/LCE-Hub/LCE-Emerald-Launcher/releases/latest")
|
TauriService.openUrl(
|
||||||
|
updateUrl ||
|
||||||
|
"https://github.com/LCE-Hub/LCE-Emerald-Launcher/releases/latest",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
title="Update Available!"
|
title="Update Available!"
|
||||||
variant="update"
|
variant="update"
|
||||||
@@ -275,7 +286,9 @@ export default function App() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isUiHidden && !displayIsDay && activeView == "devtools" && (
|
{isUiHidden &&
|
||||||
|
!displayIsDay &&
|
||||||
|
activeView == "devtools" && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="secret-swf-btn"
|
key="secret-swf-btn"
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
@@ -296,7 +309,8 @@ export default function App() {
|
|||||||
className="w-16 h-16 cursor-pointer object-contain opacity-50 group-hover:opacity-100 drop-shadow-[0_4px_4px_rgba(0,0,0,1)] grayscale group-hover:grayscale-0"
|
className="w-16 h-16 cursor-pointer object-contain opacity-50 group-hover:opacity-100 drop-shadow-[0_4px_4px_rgba(0,0,0,1)] grayscale group-hover:grayscale-0"
|
||||||
style={{ imageRendering: "pixelated" }}
|
style={{ imageRendering: "pixelated" }}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).src = "/images/Button_Background.png";
|
(e.target as HTMLImageElement).src =
|
||||||
|
"/images/Button_Background.png";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="text-[#FFFF55] text-sm mc-text-shadow opacity-0 group-hover:opacity-100 transition-opacity">
|
<span className="text-[#FFFF55] text-sm mc-text-shadow opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
@@ -414,6 +428,9 @@ export default function App() {
|
|||||||
{activeView === "swf-editor" && (
|
{activeView === "swf-editor" && (
|
||||||
<SwfView key="swf-editor-view" />
|
<SwfView key="swf-editor-view" />
|
||||||
)}
|
)}
|
||||||
|
{activeView === "lcelive" && (
|
||||||
|
<LceLiveView key="lcelive-view" />
|
||||||
|
)}
|
||||||
{activeView === "skins" && <SkinsView key="skins-view" />}
|
{activeView === "skins" && <SkinsView key="skins-view" />}
|
||||||
{activeView === "screenshots" && (
|
{activeView === "screenshots" && (
|
||||||
<ScreenshotsView key="screenshots-view" />
|
<ScreenshotsView key="screenshots-view" />
|
||||||
@@ -447,6 +464,28 @@ export default function App() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AchievementToast
|
||||||
|
message={friendRequestMessage}
|
||||||
|
onClose={clearFriendRequestMessage}
|
||||||
|
onClick={() => {
|
||||||
|
clearFriendRequestMessage();
|
||||||
|
setActiveView("lcelive");
|
||||||
|
}}
|
||||||
|
title="Friend Request"
|
||||||
|
variant="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AchievementToast
|
||||||
|
message={gameInviteMessage}
|
||||||
|
onClose={clearGameInviteMessage}
|
||||||
|
onClick={() => {
|
||||||
|
clearGameInviteMessage();
|
||||||
|
setActiveView("lcelive");
|
||||||
|
}}
|
||||||
|
title="Game Invite"
|
||||||
|
variant="update"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</MotionConfig>
|
</MotionConfig>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,377 @@
|
|||||||
|
const LOCAL_STORAGE_KEY = "lcelive_session";
|
||||||
|
const DEFAULT_BASE_URL = "https://api.lcelive.co.uk";
|
||||||
|
const MAX_REFRESH_RETRIES = 1;
|
||||||
|
import { TauriService } from "./TauriService";
|
||||||
|
export interface LceLiveAccount {
|
||||||
|
accountId: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionData {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
account: LceLiveAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceLinkStartResponse {
|
||||||
|
deviceCode: string;
|
||||||
|
userCode: string;
|
||||||
|
verificationUri: string;
|
||||||
|
verificationUriComplete?: string;
|
||||||
|
intervalSeconds: number;
|
||||||
|
expiresInSeconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceLinkPollResponse {
|
||||||
|
status: string;
|
||||||
|
isLinked?: boolean;
|
||||||
|
accessToken?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
account?: LceLiveAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameInvite {
|
||||||
|
inviteId: string;
|
||||||
|
from: LceLiveAccount | string;
|
||||||
|
hostIp: string;
|
||||||
|
hostPort: number;
|
||||||
|
hostName: string;
|
||||||
|
signalingSessionId?: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FriendRequest {
|
||||||
|
accountId: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PendingRequests {
|
||||||
|
incoming: FriendRequest[];
|
||||||
|
outgoing: FriendRequest[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LceLiveService {
|
||||||
|
private _session: SessionData | null = null;
|
||||||
|
private baseUrl: string = DEFAULT_BASE_URL;
|
||||||
|
private _refreshPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
setBaseUrl(url: string) {
|
||||||
|
this.baseUrl = url.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
get signedIn(): boolean {
|
||||||
|
return this._session !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get account(): LceLiveAccount | null {
|
||||||
|
return this._session?.account || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayUsername(): string {
|
||||||
|
if (!this._session) return "Not signed in";
|
||||||
|
return (
|
||||||
|
this._session.account.displayName ||
|
||||||
|
this._session.account.username ||
|
||||||
|
"Unknown"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get apiBaseUrl(): string {
|
||||||
|
return this.baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get accessToken(): string | null {
|
||||||
|
return this._session?.accessToken || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateDeviceId(): string {
|
||||||
|
let id = localStorage.getItem("lcelive_device_id");
|
||||||
|
if (!id) {
|
||||||
|
id = crypto.randomUUID();
|
||||||
|
localStorage.setItem("lcelive_device_id", id);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDeviceName(): string {
|
||||||
|
return "LCE Emerald Launcher";
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadSession() {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||||
|
if (data) {
|
||||||
|
this._session = JSON.parse(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to load LceLive session", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveSession() {
|
||||||
|
if (this._session) {
|
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this._session));
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(LOCAL_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: any,
|
||||||
|
authed: boolean = true,
|
||||||
|
retryCount: number = 0,
|
||||||
|
): Promise<any> {
|
||||||
|
if (authed && this._session?.refreshToken && retryCount === 0) {
|
||||||
|
try {
|
||||||
|
await this.refreshSession(); //neo: i do this on every request only because it doesnt always return 401
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Accept: "application/json",
|
||||||
|
"User-Agent": "MCLCE-LceLive/1.0",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authed && this._session?.accessToken) {
|
||||||
|
headers["Authorization"] = `Bearer ${this._session.accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyStr = body ? JSON.stringify(body) : null;
|
||||||
|
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await TauriService.httpProxyRequest(
|
||||||
|
method,
|
||||||
|
`${this.baseUrl}${path}`,
|
||||||
|
bodyStr,
|
||||||
|
headers,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Network error when calling ${path}: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
res.status === 401 &&
|
||||||
|
authed &&
|
||||||
|
this._session?.refreshToken &&
|
||||||
|
retryCount < MAX_REFRESH_RETRIES
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.refreshSession();
|
||||||
|
return this.request(method, path, body, authed, retryCount + 1);
|
||||||
|
} catch (err) {
|
||||||
|
this.logoutLocal();
|
||||||
|
throw new Error("Session expired. Please log in again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = res.body ? JSON.parse(res.body) : {};
|
||||||
|
} catch {
|
||||||
|
data = { message: res.body };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status >= 400) {
|
||||||
|
const errorMsg =
|
||||||
|
data.message ||
|
||||||
|
data.detail ||
|
||||||
|
data.title ||
|
||||||
|
data.error ||
|
||||||
|
`HTTP ${res.status}`;
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async startDeviceLink(): Promise<DeviceLinkStartResponse> {
|
||||||
|
return this.request(
|
||||||
|
"POST",
|
||||||
|
"/api/auth/device/start",
|
||||||
|
{
|
||||||
|
deviceId: this.generateDeviceId(),
|
||||||
|
deviceName: this.getDeviceName(),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async pollDeviceLink(deviceCode: string): Promise<DeviceLinkPollResponse> {
|
||||||
|
const data = await this.request(
|
||||||
|
"GET",
|
||||||
|
`/api/auth/device/poll/${deviceCode}`,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
if (data.isLinked && data.accessToken) {
|
||||||
|
this._session = {
|
||||||
|
accessToken: data.accessToken,
|
||||||
|
refreshToken: data.refreshToken,
|
||||||
|
account: data.account,
|
||||||
|
};
|
||||||
|
this.saveSession();
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshSession(): Promise<void> {
|
||||||
|
if (!this._session?.refreshToken) throw new Error("No refresh token");
|
||||||
|
|
||||||
|
if (this._refreshPromise) {
|
||||||
|
return this._refreshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._refreshPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const data = await this.request(
|
||||||
|
"POST",
|
||||||
|
"/api/auth/refresh",
|
||||||
|
{
|
||||||
|
refreshToken: this._session!.refreshToken,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
this._session = {
|
||||||
|
accessToken: data.accessToken,
|
||||||
|
refreshToken: data.refreshToken,
|
||||||
|
account: data.account,
|
||||||
|
};
|
||||||
|
this.saveSession();
|
||||||
|
} finally {
|
||||||
|
this._refreshPromise = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this._refreshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(): Promise<void> {
|
||||||
|
if (this._session?.refreshToken) {
|
||||||
|
try {
|
||||||
|
await this.request(
|
||||||
|
"POST",
|
||||||
|
"/api/auth/logout",
|
||||||
|
{
|
||||||
|
refreshToken: this._session.refreshToken,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Server logout failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logoutLocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
logoutLocal(): void {
|
||||||
|
this._session = null;
|
||||||
|
this.saveSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFriends(): Promise<LceLiveAccount[]> {
|
||||||
|
const data = await this.request("GET", "/api/social/friends");
|
||||||
|
return data.friends || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeFriend(accountId: string): Promise<void> {
|
||||||
|
await this.request("DELETE", `/api/social/friends/${accountId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendFriendRequest(username: string): Promise<void> {
|
||||||
|
await this.request("POST", "/api/social/request", { username });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPendingRequests(): Promise<PendingRequests> {
|
||||||
|
const data = await this.request("GET", "/api/social/requests");
|
||||||
|
return {
|
||||||
|
incoming: (data.incoming || []).map((r: any) => ({
|
||||||
|
accountId: r.requesterUserId || r.accountId || r.userId,
|
||||||
|
username: r.requesterUsername || r.username,
|
||||||
|
displayName: r.requesterDisplayName || r.displayName,
|
||||||
|
})),
|
||||||
|
outgoing: (data.outgoing || []).map((r: any) => ({
|
||||||
|
accountId: r.targetUserId || r.accountId || r.userId,
|
||||||
|
username: r.targetUsername || r.username,
|
||||||
|
displayName: r.targetDisplayName || r.displayName,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptFriendRequest(accountId: string): Promise<void> {
|
||||||
|
await this.request("POST", `/api/social/requests/${accountId}/accept`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async declineFriendRequest(accountId: string): Promise<void> {
|
||||||
|
await this.request("POST", `/api/social/requests/${accountId}/decline`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGameInvites(): Promise<GameInvite[]> {
|
||||||
|
const data = await this.request("GET", "/api/sessions/invites");
|
||||||
|
const incoming = data.incoming || [];
|
||||||
|
return incoming.map((inv: any) => ({
|
||||||
|
inviteId: inv.inviteId,
|
||||||
|
from: {
|
||||||
|
accountId: inv.senderAccountId,
|
||||||
|
username: inv.senderUsername,
|
||||||
|
displayName: inv.senderDisplayName,
|
||||||
|
},
|
||||||
|
hostIp: inv.hostIp,
|
||||||
|
hostPort: inv.hostPort,
|
||||||
|
hostName: inv.hostName,
|
||||||
|
status: inv.status,
|
||||||
|
signalingSessionId: inv.signalingSessionId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendGameInvite(
|
||||||
|
recipientAccountId: string,
|
||||||
|
hostIp: string,
|
||||||
|
hostPort: number,
|
||||||
|
hostName: string,
|
||||||
|
signalingSessionId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.request("POST", "/api/sessions/invites", {
|
||||||
|
recipientAccountId,
|
||||||
|
hostIp,
|
||||||
|
hostPort,
|
||||||
|
hostName,
|
||||||
|
signalingSessionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptGameInvite(inviteId: string): Promise<any> {
|
||||||
|
return this.request("POST", `/api/sessions/invites/${inviteId}/accept`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async declineGameInvite(inviteId: string): Promise<void> {
|
||||||
|
await this.request("POST", `/api/sessions/invites/${inviteId}/decline`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deactivateGameInvites(): Promise<void> {
|
||||||
|
await this.request("POST", "/api/sessions/invites/deactivate", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestJoinTicket(): Promise<string> {
|
||||||
|
const data = await this.request("POST", "/api/sessions/ticket", {});
|
||||||
|
return data.ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateJoinTicket(ticket: string): Promise<LceLiveAccount> {
|
||||||
|
return this.request("POST", "/api/sessions/validate", { ticket }, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lceLiveService = new LceLiveService();
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
import { setActivity, start } from "tauri-plugin-drpc";
|
import { setActivity, start } from "tauri-plugin-drpc";
|
||||||
import { Activity, ActivityType, Assets, Timestamps, Button } from "tauri-plugin-drpc/activity";
|
import {
|
||||||
|
Activity,
|
||||||
|
ActivityType,
|
||||||
|
Assets,
|
||||||
|
Timestamps,
|
||||||
|
Button,
|
||||||
|
} from "tauri-plugin-drpc/activity";
|
||||||
class RPC {
|
class RPC {
|
||||||
private startTime: number = Date.now();
|
private startTime: number = Date.now();
|
||||||
private initializationPromise: Promise<void> | null = null;
|
private initializationPromise: Promise<void> | null = null;
|
||||||
private initialized: boolean = false;
|
private initialized: boolean = false;
|
||||||
public async StartRPC() {
|
public async StartRPC() {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
if (sessionStorage.getItem('lce_rpc_started') === 'true') {
|
if (sessionStorage.getItem("lce_rpc_started") === "true") {
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -15,7 +21,7 @@ class RPC {
|
|||||||
this.initializationPromise = (async () => {
|
this.initializationPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
await start("1482504445152460871");
|
await start("1482504445152460871");
|
||||||
sessionStorage.setItem('lce_rpc_started', 'true');
|
sessionStorage.setItem("lce_rpc_started", "true");
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to start RPC:", e);
|
console.error("Failed to start RPC:", e);
|
||||||
@@ -26,7 +32,11 @@ class RPC {
|
|||||||
return this.initializationPromise;
|
return this.initializationPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateActivity(details: string, state: string, isPlaying: boolean = false) {
|
public async updateActivity(
|
||||||
|
details: string,
|
||||||
|
state: string,
|
||||||
|
isPlaying: boolean = false,
|
||||||
|
) {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
await this.StartRPC();
|
await this.StartRPC();
|
||||||
if (!this.initialized) return;
|
if (!this.initialized) return;
|
||||||
@@ -44,8 +54,8 @@ class RPC {
|
|||||||
activity.setAssets(assets);
|
activity.setAssets(assets);
|
||||||
activity.setTimestamps(new Timestamps(this.startTime));
|
activity.setTimestamps(new Timestamps(this.startTime));
|
||||||
activity.setButton([
|
activity.setButton([
|
||||||
new Button("Discord", "https://discord.gg/NPMp9JhGYH"),
|
new Button("Discord", "https://discord.gg/cQVKhQXcCx"),
|
||||||
new Button("GitHub", "https://github.com/LCE-Hub/LCE-Emerald-Launcher")
|
new Button("GitHub", "https://github.com/LCE-Hub/LCE-Emerald-Launcher"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface AppConfig {
|
|||||||
musicVol?: number;
|
musicVol?: number;
|
||||||
sfxVol?: number;
|
sfxVol?: number;
|
||||||
legacyMode?: boolean;
|
legacyMode?: boolean;
|
||||||
|
mangohudEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemePalette {
|
export interface ThemePalette {
|
||||||
@@ -235,4 +236,52 @@ export class TauriService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return invoke("add_to_steam", { instanceId, name, titleBase64, panoramaBase64 });
|
return invoke("add_to_steam", { instanceId, name, titleBase64, panoramaBase64 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async stunDiscover(): Promise<{ ip: string; port: number }> {
|
||||||
|
return invoke("stun_discover");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async startDirectProxy(targetIp: string, targetPort: number): Promise<number> {
|
||||||
|
return invoke("start_direct_proxy", { targetIp, targetPort });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async startRelayProxy(apiBaseUrl: string, accessToken: string, sessionId: string): Promise<number> {
|
||||||
|
return invoke("start_relay_proxy", { apiBaseUrl, accessToken, sessionId });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async startHostRelay(apiBaseUrl: string, accessToken: string, sessionId: string, gamePort: number): Promise<void> {
|
||||||
|
return invoke("start_host_relay", { apiBaseUrl, accessToken, sessionId, gamePort });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async stopProxy(): Promise<void> {
|
||||||
|
return invoke("stop_proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async joinGame(
|
||||||
|
apiBaseUrl: string,
|
||||||
|
accessToken: string,
|
||||||
|
hostIp: string,
|
||||||
|
hostPort: number,
|
||||||
|
sessionId: string,
|
||||||
|
instanceId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
return invoke("join_game", { apiBaseUrl, accessToken, hostIp, hostPort, sessionId, instanceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async httpProxyRequest(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
body: string | null,
|
||||||
|
headers: Record<string, string>
|
||||||
|
): Promise<{ status: number; body: string }> {
|
||||||
|
return invoke("http_proxy_request", { method, url, body, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getInstancePath(instanceId: string): Promise<string> {
|
||||||
|
return invoke("get_instance_path", { instanceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async readScreenshotAsDataUrl(path: string): Promise<string> {
|
||||||
|
return invoke("read_screenshot_as_data_url", { path });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||