Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84e45bf408
|
||
|
|
cefe6c3ced
|
||
|
|
03f7c62665
|
||
|
|
2223f41f5d
|
||
|
|
ace4030692
|
||
|
|
5f06db1339
|
||
|
|
c1082e6741
|
||
|
|
37cd1f9547
|
||
|
|
3d06cc4e60
|
||
|
|
577f12f669
|
||
|
|
2b061c2c38
|
||
|
|
e591e0766e
|
||
|
|
8221b16819
|
||
|
|
83894e686c
|
||
|
|
334862744a
|
||
|
|
affc6c19cb
|
||
|
|
5ce8aa76ab
|
||
|
|
28b88f4033
|
||
|
|
bac028efc3
|
||
|
|
b777a0a4ec
|
||
|
|
e9286c4b64
|
||
|
|
916e9bc08c
|
||
|
|
672d95f433
|
||
|
|
0f8e9dcbdf
|
||
|
|
0d69848a1d
|
||
|
|
57f70d4433
|
||
|
|
1407611dcc
|
||
|
|
f4e389915f
|
||
|
|
3a6e4e3eb6
|
||
|
|
b7457c0cb5
|
||
|
|
603edbdb4e
|
||
|
|
9bd2a848b6
|
||
|
|
16fa83729b
|
||
|
|
a5dafa1cdb
|
||
|
|
93c9ebedeb
|
||
|
|
f7ebc8f0f1
|
||
|
|
e4ae6a2cc3
|
||
|
|
a65045e359
|
||
|
|
0197841092
|
||
|
|
55974c1bce
|
||
|
|
910d6b30c6
|
||
|
|
dad0022353
|
||
|
|
3771a9e8fc
|
||
|
|
848a95b576
|
||
|
|
0f1c84c3c9
|
||
|
|
7103d0e3a1
|
||
|
|
36b9f6bb55
|
||
|
|
d61f090c92
|
||
|
|
fff13b0075
|
||
|
|
3cc7257923
|
||
|
|
68864356b1
|
||
|
|
43678cc64d
|
||
|
|
3f27a330b6
|
||
|
|
fd7c86a512
|
||
|
|
421871705b
|
||
|
|
41f3893348
|
||
|
|
b107bd675c
|
||
|
|
9e14aeaa0c
|
||
|
|
dfc9b32264
|
||
|
|
4dfee5dda9
|
||
|
|
377f8b7522
|
||
|
|
38275f1869
|
||
|
|
85e01d91a0
|
||
|
|
b2fefd2c6b
|
||
|
|
635cbfc14c
|
||
|
|
302a449fd7
|
||
|
|
43d286b4e5
|
||
|
|
6b8d9795b8
|
||
|
|
ca6d85e23c
|
||
|
|
f6b027d3b2
|
||
|
|
1d024296c2
|
||
|
|
403c1dee4e
|
||
|
|
e341d41d74
|
||
|
|
830378448e
|
||
|
|
9550d29821
|
||
|
|
0c96a39b3e
|
||
|
|
f3689c5236
|
||
|
|
683158e31e
|
||
|
|
a1a719ba72
|
||
|
|
e0a20ddcf1
|
||
|
|
abeeab79d7
|
||
|
|
3e45caf4f3
|
||
|
|
8270e420e4
|
||
|
|
a2382a5836
|
||
|
|
2c406ab531
|
||
|
|
7c519bf8eb
|
||
|
|
5226ce4a62
|
||
|
|
254a1d5b5c
|
||
|
|
998fb6d309
|
||
|
|
f9e67fee0f
|
||
|
|
bec02b1578
|
||
|
|
2955c5d3f9
|
||
|
|
0bd855acd3
|
||
|
|
7e88311939
|
||
|
|
12cdda13fb
|
||
|
|
8ca82777cc
|
||
|
|
c3cee9c927
|
||
|
|
3281c4e1a1
|
||
|
|
3a9e0b0358
|
||
|
|
c45e7fb47c
|
||
|
|
c7086fbc31
|
||
|
|
33a1e4e780
|
||
|
|
066f4aae8c
|
||
|
|
2ebe127def
|
||
|
|
e0300acb6d
|
||
|
|
d18dc5afb7
|
||
|
|
d20c99651c
|
||
|
|
86a5a7b744
|
||
|
|
18f8c2316f
|
||
|
|
12914e42dd
|
||
|
|
91ff7caaed
|
||
|
|
fad7073607
|
||
|
|
beb5ce175e
|
||
|
|
b8498d36e4
|
||
|
|
141d17fea0
|
||
|
|
138275d752
|
||
|
|
1f035882f7
|
||
|
|
68cd3c891d
|
||
|
|
65bd6cfe82
|
||
|
|
d91314112d
|
||
|
|
da27bcb32c
|
||
|
|
6c8dfd1200
|
||
|
|
e09d125980
|
||
|
|
35dd842576
|
||
|
|
55714c8edb
|
||
|
|
04d1f2822b
|
||
|
|
dfd99ab12e
|
||
|
|
837c4a8863
|
||
|
|
c69060a360
|
||
|
|
f2b0358605
|
||
|
|
57441a8872
|
||
|
|
df522122ac
|
||
|
|
f4e16c12e5
|
||
|
|
e2d98307c0
|
||
|
|
387bd54ce2
|
||
|
|
e7a61872b7
|
||
|
|
ac66c78dbf
|
||
|
|
d9302319fa
|
||
|
|
5206a7f25d
|
||
|
|
3ee371998a
|
||
|
|
5733c21fbb
|
||
|
|
61b9d07fc6
|
||
|
|
59cd625238
|
||
|
|
6bf2733cec
|
||
|
|
60f9f95be4
|
||
|
|
ea1c05d2d9
|
||
|
|
49c03e82b1
|
||
|
|
0c52169533
|
||
|
|
bf6da20fd8
|
||
|
|
c1f87580c7
|
||
|
|
dd7d2b96e3
|
||
|
|
874f0627f7
|
||
|
|
e8dec1a1b2
|
||
|
|
e1f0ce1290
|
||
|
|
bc5ea81170
|
||
|
|
71b998f4bc
|
||
|
|
cbac50753f
|
||
|
|
fb65348ba3
|
||
|
|
f23d1025d2
|
||
|
|
a8573c4b6a
|
||
|
|
4054fdd06b
|
||
|
|
0b4e639630
|
||
|
|
c65fb5e1a5
|
||
|
|
6486498c74
|
||
|
|
cf72d143f3
|
||
|
|
ee7c50c6f5
|
||
|
|
f7317c8232
|
||
|
|
9856cf2426
|
||
|
|
f46d9c67b8
|
||
|
|
132616dc29
|
||
|
|
8b22fdb648
|
||
|
|
dc55c7549d
|
||
|
|
9d9c6147c5
|
||
|
|
48c8365b7c
|
||
|
|
8884d08c7c
|
||
|
|
b516a9cc43
|
||
|
|
6b7b2b198a
|
||
|
|
fbf87dceba
|
||
|
|
9ae28690a7
|
||
|
|
f83cc118fe
|
||
|
|
19788a8a3c
|
||
|
|
e066dd0681
|
||
|
|
29b3c3c58b
|
||
|
|
a8390216d0
|
||
|
|
513f1a0c16
|
||
|
|
e7e624f042
|
||
|
|
654405e016
|
||
|
|
c461a0a459
|
||
|
|
16ce04ba7a
|
||
|
|
1700d127c3
|
||
|
|
7c47b32c68
|
||
|
|
56781b5d1a
|
||
|
|
6ce7c2a8ae
|
||
|
|
15dd9ad013
|
||
|
|
217222a22b
|
||
|
|
39da88a856
|
||
|
|
29c90d2f06
|
||
|
|
467dd1b0a1
|
||
|
|
a1a69c9ac5
|
||
|
|
0163e09b02
|
||
|
|
fbb4b15971
|
||
|
|
77933a2298
|
||
|
|
7209a36752
|
||
|
|
f00b78996a
|
||
|
|
38779c1715
|
||
|
|
fa67d26fe9
|
||
|
|
81d47ea71d
|
||
|
|
70433b76ab
|
||
|
|
105913503c
|
||
|
|
2ad54f976b
|
||
|
|
d8d59febbb
|
||
|
|
a618c76292
|
||
|
|
118fad49d8
|
||
|
|
62c0f84d14
|
||
|
|
4a96911885
|
||
|
|
51cd3c3620
|
||
|
|
dfbfac33d3
|
||
|
|
03ec5a220a
|
||
|
|
abebc0bb08
|
||
|
|
3c7487f20f
|
||
|
|
97231c6b38
|
||
|
|
a93e4ab521
|
||
|
|
e7c48b182f
|
||
|
|
16dd3fd613
|
||
|
|
c91303d194
|
||
|
|
a76e81789b
|
||
|
|
c909973aab
|
||
|
|
28f694e4e7
|
||
|
|
508e4d1640
|
||
|
|
41a66eaf86
|
||
|
|
ac4b76fa46
|
||
|
|
2e02275c29
|
||
|
|
dbaac6a6a6
|
||
|
|
68525f161e
|
||
|
|
d386454e91
|
||
|
|
6e05aac871
|
||
|
|
fbe3feec9d
|
||
|
|
1c540ad35b
|
||
|
|
4ec597967a
|
||
|
|
be4b8fc3ee
|
||
|
|
d9fa967ade
|
||
|
|
0f4ee9f78c
|
||
|
|
cc5d814740
|
||
|
|
53ecbbd07f
|
||
|
|
d0810ae80e
|
||
|
|
d688e5181f
|
||
|
|
1d7f888563
|
||
|
|
482708038e
|
||
|
|
ce3830ba2e
|
||
|
|
0acf23b2f6
|
||
|
|
31adc14156
|
||
|
|
bd9209359e
|
||
|
|
803cfb8742
|
||
|
|
f3fdf76817
|
||
|
|
2777e6dac0
|
||
|
|
a551d5fb52
|
||
|
|
08915ad2ae
|
||
|
|
1dd75f8ccf
|
||
|
|
3dc24ab3d7
|
||
|
|
a3e9067ba0
|
||
|
|
a0a763ae57
|
||
|
|
c56a903d55
|
||
|
|
c07828f244
|
||
|
|
cf4f1c15ea
|
||
|
|
76797c6120
|
||
|
|
36ed3e0095
|
||
|
|
e0ba25da77
|
||
|
|
e89b0fac8a
|
||
|
|
fa256dd004
|
||
|
|
b5c6598722
|
||
|
|
db111aaa2e
|
||
|
|
c1ac567204
|
||
|
|
1e11525c8e
|
||
|
|
b46c52e042
|
||
|
|
848511769e
|
||
|
|
376078d7f4
|
||
|
|
cb630a9d57
|
||
|
|
6ec7e8df82
|
||
|
|
1e93ef4d07
|
||
|
|
af4d634ed9
|
||
|
|
caf8d7fc7e
|
||
|
|
319d736274
|
||
|
|
426d507ec2
|
||
|
|
e5d9226233
|
||
|
|
bc1ca2a2b1
|
||
|
|
2e1cd07b99
|
||
|
|
370fa2b85d
|
||
|
|
7dedd4f91d
|
||
|
|
663b75e5ee
|
||
|
|
19c1d0b214
|
||
|
|
cc79eb6d4a
|
||
|
|
50d64b2905
|
||
|
|
cb10fbd991
|
||
|
|
ad6efdbbbe
|
||
|
|
2e71aea355
|
||
|
|
d2eb41f564
|
||
|
|
4fc8517123
|
||
|
|
34971787d0
|
||
|
|
9753483409
|
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
NEXT_PUBLIC_PUBLIC_SIGNING_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUNvd0JRWURLMlZ3QXlFQVJiK1N5c0VQemRYaGN4OWtyMy9rY3NNNURheDZRS1Q0VXZ3bW9PbWd2UHc9Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=
|
||||||
@@ -30,9 +30,6 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
|
||||||
.env*
|
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# Berry Dash Launcher
|
|
||||||
|
|
||||||
The version launcher for Berry Dash users
|
|
||||||
@@ -11,7 +11,8 @@ const eslintConfig = defineConfig([
|
|||||||
'.next/**',
|
'.next/**',
|
||||||
'out/**',
|
'out/**',
|
||||||
'build/**',
|
'build/**',
|
||||||
'next-env.d.ts'
|
'next-env.d.ts',
|
||||||
|
'src-tauri/**'
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "lncvrt-games-launcher",
|
"name": "lncvrt-games-launcher",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.0",
|
"version": "1.10.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -11,35 +11,45 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "7.1.0",
|
"@fortawesome/fontawesome-svg-core": "7.3.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "7.1.0",
|
"@fortawesome/free-brands-svg-icons": "7.3.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "7.1.0",
|
"@fortawesome/free-solid-svg-icons": "7.3.1",
|
||||||
"@fortawesome/react-fontawesome": "3.1.0",
|
"@fortawesome/react-fontawesome": "3.5.0",
|
||||||
"@tauri-apps/api": "2.9.0",
|
"@tauri-apps/api": "2.12.0",
|
||||||
"@tauri-apps/plugin-dialog": "2.4.2",
|
"@tauri-apps/plugin-cli": "2.5.0",
|
||||||
"@tauri-apps/plugin-fs": "2.4.4",
|
"@tauri-apps/plugin-clipboard-manager": "2.4.0",
|
||||||
"@tauri-apps/plugin-notification": "2.3.3",
|
"@tauri-apps/plugin-dialog": "2.8.0",
|
||||||
"@tauri-apps/plugin-opener": "2.5.2",
|
"@tauri-apps/plugin-fs": "2.6.0",
|
||||||
"@tauri-apps/plugin-os": "2.3.2",
|
"@tauri-apps/plugin-http": "2.7.0",
|
||||||
"axios": "1.13.2",
|
"@tauri-apps/plugin-notification": "2.5.0",
|
||||||
"date-fns": "4.1.0",
|
"@tauri-apps/plugin-opener": "2.6.0",
|
||||||
"next": "16.0.1",
|
"@tauri-apps/plugin-os": "2.4.0",
|
||||||
"pretty-bytes": "7.1.0",
|
"@tauri-apps/plugin-process": "2.4.0",
|
||||||
"react": "19.2.0",
|
"@tauri-apps/plugin-store": "2.5.0",
|
||||||
"react-dom": "19.2.0"
|
"@tauri-apps/plugin-updater": "2.13.0",
|
||||||
|
"@tauri-apps/plugin-window-state": "2.5.0",
|
||||||
|
"date-fns": "4.4.0",
|
||||||
|
"dompurify": "3.4.16",
|
||||||
|
"next": "16.3.6",
|
||||||
|
"pretty-bytes": "7.2.0",
|
||||||
|
"react": "19.3.0",
|
||||||
|
"react-dom": "19.3.0",
|
||||||
|
"semver": "7.8.5",
|
||||||
|
"swiper": "14.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "4.1.16",
|
"@eslint/eslintrc": "3.3.7",
|
||||||
"@tauri-apps/cli": "2.9.3",
|
"@tailwindcss/postcss": "4.3.3",
|
||||||
|
"@tauri-apps/cli": "2.12.0",
|
||||||
"@types/crypto-js": "4.2.2",
|
"@types/crypto-js": "4.2.2",
|
||||||
"@types/react": "19.2.2",
|
"@types/node": "26.6.3",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react": "19.3.0",
|
||||||
|
"@types/react-dom": "19.3.0",
|
||||||
|
"@types/semver": "7.8.0",
|
||||||
"crypto-js": "4.2.0",
|
"crypto-js": "4.2.0",
|
||||||
"@types/node": "24.10.0",
|
"eslint": "9.39.5",
|
||||||
"tailwindcss": "4.1.16",
|
"eslint-config-next": "16.3.6",
|
||||||
"typescript": "5.9.3",
|
"tailwindcss": "4.3.3",
|
||||||
"eslint": "9.39.1",
|
"typescript": "6.0.3"
|
||||||
"eslint-config-next": "16.0.1",
|
|
||||||
"@eslint/eslintrc": "3.3.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lncvrt-games-launcher"
|
name = "lncvrt-games-launcher"
|
||||||
version = "1.2.0"
|
version = "1.10.0"
|
||||||
authors = ["Lncvrt"]
|
authors = ["Lncvrt"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
@@ -9,30 +9,51 @@ name = "lncvrt_games_launcher_lib"
|
|||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.5.1", features = [] }
|
tauri-build = { version = "2.7.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2.9.2", features = ["macos-private-api"] }
|
tauri = { version = "2.12.0", features = [] }
|
||||||
tauri-plugin-opener = "2.5.2"
|
tauri-plugin-opener = "2.6.0"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.151"
|
||||||
tauri-plugin-os = "2.3.2"
|
tauri-plugin-os = "2.4.0"
|
||||||
reqwest = { version = "0.12.24", default-features = false, features = ["stream", "rustls-tls"] }
|
reqwest = { version = "0.13.5", default-features = false, features = [
|
||||||
tokio = "1.48.0"
|
"stream",
|
||||||
futures-util = { version = "0.3.31", features = ["io"] }
|
"rustls",
|
||||||
tauri-plugin-decorum = "1.1.1"
|
] }
|
||||||
tauri-plugin-fs = "2.4.4"
|
tokio = "1.53.1"
|
||||||
zip = "6.0.0"
|
futures-util = { version = "0.3.34", features = ["io"] }
|
||||||
libc = "0.2.177"
|
tauri-plugin-fs = "2.6.0"
|
||||||
tauri-plugin-dialog = "2.4.2"
|
zip = "8.6.0"
|
||||||
tauri-plugin-notification = "2.3.3"
|
tauri-plugin-dialog = "2.8.0"
|
||||||
sysinfo = "0.37.2"
|
tauri-plugin-notification = "2.5.0"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.11.0"
|
||||||
|
tauri-plugin-clipboard-manager = "2.4.0"
|
||||||
|
tauri-plugin-prevent-default = "6.0.0"
|
||||||
|
base64 = "0.23.1"
|
||||||
|
tauri-plugin-http = "2.8.0"
|
||||||
|
dashmap = "6.2.1"
|
||||||
|
tauri-plugin-store = "2.5.0"
|
||||||
|
hex = "0.4.3"
|
||||||
|
ed25519-dalek = { version = "3.0.0", features = ["pkcs8"] }
|
||||||
|
pem = "4.0.0"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
|
||||||
shlex = "1.3.0"
|
sysinfo = "0.39.6"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-single-instance = "2.3.6"
|
tauri-plugin-single-instance = "2.5.0"
|
||||||
|
tauri-plugin-updater = "2.13.0"
|
||||||
|
tauri-plugin-window-state = "2.5.0"
|
||||||
|
tauri-plugin-process = "2.4.0"
|
||||||
|
tauri-plugin-cli = "2.5.0"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
tauri-plugin-decorum = "1.1.1"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = "fat"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
@@ -2,9 +2,7 @@
|
|||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": [
|
"windows": ["main"],
|
||||||
"main"
|
|
||||||
],
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
@@ -19,11 +17,40 @@
|
|||||||
"core:window:allow-is-maximized",
|
"core:window:allow-is-maximized",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-toggle-maximize",
|
"core:window:allow-toggle-maximize",
|
||||||
"decorum:allow-show-snap-overlay",
|
"core:window:allow-request-user-attention",
|
||||||
|
"core:window:allow-show",
|
||||||
|
"core:window:allow-hide",
|
||||||
|
"core:webview:allow-create-webview-window",
|
||||||
"fs:default",
|
"fs:default",
|
||||||
|
"fs:allow-remove",
|
||||||
|
"dialog:default",
|
||||||
|
"dialog:allow-open",
|
||||||
|
"notification:default",
|
||||||
|
"clipboard-manager:default",
|
||||||
|
"clipboard-manager:allow-write-text",
|
||||||
|
"store:default",
|
||||||
|
"store:allow-load",
|
||||||
|
"store:allow-save",
|
||||||
"fs:allow-applocaldata-read",
|
"fs:allow-applocaldata-read",
|
||||||
"fs:allow-applocaldata-write",
|
"fs:allow-applocaldata-write",
|
||||||
"dialog:default",
|
{
|
||||||
"notification:default"
|
"identifier": "fs:scope",
|
||||||
|
"allow": [{ "path": "**" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "opener:allow-open-path",
|
||||||
|
"allow": [{ "path": "**" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "http:default",
|
||||||
|
"allow": [
|
||||||
|
{ "url": "https://games.lncvrt.xyz/api/launcher/versions" },
|
||||||
|
{ "url": "https://games.lncvrt.xyz/api/launcher/download" },
|
||||||
|
{ "url": "https://games.lncvrt.xyz/api/launcher/mods" },
|
||||||
|
{ "url": "https://games.lncvrt.xyz/api/account/login" },
|
||||||
|
{ "url": "https://games.lncvrt.xyz/api/account/change-username" },
|
||||||
|
{ "url": "https://games.lncvrt.xyz/api/account/change-password" }
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"identifier": "desktop-capability",
|
||||||
|
"platforms": ["macOS", "windows", "linux"],
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["window-state:default", "updater:default", "process:default"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"identifier": "windows-capability",
|
||||||
|
"platforms": ["windows"],
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["decorum:allow-show-snap-overlay"]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 250 KiB |
@@ -1,24 +1,16 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Lncvrt Games Launcher",
|
"productName": "Lncvrt Games Launcher",
|
||||||
"version": "1.2.0",
|
"version": "1.10.0",
|
||||||
"identifier": "xyz.lncvrt.games-launcher",
|
"identifier": "xyz.lncvrt.games-launcher",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "next dev",
|
"beforeDevCommand": "next dev --webpack",
|
||||||
"devUrl": "http://localhost:3000",
|
"devUrl": "http://localhost:3000",
|
||||||
"beforeBuildCommand": "next build",
|
"beforeBuildCommand": "next build",
|
||||||
"frontendDist": "../out"
|
"frontendDist": "../out"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [],
|
||||||
{
|
|
||||||
"title": "Lncvrt Games Launcher",
|
|
||||||
"width": 1000,
|
|
||||||
"height": 600,
|
|
||||||
"minWidth": 875,
|
|
||||||
"minHeight": 500
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Lncvrt Games Launcher",
|
"productName": "Lncvrt Games Launcher",
|
||||||
"version": "1.2.0",
|
"version": "1.10.0",
|
||||||
"identifier": "xyz.lncvrt.games-launcher",
|
"identifier": "xyz.lncvrt.games-launcher",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "next dev",
|
"beforeDevCommand": "next dev --webpack",
|
||||||
"devUrl": "http://localhost:3000",
|
"devUrl": "http://localhost:3000",
|
||||||
"beforeBuildCommand": "next build",
|
"beforeBuildCommand": "next build",
|
||||||
"frontendDist": "../out"
|
"frontendDist": "../out"
|
||||||
@@ -13,9 +13,10 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Lncvrt Games Launcher",
|
"title": "Lncvrt Games Launcher",
|
||||||
|
"url": "/update/checking",
|
||||||
"width": 1000,
|
"width": 1000,
|
||||||
"height": 600,
|
"height": 500,
|
||||||
"minWidth": 875,
|
"minWidth": 1000,
|
||||||
"minHeight": 500
|
"minHeight": 500
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -24,6 +25,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": false
|
"active": true,
|
||||||
|
"category": "Game",
|
||||||
|
"shortDescription": "A launcher for downloading, managing, and launching games with built-in mod support.",
|
||||||
|
"longDescription": "Lncvrt Games Launcher lets you download and manage multiple versions of games, install mods, and launch them directly. Linux users can run Windows exclusive games via Wine (or proton via a custom command).",
|
||||||
|
"targets": ["deb", "rpm"],
|
||||||
|
"icon": ["icons/icon.png"],
|
||||||
|
"createUpdaterArtifacts": true
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFODI3MTAwQkNBMjE4REEKUldUYUdLSzhBSEdDM3J0N1NvbENsdDQ3c3BOdWlFajErVnlLZitSc2l5WVI0VjQ1ZHNMWjYzaUwK",
|
||||||
|
"endpoints": [
|
||||||
|
"https://games.lncvrt.xyz/api/launcher/update?target={{target}}¤tVersion={{current_version}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"description": "Lncvrt Games Launcher CLI",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"short": "n",
|
||||||
|
"name": "no-check-updates",
|
||||||
|
"description": "Bypasses th eupdate checker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Lncvrt Games Launcher",
|
"productName": "Lncvrt Games Launcher",
|
||||||
"version": "1.2.0",
|
"version": "1.10.0",
|
||||||
"identifier": "xyz.lncvrt.games-launcher",
|
"identifier": "xyz.lncvrt.games-launcher",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "next dev",
|
"beforeDevCommand": "next dev --webpack",
|
||||||
"devUrl": "http://localhost:3000",
|
"devUrl": "http://localhost:3000",
|
||||||
"beforeBuildCommand": "next build",
|
"beforeBuildCommand": "next build",
|
||||||
"frontendDist": "../out"
|
"frontendDist": "../out"
|
||||||
@@ -14,9 +14,10 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Lncvrt Games Launcher",
|
"title": "Lncvrt Games Launcher",
|
||||||
|
"url": "/update/checking",
|
||||||
"width": 1000,
|
"width": 1000,
|
||||||
"height": 600,
|
"height": 500,
|
||||||
"minWidth": 875,
|
"minWidth": 1000,
|
||||||
"minHeight": 500,
|
"minHeight": 500,
|
||||||
"titleBarStyle": "Overlay",
|
"titleBarStyle": "Overlay",
|
||||||
"hiddenTitle": true
|
"hiddenTitle": true
|
||||||
@@ -28,10 +29,32 @@
|
|||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": ["app"],
|
"category": "Game",
|
||||||
|
"shortDescription": "A launcher for downloading, managing, and launching games with built-in mod support.",
|
||||||
|
"longDescription": "Lncvrt Games Launcher lets you download and manage multiple versions of games, install mods, and launch them directly. Linux users can run Windows exclusive games via Wine (or proton via a custom command).",
|
||||||
|
"targets": ["dmg", "app"],
|
||||||
|
"icon": ["icons/icon.icns"],
|
||||||
|
"createUpdaterArtifacts": true,
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"minimumSystemVersion": "13.7.8"
|
"minimumSystemVersion": "14.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"icon": ["icons/icon.icns"]
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFODI3MTAwQkNBMjE4REEKUldUYUdLSzhBSEdDM3J0N1NvbENsdDQ3c3BOdWlFajErVnlLZitSc2l5WVI0VjQ1ZHNMWjYzaUwK",
|
||||||
|
"endpoints": [
|
||||||
|
"https://games.lncvrt.xyz/api/launcher/update?target={{target}}¤tVersion={{current_version}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"description": "Lncvrt Games Launcher CLI",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"short": "n",
|
||||||
|
"name": "no-check-updates",
|
||||||
|
"description": "Bypasses th eupdate checker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Lncvrt Games Launcher",
|
"productName": "Lncvrt Games Launcher",
|
||||||
"version": "1.2.0",
|
"version": "1.10.0",
|
||||||
"identifier": "xyz.lncvrt.games-launcher",
|
"identifier": "xyz.lncvrt.games-launcher",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "next dev",
|
"beforeDevCommand": "next dev --webpack",
|
||||||
"devUrl": "http://localhost:3000",
|
"devUrl": "http://localhost:3000",
|
||||||
"beforeBuildCommand": "next build",
|
"beforeBuildCommand": "next build",
|
||||||
"frontendDist": "../out"
|
"frontendDist": "../out"
|
||||||
@@ -14,12 +14,11 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Lncvrt Games Launcher",
|
"title": "Lncvrt Games Launcher",
|
||||||
|
"url": "/update/checking",
|
||||||
"width": 1000,
|
"width": 1000,
|
||||||
"height": 632,
|
"height": 532,
|
||||||
"minWidth": 875,
|
"minWidth": 1000,
|
||||||
"minHeight": 500,
|
"minHeight": 532,
|
||||||
"titleBarStyle": "Overlay",
|
|
||||||
"hiddenTitle": true,
|
|
||||||
"decorations": false,
|
"decorations": false,
|
||||||
"shadow": true
|
"shadow": true
|
||||||
}
|
}
|
||||||
@@ -29,7 +28,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": false,
|
"active": true,
|
||||||
"icon": ["icons/icon.ico"]
|
"category": "Game",
|
||||||
|
"shortDescription": "A launcher for downloading, managing, and launching games with built-in mod support.",
|
||||||
|
"longDescription": "Lncvrt Games Launcher lets you download and manage multiple versions of games, install mods, and launch them directly. Linux users can run Windows exclusive games via Wine (or proton via a custom command).",
|
||||||
|
"targets": ["msi"],
|
||||||
|
"icon": ["icons/icon.ico"],
|
||||||
|
"createUpdaterArtifacts": true
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"updater": {
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFODI3MTAwQkNBMjE4REEKUldUYUdLSzhBSEdDM3J0N1NvbENsdDQ3c3BOdWlFajErVnlLZitSc2l5WVI0VjQ1ZHNMWjYzaUwK",
|
||||||
|
"endpoints": [
|
||||||
|
"https://games.lncvrt.xyz/api/launcher/update?target={{target}}¤tVersion={{current_version}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"description": "Lncvrt Games Launcher CLI",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"short": "n",
|
||||||
|
"name": "no-check-updates",
|
||||||
|
"description": "Bypasses th eupdate checker"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { createContext, useContext, ReactNode } from 'react'
|
|
||||||
import { DownloadProgress } from './types/DownloadProgress'
|
|
||||||
import { VersionsConfig } from './types/VersionsConfig'
|
|
||||||
import { NormalConfig } from './types/NormalConfig'
|
|
||||||
import { ServerVersionsResponse } from './types/ServerVersionsResponse'
|
|
||||||
import { GameVersion } from './types/GameVersion'
|
|
||||||
import { Game } from './types/Game'
|
|
||||||
|
|
||||||
type GlobalCtxType = {
|
|
||||||
serverVersionList: ServerVersionsResponse | null
|
|
||||||
selectedVersionList: string[]
|
|
||||||
setSelectedVersionList: (v: string[]) => void
|
|
||||||
downloadProgress: DownloadProgress[]
|
|
||||||
setDownloadProgress: (v: DownloadProgress[]) => void
|
|
||||||
showPopup: boolean
|
|
||||||
setShowPopup: (v: boolean) => void
|
|
||||||
popupMode: number | null
|
|
||||||
setPopupMode: (v: number | null) => void
|
|
||||||
fadeOut: boolean
|
|
||||||
setFadeOut: (v: boolean) => void
|
|
||||||
downloadedVersionsConfig: VersionsConfig | null
|
|
||||||
setDownloadedVersionsConfig: (v: VersionsConfig | null) => void
|
|
||||||
normalConfig: NormalConfig | null
|
|
||||||
setNormalConfig: (v: NormalConfig | null) => void
|
|
||||||
managingVersion: string | null
|
|
||||||
setManagingVersion: (v: string | null) => void
|
|
||||||
setSelectedGame: (v: number | null) => void
|
|
||||||
getVersionInfo: (id: string | undefined) => GameVersion | undefined
|
|
||||||
getVersionGame: (id: number | undefined) => Game | undefined
|
|
||||||
getListOfGames: () => Game[]
|
|
||||||
getVersionsAmountData: (gameId: number) => {
|
|
||||||
installed: number
|
|
||||||
total: number
|
|
||||||
} | null
|
|
||||||
viewingInfoFromDownloads: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const GlobalCtx = createContext<GlobalCtxType | null>(null)
|
|
||||||
|
|
||||||
export const useGlobal = () => {
|
|
||||||
const ctx = useContext(GlobalCtx)
|
|
||||||
if (!ctx) throw new Error('useGlobal must be inside GlobalProvider')
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GlobalProvider = ({
|
|
||||||
children,
|
|
||||||
value
|
|
||||||
}: {
|
|
||||||
children: ReactNode
|
|
||||||
value: GlobalCtxType
|
|
||||||
}) => {
|
|
||||||
return <GlobalCtx.Provider value={value}>{children}</GlobalCtx.Provider>
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
body {
|
|
||||||
@apply bg-(--col0) text-white select-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-theme {
|
|
||||||
--col0: rgb(8, 8, 8);
|
|
||||||
--col1: rgb(16, 16, 16);
|
|
||||||
--col2: rgb(32, 32, 32);
|
|
||||||
--col3: rgb(48, 48, 48);
|
|
||||||
--col4: rgb(64, 64, 64);
|
|
||||||
--col5: rgb(80, 80, 80);
|
|
||||||
--col6: rgb(96, 96, 96);
|
|
||||||
--col7: rgb(112, 112, 112);
|
|
||||||
--col8: rgb(128, 128, 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
.red-theme {
|
|
||||||
--col0: rgb(32, 8, 8);
|
|
||||||
--col1: rgb(40, 16, 16);
|
|
||||||
--col2: rgb(56, 32, 32);
|
|
||||||
--col3: rgb(72, 48, 48);
|
|
||||||
--col4: rgb(88, 64, 64);
|
|
||||||
--col5: rgb(104, 80, 80);
|
|
||||||
--col6: rgb(120, 96, 96);
|
|
||||||
--col7: rgb(136, 112, 112);
|
|
||||||
--col8: rgb(152, 128, 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
.green-theme {
|
|
||||||
--col0: rgb(8, 32, 8);
|
|
||||||
--col1: rgb(16, 40, 16);
|
|
||||||
--col2: rgb(32, 56, 32);
|
|
||||||
--col3: rgb(48, 72, 48);
|
|
||||||
--col4: rgb(64, 88, 64);
|
|
||||||
--col5: rgb(80, 104, 80);
|
|
||||||
--col6: rgb(96, 120, 96);
|
|
||||||
--col7: rgb(112, 136, 112);
|
|
||||||
--col8: rgb(128, 152, 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blue-theme {
|
|
||||||
--col0: rgb(8, 8, 32);
|
|
||||||
--col1: rgb(16, 16, 40);
|
|
||||||
--col2: rgb(32, 32, 56);
|
|
||||||
--col3: rgb(48, 48, 72);
|
|
||||||
--col4: rgb(64, 64, 88);
|
|
||||||
--col5: rgb(80, 80, 104);
|
|
||||||
--col6: rgb(96, 96, 120);
|
|
||||||
--col7: rgb(112, 112, 136);
|
|
||||||
--col8: rgb(128, 128, 152);
|
|
||||||
}
|
|
||||||
|
|
||||||
.purple-theme {
|
|
||||||
--col0: rgb(20, 8, 32);
|
|
||||||
--col1: rgb(28, 16, 40);
|
|
||||||
--col2: rgb(44, 32, 56);
|
|
||||||
--col3: rgb(60, 48, 72);
|
|
||||||
--col4: rgb(76, 64, 88);
|
|
||||||
--col5: rgb(92, 80, 104);
|
|
||||||
--col6: rgb(108, 96, 120);
|
|
||||||
--col7: rgb(124, 112, 136);
|
|
||||||
--col8: rgb(140, 128, 152);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
@apply bg-[#0a6ec8] hover:bg-[#1361ad] disabled:bg-[#124c7e] disabled:hover:bg-[#1b3f63] disabled:text-[#bdbdbd] disabled:hover:text-[#e6e6e6] rounded-md cursor-pointer text-[16px] py-1.5 px-3 transition-colors duration-[0.25s];
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-green {
|
|
||||||
@apply bg-[#28a745] hover:bg-[#218838] disabled:bg-[#1c7430] disabled:hover:bg-[#1a5c24] disabled:text-[#bdbdbd] disabled:hover:text-[#e6e6e6];
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
@apply w-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
@apply bg-(--col2) rounded-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
@apply bg-(--col4) w-1 rounded-lg active:bg-(--col5);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOut {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-overlay {
|
|
||||||
@apply fixed w-screen h-screen z-99999 flex justify-center items-center animate-[fadeIn_0.2s_ease-out_forwards] left-0 top-0 bg-[rgba(0,0,0,0.5)];
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-overlay.fade-out {
|
|
||||||
@apply animate-[fadeOut_0.2s_ease-out_forwards];
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-box {
|
|
||||||
@apply relative w-[600px] h-[480px] rounded-lg bg-(--col1) border border-(--col3) flex flex-col p-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-content {
|
|
||||||
@apply flex-1 overflow-auto bg-(--col2) border border-(--col4) rounded-lg mt-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-entry {
|
|
||||||
@apply relative h-fit bg-(--col3) m-2 p-2 rounded-lg border border-(--col5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-entry button {
|
|
||||||
@apply absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-button {
|
|
||||||
@apply flex justify-center items-center absolute bg-(--col3) hover:bg-(--col4) text-2xl cursor-pointer text-gray-300 hover:text-white h-12 w-12 p-3 rounded-xl left-2 top-2 transition-colors border border-(--col4) hover:border-(--col5);
|
|
||||||
}
|
|
||||||
|
|
||||||
*:focus {
|
|
||||||
@apply outline-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-field {
|
|
||||||
@apply border-2 border-(--col4) rounded-md bg-(--col2) p-2 px-4 focus:border-blue-600 transition-colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-info-item {
|
|
||||||
@apply flex flex-row items-center gap-1 bg-(--col1) border border-(--col3) text-gray-300 py-1 px-2 rounded-lg w-fit text-[16px] transition-colors cursor-pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.downloads-entry:hover .entry-info-item {
|
|
||||||
@apply bg-(--col2) border-(--col4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-content .entry-info-item:hover {
|
|
||||||
@apply bg-(--col3) border-(--col5);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
.downloads-container {
|
|
||||||
@apply flex justify-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.downloads-scroll {
|
|
||||||
@apply bg-(--col1) border border-(--col3) rounded-lg overflow-y-auto w-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.downloads-entry {
|
|
||||||
@apply flex justify-between items-center m-2 p-4 rounded-lg text-gray-200 text-lg transition-colors cursor-default bg-(--col2) hover:bg-(--col3) border border-(--col4) hover:border-(--col5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.downloads-entry p.score {
|
|
||||||
@apply font-mono text-blue-500 text-lg;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 88 KiB |
@@ -1,48 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
|
||||||
import BerryNoColor from '../assets/berries/BerryNoColor.png'
|
|
||||||
|
|
||||||
export default function RandomBerry () {
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
||||||
const imgRef = useRef<HTMLImageElement>(document.createElement('img'))
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
imgRef.current.src = BerryNoColor.src
|
|
||||||
const canvas = canvasRef.current
|
|
||||||
const ctx = canvas?.getContext('2d')
|
|
||||||
if (!canvas || !ctx) return
|
|
||||||
|
|
||||||
canvas.width = 24
|
|
||||||
canvas.height = 24
|
|
||||||
|
|
||||||
let frame: number
|
|
||||||
const frequency = 5
|
|
||||||
|
|
||||||
const update = () => {
|
|
||||||
const t = (performance.now() / 1000) * frequency
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
||||||
ctx.drawImage(imgRef.current, 0, 0, canvas.width, canvas.height)
|
|
||||||
|
|
||||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
||||||
const data = imageData.data
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
|
||||||
const gray = data[i]
|
|
||||||
const r = (Math.sin(t) * 0.5 + 0.5) * 255 * (gray / 255)
|
|
||||||
const g = (Math.sin(t + 2) * 0.5 + 0.5) * 255 * (gray / 255)
|
|
||||||
const b = (Math.sin(t + 4) * 0.5 + 0.5) * 255 * (gray / 255)
|
|
||||||
data[i] = r
|
|
||||||
data[i + 1] = g
|
|
||||||
data[i + 2] = b
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.putImageData(imageData, 0, 0)
|
|
||||||
frame = requestAnimationFrame(update)
|
|
||||||
}
|
|
||||||
|
|
||||||
imgRef.current.onload = () => update()
|
|
||||||
return () => cancelAnimationFrame(frame)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return <canvas ref={canvasRef} style={{ width: 24, height: 24 }} />
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
.setting-checkbox-wrapper {
|
|
||||||
@apply relative w-5 h-5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-checkbox {
|
|
||||||
@apply appearance-none w-full h-full border-2 border-(--col4) rounded-md bg-(--col2) transition-colors duration-200 cursor-pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-checkbox:checked {
|
|
||||||
@apply bg-blue-500 border-blue-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-check-icon {
|
|
||||||
@apply absolute top-1/2 left-1/2 text-white text-[11px] pointer-events-none -translate-x-2/4 -translate-y-2/4;
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import './Sidebar.css'
|
|
||||||
import Icon from '../assets/Icon.png'
|
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import {
|
|
||||||
faCog,
|
|
||||||
faDownload,
|
|
||||||
faGamepad,
|
|
||||||
faHexagonNodes
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { faDiscord } from '@fortawesome/free-brands-svg-icons'
|
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
|
||||||
import { useGlobal } from '../GlobalProvider'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { usePathname, useSearchParams } from 'next/navigation'
|
|
||||||
|
|
||||||
export default function Sidebar () {
|
|
||||||
const {
|
|
||||||
normalConfig,
|
|
||||||
getListOfGames,
|
|
||||||
setShowPopup,
|
|
||||||
setPopupMode,
|
|
||||||
setFadeOut,
|
|
||||||
downloadProgress
|
|
||||||
} = useGlobal()
|
|
||||||
|
|
||||||
const pathname = usePathname()
|
|
||||||
const params = useSearchParams()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className='sidebar'>
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
style={{
|
|
||||||
height: '30px',
|
|
||||||
width: 'calc(var(--spacing) * 60)',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
marginBottom: '-15px',
|
|
||||||
position: 'absolute',
|
|
||||||
zIndex: 9999,
|
|
||||||
display: platform() == 'macos' ? 'block' : 'none',
|
|
||||||
pointerEvents: 'auto'
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
<div className='logo'>
|
|
||||||
<Image
|
|
||||||
draggable={false}
|
|
||||||
src={Icon}
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
alt=''
|
|
||||||
style={{
|
|
||||||
marginTop: ['windows', 'macos'].includes(platform())
|
|
||||||
? '20px'
|
|
||||||
: '0px',
|
|
||||||
marginBottom: '-20px'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<nav className='nav-links'>
|
|
||||||
<Link
|
|
||||||
draggable={false}
|
|
||||||
href='/'
|
|
||||||
className={`link relative flex items-center ${
|
|
||||||
pathname === '/' || pathname === '/game' ? 'active' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faHexagonNodes} className='mr-2' /> Games
|
|
||||||
</Link>
|
|
||||||
{getListOfGames()
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a.id - b.id
|
|
||||||
})
|
|
||||||
.map(i => (
|
|
||||||
<Link
|
|
||||||
key={i.id}
|
|
||||||
draggable={false}
|
|
||||||
href={'/game?id=' + i.id}
|
|
||||||
className={`link ${
|
|
||||||
pathname === '/game' && Number(params.get('id') || 0) == i.id
|
|
||||||
? 'active'
|
|
||||||
: ''
|
|
||||||
} ml-auto w-50 ${
|
|
||||||
normalConfig?.settings.alwaysShowGamesInSidebar ||
|
|
||||||
pathname === '/' ||
|
|
||||||
pathname === '/game'
|
|
||||||
? ''
|
|
||||||
: 'hidden'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faGamepad} className='mr-1' />{' '}
|
|
||||||
{i.cutOff == null
|
|
||||||
? i.name
|
|
||||||
: i.name.substring(0, i.cutOff) + '...'}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
<Link
|
|
||||||
draggable={false}
|
|
||||||
href='/settings'
|
|
||||||
className={`link ${pathname === '/settings' ? 'active' : ''}`}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faCog} className='mr-1' /> Settings
|
|
||||||
</Link>
|
|
||||||
<a
|
|
||||||
draggable={false}
|
|
||||||
onClick={() => openUrl('https://games.lncvrt.xyz/discord')}
|
|
||||||
className='link'
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faDiscord} className='mr-1' /> Community
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
<div
|
|
||||||
className='sidebar-downloads'
|
|
||||||
style={{ display: downloadProgress.length != 0 ? 'block' : 'none' }}
|
|
||||||
onClick={() => {
|
|
||||||
setPopupMode(1)
|
|
||||||
setShowPopup(true)
|
|
||||||
setFadeOut(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
<FontAwesomeIcon icon={faDownload} /> Downloads
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import {
|
|
||||||
faCheck,
|
|
||||||
faCode,
|
|
||||||
faHardDrive,
|
|
||||||
faShieldHalved,
|
|
||||||
faWarning
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import { format } from 'date-fns'
|
|
||||||
import { useGlobal } from '../GlobalProvider'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import prettyBytes from 'pretty-bytes'
|
|
||||||
|
|
||||||
export default function VersionInfo () {
|
|
||||||
const {
|
|
||||||
getVersionGame,
|
|
||||||
getVersionInfo,
|
|
||||||
managingVersion,
|
|
||||||
downloadedVersionsConfig,
|
|
||||||
viewingInfoFromDownloads
|
|
||||||
} = useGlobal()
|
|
||||||
if (!managingVersion || !downloadedVersionsConfig) return <></>
|
|
||||||
|
|
||||||
const versionInfo = getVersionInfo(managingVersion)
|
|
||||||
const gameInfo = getVersionGame(versionInfo?.game)
|
|
||||||
const [versionSize, setVersionSize] = useState<number | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
invoke<string>('folder_size', {
|
|
||||||
version: managingVersion
|
|
||||||
}).then(size => {
|
|
||||||
setVersionSize(parseInt(size, 10))
|
|
||||||
})
|
|
||||||
}, [managingVersion, setVersionSize])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p className='text-xl text-center'>
|
|
||||||
Viewing info for{' '}
|
|
||||||
{getVersionGame(getVersionInfo(managingVersion)?.game)?.name} v
|
|
||||||
{getVersionInfo(managingVersion)?.versionName}
|
|
||||||
</p>
|
|
||||||
<div className='popup-content flex flex-col items-center justify-center gap-2 h-full'>
|
|
||||||
<div className='entry-info-item' hidden={viewingInfoFromDownloads}>
|
|
||||||
<p>
|
|
||||||
Installed{' '}
|
|
||||||
{format(
|
|
||||||
new Date(
|
|
||||||
downloadedVersionsConfig.timestamps[managingVersion] ?? 0
|
|
||||||
),
|
|
||||||
'MM/dd/yyyy'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className='entry-info-item'
|
|
||||||
hidden={!versionInfo || versionInfo.releaseDate == 0}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Released{' '}
|
|
||||||
{format(
|
|
||||||
new Date(
|
|
||||||
versionInfo?.releaseDate ? versionInfo.releaseDate * 1000 : 0
|
|
||||||
),
|
|
||||||
'MM/dd/yyyy'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='entry-info-item' hidden={!gameInfo?.official}>
|
|
||||||
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
|
|
||||||
<p>Official</p>
|
|
||||||
</div>
|
|
||||||
<div className='entry-info-item' hidden={gameInfo?.official}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={gameInfo?.verified ? faShieldHalved : faWarning}
|
|
||||||
color={gameInfo?.verified ? '#19c84b' : '#ffc800'}
|
|
||||||
/>
|
|
||||||
<p>{gameInfo?.verified ? 'Verified' : 'Unverified'}</p>
|
|
||||||
</div>
|
|
||||||
<div className='entry-info-item' hidden={gameInfo?.official}>
|
|
||||||
<FontAwesomeIcon icon={faCode} color='lightgray' />
|
|
||||||
<p>Developer: {gameInfo?.developer}</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className='entry-info-item'
|
|
||||||
hidden={viewingInfoFromDownloads || versionSize === null}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faHardDrive} color='lightgray' />
|
|
||||||
<p>
|
|
||||||
Size on disk:{' '}
|
|
||||||
{versionSize !== null
|
|
||||||
? prettyBytes(versionSize, {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2
|
|
||||||
})
|
|
||||||
: 'Loading...'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='entry-info-item' hidden={!versionInfo}>
|
|
||||||
<FontAwesomeIcon icon={faHardDrive} color='lightgray' />
|
|
||||||
<p>
|
|
||||||
Size when downloaded (zipped):{' '}
|
|
||||||
{versionInfo
|
|
||||||
? prettyBytes(versionInfo.size, {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2
|
|
||||||
})
|
|
||||||
: 'Loading...'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import '../Installs.css'
|
|
||||||
import { format } from 'date-fns'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import { useGlobal } from '../GlobalProvider'
|
|
||||||
import { useSearchParams } from 'next/navigation'
|
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
|
||||||
|
|
||||||
export default function Installs () {
|
|
||||||
const {
|
|
||||||
downloadProgress,
|
|
||||||
showPopup,
|
|
||||||
setShowPopup,
|
|
||||||
setPopupMode,
|
|
||||||
setFadeOut,
|
|
||||||
setSelectedVersionList,
|
|
||||||
downloadedVersionsConfig,
|
|
||||||
normalConfig,
|
|
||||||
setManagingVersion,
|
|
||||||
getVersionInfo,
|
|
||||||
getVersionGame,
|
|
||||||
setSelectedGame
|
|
||||||
} = useGlobal()
|
|
||||||
|
|
||||||
const params = useSearchParams()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showPopup) return
|
|
||||||
setSelectedVersionList([])
|
|
||||||
}, [normalConfig, setSelectedVersionList, showPopup])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='mx-4 mt-4'>
|
|
||||||
<div className='flex justify-between items-center mb-4'>
|
|
||||||
<p className='text-3xl'>Installs</p>
|
|
||||||
<button
|
|
||||||
className='button text-3xl'
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedGame(Number(params.get('id') || 0))
|
|
||||||
setPopupMode(0)
|
|
||||||
setShowPopup(true)
|
|
||||||
setFadeOut(false)
|
|
||||||
}}
|
|
||||||
disabled={downloadProgress.length != 0}
|
|
||||||
>
|
|
||||||
Download versions
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className='downloads-container'>
|
|
||||||
<div
|
|
||||||
className={`downloads-scroll ${
|
|
||||||
platform() === 'windows'
|
|
||||||
? 'h-[calc(100vh-116px)]'
|
|
||||||
: 'h-[calc(100vh-84px)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{downloadedVersionsConfig && downloadedVersionsConfig.list.length ? (
|
|
||||||
downloadedVersionsConfig.list
|
|
||||||
.filter(v => {
|
|
||||||
const info = getVersionInfo(v)
|
|
||||||
if (!info) return false
|
|
||||||
return info.game === Number(params.get('id') || 0)
|
|
||||||
})
|
|
||||||
.map((entry, i) => (
|
|
||||||
<div key={i} className='downloads-entry'>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<p className='text-2xl'>
|
|
||||||
{getVersionGame(getVersionInfo(entry)?.game)?.name} v
|
|
||||||
{getVersionInfo(entry)?.versionName}
|
|
||||||
</p>
|
|
||||||
<div className='entry-info-item'>
|
|
||||||
<p>
|
|
||||||
Installed{' '}
|
|
||||||
{format(
|
|
||||||
new Date(downloadedVersionsConfig.timestamps[entry]),
|
|
||||||
'MM/dd/yyyy'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-row items-center gap-2'>
|
|
||||||
<button
|
|
||||||
className='button'
|
|
||||||
onClick={async () => {
|
|
||||||
setManagingVersion(entry)
|
|
||||||
setPopupMode(3)
|
|
||||||
setShowPopup(true)
|
|
||||||
setFadeOut(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
View Info
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className='button'
|
|
||||||
onClick={async () => {
|
|
||||||
setManagingVersion(entry)
|
|
||||||
setPopupMode(2)
|
|
||||||
setShowPopup(true)
|
|
||||||
setFadeOut(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Manage
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className='button button-green'
|
|
||||||
onClick={async () => {
|
|
||||||
const verInfo = getVersionInfo(entry)
|
|
||||||
if (verInfo == undefined) return
|
|
||||||
invoke('launch_game', {
|
|
||||||
name: verInfo.id,
|
|
||||||
executable: verInfo.executable
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Launch
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className='flex justify-center items-center h-full'>
|
|
||||||
<p className='text-3xl'>No games installed</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,818 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
|
||||||
import Sidebar from './componets/Sidebar'
|
|
||||||
import './Globals.css'
|
|
||||||
import { DownloadProgress } from './types/DownloadProgress'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import {
|
|
||||||
faAdd,
|
|
||||||
faCheck,
|
|
||||||
faChevronLeft,
|
|
||||||
faCode,
|
|
||||||
faDownload,
|
|
||||||
faInfo,
|
|
||||||
faRemove,
|
|
||||||
faShieldHalved,
|
|
||||||
faWarning,
|
|
||||||
faXmark
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import {
|
|
||||||
isPermissionGranted,
|
|
||||||
requestPermission,
|
|
||||||
sendNotification
|
|
||||||
} from '@tauri-apps/plugin-notification'
|
|
||||||
import {
|
|
||||||
readNormalConfig,
|
|
||||||
readVersionsConfig,
|
|
||||||
writeVersionsConfig
|
|
||||||
} from './util/BazookaManager'
|
|
||||||
import { VersionsConfig } from './types/VersionsConfig'
|
|
||||||
import { NormalConfig } from './types/NormalConfig'
|
|
||||||
import { app } from '@tauri-apps/api'
|
|
||||||
import axios from 'axios'
|
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
||||||
import { GlobalProvider } from './GlobalProvider'
|
|
||||||
import { Roboto } from 'next/font/google'
|
|
||||||
import { ServerVersionsResponse } from './types/ServerVersionsResponse'
|
|
||||||
import { GameVersion } from './types/GameVersion'
|
|
||||||
import { Game } from './types/Game'
|
|
||||||
import { listen } from '@tauri-apps/api/event'
|
|
||||||
import { usePathname } from 'next/navigation'
|
|
||||||
import { arch, platform } from '@tauri-apps/plugin-os'
|
|
||||||
import VersionInfo from './componets/VersionInfo'
|
|
||||||
import prettyBytes from 'pretty-bytes'
|
|
||||||
import ProgressBar from './componets/ProgressBar'
|
|
||||||
|
|
||||||
const roboto = Roboto({
|
|
||||||
subsets: ['latin']
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function RootLayout ({
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [loadingText, setLoadingText] = useState('Loading...')
|
|
||||||
const [outdated, setOutdated] = useState(false)
|
|
||||||
|
|
||||||
const [serverVersionList, setServerVersionList] =
|
|
||||||
useState<null | ServerVersionsResponse>(null)
|
|
||||||
const [selectedVersionList, setSelectedVersionList] = useState<string[]>([])
|
|
||||||
|
|
||||||
const [downloadedVersionsConfig, setDownloadedVersionsConfig] =
|
|
||||||
useState<VersionsConfig | null>(null)
|
|
||||||
const [normalConfig, setNormalConfig] = useState<NormalConfig | null>(null)
|
|
||||||
|
|
||||||
const [showPopup, setShowPopup] = useState(false)
|
|
||||||
const [popupMode, setPopupMode] = useState<null | number>(null)
|
|
||||||
const [fadeOut, setFadeOut] = useState(false)
|
|
||||||
|
|
||||||
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress[]>(
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const [managingVersion, setManagingVersion] = useState<string | null>(null)
|
|
||||||
const [viewingInfoFromDownloads, setViewingInfoFromDownloads] =
|
|
||||||
useState<boolean>(false)
|
|
||||||
const [selectedGame, setSelectedGame] = useState<number | null>(null)
|
|
||||||
|
|
||||||
function handleOverlayClick (e: React.MouseEvent<HTMLDivElement>) {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
if (viewingInfoFromDownloads) {
|
|
||||||
setPopupMode(0)
|
|
||||||
setViewingInfoFromDownloads(false)
|
|
||||||
setManagingVersion(null)
|
|
||||||
setSelectedGame(null)
|
|
||||||
}
|
|
||||||
setFadeOut(true)
|
|
||||||
setTimeout(() => setShowPopup(false), 200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathname = usePathname()
|
|
||||||
|
|
||||||
const notifyUser = useCallback(
|
|
||||||
async (title: string, body: string) => {
|
|
||||||
if (!normalConfig?.settings.allowNotifications) return
|
|
||||||
|
|
||||||
let permissionGranted = await isPermissionGranted()
|
|
||||||
if (!permissionGranted) {
|
|
||||||
const permission = await requestPermission()
|
|
||||||
permissionGranted = permission === 'granted'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (permissionGranted) {
|
|
||||||
sendNotification({ title, body })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[normalConfig]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let unlistenProgress: (() => void) | null = null
|
|
||||||
let unlistenUninstalled: (() => void) | null = null
|
|
||||||
|
|
||||||
listen<string>('download-progress', event => {
|
|
||||||
const [versionName, progStr, totalSizeStr, speedStr, etaSecsStr] =
|
|
||||||
event.payload.split(':')
|
|
||||||
const prog = Number(progStr)
|
|
||||||
const progBytes = Number(totalSizeStr)
|
|
||||||
const speed = Number(speedStr)
|
|
||||||
const etaSecs = Number(etaSecsStr)
|
|
||||||
setDownloadProgress(prev => {
|
|
||||||
const i = prev.findIndex(d => d.version === versionName)
|
|
||||||
if (i === -1) return prev
|
|
||||||
const copy = [...prev]
|
|
||||||
copy[i] = {
|
|
||||||
...copy[i],
|
|
||||||
progress: prog,
|
|
||||||
progressBytes: progBytes,
|
|
||||||
speed,
|
|
||||||
etaSecs
|
|
||||||
}
|
|
||||||
return copy
|
|
||||||
})
|
|
||||||
}).then(f => (unlistenProgress = f))
|
|
||||||
|
|
||||||
listen<string>('download-hash-checking', event => {
|
|
||||||
const versionName = event.payload
|
|
||||||
setDownloadProgress(prev => {
|
|
||||||
const i = prev.findIndex(d => d.version === versionName)
|
|
||||||
if (i === -1) return prev
|
|
||||||
const copy = [...prev]
|
|
||||||
copy[i] = { ...copy[i], hash_checking: true }
|
|
||||||
return copy
|
|
||||||
})
|
|
||||||
}).then(f => (unlistenProgress = f))
|
|
||||||
|
|
||||||
listen<string>('download-finishing', event => {
|
|
||||||
const versionName = event.payload
|
|
||||||
setDownloadProgress(prev => {
|
|
||||||
const i = prev.findIndex(d => d.version === versionName)
|
|
||||||
if (i === -1) return prev
|
|
||||||
const copy = [...prev]
|
|
||||||
copy[i] = { ...copy[i], hash_checking: false, finishing: true }
|
|
||||||
return copy
|
|
||||||
})
|
|
||||||
}).then(f => (unlistenProgress = f))
|
|
||||||
|
|
||||||
listen<string>('version-uninstalled', event => {
|
|
||||||
const versionName = event.payload
|
|
||||||
setDownloadedVersionsConfig(prev => {
|
|
||||||
if (!prev) return prev
|
|
||||||
const updatedList = prev.list.filter(v => v !== versionName)
|
|
||||||
const updatedTimestamps = Object.fromEntries(
|
|
||||||
Object.entries(prev.timestamps).filter(([k]) => k !== versionName)
|
|
||||||
)
|
|
||||||
const updatedConfig = {
|
|
||||||
...prev,
|
|
||||||
list: updatedList,
|
|
||||||
timestamps: updatedTimestamps
|
|
||||||
}
|
|
||||||
writeVersionsConfig(updatedConfig)
|
|
||||||
setManagingVersion(null)
|
|
||||||
setFadeOut(true)
|
|
||||||
setTimeout(() => setShowPopup(false), 200)
|
|
||||||
return updatedConfig
|
|
||||||
})
|
|
||||||
}).then(f => (unlistenUninstalled = f))
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unlistenProgress?.()
|
|
||||||
unlistenUninstalled?.()
|
|
||||||
}
|
|
||||||
}, [notifyUser])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
;(async () => {
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
setLoadingText('Checking latest version...')
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
'https://games.lncvrt.xyz/api/launcher/latest'
|
|
||||||
)
|
|
||||||
const client = await app.getVersion()
|
|
||||||
if (response.data !== client) {
|
|
||||||
setOutdated(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setLoadingText('Failed to check latest version.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setLoadingText('Downloading version list...')
|
|
||||||
try {
|
|
||||||
const res = await axios.get(
|
|
||||||
`https://games.lncvrt.xyz/api/launcher/versions?platform=${platform()}&arch=${arch()}`
|
|
||||||
)
|
|
||||||
setServerVersionList(res.data)
|
|
||||||
} catch {
|
|
||||||
setLoadingText('Failed to download versions list.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLoadingText('Loading configs...')
|
|
||||||
const normalConfig = await readNormalConfig()
|
|
||||||
const versionsConfig = await readVersionsConfig()
|
|
||||||
setDownloadedVersionsConfig(versionsConfig)
|
|
||||||
setNormalConfig(normalConfig)
|
|
||||||
setLoading(false)
|
|
||||||
|
|
||||||
let permissionGranted = await isPermissionGranted()
|
|
||||||
if (!permissionGranted) {
|
|
||||||
const permission = await requestPermission()
|
|
||||||
permissionGranted = permission === 'granted'
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = (e: MouseEvent) => e.preventDefault()
|
|
||||||
document.addEventListener('contextmenu', handler)
|
|
||||||
return () => document.removeEventListener('contextmenu', handler)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
function getSpecialVersionsList (game?: number): GameVersion[] {
|
|
||||||
if (!normalConfig || !serverVersionList) return []
|
|
||||||
|
|
||||||
return serverVersionList.versions
|
|
||||||
.filter(v => !downloadedVersionsConfig?.list.includes(v.id))
|
|
||||||
.filter(v => {
|
|
||||||
if (game && v.game != game) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (b.game !== a.game) return a.game - b.game
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersionInfo (id: string | undefined): GameVersion | undefined {
|
|
||||||
if (!id) return undefined
|
|
||||||
return serverVersionList?.versions.find(v => v.id === id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersionGame (game: number | undefined): Game | undefined {
|
|
||||||
if (!game) return undefined
|
|
||||||
return serverVersionList?.games.find(g => g.id === game)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getListOfGames (): Game[] {
|
|
||||||
if (!downloadedVersionsConfig?.list) return []
|
|
||||||
|
|
||||||
const gamesMap = new Map<number, Game>()
|
|
||||||
|
|
||||||
downloadedVersionsConfig.list.forEach(i => {
|
|
||||||
const version = getVersionInfo(i)
|
|
||||||
if (!version) return
|
|
||||||
const game = getVersionGame(version.game)
|
|
||||||
if (!game) return
|
|
||||||
gamesMap.set(game.id, game)
|
|
||||||
})
|
|
||||||
|
|
||||||
return Array.from(gamesMap.values())
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadVersions (): Promise<void> {
|
|
||||||
const list = selectedVersionList
|
|
||||||
setSelectedVersionList([])
|
|
||||||
|
|
||||||
const newDownloads = list.map(
|
|
||||||
version =>
|
|
||||||
new DownloadProgress(version, 0, 0, false, true, false, false, 0, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
setDownloadProgress(newDownloads)
|
|
||||||
|
|
||||||
for (const download of newDownloads) {
|
|
||||||
const info = getVersionInfo(download.version)
|
|
||||||
if (!info) {
|
|
||||||
setDownloadProgress(prev =>
|
|
||||||
prev.filter(d => d.version !== download.version)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const gameInfo = getVersionGame(info.game)
|
|
||||||
if (!gameInfo) {
|
|
||||||
setDownloadProgress(prev =>
|
|
||||||
prev.filter(d => d.version !== download.version)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setDownloadProgress(prev =>
|
|
||||||
prev.map(d =>
|
|
||||||
d.version === download.version ? { ...d, queued: false } : d
|
|
||||||
)
|
|
||||||
)
|
|
||||||
const res = await invoke<string>('download', {
|
|
||||||
url: info.downloadUrl,
|
|
||||||
name: info.id,
|
|
||||||
executable: info.executable,
|
|
||||||
hash: info.sha512sum
|
|
||||||
})
|
|
||||||
if (res == '1') {
|
|
||||||
setDownloadProgress(prev =>
|
|
||||||
prev.filter(d => d.version !== download.version)
|
|
||||||
)
|
|
||||||
let data = downloadedVersionsConfig
|
|
||||||
if (!data) {
|
|
||||||
setDownloadProgress(prev =>
|
|
||||||
prev.filter(d => d.version !== download.version)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const date = Date.now()
|
|
||||||
data.list = [...data.list, download.version]
|
|
||||||
data.timestamps = { ...data.timestamps, [download.version]: date }
|
|
||||||
setDownloadedVersionsConfig(data)
|
|
||||||
writeVersionsConfig(data)
|
|
||||||
} else {
|
|
||||||
setDownloadProgress(prev =>
|
|
||||||
prev.map(d =>
|
|
||||||
d.version === download.version
|
|
||||||
? { ...d, queued: false, failed: true, progress: 0 }
|
|
||||||
: d
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await notifyUser(
|
|
||||||
'Download Failed',
|
|
||||||
`The download for version ${gameInfo.name} v${info.versionName} has failed.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await notifyUser('Downloads Finished', 'All downloads have finished.')
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersionsAmountData (gameId: number): {
|
|
||||||
installed: number
|
|
||||||
total: number
|
|
||||||
} | null {
|
|
||||||
if (!downloadedVersionsConfig || !serverVersionList) return null
|
|
||||||
|
|
||||||
const installed = downloadedVersionsConfig.list.filter(
|
|
||||||
v => getVersionGame(getVersionInfo(v)?.game)?.id === gameId
|
|
||||||
).length
|
|
||||||
|
|
||||||
const total = serverVersionList.versions.filter(
|
|
||||||
v => getVersionGame(v?.game)?.id === gameId
|
|
||||||
).length
|
|
||||||
|
|
||||||
return { installed, total }
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatEtaSmart (seconds: number) {
|
|
||||||
if (seconds < 60) return `${Math.floor(seconds)}s`
|
|
||||||
if (seconds < 3600)
|
|
||||||
return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`
|
|
||||||
if (seconds < 86400) {
|
|
||||||
const h = Math.floor(seconds / 3600)
|
|
||||||
const m = Math.floor((seconds % 3600) / 60)
|
|
||||||
return `${h}h ${m}m`
|
|
||||||
}
|
|
||||||
const d = Math.floor(seconds / 86400)
|
|
||||||
const h = Math.floor((seconds % 86400) / 3600)
|
|
||||||
return `${d}d ${h}h`
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<html lang='en' className={roboto.className}>
|
|
||||||
<body
|
|
||||||
className={
|
|
||||||
normalConfig?.settings.theme === 1
|
|
||||||
? 'red-theme'
|
|
||||||
: normalConfig?.settings.theme === 2
|
|
||||||
? 'green-theme'
|
|
||||||
: normalConfig?.settings.theme === 3
|
|
||||||
? 'blue-theme'
|
|
||||||
: normalConfig?.settings.theme === 4
|
|
||||||
? 'purple-theme'
|
|
||||||
: 'dark-theme'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<div className='w-screen h-screen flex items-center justify-center'>
|
|
||||||
{outdated ? (
|
|
||||||
<div className='text-center'>
|
|
||||||
<p className='text-8xl mb-4'>Outdated Launcher!</p>
|
|
||||||
<p className='text-4xl mb-4'>
|
|
||||||
Please update to the latest version to continue.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
className='button'
|
|
||||||
onClick={() =>
|
|
||||||
openUrl('https://games.lncvrt.xyz/berrydash/download')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Download latest version
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className='text-7xl text-center'>{loadingText}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<GlobalProvider
|
|
||||||
value={{
|
|
||||||
serverVersionList,
|
|
||||||
selectedVersionList,
|
|
||||||
setSelectedVersionList,
|
|
||||||
downloadProgress,
|
|
||||||
setDownloadProgress,
|
|
||||||
showPopup,
|
|
||||||
setShowPopup,
|
|
||||||
popupMode,
|
|
||||||
setPopupMode,
|
|
||||||
fadeOut,
|
|
||||||
setFadeOut,
|
|
||||||
downloadedVersionsConfig,
|
|
||||||
setDownloadedVersionsConfig,
|
|
||||||
normalConfig,
|
|
||||||
setNormalConfig,
|
|
||||||
managingVersion,
|
|
||||||
setManagingVersion,
|
|
||||||
getVersionInfo,
|
|
||||||
getVersionGame,
|
|
||||||
getListOfGames,
|
|
||||||
setSelectedGame,
|
|
||||||
getVersionsAmountData,
|
|
||||||
viewingInfoFromDownloads
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (showPopup && e.key === 'Escape') {
|
|
||||||
if (popupMode == 0 && selectedGame) {
|
|
||||||
setSelectedGame(null)
|
|
||||||
setSelectedVersionList([])
|
|
||||||
} else if (viewingInfoFromDownloads) {
|
|
||||||
setViewingInfoFromDownloads(false)
|
|
||||||
setPopupMode(0)
|
|
||||||
} else {
|
|
||||||
setFadeOut(true)
|
|
||||||
setTimeout(() => setShowPopup(false), 200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Sidebar />
|
|
||||||
<div
|
|
||||||
className='relative z-2 ml-[239px] w-[calc(100vw-239px)] border-b border-(--col3) h-[33px] bg-(--col1)'
|
|
||||||
style={{
|
|
||||||
display: platform() === 'windows' ? 'block' : 'none'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className='relative z-0'>
|
|
||||||
<main style={{ marginLeft: '15rem' }}>{children}</main>
|
|
||||||
</div>
|
|
||||||
{showPopup && (
|
|
||||||
<div
|
|
||||||
className={`popup-overlay ${fadeOut ? 'fade-out' : ''}`}
|
|
||||||
onClick={handleOverlayClick}
|
|
||||||
>
|
|
||||||
<div className='popup-box'>
|
|
||||||
<button
|
|
||||||
className='close-button'
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
popupMode == 0 &&
|
|
||||||
selectedGame &&
|
|
||||||
pathname === '/'
|
|
||||||
) {
|
|
||||||
setSelectedGame(null)
|
|
||||||
setSelectedVersionList([])
|
|
||||||
} else if (viewingInfoFromDownloads) {
|
|
||||||
setViewingInfoFromDownloads(false)
|
|
||||||
setPopupMode(0)
|
|
||||||
} else {
|
|
||||||
setFadeOut(true)
|
|
||||||
setTimeout(() => setShowPopup(false), 200)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={
|
|
||||||
(popupMode == 0 &&
|
|
||||||
selectedGame &&
|
|
||||||
pathname === '/') ||
|
|
||||||
viewingInfoFromDownloads
|
|
||||||
? faChevronLeft
|
|
||||||
: faXmark
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
{popupMode === 0 && selectedGame ? (
|
|
||||||
<>
|
|
||||||
<p className='text-xl text-center'>
|
|
||||||
Select versions to download
|
|
||||||
</p>
|
|
||||||
<div className='popup-content'>
|
|
||||||
{getSpecialVersionsList(selectedGame).map(
|
|
||||||
(v, i) => (
|
|
||||||
<div key={i} className='popup-entry'>
|
|
||||||
<p className='text-2xl'>
|
|
||||||
{getVersionGame(v.game)?.cutOff == null
|
|
||||||
? getVersionGame(v.game)?.name
|
|
||||||
: getVersionGame(v.game)?.name.substring(
|
|
||||||
0,
|
|
||||||
getVersionGame(v.game)?.cutOff ?? 0
|
|
||||||
) + '...'}{' '}
|
|
||||||
v{v.versionName}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
className='button right-22 bottom-1.5'
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedVersionList(prev =>
|
|
||||||
prev.includes(v.id)
|
|
||||||
? prev.filter(i => i !== v.id)
|
|
||||||
: [...prev, v.id]
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedVersionList.includes(v.id) ? (
|
|
||||||
<>
|
|
||||||
<FontAwesomeIcon icon={faRemove} />{' '}
|
|
||||||
Remove
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FontAwesomeIcon icon={faAdd} /> Add
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className='button right-1.5 bottom-1.5'
|
|
||||||
onClick={() => {
|
|
||||||
setManagingVersion(v.id)
|
|
||||||
setViewingInfoFromDownloads(true)
|
|
||||||
setPopupMode(3)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faInfo} /> Info
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : popupMode === 0 && !selectedGame ? (
|
|
||||||
<>
|
|
||||||
<p className='text-xl text-center'>
|
|
||||||
Select a game to download
|
|
||||||
</p>
|
|
||||||
<div className='popup-content'>
|
|
||||||
{serverVersionList?.games.map((v, i) => (
|
|
||||||
<div key={i} className='popup-entry'>
|
|
||||||
<p className='text-2xl'>{v.name}</p>
|
|
||||||
<div className='flex gap-2'>
|
|
||||||
<div className='entry-info-item'>
|
|
||||||
<p>
|
|
||||||
{(() => {
|
|
||||||
const data = getVersionsAmountData(v.id)
|
|
||||||
if (!data) return 'N/A'
|
|
||||||
return `${data.installed}/${data.total}`
|
|
||||||
})()}{' '}
|
|
||||||
versions installed
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className='entry-info-item'
|
|
||||||
hidden={!v.official}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCheck}
|
|
||||||
color='#19c84b'
|
|
||||||
/>
|
|
||||||
<p>Official</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className='entry-info-item'
|
|
||||||
hidden={v.official}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={
|
|
||||||
v.verified ? faShieldHalved : faWarning
|
|
||||||
}
|
|
||||||
color={v.verified ? '#19c84b' : '#ffc800'}
|
|
||||||
/>
|
|
||||||
<p>
|
|
||||||
{v.verified ? 'Verified' : 'Unverified'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className='entry-info-item mt-2'
|
|
||||||
hidden={v.official}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCode}
|
|
||||||
color='lightgray'
|
|
||||||
/>
|
|
||||||
<p>Developer: {v.developer}</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className='button right-2 bottom-2'
|
|
||||||
onClick={() => setSelectedGame(v.id)}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<FontAwesomeIcon icon={faDownload} />{' '}
|
|
||||||
Download
|
|
||||||
</>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : popupMode === 1 ? (
|
|
||||||
<>
|
|
||||||
<p className='text-xl text-center'>Downloads</p>
|
|
||||||
<div className='popup-content'>
|
|
||||||
{downloadProgress.length === 0 ? (
|
|
||||||
<p className='text-center mt-6'>
|
|
||||||
No more downloads!
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
downloadProgress.map((v, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className='popup-entry flex flex-col justify-between'
|
|
||||||
>
|
|
||||||
<p className='text-2xl text-center'>
|
|
||||||
{
|
|
||||||
getVersionGame(
|
|
||||||
getVersionInfo(v.version)?.game
|
|
||||||
)?.name
|
|
||||||
}{' '}
|
|
||||||
v{getVersionInfo(v.version)?.versionName}
|
|
||||||
</p>
|
|
||||||
<div className='mt-[25px] flex items-center justify-between'>
|
|
||||||
{v.failed ? (
|
|
||||||
<>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<span className='text-red-500 inline-block w-full text-center'>
|
|
||||||
Download failed
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
className='button ml-30 mb-2'
|
|
||||||
onClick={() => {
|
|
||||||
setDownloadProgress(prev =>
|
|
||||||
prev.filter(
|
|
||||||
d => d.version !== v.version
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : v.queued ? (
|
|
||||||
<span className='text-yellow-500 inline-block w-full text-center'>
|
|
||||||
Queued…
|
|
||||||
</span>
|
|
||||||
) : v.queued ? (
|
|
||||||
<span className='text-yellow-500 inline-block w-full text-center'>
|
|
||||||
Queued…
|
|
||||||
</span>
|
|
||||||
) : v.hash_checking ? (
|
|
||||||
<span className='text-blue-500 inline-block w-full text-center'>
|
|
||||||
Checking hash...
|
|
||||||
</span>
|
|
||||||
) : v.finishing ? (
|
|
||||||
<span className='text-green-500 inline-block w-full text-center'>
|
|
||||||
Finishing...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<div className='flex flex-col gap-1 w-full'>
|
|
||||||
<span className='text-center'>
|
|
||||||
Downloaded{' '}
|
|
||||||
{prettyBytes(v.progressBytes, {
|
|
||||||
minimumFractionDigits: 1,
|
|
||||||
maximumFractionDigits: 1
|
|
||||||
})}{' '}
|
|
||||||
of{' '}
|
|
||||||
{prettyBytes(
|
|
||||||
getVersionInfo(v.version)?.size ??
|
|
||||||
0,
|
|
||||||
{
|
|
||||||
minimumFractionDigits: 1,
|
|
||||||
maximumFractionDigits: 1
|
|
||||||
}
|
|
||||||
)}{' '}
|
|
||||||
(ETA: {formatEtaSmart(v.etaSecs)}{' '}
|
|
||||||
• Speed:{' '}
|
|
||||||
{prettyBytes(v.speed, {
|
|
||||||
minimumFractionDigits: 1,
|
|
||||||
maximumFractionDigits: 1
|
|
||||||
})}
|
|
||||||
/s)
|
|
||||||
</span>
|
|
||||||
<ProgressBar
|
|
||||||
progress={v.progress}
|
|
||||||
className='w-full'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : popupMode === 2 ? (
|
|
||||||
managingVersion ? (
|
|
||||||
<>
|
|
||||||
<p className='text-xl text-center'>
|
|
||||||
Manage{' '}
|
|
||||||
{
|
|
||||||
getVersionGame(
|
|
||||||
getVersionInfo(managingVersion)?.game
|
|
||||||
)?.name
|
|
||||||
}{' '}
|
|
||||||
v{getVersionInfo(managingVersion)?.versionName}
|
|
||||||
</p>
|
|
||||||
<div className='popup-content flex flex-col items-center justify-center gap-2 h-full'>
|
|
||||||
<button
|
|
||||||
className='button'
|
|
||||||
onClick={() =>
|
|
||||||
invoke('uninstall_version', {
|
|
||||||
name: managingVersion
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Uninstall
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className='button'
|
|
||||||
onClick={async () =>
|
|
||||||
invoke('open_folder', {
|
|
||||||
name: managingVersion
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Open Folder
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className='text-xl text-center'>
|
|
||||||
No version selected
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
) : popupMode === 3 ? (
|
|
||||||
managingVersion && downloadedVersionsConfig ? (
|
|
||||||
<VersionInfo />
|
|
||||||
) : (
|
|
||||||
<p className='text-xl text-center'>
|
|
||||||
No version selected
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
{popupMode == 0 &&
|
|
||||||
selectedGame &&
|
|
||||||
serverVersionList != null && (
|
|
||||||
<div className='flex justify-center'>
|
|
||||||
<button
|
|
||||||
className='button w-fit mt-2 -mb-4'
|
|
||||||
onClick={() => {
|
|
||||||
setFadeOut(true)
|
|
||||||
setTimeout(() => setShowPopup(false), 200)
|
|
||||||
downloadVersions()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Download {selectedVersionList.length} version
|
|
||||||
{selectedVersionList.length == 1 ? '' : 's'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className='button w-fit mt-2 ml-2 -mb-4'
|
|
||||||
onClick={() => {
|
|
||||||
const allIds = getSpecialVersionsList(
|
|
||||||
selectedGame
|
|
||||||
).map(v => v.id)
|
|
||||||
setSelectedVersionList(prev =>
|
|
||||||
prev.length === allIds.length ? [] : allIds
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedVersionList.length ===
|
|
||||||
getSpecialVersionsList(selectedGame).length
|
|
||||||
? 'Deselect All'
|
|
||||||
: 'Select All'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</GlobalProvider>
|
|
||||||
)}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { verifySignature } from '@/lib/util'
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { app } from '@tauri-apps/api'
|
||||||
|
import { message } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
import { arch, platform } from '@tauri-apps/plugin-os'
|
||||||
|
import { Dispatch, SetStateAction, useState } from 'react'
|
||||||
|
|
||||||
|
function AccountLoggedOut () {
|
||||||
|
const { account, setServerVersionList } = useGlobal()
|
||||||
|
const [username, setUsername] = useState<string>('')
|
||||||
|
const [password, setPassword] = useState<string>('')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className='text-3xl ml-4 mt-4'>Account Login</p>
|
||||||
|
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
||||||
|
<form
|
||||||
|
className='flex flex-col gap-2'
|
||||||
|
onSubmit={async e => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
'https://games.lncvrt.xyz/api/account/login',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
|
const data = await response.json()
|
||||||
|
if (
|
||||||
|
response.status != 200 ||
|
||||||
|
!(await verifySignature(JSON.stringify(data), signature))
|
||||||
|
) {
|
||||||
|
await message(
|
||||||
|
"Failed to login, there was an issue validating the data wasn't tampered with.",
|
||||||
|
{ title: 'Request tampered', kind: 'error' }
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (data.success) {
|
||||||
|
const response2 = await fetch(
|
||||||
|
'https://games.lncvrt.xyz/api/launcher/versions',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Requester: 'LncvrtGamesLauncherClient',
|
||||||
|
ClientVersion: await app.getVersion(),
|
||||||
|
ClientPlatform: platform() + '-' + arch(),
|
||||||
|
Authorization: data.data.session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const signature2 = response2.headers.get('x-signature') ?? ''
|
||||||
|
const data2 = await response2.json()
|
||||||
|
if (
|
||||||
|
response2.status == 200 &&
|
||||||
|
(await verifySignature(JSON.stringify(data2), signature2))
|
||||||
|
) {
|
||||||
|
setServerVersionList(data2)
|
||||||
|
}
|
||||||
|
|
||||||
|
await account?.set('session', data.data.session)
|
||||||
|
await account?.set('name', data.data.username)
|
||||||
|
await account?.set('id', data.data.id)
|
||||||
|
await account?.set('admin', data.data.admin)
|
||||||
|
} else {
|
||||||
|
await message(data.message || 'n/a', {
|
||||||
|
title: 'Failed to login',
|
||||||
|
kind: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id='username'
|
||||||
|
name='username'
|
||||||
|
placeholder='Username'
|
||||||
|
type='username'
|
||||||
|
value={username}
|
||||||
|
onChange={e => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
className='input-field btntheme1'
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
id='password'
|
||||||
|
name='password'
|
||||||
|
placeholder='Password'
|
||||||
|
type='password'
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className='input-field btntheme1'
|
||||||
|
/>
|
||||||
|
<button type='submit' className='button btntheme1'>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<div className='flex justify-center flex-col gap-2 mt-6 text-center'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={async () =>
|
||||||
|
await openUrl(
|
||||||
|
'https://games.lncvrt.xyz/account/forgot-username'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className='button btntheme1'
|
||||||
|
>
|
||||||
|
Forgot username?
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={async () =>
|
||||||
|
await openUrl(
|
||||||
|
'https://games.lncvrt.xyz/account/forgot-password'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className='button btntheme1'
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={async () =>
|
||||||
|
await openUrl('https://games.lncvrt.xyz/account/register')
|
||||||
|
}
|
||||||
|
className='button btntheme1'
|
||||||
|
>
|
||||||
|
Don't have an account?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountChangeUsername ({
|
||||||
|
setChangingUsername
|
||||||
|
}: {
|
||||||
|
setChangingUsername: Dispatch<SetStateAction<boolean>>
|
||||||
|
}) {
|
||||||
|
const { account, accountSession } = useGlobal()
|
||||||
|
|
||||||
|
const [newUsername, setNewUsername] = useState<string>('')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className='text-3xl ml-4 mt-4'>Change Account Username</p>
|
||||||
|
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
||||||
|
<form
|
||||||
|
className='flex flex-col gap-2'
|
||||||
|
onSubmit={async e => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
'https://games.lncvrt.xyz/api/account/change-username',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: accountSession ?? ''
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
newUsername
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
|
const data = await response.json()
|
||||||
|
if (
|
||||||
|
response.status != 200 ||
|
||||||
|
!(await verifySignature(JSON.stringify(data), signature))
|
||||||
|
) {
|
||||||
|
await message(
|
||||||
|
"Failed to change username, there was an issue validating the data wasn't tampered with.",
|
||||||
|
{ title: 'Request tampered', kind: 'error' }
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (data.success) {
|
||||||
|
await account?.set('session', data.data)
|
||||||
|
await account?.set('name', newUsername)
|
||||||
|
setChangingUsername(false)
|
||||||
|
} else {
|
||||||
|
await message(data.message || 'n/a', {
|
||||||
|
title: 'Failed to change username',
|
||||||
|
kind: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id='new-username'
|
||||||
|
name='new-username'
|
||||||
|
placeholder='New username'
|
||||||
|
type='username'
|
||||||
|
value={newUsername}
|
||||||
|
onChange={e => setNewUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
className='input-field btntheme1'
|
||||||
|
/>
|
||||||
|
<button type='submit' className='button btntheme1'>
|
||||||
|
Change Username
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={() => setChangingUsername(false)}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountChangePassword ({
|
||||||
|
setChangingPassword
|
||||||
|
}: {
|
||||||
|
setChangingPassword: Dispatch<SetStateAction<boolean>>
|
||||||
|
}) {
|
||||||
|
const { account, accountSession, setServerVersionList } = useGlobal()
|
||||||
|
|
||||||
|
const [newPassword, setNewPassword] = useState<string>('')
|
||||||
|
const [retypeNewPassword, setRetypeNewPassword] = useState<string>('')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className='text-3xl ml-4 mt-4'>Change Account Password</p>
|
||||||
|
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
||||||
|
<form
|
||||||
|
className='flex flex-col gap-2'
|
||||||
|
onSubmit={async e => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (newPassword !== retypeNewPassword) {
|
||||||
|
await message('Passwords must match', {
|
||||||
|
title: "Passwords don't match!",
|
||||||
|
kind: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
'https://games.lncvrt.xyz/api/account/change-password',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: accountSession ?? ''
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
newPassword
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
|
const data = await response.json()
|
||||||
|
if (
|
||||||
|
response.status != 200 ||
|
||||||
|
!(await verifySignature(JSON.stringify(data), signature))
|
||||||
|
) {
|
||||||
|
await message(
|
||||||
|
"Failed to change password, there was an issue validating the data wasn't tampered with.",
|
||||||
|
{ title: 'Request tampered', kind: 'error' }
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (data.success) {
|
||||||
|
const response2 = await fetch(
|
||||||
|
'https://games.lncvrt.xyz/api/launcher/versions',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Requester: 'LncvrtGamesLauncherClient',
|
||||||
|
ClientVersion: await app.getVersion(),
|
||||||
|
ClientPlatform: platform() + '-' + arch(),
|
||||||
|
Authorization: data.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const signature2 = response2.headers.get('x-signature') ?? ''
|
||||||
|
const data2 = await response2.json()
|
||||||
|
if (
|
||||||
|
response2.status == 200 &&
|
||||||
|
(await verifySignature(JSON.stringify(data2), signature2))
|
||||||
|
) {
|
||||||
|
setServerVersionList(data2)
|
||||||
|
}
|
||||||
|
|
||||||
|
await account?.set('session', data.data)
|
||||||
|
setChangingPassword(false)
|
||||||
|
} else {
|
||||||
|
await message(data.message || 'n/a', {
|
||||||
|
title: 'Failed to change password',
|
||||||
|
kind: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id='new-password'
|
||||||
|
name='new-password'
|
||||||
|
placeholder='New password'
|
||||||
|
type='password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={newPassword}
|
||||||
|
onChange={e => setNewPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className='input-field btntheme1'
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
id='retype-new-password'
|
||||||
|
name='retype-new-password'
|
||||||
|
placeholder='Re-type password'
|
||||||
|
type='password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={retypeNewPassword}
|
||||||
|
onChange={e => setRetypeNewPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className='input-field btntheme1'
|
||||||
|
/>
|
||||||
|
<button type='submit' className='button btntheme1'>
|
||||||
|
Change Password
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={() => setChangingPassword(false)}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountLoggedIn () {
|
||||||
|
const { account, accountName, setServerVersionList } = useGlobal()
|
||||||
|
const [changingUsername, setChangingUsername] = useState<boolean>(false)
|
||||||
|
const [changingPassword, setChangingPassword] = useState<boolean>(false)
|
||||||
|
|
||||||
|
if (changingUsername)
|
||||||
|
return <AccountChangeUsername setChangingUsername={setChangingUsername} />
|
||||||
|
|
||||||
|
if (changingPassword)
|
||||||
|
return <AccountChangePassword setChangingPassword={setChangingPassword} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className='text-3xl ml-4 mt-4'>Account</p>
|
||||||
|
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
||||||
|
<p>Logged in as: {accountName}</p>
|
||||||
|
<div className='flex justify-center flex-col gap-2 mt-4 text-center'>
|
||||||
|
<button
|
||||||
|
onClick={() => setChangingUsername(true)}
|
||||||
|
className='button btntheme1'
|
||||||
|
>
|
||||||
|
Change username
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setChangingPassword(true)}
|
||||||
|
className='button btntheme1'
|
||||||
|
>
|
||||||
|
Change password
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://games.lncvrt.xyz/api/launcher/versions',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Requester: 'LncvrtGamesLauncherClient',
|
||||||
|
ClientVersion: await app.getVersion(),
|
||||||
|
ClientPlatform: platform() + '-' + arch(),
|
||||||
|
Authorization: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
|
const data = await response.json()
|
||||||
|
if (
|
||||||
|
response.status == 200 &&
|
||||||
|
(await verifySignature(JSON.stringify(data), signature))
|
||||||
|
) {
|
||||||
|
setServerVersionList(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
await account?.clear()
|
||||||
|
}}
|
||||||
|
className='button btntheme1'
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AccountsPage () {
|
||||||
|
const { accountSession, accountId, accountName } = useGlobal()
|
||||||
|
|
||||||
|
if (!accountSession || !accountId || !accountName) return <AccountLoggedOut />
|
||||||
|
else return <AccountLoggedIn />
|
||||||
|
}
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
|
import { faWarning } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { ask } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { BaseDirectory, exists } from '@tauri-apps/plugin-fs'
|
||||||
|
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
|
|
||||||
|
export default function GamePage () {
|
||||||
|
const {
|
||||||
|
versionsList,
|
||||||
|
serverVersionList,
|
||||||
|
showPopup,
|
||||||
|
setShowPopup,
|
||||||
|
setPopupMode,
|
||||||
|
setFadeOut,
|
||||||
|
setSelectedVersionList,
|
||||||
|
setManagingVersion,
|
||||||
|
setSelectedGame,
|
||||||
|
category,
|
||||||
|
setCategory,
|
||||||
|
downloadVersions,
|
||||||
|
downloadProgress,
|
||||||
|
linuxUseWine,
|
||||||
|
customDataLocation,
|
||||||
|
needsRevisionUpdate,
|
||||||
|
launchGame,
|
||||||
|
developerMode,
|
||||||
|
theme,
|
||||||
|
accountSession,
|
||||||
|
accountId,
|
||||||
|
accountName,
|
||||||
|
version
|
||||||
|
} = useGlobal()
|
||||||
|
|
||||||
|
const params = useSearchParams()
|
||||||
|
|
||||||
|
const id = Number(params.get('id') || 0)
|
||||||
|
const game = serverVersionList?.games.find(g => g.id === id)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showPopup) return
|
||||||
|
setSelectedVersionList([])
|
||||||
|
}, [setSelectedVersionList, showPopup])
|
||||||
|
|
||||||
|
if (!id || !game) return null
|
||||||
|
|
||||||
|
const filteredVersions = Object.keys(versionsList).filter(v => {
|
||||||
|
const info = serverVersionList?.versions.find(vf => vf.id == v)
|
||||||
|
if (!info) return false
|
||||||
|
if (platform() == 'linux' && info.wine && !linuxUseWine) return false
|
||||||
|
return (
|
||||||
|
info.game === id &&
|
||||||
|
(category == -1 ? info.category == -1 : info.category == category)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mx-4 mt-4'>
|
||||||
|
<div className='flex justify-between items-center mb-4'>
|
||||||
|
<p className='text-3xl truncate min-w-0 flex-1'>{game.name} Installs</p>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
{game.id == 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={async () => {
|
||||||
|
const window = await WebviewWindow.getByLabel(
|
||||||
|
'berrydashchatroom'
|
||||||
|
)
|
||||||
|
if (window) {
|
||||||
|
await window.setFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
new WebviewWindow('berrydashchatroom', {
|
||||||
|
title: 'Berry Dash Chatroom',
|
||||||
|
url: `https://games.lncvrt.xyz/game/berry-dash/chatroom?launcher=${version}&theme=${theme}&accountToken=${accountSession}&accountId=${accountId}&accountName=${accountName}`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
minWidth: 800,
|
||||||
|
minHeight: 600
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
title='View the chatroom for this game.'
|
||||||
|
>
|
||||||
|
Chatroom
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={async () => {
|
||||||
|
const window = await WebviewWindow.getByLabel(
|
||||||
|
'berrydashleaderboards'
|
||||||
|
)
|
||||||
|
if (window) {
|
||||||
|
await window.setFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
new WebviewWindow('berrydashleaderboards', {
|
||||||
|
title: 'Berry Dash Leaderboards',
|
||||||
|
url: `https://games.lncvrt.xyz/game/berry-dash/leaderboards?launcher=${version}&theme=${theme}`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
minWidth: 800,
|
||||||
|
minHeight: 600
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
title='View the leaderboards for this game.'
|
||||||
|
>
|
||||||
|
Leaderboards
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{game.id == 9 && category == -1 && (
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={async () => {
|
||||||
|
const window = await WebviewWindow.getByLabel('xpsdashboard')
|
||||||
|
if (window) {
|
||||||
|
await window.setFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
new WebviewWindow('xpsdashboard', {
|
||||||
|
title: 'XPS Dashboard',
|
||||||
|
url: 'https://xps.lncvrt.xyz/dashboard/',
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
minWidth: 1280,
|
||||||
|
minHeight: 720
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
title='Open the GDPS Dashboard!'
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{game.id == 11 && category == -1 && (
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={async () => {
|
||||||
|
const window = await WebviewWindow.getByLabel('georayschatroom')
|
||||||
|
if (window) {
|
||||||
|
await window.setFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
new WebviewWindow('georayschatroom', {
|
||||||
|
title: 'Geometry Rays Chatroom',
|
||||||
|
url: `https://games.lncvrt.xyz/game/geometry-rays/chatroom?launcher=${version}&theme=${theme}`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
minWidth: 800,
|
||||||
|
minHeight: 600
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
title='View the chatroom for this game.'
|
||||||
|
>
|
||||||
|
Chatroom
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{category != -1 && (
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={() => {
|
||||||
|
setCategory(-1)
|
||||||
|
}}
|
||||||
|
title='Click to go up a level.'
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedGame(id)
|
||||||
|
setPopupMode(0)
|
||||||
|
setShowPopup(true)
|
||||||
|
setFadeOut(false)
|
||||||
|
}}
|
||||||
|
title='Click to download more versions of this game.'
|
||||||
|
>
|
||||||
|
Download versions
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`main-container main-flex ${
|
||||||
|
platform() == 'windows'
|
||||||
|
? 'h-[calc(100vh-116px)]'
|
||||||
|
: 'h-[calc(100vh-84px)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category == -1 &&
|
||||||
|
Object.entries(game.categoryNames)
|
||||||
|
.filter(([key]) => {
|
||||||
|
const count = Object.keys(versionsList).filter(v => {
|
||||||
|
const info = serverVersionList?.versions.find(vf => vf.id == v)
|
||||||
|
if (!info) return false
|
||||||
|
|
||||||
|
if (platform() == 'linux' && info.wine && !linuxUseWine)
|
||||||
|
return false
|
||||||
|
|
||||||
|
return info.game === id && info.category === Number(key)
|
||||||
|
}).length
|
||||||
|
|
||||||
|
return count >= 1
|
||||||
|
})
|
||||||
|
.sort(([a], [b]) => Number(b) - Number(a))
|
||||||
|
.map(([key, value]) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className='main-entry'
|
||||||
|
title='Click to view category.'
|
||||||
|
onClick={() => setCategory(Number(key))}
|
||||||
|
>
|
||||||
|
<div className='h-18 w-screen relative'>
|
||||||
|
{developerMode && (
|
||||||
|
<p className='absolute bottom-0 right-0 text-sm'>{key}</p>
|
||||||
|
)}
|
||||||
|
<p className='text-2xl'>{value}</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className='entry-info-item flex absolute left-0 bottom-0'
|
||||||
|
title='The amount of versions installed of this game in installed/installable format.'
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{(() => {
|
||||||
|
const count =
|
||||||
|
Object.keys(versionsList).filter(v => {
|
||||||
|
const info = serverVersionList?.versions.find(
|
||||||
|
vf => vf.id == v
|
||||||
|
)
|
||||||
|
if (!info) return false
|
||||||
|
if (
|
||||||
|
platform() == 'linux' &&
|
||||||
|
info.wine &&
|
||||||
|
!linuxUseWine
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
return (
|
||||||
|
info.game === id && info.category == Number(key)
|
||||||
|
)
|
||||||
|
}).length ?? 0
|
||||||
|
return `${count} install${count === 1 ? '' : 's'}`
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{filteredVersions.length === 0 &&
|
||||||
|
(category !== -1 ||
|
||||||
|
Object.keys(versionsList).filter(v => {
|
||||||
|
const info = serverVersionList?.versions.find(vf => vf.id == v)
|
||||||
|
if (!info) return false
|
||||||
|
|
||||||
|
if (platform() == 'linux' && info.wine && !linuxUseWine)
|
||||||
|
return false
|
||||||
|
|
||||||
|
return info.game === id
|
||||||
|
}).length === 0) && (
|
||||||
|
<div className='flex justify-center items-center h-full'>
|
||||||
|
<p className='text-3xl'>No versions installed</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{filteredVersions
|
||||||
|
.sort((a, b) => {
|
||||||
|
const infoA = serverVersionList?.versions.find(vf => vf.id == a)
|
||||||
|
const infoB = serverVersionList?.versions.find(vf => vf.id == b)
|
||||||
|
if (!infoA || !infoB) return 0
|
||||||
|
return infoB.place - infoA.place
|
||||||
|
})
|
||||||
|
.map(v => {
|
||||||
|
const versionInfo = serverVersionList?.versions.find(
|
||||||
|
vf => vf.id == v
|
||||||
|
)
|
||||||
|
if (!versionInfo) return
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={v}
|
||||||
|
className='main-entry'
|
||||||
|
title='Click to launch game. Right-click to manage this version install.'
|
||||||
|
onClick={async () => await launchGame(versionInfo)}
|
||||||
|
onContextMenu={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
setManagingVersion(v)
|
||||||
|
setPopupMode(2)
|
||||||
|
setShowPopup(true)
|
||||||
|
setFadeOut(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='h-18 w-screen relative'>
|
||||||
|
{developerMode && (
|
||||||
|
<p className='absolute bottom-0 right-0 text-sm'>
|
||||||
|
{versionInfo.id}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className='text-2xl'>{versionInfo.displayName}</p>
|
||||||
|
|
||||||
|
<div className='flex gap-2 absolute left-0 bottom-0'>
|
||||||
|
<div
|
||||||
|
className='entry-info-item'
|
||||||
|
title='The date the game was installed.'
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Installed{' '}
|
||||||
|
{new Intl.DateTimeFormat(undefined).format(
|
||||||
|
versionsList[v]
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{platform() == 'linux' &&
|
||||||
|
versionInfo.wine &&
|
||||||
|
!needsRevisionUpdate(versionInfo.lastRevision, v) && (
|
||||||
|
<div
|
||||||
|
className='entry-info-item'
|
||||||
|
title='This version is using wine. It cannot be guarenteed to work fully and might not work at all.'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faWarning} color='#ffc800' />
|
||||||
|
<p>Uses wine</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{needsRevisionUpdate(versionInfo.lastRevision, v) && (
|
||||||
|
<div className='entry-info-item'>
|
||||||
|
<FontAwesomeIcon icon={faWarning} color='#ffc800' />
|
||||||
|
<p>Needs revision update!</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{versionInfo.modSupportDownload && (
|
||||||
|
<button
|
||||||
|
className='absolute right-0 bottom-0 button'
|
||||||
|
title='Click to manage mods for this game!'
|
||||||
|
onClick={async e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await exists(
|
||||||
|
customDataLocation
|
||||||
|
? customDataLocation + '/'
|
||||||
|
: '' + 'game/' + v + '/BepInEx',
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData
|
||||||
|
}
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
(await ask(
|
||||||
|
"You don't have BepInEx (the mod loader for Unity Games), would you like to install it now? It is about 1MB in size. If you choose yes, it will download the recommended BepInEx version for " +
|
||||||
|
versionInfo.displayName +
|
||||||
|
'.',
|
||||||
|
{ title: 'BepInEx not found!', kind: 'error' }
|
||||||
|
)) &&
|
||||||
|
!downloadProgress.find(d => d.version == v)
|
||||||
|
) {
|
||||||
|
await downloadVersions([
|
||||||
|
{
|
||||||
|
id: v,
|
||||||
|
type: 1
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setManagingVersion(v)
|
||||||
|
setPopupMode(3)
|
||||||
|
setShowPopup(true)
|
||||||
|
setFadeOut(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Mod Manager
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,992 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import '@/styles/globals.css'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import Sidebar from '@/components/Sidebar'
|
||||||
|
import { DownloadProgress } from '@/types/DownloadProgress'
|
||||||
|
import { app } from '@tauri-apps/api'
|
||||||
|
import { GlobalProvider } from '@/providers/GlobalProvider'
|
||||||
|
import { Roboto } from 'next/font/google'
|
||||||
|
import { ServerVersionsResponse } from '@/types/ServerVersionsResponse'
|
||||||
|
import { listen } from '@tauri-apps/api/event'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
import { arch, platform } from '@tauri-apps/plugin-os'
|
||||||
|
import {
|
||||||
|
isPermissionGranted,
|
||||||
|
requestPermission
|
||||||
|
} from '@tauri-apps/plugin-notification'
|
||||||
|
import {
|
||||||
|
BaseDirectory,
|
||||||
|
copyFile,
|
||||||
|
exists,
|
||||||
|
readTextFile,
|
||||||
|
remove
|
||||||
|
} from '@tauri-apps/plugin-fs'
|
||||||
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
|
import { openFolder, verifySignature } from '@/lib/util'
|
||||||
|
import { getCurrentWindow, UserAttentionType } from '@tauri-apps/api/window'
|
||||||
|
import { Mod } from '@/types/Mod'
|
||||||
|
import { load, Store } from '@tauri-apps/plugin-store'
|
||||||
|
import { notifyUser } from '@/lib/notifications'
|
||||||
|
import { GameVersion } from '@/types/GameVersion'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faChevronLeft, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import semver from 'semver'
|
||||||
|
import { exit } from '@tauri-apps/plugin-process'
|
||||||
|
import { ask, message } from '@tauri-apps/plugin-dialog'
|
||||||
|
|
||||||
|
import VersionsDownloadPopup from '@/popups/VersionsDownload'
|
||||||
|
import GamesDownloadPopup from '@/popups/GamesDownload'
|
||||||
|
import DownloadsPopup from '@/popups/Downloads'
|
||||||
|
import ManageVersionPopup from '@/popups/ManageVersion'
|
||||||
|
import ModDownloadsPopup from '@/popups/ModDownloads'
|
||||||
|
import VersionChangelogPopup from '@/popups/VersionChangelog'
|
||||||
|
import ManageGamePopup from '@/popups/ManageGame'
|
||||||
|
|
||||||
|
const roboto = Roboto({
|
||||||
|
subsets: ['latin']
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function RootLayout ({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [version, setVersion] = useState<string | null>(null)
|
||||||
|
const [platformName, setPlatformName] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const [serverVersionList, setServerVersionList] =
|
||||||
|
useState<null | ServerVersionsResponse>(null)
|
||||||
|
const [selectedVersionList, setSelectedVersionList] = useState<string[]>([])
|
||||||
|
|
||||||
|
const [showPopup, setShowPopup] = useState(false)
|
||||||
|
const [popupMode, setPopupMode] = useState<null | number>(null)
|
||||||
|
const [fadeOut, setFadeOut] = useState(false)
|
||||||
|
|
||||||
|
const [downloadProgress, setDownloadProgress] = useState<DownloadProgress[]>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const [downloadQueue, setDownloadQueue] = useState<string[]>([])
|
||||||
|
const [isProcessingQueue, setIsProcessingQueue] = useState<boolean>(false)
|
||||||
|
const [managingVersion, setManagingVersion] = useState<string | null>(null)
|
||||||
|
const [managingGame, setManagingGame] = useState<number | null>(null)
|
||||||
|
const [viewingInfoFromDownloads, setViewingInfoFromDownloads] =
|
||||||
|
useState<boolean>(false)
|
||||||
|
const [selectedGame, setSelectedGame] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const [category, setCategory] = useState<number>(-1)
|
||||||
|
const [showModInfo, setShowModInfo] = useState<Mod | null>(null)
|
||||||
|
|
||||||
|
const pathname = usePathname()
|
||||||
|
const revisionCheck = useRef(false)
|
||||||
|
|
||||||
|
const [settings, setSettings] = useState<Store | null>(null)
|
||||||
|
const [versions, setVersions] = useState<Store | null>(null)
|
||||||
|
const [account, setAccount] = useState<Store | null>(null)
|
||||||
|
|
||||||
|
const [sidebarShowGames, setSidebarShowGames] = useState<boolean>(false)
|
||||||
|
const [linuxUseWine, setLinuxUseWine] = useState<boolean>(false)
|
||||||
|
const [linuxWineCommand, setLinuxWineCommand] =
|
||||||
|
useState<string>('wine %path%')
|
||||||
|
const [theme, setTheme] = useState<string>('dark')
|
||||||
|
const [customDataLocation, setCustomDataLocation] = useState<string>('')
|
||||||
|
const [developerMode, setDeveloperMode] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const [versionsList, setVersionsList] = useState<Record<string, number>>({})
|
||||||
|
const [modsList, setModsList] = useState<
|
||||||
|
Record<string, Record<string, number>>
|
||||||
|
>({})
|
||||||
|
const [launchesList, setLaunchesList] = useState<
|
||||||
|
Record<string, Record<string, number>>
|
||||||
|
>({})
|
||||||
|
|
||||||
|
const [accountSession, setAccountSession] = useState<string | null>(null)
|
||||||
|
const [accountId, setAccountId] = useState<string | null>(null)
|
||||||
|
const [accountName, setAccountName] = useState<string | null>(null)
|
||||||
|
const [accountAdmin, setAccountAdmin] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const downloadVersions = useCallback(
|
||||||
|
async (
|
||||||
|
list: Array<{
|
||||||
|
id: string
|
||||||
|
type: 0 | 1 | 2
|
||||||
|
modDownload?: number
|
||||||
|
gameId?: number
|
||||||
|
modId?: number
|
||||||
|
modVersion?: string
|
||||||
|
}>
|
||||||
|
): Promise<void> => {
|
||||||
|
if (list.length === 0) return
|
||||||
|
setSelectedVersionList([])
|
||||||
|
|
||||||
|
const newVersions = list.filter(
|
||||||
|
item =>
|
||||||
|
!downloadQueue.includes(item.id) &&
|
||||||
|
!downloadProgress.some(d => d.version === item.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (newVersions.length === 0) return
|
||||||
|
|
||||||
|
const newDownloads = newVersions.map(
|
||||||
|
item =>
|
||||||
|
new DownloadProgress(
|
||||||
|
item.id,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
item.type,
|
||||||
|
item.modDownload ?? null,
|
||||||
|
item.gameId ?? null,
|
||||||
|
item.modId ?? null,
|
||||||
|
item.modVersion ?? null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
setDownloadProgress(prev => [...prev, ...newDownloads])
|
||||||
|
setDownloadQueue(prev => [...prev, ...newVersions.map(v => v.id)])
|
||||||
|
},
|
||||||
|
[downloadQueue, downloadProgress]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isProcessingQueue || downloadQueue.length === 0) return
|
||||||
|
|
||||||
|
const processNextDownload = async () => {
|
||||||
|
setIsProcessingQueue(true)
|
||||||
|
|
||||||
|
const versionId = downloadQueue[0]
|
||||||
|
|
||||||
|
const downloadInfo = downloadProgress.find(d => d.version == versionId)
|
||||||
|
if (!downloadInfo) {
|
||||||
|
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||||
|
setDownloadQueue(prev => prev.slice(1))
|
||||||
|
setIsProcessingQueue(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = serverVersionList?.versions.find(vf => vf.id == versionId)
|
||||||
|
if (!info) {
|
||||||
|
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||||
|
setDownloadQueue(prev => prev.slice(1))
|
||||||
|
setIsProcessingQueue(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameInfo = serverVersionList?.games.find(vf => vf.id == info.game)
|
||||||
|
if (!gameInfo) {
|
||||||
|
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||||
|
setDownloadQueue(prev => prev.slice(1))
|
||||||
|
setIsProcessingQueue(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setDownloadProgress(prev =>
|
||||||
|
prev.map(d => (d.version === versionId ? { ...d, queued: false } : d))
|
||||||
|
)
|
||||||
|
|
||||||
|
const downloadInfoRequest = await fetch(
|
||||||
|
'https://games.lncvrt.xyz/api/launcher/download' +
|
||||||
|
(downloadInfo.type == 0
|
||||||
|
? '?versionId=' + info.id + '&downloadId=' + info.download
|
||||||
|
: '?downloadId=' +
|
||||||
|
(downloadInfo.type == 1
|
||||||
|
? info.modSupportDownload
|
||||||
|
: downloadInfo.modDownload)) +
|
||||||
|
(downloadInfo.type == 2 ? '&modId=' + downloadInfo.modId : ''),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Requester: 'LncvrtGamesLauncherClient',
|
||||||
|
ClientVersion: await app.getVersion(),
|
||||||
|
ClientPlatform: platform() + '-' + arch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const signature = downloadInfoRequest.headers.get('x-signature') ?? ''
|
||||||
|
const data = await downloadInfoRequest.json()
|
||||||
|
if (
|
||||||
|
downloadInfoRequest.status != 200 ||
|
||||||
|
!(await verifySignature(JSON.stringify(data), signature)) ||
|
||||||
|
!data.success
|
||||||
|
) {
|
||||||
|
setDownloadProgress(prev =>
|
||||||
|
prev.map(d =>
|
||||||
|
d.version === versionId
|
||||||
|
? { ...d, queued: false, failed: true, progress: 0 }
|
||||||
|
: d
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await notifyUser(
|
||||||
|
'Download Failed',
|
||||||
|
`The download for version ${info.displayName} has failed.`
|
||||||
|
)
|
||||||
|
await getCurrentWindow().requestUserAttention(
|
||||||
|
UserAttentionType.Critical
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setDownloadProgress(prev =>
|
||||||
|
prev.map(d =>
|
||||||
|
d.version === versionId
|
||||||
|
? {
|
||||||
|
...d,
|
||||||
|
url: data.data.url,
|
||||||
|
executable: downloadInfo.type == 0 ? info.executable : null,
|
||||||
|
hash: data.data.hash
|
||||||
|
}
|
||||||
|
: d
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await invoke<string>('download', {
|
||||||
|
url: data.data.url,
|
||||||
|
name: info.id,
|
||||||
|
hash: data.data.hash,
|
||||||
|
downloadType: downloadInfo.type,
|
||||||
|
modId: String(downloadInfo.modId ?? '')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res === '1') {
|
||||||
|
setDownloadProgress(prev => prev.filter(d => d.version !== versionId))
|
||||||
|
if (downloadInfo.type != 1) {
|
||||||
|
if (downloadInfo.type == 2) {
|
||||||
|
const currentMods =
|
||||||
|
(await versions?.get<Record<string, Record<string, number>>>(
|
||||||
|
'mods'
|
||||||
|
)) ?? {}
|
||||||
|
await versions?.set('mods', {
|
||||||
|
...currentMods,
|
||||||
|
[downloadInfo.modGame! + '-' + downloadInfo.modId!]: {
|
||||||
|
[downloadInfo.modVersion!]: Date.now()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const currentList =
|
||||||
|
(await versions?.get<Record<string, number>>('list')) ?? {}
|
||||||
|
await versions?.set('list', {
|
||||||
|
...currentList,
|
||||||
|
[versionId]: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (res == '0') {
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === info.id)
|
||||||
|
if (i === -1) return prev
|
||||||
|
if (prev[i].canceled) {
|
||||||
|
return prev.filter((_, idx) => idx !== i)
|
||||||
|
}
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
downloading: false,
|
||||||
|
paused: true,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
} else if (res == '-1') {
|
||||||
|
setDownloadProgress(prev =>
|
||||||
|
prev.map(d =>
|
||||||
|
d.version === versionId
|
||||||
|
? { ...d, queued: false, failed: true, progress: 0 }
|
||||||
|
: d
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await notifyUser(
|
||||||
|
'Download Failed',
|
||||||
|
`The download for version ${info.displayName} has failed.`
|
||||||
|
)
|
||||||
|
await getCurrentWindow().requestUserAttention(
|
||||||
|
UserAttentionType.Critical
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setDownloadQueue(prev => prev.slice(1))
|
||||||
|
setIsProcessingQueue(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
processNextDownload()
|
||||||
|
}, [
|
||||||
|
downloadQueue,
|
||||||
|
isProcessingQueue,
|
||||||
|
downloadProgress,
|
||||||
|
serverVersionList,
|
||||||
|
settings,
|
||||||
|
versions,
|
||||||
|
versionsList,
|
||||||
|
modsList
|
||||||
|
])
|
||||||
|
|
||||||
|
function getSpecialVersionsList (game?: number): GameVersion[] {
|
||||||
|
if (!serverVersionList) return []
|
||||||
|
|
||||||
|
return serverVersionList.versions
|
||||||
|
.filter(v => !Object.keys(versionsList).includes(v.id))
|
||||||
|
.filter(v => {
|
||||||
|
if (platformName == 'linux' && v.wine && !linuxUseWine) return false
|
||||||
|
|
||||||
|
if (game && v.game != game) return false
|
||||||
|
if (category != -1 && v.category != category) return false
|
||||||
|
if (downloadProgress.length != 0) {
|
||||||
|
return !downloadProgress.some(d => d.version === v.id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (b.game !== a.game) return a.game - b.game
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePopup = useCallback(() => {
|
||||||
|
if (popupMode == 0 && selectedGame && pathname === '/main/library') {
|
||||||
|
setSelectedGame(null)
|
||||||
|
setSelectedVersionList([])
|
||||||
|
} else if (viewingInfoFromDownloads) {
|
||||||
|
setViewingInfoFromDownloads(false)
|
||||||
|
setPopupMode(0)
|
||||||
|
} else if (popupMode == 3 && showModInfo) {
|
||||||
|
setShowModInfo(null)
|
||||||
|
} else if (popupMode == 4) {
|
||||||
|
setPopupMode(2)
|
||||||
|
} else {
|
||||||
|
setFadeOut(true)
|
||||||
|
setTimeout(() => setShowPopup(false), 200)
|
||||||
|
}
|
||||||
|
}, [popupMode, selectedGame, pathname, viewingInfoFromDownloads, showModInfo])
|
||||||
|
|
||||||
|
const needsRevisionUpdate = (
|
||||||
|
lastRevision: number | undefined,
|
||||||
|
version: string
|
||||||
|
) => {
|
||||||
|
if (!lastRevision) return false
|
||||||
|
return (
|
||||||
|
lastRevision > 0 &&
|
||||||
|
(versionsList == undefined ? 0 : versionsList[version]) / 1000 <=
|
||||||
|
lastRevision
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const uninstallVersion = async (
|
||||||
|
version: string,
|
||||||
|
updateList: boolean = true
|
||||||
|
) => {
|
||||||
|
if (updateList)
|
||||||
|
await versions?.set(
|
||||||
|
'list',
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(versionsList).filter(([k]) => k !== version)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
await exists(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
'game/' +
|
||||||
|
version,
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation ? undefined : BaseDirectory.AppLocalData
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await remove(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
'game/' +
|
||||||
|
version,
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation ? undefined : BaseDirectory.AppLocalData,
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const launchGame = async (versionInfo: GameVersion) => {
|
||||||
|
if (needsRevisionUpdate(versionInfo.lastRevision, versionInfo.id)) {
|
||||||
|
const answer = await ask(
|
||||||
|
'Before proceeding, if you do not want your installation directory wiped just yet, please backup the files to another directory. When you click "Yes", it will be wiped. Click "No" if you want to open the installation folder instead.',
|
||||||
|
{
|
||||||
|
title: 'Revision Update',
|
||||||
|
kind: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (answer) {
|
||||||
|
const answer2 = await ask(
|
||||||
|
'Are you sure you want to update? If you did not read the last popup, please go back and read it.',
|
||||||
|
{
|
||||||
|
title: 'Revision Update',
|
||||||
|
kind: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!answer2) return
|
||||||
|
|
||||||
|
//open downloads popup
|
||||||
|
setPopupMode(1)
|
||||||
|
setShowPopup(true)
|
||||||
|
setFadeOut(false)
|
||||||
|
|
||||||
|
//uninstall
|
||||||
|
await uninstallVersion(versionInfo.id)
|
||||||
|
|
||||||
|
//reinstall
|
||||||
|
setSelectedVersionList([versionInfo.id])
|
||||||
|
await downloadVersions([
|
||||||
|
{
|
||||||
|
id: versionInfo.id,
|
||||||
|
type: 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
await openFolder(versionInfo.id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await invoke('launch_game', {
|
||||||
|
name: versionInfo.id,
|
||||||
|
executable: versionInfo.executable,
|
||||||
|
displayName: versionInfo.displayName,
|
||||||
|
useWine: !!(platform() == 'linux' && versionInfo.wine && linuxUseWine),
|
||||||
|
wineCommand: linuxWineCommand
|
||||||
|
})
|
||||||
|
await versions?.set('launches', {
|
||||||
|
...launchesList,
|
||||||
|
[versionInfo.game]: {
|
||||||
|
...launchesList[versionInfo.game],
|
||||||
|
[versionInfo.id]: Date.now()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisteners: (() => void)[] = []
|
||||||
|
|
||||||
|
const setupListeners = async () => {
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('download-size', event => {
|
||||||
|
const [displayName, sizeStr] = event.payload.split(':')
|
||||||
|
const size = Number(sizeStr)
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = { ...copy[i], size }
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('download-progress', async event => {
|
||||||
|
const [displayName, progStr, totalSizeStr, speedStr, etaSecsStr] =
|
||||||
|
event.payload.split(':')
|
||||||
|
const prog = Number(progStr)
|
||||||
|
const progBytes = Number(totalSizeStr)
|
||||||
|
const speed = Number(speedStr)
|
||||||
|
const etaSecs = Number(etaSecsStr)
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
progress: prog,
|
||||||
|
progressBytes: progBytes,
|
||||||
|
speed,
|
||||||
|
etaSecs
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('download-hash-checking', event => {
|
||||||
|
const displayName = event.payload
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = { ...copy[i], hash_checking: true, downloading: false }
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('unzip-start', event => {
|
||||||
|
const displayName = event.payload
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = { ...copy[i], hash_checking: false, unzipping: true }
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('unzip-progress', async event => {
|
||||||
|
const [displayName, unzippedStr, unzipTotalStr] =
|
||||||
|
event.payload.split(':')
|
||||||
|
const unzipped = Number(unzippedStr)
|
||||||
|
const unzipTotal = Number(unzipTotalStr)
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
unzipped,
|
||||||
|
unzipTotal
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('download-start', event => {
|
||||||
|
const displayName = event.payload
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
downloading: true,
|
||||||
|
paused: false,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('download-stop', event => {
|
||||||
|
const displayName = event.payload
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = { ...copy[i], downloading: false }
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupListeners()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisteners.forEach(unlisten => unlisten())
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
setPlatformName(platform())
|
||||||
|
const client = await app.getVersion()
|
||||||
|
setVersion(client)
|
||||||
|
const legacyOptions = {
|
||||||
|
baseDir: BaseDirectory.AppLocalData
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
platform() == 'windows' &&
|
||||||
|
(await exists('versions.json', legacyOptions))
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
await exists('versions.json', {
|
||||||
|
baseDir: BaseDirectory.AppConfig
|
||||||
|
})
|
||||||
|
)
|
||||||
|
await remove('versions.json', {
|
||||||
|
baseDir: BaseDirectory.AppConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
await copyFile('versions.json', 'versions.json', {
|
||||||
|
fromPathBaseDir: BaseDirectory.AppLocalData,
|
||||||
|
toPathBaseDir: BaseDirectory.AppConfig
|
||||||
|
})
|
||||||
|
await remove('versions.json', legacyOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsLocal = await load('settings.json', {
|
||||||
|
autoSave: true,
|
||||||
|
defaults: {
|
||||||
|
version: client,
|
||||||
|
sidebarAlwaysShowGames: true,
|
||||||
|
linuxUseWine: false,
|
||||||
|
linuxWineCommand: 'wine %path%',
|
||||||
|
theme: 'dark',
|
||||||
|
customDataLocation: '',
|
||||||
|
developerMode: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const versionsLocal = await load('versions.json', {
|
||||||
|
autoSave: true,
|
||||||
|
defaults: {
|
||||||
|
version: client,
|
||||||
|
list: {},
|
||||||
|
mods: {},
|
||||||
|
launches: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const accountLocal = await load('account.json', {
|
||||||
|
autoSave: true,
|
||||||
|
defaults: {
|
||||||
|
version: client,
|
||||||
|
session: null,
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
admin: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const cfgVer = (await settingsLocal.get<string>('version')) ?? client
|
||||||
|
const versVer = (await versionsLocal.get<string>('version')) ?? client
|
||||||
|
if (semver.gt(cfgVer, client) || semver.gt(versVer, client)) {
|
||||||
|
exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await settingsLocal.set('version', client)
|
||||||
|
await versionsLocal.set('version', client)
|
||||||
|
await accountLocal.set('version', client)
|
||||||
|
|
||||||
|
if (await exists('config.json', legacyOptions)) {
|
||||||
|
const config = await readTextFile('config.json', legacyOptions)
|
||||||
|
const raw = JSON.parse(config)
|
||||||
|
if (
|
||||||
|
raw.settings &&
|
||||||
|
raw.settings.theme &&
|
||||||
|
semver.satisfies(raw.version, '>=1.0.0 <=1.5.4')
|
||||||
|
) {
|
||||||
|
const parsed = Number(raw.settings.theme)
|
||||||
|
if (parsed == 3) raw.settings.theme = 2
|
||||||
|
if (parsed == 4) raw.settings.theme = 3
|
||||||
|
else if (parsed != 0 && parsed != 1) raw.settings.theme = 0
|
||||||
|
}
|
||||||
|
if (raw.settings.allowNotifications)
|
||||||
|
await settingsLocal?.set(
|
||||||
|
'notificationsAllowed',
|
||||||
|
Boolean(raw.settings.allowNotifications)
|
||||||
|
)
|
||||||
|
if (raw.settings.alwaysShowGamesInSidebar)
|
||||||
|
await settingsLocal?.set(
|
||||||
|
'sidebarAlwaysShowGames',
|
||||||
|
Boolean(raw.settings.alwaysShowGamesInSidebar)
|
||||||
|
)
|
||||||
|
if (raw.settings.useWineOnUnixWhenNeeded)
|
||||||
|
await settingsLocal?.set(
|
||||||
|
'linuxUseWine',
|
||||||
|
Boolean(raw.settings.useWineOnUnixWhenNeeded)
|
||||||
|
)
|
||||||
|
if (raw.settings.wineOnUnixCommand)
|
||||||
|
await settingsLocal?.set(
|
||||||
|
'linuxWineCommand',
|
||||||
|
String(raw.settings.wineOnUnixCommand)
|
||||||
|
)
|
||||||
|
if (raw.settings.theme) {
|
||||||
|
const tempTheme = (() => {
|
||||||
|
switch (Number(raw.settings.theme)) {
|
||||||
|
case 1:
|
||||||
|
return 'red'
|
||||||
|
case 2:
|
||||||
|
return 'blue'
|
||||||
|
case 3:
|
||||||
|
return 'purple'
|
||||||
|
default:
|
||||||
|
return 'dark'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
await settingsLocal?.set('theme', tempTheme)
|
||||||
|
}
|
||||||
|
await remove('config.json', legacyOptions)
|
||||||
|
}
|
||||||
|
setSettings(settingsLocal)
|
||||||
|
setVersions(versionsLocal)
|
||||||
|
setAccount(accountLocal)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://games.lncvrt.xyz/api/launcher/versions',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Requester: 'LncvrtGamesLauncherClient',
|
||||||
|
ClientVersion: await app.getVersion(),
|
||||||
|
ClientPlatform: platform() + '-' + arch(),
|
||||||
|
Authorization: (await accountLocal.get('session')) ?? ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
|
const data = await response.json()
|
||||||
|
if (
|
||||||
|
response.status == 200 &&
|
||||||
|
(await verifySignature(JSON.stringify(data), signature))
|
||||||
|
) {
|
||||||
|
setServerVersionList(data)
|
||||||
|
} else {
|
||||||
|
await message(
|
||||||
|
'Failed to get version manifest, please try again later.',
|
||||||
|
{ title: 'Error fetching launcher manifest', kind: 'error' }
|
||||||
|
)
|
||||||
|
await exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await message(
|
||||||
|
'Failed to get version manifest, please try again later.',
|
||||||
|
{ title: 'Error fetching launcher manifest', kind: 'error' }
|
||||||
|
)
|
||||||
|
await exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await isPermissionGranted())) {
|
||||||
|
await requestPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
})()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (revisionCheck.current) return
|
||||||
|
if (!serverVersionList) return
|
||||||
|
revisionCheck.current = true
|
||||||
|
;(async () => {
|
||||||
|
for (const [key, value] of Object.entries(versionsList)) {
|
||||||
|
const verInfo = serverVersionList.versions.find(item => item.id === key)
|
||||||
|
|
||||||
|
if (
|
||||||
|
verInfo &&
|
||||||
|
verInfo.lastRevision > 0 &&
|
||||||
|
value / 1000 <= verInfo.lastRevision
|
||||||
|
) {
|
||||||
|
await invoke('remove_stale_executable', {
|
||||||
|
version: key,
|
||||||
|
executable: verInfo.executable
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [serverVersionList, versionsList])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
downloadProgress.length === 0 &&
|
||||||
|
downloadQueue.length === 0 &&
|
||||||
|
showPopup &&
|
||||||
|
popupMode === 1
|
||||||
|
) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await notifyUser('Downloads Complete', 'All downloads finished!')
|
||||||
|
await getCurrentWindow().requestUserAttention(
|
||||||
|
UserAttentionType.Informational
|
||||||
|
)
|
||||||
|
closePopup()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}, [downloadProgress, downloadQueue, showPopup, popupMode, closePopup])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!settings || !versions) return
|
||||||
|
const unlisteners: Promise<() => void>[] = []
|
||||||
|
|
||||||
|
function watchSettings<T> (key: string, fn: (v: T | undefined) => void) {
|
||||||
|
settings!.get<T>(key).then(fn)
|
||||||
|
unlisteners.push(settings!.onKeyChange<T>(key, fn))
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchVersions<T> (key: string, fn: (v: T | undefined) => void) {
|
||||||
|
versions!.get<T>(key).then(fn)
|
||||||
|
unlisteners.push(versions!.onKeyChange<T>(key, fn))
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchAccount<T> (key: string, fn: (v: T | undefined) => void) {
|
||||||
|
account!.get<T>(key).then(fn)
|
||||||
|
unlisteners.push(account!.onKeyChange<T>(key, fn))
|
||||||
|
}
|
||||||
|
|
||||||
|
watchSettings<string>('theme', v => setTheme(v ?? 'dark'))
|
||||||
|
watchSettings<boolean>('sidebarShowGames', v =>
|
||||||
|
setSidebarShowGames(v ?? true)
|
||||||
|
)
|
||||||
|
watchSettings<boolean>('linuxUseWine', v => setLinuxUseWine(v ?? false))
|
||||||
|
watchSettings<string>('linuxWineCommand', v =>
|
||||||
|
setLinuxWineCommand(v ?? 'wine %path%')
|
||||||
|
)
|
||||||
|
watchSettings<string>('customDataLocation', v =>
|
||||||
|
setCustomDataLocation(v ?? '')
|
||||||
|
)
|
||||||
|
watchSettings<boolean>('developerMode', v => setDeveloperMode(v ?? false))
|
||||||
|
|
||||||
|
watchVersions<Record<string, number>>('list', v => setVersionsList(v ?? {}))
|
||||||
|
watchVersions<Record<string, Record<string, number>>>('mods', v =>
|
||||||
|
setModsList(v ?? {})
|
||||||
|
)
|
||||||
|
watchVersions<Record<string, Record<string, number>>>('launches', v =>
|
||||||
|
setLaunchesList(v ?? {})
|
||||||
|
)
|
||||||
|
|
||||||
|
watchAccount<string | null>('session', v => setAccountSession(v ?? null))
|
||||||
|
watchAccount<string | null>('id', v => setAccountId(v ?? null))
|
||||||
|
watchAccount<string | null>('name', v => setAccountName(v ?? null))
|
||||||
|
watchAccount<boolean>('admin', v => setAccountAdmin(v ?? false))
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisteners.forEach(u => u.then(fn => fn()))
|
||||||
|
}
|
||||||
|
}, [settings, versions, account])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<html lang='en' className={roboto.className}>
|
||||||
|
<body className={theme + '-theme'}>
|
||||||
|
{!loading && (
|
||||||
|
<GlobalProvider
|
||||||
|
value={{
|
||||||
|
serverVersionList,
|
||||||
|
setServerVersionList,
|
||||||
|
selectedVersionList,
|
||||||
|
setSelectedVersionList,
|
||||||
|
downloadProgress,
|
||||||
|
setDownloadProgress,
|
||||||
|
showPopup,
|
||||||
|
setShowPopup,
|
||||||
|
popupMode,
|
||||||
|
setPopupMode,
|
||||||
|
fadeOut,
|
||||||
|
setFadeOut,
|
||||||
|
managingVersion,
|
||||||
|
setManagingVersion,
|
||||||
|
setSelectedGame,
|
||||||
|
viewingInfoFromDownloads,
|
||||||
|
version,
|
||||||
|
downloadVersions,
|
||||||
|
category,
|
||||||
|
setCategory,
|
||||||
|
downloadQueue,
|
||||||
|
setDownloadQueue,
|
||||||
|
closePopup,
|
||||||
|
selectedGame,
|
||||||
|
setViewingInfoFromDownloads,
|
||||||
|
showModInfo,
|
||||||
|
setShowModInfo,
|
||||||
|
getSpecialVersionsList,
|
||||||
|
settings,
|
||||||
|
versions,
|
||||||
|
account,
|
||||||
|
sidebarShowGames,
|
||||||
|
linuxUseWine,
|
||||||
|
linuxWineCommand,
|
||||||
|
theme,
|
||||||
|
customDataLocation,
|
||||||
|
developerMode,
|
||||||
|
versionsList,
|
||||||
|
modsList,
|
||||||
|
launchesList,
|
||||||
|
accountSession,
|
||||||
|
accountId,
|
||||||
|
accountName,
|
||||||
|
accountAdmin,
|
||||||
|
managingGame,
|
||||||
|
setManagingGame,
|
||||||
|
needsRevisionUpdate,
|
||||||
|
uninstallVersion,
|
||||||
|
launchGame
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (showPopup && e.key === 'Escape') closePopup()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sidebar />
|
||||||
|
{platformName == 'windows' && (
|
||||||
|
<div className='relative z-2 ml-59.75 w-[calc(100vw-239px)] border-b border-b-(--col3) h-8.25 bg-(--col1)' />
|
||||||
|
)}
|
||||||
|
<div className='relative z-0'>
|
||||||
|
<main className='ml-60'>{children}</main>
|
||||||
|
</div>
|
||||||
|
{showPopup && (
|
||||||
|
<div
|
||||||
|
className={`popup-overlay ${fadeOut ? 'fade-out' : ''}`}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
if (showModInfo) setShowModInfo(null)
|
||||||
|
if (viewingInfoFromDownloads) {
|
||||||
|
setPopupMode(0)
|
||||||
|
setViewingInfoFromDownloads(false)
|
||||||
|
setManagingVersion(null)
|
||||||
|
setSelectedGame(null)
|
||||||
|
}
|
||||||
|
setFadeOut(true)
|
||||||
|
setTimeout(() => setShowPopup(false), 200)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='popup-box'>
|
||||||
|
<button
|
||||||
|
className='popup-top-button left-2 btntheme1'
|
||||||
|
onClick={() => closePopup()}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
(popupMode == 0 &&
|
||||||
|
selectedGame &&
|
||||||
|
pathname === '/main/library') ||
|
||||||
|
viewingInfoFromDownloads ||
|
||||||
|
(popupMode == 3 && showModInfo) ||
|
||||||
|
popupMode == 4
|
||||||
|
? faChevronLeft
|
||||||
|
: faXmark
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{popupMode === 0 && selectedGame ? (
|
||||||
|
<VersionsDownloadPopup />
|
||||||
|
) : popupMode === 0 && !selectedGame ? (
|
||||||
|
<GamesDownloadPopup />
|
||||||
|
) : popupMode === 1 ? (
|
||||||
|
<DownloadsPopup />
|
||||||
|
) : popupMode === 2 ? (
|
||||||
|
<ManageVersionPopup />
|
||||||
|
) : popupMode === 3 ? (
|
||||||
|
<ModDownloadsPopup />
|
||||||
|
) : popupMode === 4 ? (
|
||||||
|
<VersionChangelogPopup />
|
||||||
|
) : popupMode === 5 ? (
|
||||||
|
<ManageGamePopup />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</GlobalProvider>
|
||||||
|
)}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import {
|
||||||
|
faCheck,
|
||||||
|
faShieldHalved,
|
||||||
|
faWarning
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function LibraryPage () {
|
||||||
|
const {
|
||||||
|
versionsList,
|
||||||
|
serverVersionList,
|
||||||
|
showPopup,
|
||||||
|
setShowPopup,
|
||||||
|
setPopupMode,
|
||||||
|
setFadeOut,
|
||||||
|
setSelectedVersionList,
|
||||||
|
setSelectedGame,
|
||||||
|
setCategory,
|
||||||
|
setManagingGame,
|
||||||
|
developerMode
|
||||||
|
} = useGlobal()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showPopup) return
|
||||||
|
setSelectedVersionList([])
|
||||||
|
}, [setSelectedVersionList, showPopup])
|
||||||
|
|
||||||
|
const filteredGames =
|
||||||
|
serverVersionList?.games.filter(g =>
|
||||||
|
serverVersionList.versions
|
||||||
|
.filter(v => v.game === g.id)
|
||||||
|
.some(v => Object.keys(versionsList).includes(v.id))
|
||||||
|
) ?? []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mx-4 mt-4'>
|
||||||
|
<div className='flex justify-between items-center mb-4'>
|
||||||
|
<p className='text-3xl'>Games</p>
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedGame(null)
|
||||||
|
setPopupMode(0)
|
||||||
|
setShowPopup(true)
|
||||||
|
setFadeOut(false)
|
||||||
|
}}
|
||||||
|
title='Click to download more games.'
|
||||||
|
>
|
||||||
|
Download game
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`main-container main-flex ${
|
||||||
|
platform() == 'windows'
|
||||||
|
? 'h-[calc(100vh-116px)]'
|
||||||
|
: 'h-[calc(100vh-84px)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{filteredGames.length == 0 ? (
|
||||||
|
<div className='flex justify-center items-center h-full'>
|
||||||
|
<p className='text-3xl'>No games installed</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredGames.map(i => (
|
||||||
|
<div
|
||||||
|
key={i.id}
|
||||||
|
className='main-entry'
|
||||||
|
title='Click to view game installs.'
|
||||||
|
onClick={() => {
|
||||||
|
setCategory(-1)
|
||||||
|
router.push('/main/game?id=' + i.id)
|
||||||
|
}}
|
||||||
|
onContextMenu={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
setManagingGame(i.id)
|
||||||
|
setPopupMode(5)
|
||||||
|
setShowPopup(true)
|
||||||
|
setFadeOut(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='h-18 w-screen relative'>
|
||||||
|
{developerMode && (
|
||||||
|
<p className='absolute bottom-0 right-0 text-sm'>{i.id}</p>
|
||||||
|
)}
|
||||||
|
<p className='text-2xl'>{i.name}</p>
|
||||||
|
|
||||||
|
<div className='flex gap-2 absolute left-0 bottom-0'>
|
||||||
|
<div
|
||||||
|
className='entry-info-item'
|
||||||
|
title='The amount of versions installed of this game in installed/installable format.'
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{(() => {
|
||||||
|
const gameVersions = (
|
||||||
|
serverVersionList?.versions ?? []
|
||||||
|
).filter(v => v.game === i.id)
|
||||||
|
const installed = gameVersions.filter(v =>
|
||||||
|
Object.keys(versionsList).includes(v.id)
|
||||||
|
).length
|
||||||
|
return gameVersions.length
|
||||||
|
? `${installed}/${gameVersions.length}`
|
||||||
|
: 'N/A'
|
||||||
|
})()}{' '}
|
||||||
|
versions installed
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{i.official ? (
|
||||||
|
<div
|
||||||
|
className='entry-info-item'
|
||||||
|
title='This game is official.'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
|
||||||
|
<p>Official</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className='entry-info-item'
|
||||||
|
title={
|
||||||
|
i.verified
|
||||||
|
? 'This game is verified to be safe'
|
||||||
|
: 'This game is not verified to be save. Proceed with caution.'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={i.verified ? faShieldHalved : faWarning}
|
||||||
|
color={i.verified ? '#19c84b' : '#ffc800'}
|
||||||
|
/>
|
||||||
|
<p>{i.verified ? 'Verified' : 'Unverified'}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||||
|
import type { Swiper as SwiperType } from 'swiper'
|
||||||
|
import { Navigation } from 'swiper/modules'
|
||||||
|
import 'swiper/css'
|
||||||
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import {
|
||||||
|
faChevronLeft,
|
||||||
|
faChevronRight,
|
||||||
|
faEllipsis,
|
||||||
|
faPlay
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export default function HomePage () {
|
||||||
|
const {
|
||||||
|
launchesList,
|
||||||
|
serverVersionList,
|
||||||
|
versionsList,
|
||||||
|
accountName,
|
||||||
|
launchGame,
|
||||||
|
setManagingVersion,
|
||||||
|
setPopupMode,
|
||||||
|
setShowPopup,
|
||||||
|
setFadeOut
|
||||||
|
} = useGlobal()
|
||||||
|
|
||||||
|
const filteredLaunchesList = Object.fromEntries(
|
||||||
|
Object.entries(launchesList)
|
||||||
|
.flatMap(([gameId, launches]) => {
|
||||||
|
const entries = Object.entries(launches).filter(([version]) => {
|
||||||
|
return (
|
||||||
|
serverVersionList?.versions.find(v => version == v.id) &&
|
||||||
|
Object.entries(versionsList).find(([v]) => v == version)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (entries.length === 0) return []
|
||||||
|
|
||||||
|
const [version, timestamp] = entries.reduce((latest, current) =>
|
||||||
|
current[1] > latest[1] ? current : latest
|
||||||
|
)
|
||||||
|
|
||||||
|
return [[`${gameId}-${version}`, timestamp] as const]
|
||||||
|
})
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
)
|
||||||
|
|
||||||
|
const swiperRef = useRef<SwiperType | null>(null)
|
||||||
|
const prevRef = useRef<HTMLButtonElement>(null)
|
||||||
|
const nextRef = useRef<HTMLButtonElement>(null)
|
||||||
|
const [isLocked, setIsLocked] = useState(false)
|
||||||
|
|
||||||
|
const [recentSlidesPerView, setRecentSlidesPerView] = useState(1)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const update = () => {
|
||||||
|
setRecentSlidesPerView(Math.floor(window.innerWidth / 400))
|
||||||
|
}
|
||||||
|
|
||||||
|
update()
|
||||||
|
window.addEventListener('resize', update)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', update)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mx-4 mt-4'>
|
||||||
|
<div className='flex flex-col gap-1 mb-4'>
|
||||||
|
<p className='text-3xl'>Home</p>
|
||||||
|
<p className='text-gray-300'>Welcome back, {accountName ?? 'User'}!</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className='text-2xl'>Jump back in</p>
|
||||||
|
{Object.entries(filteredLaunchesList).length == 0 ? (
|
||||||
|
<p className='text-gray-300'>You have no recently launched games</p>
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col items-center gap-2'>
|
||||||
|
<Swiper
|
||||||
|
modules={[Navigation]}
|
||||||
|
spaceBetween={16}
|
||||||
|
slidesPerView={Math.min(
|
||||||
|
recentSlidesPerView,
|
||||||
|
Object.entries(filteredLaunchesList).length
|
||||||
|
)}
|
||||||
|
autoHeight={false}
|
||||||
|
className='w-full'
|
||||||
|
onSwiper={swiper => {
|
||||||
|
swiperRef.current = swiper
|
||||||
|
setIsLocked(swiper.isLocked)
|
||||||
|
}}
|
||||||
|
onResize={swiper => {
|
||||||
|
setIsLocked(swiper.isLocked)
|
||||||
|
}}
|
||||||
|
onUpdate={swiper => {
|
||||||
|
setIsLocked(swiper.isLocked)
|
||||||
|
}}
|
||||||
|
onBeforeInit={swiper => {
|
||||||
|
if (
|
||||||
|
swiper.params.navigation &&
|
||||||
|
typeof swiper.params.navigation !== 'boolean'
|
||||||
|
) {
|
||||||
|
swiper.params.navigation.prevEl = prevRef.current
|
||||||
|
swiper.params.navigation.nextEl = nextRef.current
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(filteredLaunchesList).map(([key, timestamp]) => {
|
||||||
|
const versionInfo = serverVersionList?.versions.find(
|
||||||
|
v => v.id == key.split('-').slice(1).join('-')
|
||||||
|
)
|
||||||
|
if (!versionInfo) return
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwiperSlide
|
||||||
|
key={key}
|
||||||
|
className='flex flex-col gap-2 bg-(--col1) border border-(--col3) rounded-lg px-3 py-2'
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className='truncate text-2xl'
|
||||||
|
title={versionInfo?.displayName}
|
||||||
|
>
|
||||||
|
{versionInfo?.displayName}
|
||||||
|
</p>
|
||||||
|
<p className='truncate text-gray-300'>
|
||||||
|
Last played{' '}
|
||||||
|
<span title={new Date(timestamp).toString()}>
|
||||||
|
{formatDistanceToNow(new Date(timestamp), {
|
||||||
|
addSuffix: true
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div className='flex flex-row gap-2 my-1'>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faPlay}
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={async () => await launchGame(versionInfo)}
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faEllipsis}
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={() => {
|
||||||
|
setManagingVersion(versionInfo.id)
|
||||||
|
setPopupMode(2)
|
||||||
|
setShowPopup(true)
|
||||||
|
setFadeOut(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Swiper>
|
||||||
|
{!isLocked && (
|
||||||
|
<div className='flex flex-row gap-2'>
|
||||||
|
<button
|
||||||
|
ref={prevRef}
|
||||||
|
onClick={() => swiperRef.current?.slidePrev()}
|
||||||
|
className='button btntheme1 w-8 h-8 flex justify-center items-center'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
ref={nextRef}
|
||||||
|
onClick={() => swiperRef.current?.slideNext()}
|
||||||
|
className='button btntheme1 w-8 h-8 flex justify-center items-center'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faChevronRight} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Setting } from '@/components/Setting'
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { copyToClipboard } from '@/lib/clipboard'
|
||||||
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
|
import { confirm, message, open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import Dropdown from '@/components/Dropdown'
|
||||||
|
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
|
import { exit, relaunch } from '@tauri-apps/plugin-process'
|
||||||
|
import { BaseDirectory, exists, readDir, remove } from '@tauri-apps/plugin-fs'
|
||||||
|
import { openPath } from '@tauri-apps/plugin-opener'
|
||||||
|
import { appLocalDataDir } from '@tauri-apps/api/path'
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
|
|
||||||
|
export default function SettingsPage () {
|
||||||
|
const {
|
||||||
|
settings,
|
||||||
|
versions,
|
||||||
|
account,
|
||||||
|
version,
|
||||||
|
versionsList,
|
||||||
|
linuxUseWine,
|
||||||
|
linuxWineCommand,
|
||||||
|
theme,
|
||||||
|
customDataLocation,
|
||||||
|
downloadProgress
|
||||||
|
} = useGlobal()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className='text-3xl ml-4 mt-4'>Settings</p>
|
||||||
|
<div className='flex flex-row p-4 gap-4'>
|
||||||
|
<div className='bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
||||||
|
<Setting
|
||||||
|
label='Use wine if needed'
|
||||||
|
value={linuxUseWine}
|
||||||
|
onChange={async () => {
|
||||||
|
await settings?.set('linuxUseWine', !linuxUseWine)
|
||||||
|
}}
|
||||||
|
className={platform() == 'linux' ? '' : 'hidden'}
|
||||||
|
/>
|
||||||
|
{platform() == 'linux' && linuxUseWine && (
|
||||||
|
<>
|
||||||
|
<p>Wine Command:</p>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={linuxWineCommand}
|
||||||
|
onChange={async e =>
|
||||||
|
await settings?.set('linuxWineCommand', e.target.value)
|
||||||
|
}
|
||||||
|
className='input-field my-1'
|
||||||
|
></input>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Setting
|
||||||
|
label='Use custom data location'
|
||||||
|
value={!!customDataLocation}
|
||||||
|
disabled={downloadProgress.length > 0}
|
||||||
|
onChange={async () => {
|
||||||
|
if (customDataLocation) {
|
||||||
|
try {
|
||||||
|
const confirmation = await confirm(
|
||||||
|
'Data will be moved back to the original location. The launcher will restart when completed and will not be visible until the restart.',
|
||||||
|
{ title: 'Moving data', kind: 'warning' }
|
||||||
|
)
|
||||||
|
if (!confirmation) return
|
||||||
|
|
||||||
|
const window = getCurrentWindow()
|
||||||
|
await window.hide()
|
||||||
|
await invoke('move_game_data', { destination: '' })
|
||||||
|
await settings?.set('customDataLocation', '')
|
||||||
|
await relaunch()
|
||||||
|
} catch {
|
||||||
|
await exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const selected = await open({
|
||||||
|
directory: true,
|
||||||
|
title: 'Select data location'
|
||||||
|
})
|
||||||
|
if (!selected || typeof selected !== 'string') return
|
||||||
|
try {
|
||||||
|
const confirmation = await confirm(
|
||||||
|
`Data will be moved to the directory "${selected}". The launcher will restart when completed and will not be visible until the restart.`,
|
||||||
|
{ title: 'Moving data', kind: 'warning' }
|
||||||
|
)
|
||||||
|
if (!confirmation) return
|
||||||
|
|
||||||
|
const window = getCurrentWindow()
|
||||||
|
await window.hide()
|
||||||
|
await invoke('move_game_data', { destination: selected })
|
||||||
|
await settings?.set('customDataLocation', selected)
|
||||||
|
await relaunch()
|
||||||
|
} catch {
|
||||||
|
await exit(1)
|
||||||
|
await exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title='Move game data to a custom storage location, like a different disk.'
|
||||||
|
/>
|
||||||
|
{customDataLocation && (
|
||||||
|
<p className='text-sm opacity-50 ml-6 -mt-1 mb-2'>
|
||||||
|
{customDataLocation}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className='flex flex-row gap-2 items-center'>
|
||||||
|
<p
|
||||||
|
title='The theme you want the launcher to use.'
|
||||||
|
className='text-lg'
|
||||||
|
>
|
||||||
|
Theme:
|
||||||
|
</p>
|
||||||
|
<Dropdown
|
||||||
|
value={theme}
|
||||||
|
options={[
|
||||||
|
{ label: 'Dark', value: 'dark' },
|
||||||
|
{ label: 'Red', value: 'red' },
|
||||||
|
{ label: 'Blue', value: 'blue' },
|
||||||
|
{ label: 'Purple', value: 'purple' }
|
||||||
|
]}
|
||||||
|
onChange={async val => {
|
||||||
|
await settings?.set('theme', val)
|
||||||
|
|
||||||
|
const window = await WebviewWindow.getByLabel(
|
||||||
|
'berrydashleaderboards'
|
||||||
|
)
|
||||||
|
if (window) {
|
||||||
|
await window.close()
|
||||||
|
|
||||||
|
new WebviewWindow('berrydashleaderboards', {
|
||||||
|
title: 'Berry Dash Leaderboards',
|
||||||
|
url:
|
||||||
|
'https://games.lncvrt.xyz/game/berry-dash/leaderboards?launcher=1&theme=' +
|
||||||
|
val,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
resizable: false,
|
||||||
|
maximizable: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit flex flex-col gap-2'>
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
title='Restarts the launcher. This may help if the launcher is having issues.'
|
||||||
|
onClick={async () => await relaunch()}
|
||||||
|
>
|
||||||
|
Restart launcher
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
title='Completely reset the launcher, delete games, logout of account & reset settings.'
|
||||||
|
onClick={async () => {
|
||||||
|
if (
|
||||||
|
await exists(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') + 'game',
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await remove(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') + 'game',
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData,
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
await exists(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
'downloads',
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await remove(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
'downloads',
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData,
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await settings?.clear()
|
||||||
|
await versions?.clear()
|
||||||
|
await account?.clear()
|
||||||
|
await relaunch()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset launcher
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
title='If you pause or cancel a download, it will be removed here.'
|
||||||
|
onClick={async () => {
|
||||||
|
if (
|
||||||
|
await exists(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
'downloads',
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await remove(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
'downloads',
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData,
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete download cache
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
title='If you had a game that was downloaded before it was taken down after you downloaded it, it will get removed here.'
|
||||||
|
onClick={async () => {
|
||||||
|
const items = await readDir(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') + 'game',
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const removed: string[] = []
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.isFile || item.isSymlink) continue
|
||||||
|
if (
|
||||||
|
!Object.entries(versionsList).find(v => v[0] == item.name)
|
||||||
|
) {
|
||||||
|
removed.push(item.name)
|
||||||
|
await remove(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
'game/' +
|
||||||
|
item.name,
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData,
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await message(
|
||||||
|
'Removed the following game items: ' +
|
||||||
|
(removed.length == 0 ? '(none)' : removed.join(', ')),
|
||||||
|
{ title: 'Sucessfully cleaned games folder', kind: 'info' }
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete invalid/unavailable games
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
title='This will open the directory with the app data.'
|
||||||
|
onClick={async () => await openPath(await appLocalDataDir())}
|
||||||
|
>
|
||||||
|
Open app data folder
|
||||||
|
</button>
|
||||||
|
{customDataLocation && (
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
title='This will open the directory with the custom app data.'
|
||||||
|
onClick={async () => await openPath(customDataLocation)}
|
||||||
|
>
|
||||||
|
Open custom app data folder
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<p className='text-center'>Hover buttons for descriptions</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className='fixed bottom-1.5 right-1.5 rounded-md cursor-pointer px-1 border z-100 transition-colors btntheme1'
|
||||||
|
onClick={async () => await copyToClipboard(`v${version}`)}
|
||||||
|
title='The current launcher version.'
|
||||||
|
>
|
||||||
|
v{version}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import './Installs.css'
|
|
||||||
import { useGlobal } from './GlobalProvider'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import {
|
|
||||||
faCheck,
|
|
||||||
faShieldHalved,
|
|
||||||
faWarning
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
|
||||||
|
|
||||||
export default function Installs () {
|
|
||||||
const {
|
|
||||||
downloadProgress,
|
|
||||||
showPopup,
|
|
||||||
setShowPopup,
|
|
||||||
setPopupMode,
|
|
||||||
setFadeOut,
|
|
||||||
setSelectedVersionList,
|
|
||||||
downloadedVersionsConfig,
|
|
||||||
normalConfig,
|
|
||||||
setSelectedGame,
|
|
||||||
getListOfGames,
|
|
||||||
getVersionsAmountData
|
|
||||||
} = useGlobal()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showPopup) return
|
|
||||||
setSelectedVersionList([])
|
|
||||||
}, [normalConfig, setSelectedVersionList, showPopup])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='mx-4 mt-4'>
|
|
||||||
<div className='flex justify-between items-center mb-4'>
|
|
||||||
<p className='text-3xl'>Games</p>
|
|
||||||
<button
|
|
||||||
className='button text-3xl'
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedGame(null)
|
|
||||||
setPopupMode(0)
|
|
||||||
setShowPopup(true)
|
|
||||||
setFadeOut(false)
|
|
||||||
}}
|
|
||||||
disabled={downloadProgress.length != 0}
|
|
||||||
>
|
|
||||||
Download game
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className='downloads-container'>
|
|
||||||
<div
|
|
||||||
className={`downloads-scroll ${
|
|
||||||
platform() === 'windows'
|
|
||||||
? 'h-[calc(100vh-116px)]'
|
|
||||||
: 'h-[calc(100vh-84px)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{downloadedVersionsConfig && downloadedVersionsConfig.list.length ? (
|
|
||||||
getListOfGames()
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a.id - b.id
|
|
||||||
})
|
|
||||||
.map(i => (
|
|
||||||
<div key={i.id} className='downloads-entry'>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<p className='text-2xl'>{i.name}</p>
|
|
||||||
<div className='flex gap-2'>
|
|
||||||
<div className='entry-info-item'>
|
|
||||||
<p>
|
|
||||||
{(() => {
|
|
||||||
const data = getVersionsAmountData(i.id)
|
|
||||||
if (!data) return 'N/A'
|
|
||||||
return `${data.installed}/${data.total}`
|
|
||||||
})()}{' '}
|
|
||||||
versions installed
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='entry-info-item' hidden={!i.official}>
|
|
||||||
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
|
|
||||||
<p>Official</p>
|
|
||||||
</div>
|
|
||||||
<div className='entry-info-item' hidden={i.official}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={i.verified ? faShieldHalved : faWarning}
|
|
||||||
color={i.verified ? '#19c84b' : '#ffc800'}
|
|
||||||
/>
|
|
||||||
<p>{i.verified ? 'Verified' : 'Unverified'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-row items-center gap-2'>
|
|
||||||
<Link className='button' href={'/game?id=' + i.id}>
|
|
||||||
Installs
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className='flex justify-center items-center h-full'>
|
|
||||||
<p className='text-3xl'>No games installed</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { Setting } from '../componets/Setting'
|
|
||||||
import { writeNormalConfig } from '../util/BazookaManager'
|
|
||||||
import { useGlobal } from '../GlobalProvider'
|
|
||||||
|
|
||||||
export default function Settings () {
|
|
||||||
const [allowNotifications, setAllowNotifications] = useState(false)
|
|
||||||
const [alwaysShowGamesInSidebar, setAlwaysShowGamesInSidebar] =
|
|
||||||
useState(false)
|
|
||||||
const [theme, setTheme] = useState(0)
|
|
||||||
|
|
||||||
const [loaded, setLoaded] = useState(false)
|
|
||||||
const { normalConfig, setNormalConfig } = useGlobal()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
;(async () => {
|
|
||||||
while (normalConfig != null) {
|
|
||||||
setAllowNotifications(normalConfig.settings.allowNotifications)
|
|
||||||
setAlwaysShowGamesInSidebar(
|
|
||||||
normalConfig.settings.alwaysShowGamesInSidebar
|
|
||||||
)
|
|
||||||
setTheme(normalConfig.settings.theme)
|
|
||||||
setLoaded(true)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}, [normalConfig])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p className='text-3xl ml-4 mt-4'>Settings</p>
|
|
||||||
{loaded && (
|
|
||||||
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
|
||||||
<Setting
|
|
||||||
label='Allow sending notifications'
|
|
||||||
value={allowNotifications}
|
|
||||||
onChange={async () => {
|
|
||||||
while (normalConfig != null) {
|
|
||||||
setAllowNotifications(!allowNotifications)
|
|
||||||
setNormalConfig({
|
|
||||||
...normalConfig,
|
|
||||||
settings: {
|
|
||||||
...normalConfig.settings,
|
|
||||||
allowNotifications: !allowNotifications
|
|
||||||
}
|
|
||||||
})
|
|
||||||
writeNormalConfig({
|
|
||||||
...normalConfig,
|
|
||||||
settings: {
|
|
||||||
...normalConfig.settings,
|
|
||||||
allowNotifications: !allowNotifications
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Setting
|
|
||||||
label='Always show games in sidebar'
|
|
||||||
value={alwaysShowGamesInSidebar}
|
|
||||||
onChange={async () => {
|
|
||||||
while (normalConfig != null) {
|
|
||||||
setAlwaysShowGamesInSidebar(!alwaysShowGamesInSidebar)
|
|
||||||
setNormalConfig({
|
|
||||||
...normalConfig,
|
|
||||||
settings: {
|
|
||||||
...normalConfig.settings,
|
|
||||||
alwaysShowGamesInSidebar: !alwaysShowGamesInSidebar
|
|
||||||
}
|
|
||||||
})
|
|
||||||
writeNormalConfig({
|
|
||||||
...normalConfig,
|
|
||||||
settings: {
|
|
||||||
...normalConfig.settings,
|
|
||||||
alwaysShowGamesInSidebar: !alwaysShowGamesInSidebar
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<label className='text-lg'>Theme:</label>
|
|
||||||
<select
|
|
||||||
className='ml-2 bg-(--col2) border border-(--col4) rounded-md'
|
|
||||||
value={theme}
|
|
||||||
onChange={async e => {
|
|
||||||
const newTheme = parseInt(e.target.value)
|
|
||||||
while (normalConfig != null) {
|
|
||||||
setTheme(newTheme)
|
|
||||||
setNormalConfig({
|
|
||||||
...normalConfig,
|
|
||||||
settings: {
|
|
||||||
...normalConfig.settings,
|
|
||||||
theme: newTheme
|
|
||||||
}
|
|
||||||
})
|
|
||||||
writeNormalConfig({
|
|
||||||
...normalConfig,
|
|
||||||
settings: {
|
|
||||||
...normalConfig.settings,
|
|
||||||
theme: newTheme
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value={0}>Dark (default)</option>
|
|
||||||
<option value={1}>Red</option>
|
|
||||||
<option value={2}>Green</option>
|
|
||||||
<option value={3}>Blue</option>
|
|
||||||
<option value={4}>Purple</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export class DownloadProgress {
|
|
||||||
constructor(
|
|
||||||
public version: string,
|
|
||||||
public progress: number,
|
|
||||||
public progressBytes: number,
|
|
||||||
public failed: boolean,
|
|
||||||
public queued: boolean,
|
|
||||||
public hash_checking: boolean,
|
|
||||||
public finishing: boolean,
|
|
||||||
public speed: number,
|
|
||||||
public etaSecs: number
|
|
||||||
) { }
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export interface GameVersion {
|
|
||||||
id: string
|
|
||||||
versionName: string
|
|
||||||
releaseDate: number
|
|
||||||
game: number
|
|
||||||
downloadUrl: string
|
|
||||||
executable: string
|
|
||||||
sha512sum: string
|
|
||||||
size: number
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export interface LeaderboardEntry {
|
|
||||||
username: string
|
|
||||||
userid: bigint
|
|
||||||
value: bigint
|
|
||||||
icon: number
|
|
||||||
overlay: number
|
|
||||||
birdColor: number[]
|
|
||||||
overlayColor: number[]
|
|
||||||
customIcon: string | null
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { LeaderboardEntry } from './LeaderboardEntry'
|
|
||||||
|
|
||||||
export interface LeaderboardResponse {
|
|
||||||
entries: LeaderboardEntry[]
|
|
||||||
customIcons: Record<string, string>
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { SettingsType } from './SettingsType'
|
|
||||||
|
|
||||||
type NormalConfigData = {
|
|
||||||
version: string
|
|
||||||
settings?: Partial<SettingsType>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NormalConfig {
|
|
||||||
constructor (
|
|
||||||
public version: string,
|
|
||||||
public settings: SettingsType = new SettingsType()
|
|
||||||
) {}
|
|
||||||
|
|
||||||
static import (data: NormalConfigData) {
|
|
||||||
const cfg = new NormalConfig(data.version)
|
|
||||||
if (data.settings) {
|
|
||||||
cfg.settings = { ...cfg.settings, ...data.settings }
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export class SettingsType {
|
|
||||||
constructor(
|
|
||||||
public allowNotifications: boolean = true,
|
|
||||||
public alwaysShowGamesInSidebar: boolean = true,
|
|
||||||
public theme: number = 0
|
|
||||||
) { }
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
type VersionsConfigData = {
|
|
||||||
version: string
|
|
||||||
list: string[]
|
|
||||||
timestamps: Record<string, number>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class VersionsConfig {
|
|
||||||
constructor(
|
|
||||||
public version: string,
|
|
||||||
public list: string[] = [],
|
|
||||||
public timestamps: Record<string, number> = {}
|
|
||||||
) { }
|
|
||||||
|
|
||||||
static import(data: VersionsConfigData) {
|
|
||||||
const cfg = new VersionsConfig(data.version)
|
|
||||||
cfg.list = [...data.list]
|
|
||||||
cfg.timestamps = { ...data.timestamps }
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export default function UpdateCheckingPage () {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
router.push('/main')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <p className='text-5xl text-center'>Checking for updates</p>
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import '@/styles/globals.css'
|
||||||
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
|
import { Roboto } from 'next/font/google'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
const roboto = Roboto({
|
||||||
|
subsets: ['latin']
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function RootLayout ({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const [platformName, setPlatformName] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => setPlatformName(platform()), 0)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<html lang='en' className={roboto.className}>
|
||||||
|
<body className='dark-theme'>
|
||||||
|
{platformName == 'windows' && (
|
||||||
|
<div className='relative z-2 w-screen border-b border-b-(--col3) h-8.25 bg-(--col1)' />
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`w-screen ${
|
||||||
|
platformName == 'windows' ? 'h-[calc(100vh-64px)]' : 'h-screen'
|
||||||
|
} flex items-center justify-center`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
|
||||||
|
export default function UpdateCheckingPage () {
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
<p className='text-6xl mb-4'>Outdated Launcher!</p>
|
||||||
|
<p className='text-2xl mb-4'>
|
||||||
|
Please update to the latest version to continue.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className='button btntheme1'
|
||||||
|
onClick={() => openUrl('https://games.lncvrt.xyz/download')}
|
||||||
|
>
|
||||||
|
Download latest version!
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
export default function UpdateCheckingPage () {
|
||||||
|
return <p className='text-5xl text-center'>Updating launcher</p>
|
||||||
|
}
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { app } from '@tauri-apps/api'
|
|
||||||
import { NormalConfig } from '../types/NormalConfig'
|
|
||||||
import {
|
|
||||||
BaseDirectory,
|
|
||||||
create,
|
|
||||||
exists,
|
|
||||||
mkdir,
|
|
||||||
readTextFile,
|
|
||||||
writeFile
|
|
||||||
} from '@tauri-apps/plugin-fs'
|
|
||||||
import { VersionsConfig } from '../types/VersionsConfig'
|
|
||||||
|
|
||||||
export async function readNormalConfig(): Promise<NormalConfig> {
|
|
||||||
const version = await app.getVersion()
|
|
||||||
try {
|
|
||||||
const options = {
|
|
||||||
baseDir: BaseDirectory.AppLocalData
|
|
||||||
}
|
|
||||||
const doesFolderExist = await exists('', options)
|
|
||||||
const doesConfigExist = await exists('config.json', options)
|
|
||||||
if (!doesFolderExist || !doesConfigExist) {
|
|
||||||
if (!doesFolderExist) {
|
|
||||||
await mkdir('', options)
|
|
||||||
}
|
|
||||||
const file = await create('config.json', options)
|
|
||||||
await file.write(
|
|
||||||
new TextEncoder().encode(
|
|
||||||
JSON.stringify(new NormalConfig(version), null, 2),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await file.close()
|
|
||||||
return new NormalConfig(version)
|
|
||||||
}
|
|
||||||
const config = await readTextFile('config.json', options)
|
|
||||||
return NormalConfig.import(
|
|
||||||
JSON.parse(config)
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return new NormalConfig(version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function writeNormalConfig(data: NormalConfig) {
|
|
||||||
const options = {
|
|
||||||
baseDir: BaseDirectory.AppLocalData
|
|
||||||
}
|
|
||||||
const doesFolderExist = await exists('', options)
|
|
||||||
const doesConfigExist = await exists('config.json', options)
|
|
||||||
if (!doesFolderExist || !doesConfigExist) {
|
|
||||||
if (!doesFolderExist) {
|
|
||||||
await mkdir('', options)
|
|
||||||
}
|
|
||||||
const file = await create('config.json', options)
|
|
||||||
await file.write(
|
|
||||||
new TextEncoder().encode(
|
|
||||||
JSON.stringify(data, null, 2)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await file.close()
|
|
||||||
} else {
|
|
||||||
await writeFile(
|
|
||||||
'config.json',
|
|
||||||
new TextEncoder().encode(
|
|
||||||
JSON.stringify(data, null, 2)
|
|
||||||
),
|
|
||||||
options
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readVersionsConfig(): Promise<VersionsConfig> {
|
|
||||||
const version = await app.getVersion()
|
|
||||||
try {
|
|
||||||
const options = {
|
|
||||||
baseDir: BaseDirectory.AppLocalData
|
|
||||||
}
|
|
||||||
const doesFolderExist = await exists('', options)
|
|
||||||
const doesConfigExist = await exists('versions.json', options)
|
|
||||||
if (!doesFolderExist || !doesConfigExist) {
|
|
||||||
if (!doesFolderExist) {
|
|
||||||
await mkdir('', options)
|
|
||||||
}
|
|
||||||
const file = await create('versions.json', options)
|
|
||||||
await file.write(
|
|
||||||
new TextEncoder().encode(
|
|
||||||
JSON.stringify(new VersionsConfig(version), null, 2)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await file.close()
|
|
||||||
return new VersionsConfig(version)
|
|
||||||
}
|
|
||||||
const config = await readTextFile('versions.json', options)
|
|
||||||
return VersionsConfig.import(
|
|
||||||
JSON.parse(config)
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return new VersionsConfig(version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function writeVersionsConfig(data: VersionsConfig) {
|
|
||||||
const options = {
|
|
||||||
baseDir: BaseDirectory.AppLocalData
|
|
||||||
}
|
|
||||||
const doesFolderExist = await exists('', options)
|
|
||||||
const doesConfigExist = await exists('versions.json', options)
|
|
||||||
if (!doesFolderExist || !doesConfigExist) {
|
|
||||||
if (!doesFolderExist) {
|
|
||||||
await mkdir('', options)
|
|
||||||
}
|
|
||||||
const file = await create('versions.json', options)
|
|
||||||
await file.write(
|
|
||||||
new TextEncoder().encode(
|
|
||||||
JSON.stringify(data, null, 2)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await file.close()
|
|
||||||
} else {
|
|
||||||
await writeFile(
|
|
||||||
'versions.json',
|
|
||||||
new TextEncoder().encode(
|
|
||||||
JSON.stringify(data, null, 2)
|
|
||||||
),
|
|
||||||
options
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,61 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react'
|
||||||
|
import '@/styles/dropdown.css'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
label: string
|
||||||
|
value: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string | number
|
||||||
|
options: Option[]
|
||||||
|
onChange: (value: string | number) => void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Dropdown ({ value, options, onChange }: Props) {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const selected = options.find(o => o.value === value)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (!ref.current?.contains(e.target as Node)) {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('mousedown', handleClick)
|
||||||
|
return () => window.removeEventListener('mousedown', handleClick)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className='dropdown'>
|
||||||
|
<button className='dropdown-btn' onClick={() => setOpen(o => !o)}>
|
||||||
|
<span>{selected?.label ?? 'Select...'}</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faChevronDown}
|
||||||
|
className={`dropdown-arrow ${open ? 'open' : ''}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div className={`dropdown-menu ${open ? 'open' : ''}`}>
|
||||||
|
{options.map((option, i) => (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className={`dropdown-item ${
|
||||||
|
option.value === value ? 'active' : ''
|
||||||
|
}`}
|
||||||
|
style={{ ['--i']: i } as React.CSSProperties}
|
||||||
|
onClick={async () => {
|
||||||
|
await onChange(option.value)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import '@/styles/dropdown.css'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
export default function ExpandArrow ({ open }: { open: boolean }) {
|
||||||
|
return (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faChevronDown}
|
||||||
|
className={`dropdown-arrow ${!open ? 'open' : ''}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,20 +1,28 @@
|
|||||||
import './Setting.css'
|
import '@/styles/setting.css'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { SettingProps } from '../types/SettingProps'
|
import { SettingProps } from '@/types/SettingProps'
|
||||||
|
|
||||||
export function Setting ({ label, value, onChange, className }: SettingProps) {
|
export function Setting ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
disabled
|
||||||
|
}: SettingProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`flex items-center gap-2 mb-2 ${className}`}>
|
<div className={`flex items-center gap-2 mb-2 ${className}`} title={title}>
|
||||||
<label className='text-white text-lg'>{label}</label>
|
<label className='text-white text-lg'>{label}</label>
|
||||||
<div className='setting-checkbox-wrapper'>
|
<div className='setting-checkbox-wrapper'>
|
||||||
<input
|
<input
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
className='setting-checkbox'
|
className='setting-checkbox'
|
||||||
checked={value}
|
checked={value}
|
||||||
|
disabled={disabled}
|
||||||
onChange={() => onChange(!value)}
|
onChange={() => onChange(!value)}
|
||||||
/>
|
/>
|
||||||
{value && <FontAwesomeIcon icon={faCheck} className='fa-check-icon' />}
|
{value && <FontAwesomeIcon icon={faCheck} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import '@/styles/sidebar.css'
|
||||||
|
import Icon from '@/assets/Icon.png'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import {
|
||||||
|
faCog,
|
||||||
|
faDownload,
|
||||||
|
faGamepad,
|
||||||
|
faHexagonNodes,
|
||||||
|
faHome,
|
||||||
|
faLayerGroup,
|
||||||
|
faUser,
|
||||||
|
faUserShield
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { faDiscord } from '@fortawesome/free-brands-svg-icons'
|
||||||
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
|
import { Lexend } from 'next/font/google'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { message } from '@tauri-apps/plugin-dialog'
|
||||||
|
import ExpandArrow from './ExpandArrow'
|
||||||
|
|
||||||
|
const lexend = Lexend({
|
||||||
|
subsets: ['latin']
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function Sidebar () {
|
||||||
|
const {
|
||||||
|
serverVersionList,
|
||||||
|
versionsList,
|
||||||
|
setShowPopup,
|
||||||
|
setPopupMode,
|
||||||
|
setFadeOut,
|
||||||
|
downloadProgress,
|
||||||
|
category,
|
||||||
|
setCategory,
|
||||||
|
linuxUseWine,
|
||||||
|
setManagingGame,
|
||||||
|
settings,
|
||||||
|
developerMode,
|
||||||
|
accountName,
|
||||||
|
accountAdmin,
|
||||||
|
sidebarShowGames
|
||||||
|
} = useGlobal()
|
||||||
|
|
||||||
|
const pathname = usePathname()
|
||||||
|
const params = useSearchParams()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const [devModeClicks, setDevModeClicks] = useState<number>(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className='sidebar'>
|
||||||
|
{platform() == 'macos' && (
|
||||||
|
<div
|
||||||
|
className='macos-drag'
|
||||||
|
onMouseDown={async e => {
|
||||||
|
if (e.buttons === 1) {
|
||||||
|
if (e.detail === 2) {
|
||||||
|
await getCurrentWindow().toggleMaximize()
|
||||||
|
} else {
|
||||||
|
await getCurrentWindow().startDragging()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'flex items-center h-10 w-60 ' +
|
||||||
|
(platform() == 'windows' ? 'pl-1 pt-1' : 'pl-2 pt-2') +
|
||||||
|
(platform() == 'macos'
|
||||||
|
? // ? Number(version().split('.')[0]) >= 26
|
||||||
|
// ? ' mt-6'
|
||||||
|
' mt-5'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
onMouseDown={async e => {
|
||||||
|
if (platform() != 'macos') return
|
||||||
|
if (e.buttons === 1) {
|
||||||
|
if (e.detail === 2) {
|
||||||
|
await getCurrentWindow().toggleMaximize()
|
||||||
|
} else {
|
||||||
|
await getCurrentWindow().startDragging()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image draggable={false} src={Icon} width={36} height={36} alt='' />
|
||||||
|
<p className={`ml-1 text-[16px] whitespace-nowrap ${lexend.className}`}>
|
||||||
|
Lncvrt Games Launcher
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<nav className='nav-links overflow-auto'>
|
||||||
|
<Link
|
||||||
|
draggable={false}
|
||||||
|
href='/main'
|
||||||
|
className={`link relative flex items-center ${
|
||||||
|
pathname === '/main' ? 'active' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faHome} className='mr-2' /> Home
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
draggable={false}
|
||||||
|
href='/main/library'
|
||||||
|
className={`link relative flex items-center ${
|
||||||
|
pathname === '/main/library' || pathname.startsWith('/main/game')
|
||||||
|
? 'active'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
onContextMenu={async e => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (developerMode) {
|
||||||
|
await settings?.set('developerMode', false)
|
||||||
|
await message(
|
||||||
|
'Developer mode disabled! You can enable it again by right clicking the games button 3 times again.',
|
||||||
|
{ title: 'Developer mode off', kind: 'info' }
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devModeClicks == 2) {
|
||||||
|
await settings?.set('developerMode', true)
|
||||||
|
setDevModeClicks(0)
|
||||||
|
await message(
|
||||||
|
'Developer mode sucessfully enabled! You can disable it by right clicking the games button again.',
|
||||||
|
{ title: 'Developer mode on', kind: 'info' }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setDevModeClicks(devModeClicks + 1)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faHexagonNodes} className='mr-2' /> Library
|
||||||
|
{(pathname === '/main/library' ||
|
||||||
|
pathname.startsWith('/main/game')) && (
|
||||||
|
<div
|
||||||
|
className='ml-auto w-6 h-6 flex justify-center items-center'
|
||||||
|
onClick={async e => {
|
||||||
|
e.preventDefault()
|
||||||
|
await settings?.set('sidebarShowGames', !sidebarShowGames)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExpandArrow open={sidebarShowGames} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
{(pathname === '/main/library' || pathname.startsWith('/main/game')) &&
|
||||||
|
sidebarShowGames &&
|
||||||
|
serverVersionList?.games
|
||||||
|
.filter(g =>
|
||||||
|
serverVersionList.versions
|
||||||
|
.filter(v => v.game === g.id)
|
||||||
|
.some(v => Object.keys(versionsList).includes(v.id))
|
||||||
|
)
|
||||||
|
.map(i => (
|
||||||
|
<React.Fragment key={i.id}>
|
||||||
|
<div
|
||||||
|
draggable={false}
|
||||||
|
className={`link ${
|
||||||
|
pathname === '/main/game' &&
|
||||||
|
Number(params.get('id') || 0) == i.id
|
||||||
|
? 'active'
|
||||||
|
: ''
|
||||||
|
} ml-auto w-50`}
|
||||||
|
onClick={() => {
|
||||||
|
setCategory(-1)
|
||||||
|
router.push('/main/game?id=' + i.id)
|
||||||
|
}}
|
||||||
|
onContextMenu={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
setManagingGame(i.id)
|
||||||
|
setPopupMode(5)
|
||||||
|
setShowPopup(true)
|
||||||
|
setFadeOut(false)
|
||||||
|
}}
|
||||||
|
title='Click to view game installs.'
|
||||||
|
>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
Object.entries(i.categoryNames).length > 0
|
||||||
|
? faLayerGroup
|
||||||
|
: faGamepad
|
||||||
|
}
|
||||||
|
className='mr-2'
|
||||||
|
/>
|
||||||
|
<span className='truncate max-w-full'>{i.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{Object.entries(i.categoryNames)
|
||||||
|
.sort(([a], [b]) => Number(b) - Number(a))
|
||||||
|
.filter(([key]) => {
|
||||||
|
const count = Object.keys(versionsList).filter(v => {
|
||||||
|
const info = serverVersionList.versions.find(
|
||||||
|
vf => vf.id == v
|
||||||
|
)
|
||||||
|
if (!info) return false
|
||||||
|
|
||||||
|
if (platform() == 'linux' && info.wine && !linuxUseWine)
|
||||||
|
return false
|
||||||
|
|
||||||
|
return info.game === i.id && info.category === Number(key)
|
||||||
|
}).length
|
||||||
|
|
||||||
|
return count >= 1
|
||||||
|
})
|
||||||
|
.map(([key, value]) => (
|
||||||
|
<div
|
||||||
|
key={`${i.id}-${key}`}
|
||||||
|
draggable={false}
|
||||||
|
className={`link ${
|
||||||
|
pathname === '/main/game' &&
|
||||||
|
Number(params.get('id') || 0) == i.id &&
|
||||||
|
category == Number(key)
|
||||||
|
? 'active'
|
||||||
|
: ''
|
||||||
|
} ml-auto w-47.5`}
|
||||||
|
onClick={() => {
|
||||||
|
setCategory(Number(key))
|
||||||
|
router.push('/main/game?id=' + i.id)
|
||||||
|
}}
|
||||||
|
title="Click to view this game's category."
|
||||||
|
>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<FontAwesomeIcon icon={faGamepad} className='mr-1' />
|
||||||
|
<span className='truncate max-w-full'>{value}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
<Link
|
||||||
|
draggable={false}
|
||||||
|
href='/main/settings'
|
||||||
|
className={`link ${pathname === '/main/settings' ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCog} className='mr-1' /> Settings
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
draggable={false}
|
||||||
|
href='/main/account'
|
||||||
|
className={`link ${pathname === '/main/account' ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={accountAdmin ? faUserShield : faUser}
|
||||||
|
className='mr-1'
|
||||||
|
/>{' '}
|
||||||
|
{accountName ?? 'Account'}
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => openUrl('https://games.lncvrt.xyz/discord')}
|
||||||
|
className='link mr-auto'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faDiscord} className='mr-1' /> Community
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
{downloadProgress.length != 0 && (
|
||||||
|
<div
|
||||||
|
className='sidebar-downloads'
|
||||||
|
onClick={() => {
|
||||||
|
setPopupMode(1)
|
||||||
|
setShowPopup(true)
|
||||||
|
setFadeOut(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<FontAwesomeIcon icon={faDownload} /> Downloads
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
||||||
|
import { notifyUser } from './notifications'
|
||||||
|
|
||||||
|
export async function copyToClipboard (text: string) {
|
||||||
|
await writeText(text)
|
||||||
|
await notifyUser('Copied', 'Text "' + text + '" copied to clipboard')
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
isPermissionGranted,
|
||||||
|
requestPermission,
|
||||||
|
sendNotification
|
||||||
|
} from '@tauri-apps/plugin-notification'
|
||||||
|
|
||||||
|
export async function notifyUser (title: string, body: string) {
|
||||||
|
let permissionGranted = await isPermissionGranted()
|
||||||
|
if (!permissionGranted) {
|
||||||
|
const permission = await requestPermission()
|
||||||
|
permissionGranted = permission === 'granted'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionGranted) {
|
||||||
|
sendNotification({ title, body })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { message } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
export const openFolder = async (name: string) => {
|
||||||
|
try {
|
||||||
|
await invoke('open_game_folder', { version: name })
|
||||||
|
} catch (e) {
|
||||||
|
await message(String(e), {
|
||||||
|
title: 'Folder not found',
|
||||||
|
kind: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatEtaSmart = (seconds: number) => {
|
||||||
|
if (seconds < 60) return `${Math.floor(seconds)}s`
|
||||||
|
if (seconds < 3600)
|
||||||
|
return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`
|
||||||
|
if (seconds < 86400) {
|
||||||
|
const h = Math.floor(seconds / 3600)
|
||||||
|
const m = Math.floor((seconds % 3600) / 60)
|
||||||
|
return `${h}h ${m}m`
|
||||||
|
}
|
||||||
|
const d = Math.floor(seconds / 86400)
|
||||||
|
const h = Math.floor((seconds % 86400) / 3600)
|
||||||
|
return `${d}d ${h}h`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifySignature = async (body: string, signature: string) => {
|
||||||
|
try {
|
||||||
|
const result = await invoke<boolean>('verify_signature', {
|
||||||
|
body: body ?? '',
|
||||||
|
signature: signature ?? '',
|
||||||
|
publicKey: process.env.NEXT_PUBLIC_PUBLIC_SIGNING_KEY ?? ''
|
||||||
|
})
|
||||||
|
return Boolean(result)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import prettyBytes from 'pretty-bytes'
|
||||||
|
import ProgressBar from '@/components/ProgressBar'
|
||||||
|
import { formatEtaSmart } from '@/lib/util'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import {
|
||||||
|
faCancel,
|
||||||
|
faPause,
|
||||||
|
faPlay,
|
||||||
|
faTrash
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { notifyUser } from '@/lib/notifications'
|
||||||
|
import { getCurrentWindow, UserAttentionType } from '@tauri-apps/api/window'
|
||||||
|
|
||||||
|
export default function DownloadsPopup () {
|
||||||
|
const {
|
||||||
|
downloadProgress,
|
||||||
|
setDownloadProgress,
|
||||||
|
downloadQueue,
|
||||||
|
setDownloadQueue,
|
||||||
|
versionsList,
|
||||||
|
modsList,
|
||||||
|
versions,
|
||||||
|
serverVersionList
|
||||||
|
} = useGlobal()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className='text-xl text-center'>Downloads</p>
|
||||||
|
<div className='popup-content'>
|
||||||
|
{downloadProgress.map((v, i) => {
|
||||||
|
const queuePosition = downloadQueue.indexOf(v.version)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className='popup-entry flex flex-col justify-between relative'
|
||||||
|
>
|
||||||
|
<div className='absolute right-2 top-2 flex flex-row gap-2'>
|
||||||
|
{(v.downloading || v.paused) && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className='cursor-pointer bg-(--col5) hover:bg-(--col7) border border-(--col7) hover:border-(--col9) transition-colors w-8 h-8 flex items-center justify-center rounded-full'
|
||||||
|
onClick={async () => {
|
||||||
|
if (v.downloading) {
|
||||||
|
await invoke('cancel_download', { name: v.version })
|
||||||
|
} else {
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(
|
||||||
|
d => d.version === v.version
|
||||||
|
)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
paused: false,
|
||||||
|
downloading: true
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
const res = await invoke<string>('download', {
|
||||||
|
url: v.url,
|
||||||
|
name: v.version,
|
||||||
|
hash: v.hash,
|
||||||
|
downloadType: v.type,
|
||||||
|
modId: String(v.modId ?? '')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res === '1') {
|
||||||
|
setDownloadProgress(prev =>
|
||||||
|
prev.filter(d => d.version !== v.version)
|
||||||
|
)
|
||||||
|
if (v.type != 1) {
|
||||||
|
if (v.type == 2) {
|
||||||
|
await versions?.set('mods', {
|
||||||
|
...modsList,
|
||||||
|
[v.modGame! + '-' + v.modId!]: {
|
||||||
|
[v.modVersion!]: Date.now()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await versions?.set('list', {
|
||||||
|
...versionsList,
|
||||||
|
[v.version]: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (res == '0') {
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(
|
||||||
|
d => d.version === v.version
|
||||||
|
)
|
||||||
|
if (i === -1) return prev
|
||||||
|
if (prev[i].canceled) {
|
||||||
|
return prev.filter((_, idx) => idx !== i)
|
||||||
|
}
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
downloading: false,
|
||||||
|
paused: true,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
} else if (res == '-1') {
|
||||||
|
setDownloadProgress(prev =>
|
||||||
|
prev.map(d =>
|
||||||
|
d.version === v.version
|
||||||
|
? {
|
||||||
|
...d,
|
||||||
|
queued: false,
|
||||||
|
failed: true,
|
||||||
|
progress: 0
|
||||||
|
}
|
||||||
|
: d
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await notifyUser(
|
||||||
|
'Download Failed',
|
||||||
|
`The download for version ${
|
||||||
|
serverVersionList?.versions.find(
|
||||||
|
vf => vf.id == v.version
|
||||||
|
)?.displayName
|
||||||
|
} has failed.`
|
||||||
|
)
|
||||||
|
await getCurrentWindow().requestUserAttention(
|
||||||
|
UserAttentionType.Critical
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={v.downloading ? faPause : faPlay}
|
||||||
|
className='w-6 h-6'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='cursor-pointer bg-(--col5) hover:bg-(--col7) border border-(--col7) hover:border-(--col9) transition-colors w-8 h-8 flex items-center justify-center rounded-full'
|
||||||
|
onClick={async () => {
|
||||||
|
if (!v.paused) {
|
||||||
|
await invoke('cancel_download', { name: v.version })
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(
|
||||||
|
d => d.version === v.version
|
||||||
|
)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
canceled: true
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(
|
||||||
|
d => d.version === v.version
|
||||||
|
)
|
||||||
|
if (i === -1) return prev
|
||||||
|
return prev.filter((_, idx) => idx !== i)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCancel} className='w-6 h-6' />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(v.failed || v.queued) && (
|
||||||
|
<div
|
||||||
|
className='cursor-pointer bg-(--col5) hover:bg-(--col7) border border-(--col7) hover:border-(--col9) transition-colors w-8 h-8 flex items-center justify-center rounded-full'
|
||||||
|
onClick={() => {
|
||||||
|
setDownloadQueue(prev =>
|
||||||
|
prev.filter(id => id !== v.version)
|
||||||
|
)
|
||||||
|
setDownloadProgress(prev =>
|
||||||
|
prev.filter(d => d.version !== v.version)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} className='w-6 h-6' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className='text-2xl text-center'>
|
||||||
|
{v.type == 1
|
||||||
|
? 'Mod loader for '
|
||||||
|
: v.type == 2
|
||||||
|
? 'Mod for '
|
||||||
|
: null}{' '}
|
||||||
|
{
|
||||||
|
serverVersionList?.versions.find(vf => vf.id == v.version)
|
||||||
|
?.displayName
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<div className='mt-6.25 flex items-center justify-between'>
|
||||||
|
{v.failed || v.queued ? (
|
||||||
|
<div className='flex items-center justify-between w-full'>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
v.failed ? 'text-red-500' : 'text-yellow-300'
|
||||||
|
} inline-block text-center flex-1`}
|
||||||
|
>
|
||||||
|
{v.failed
|
||||||
|
? 'Download failed'
|
||||||
|
: queuePosition === 0
|
||||||
|
? 'Starting soon...'
|
||||||
|
: `Queued (Position ${queuePosition + 1})`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : v.hash_checking ? (
|
||||||
|
<span className='text-blue-300 inline-block w-full text-center'>
|
||||||
|
Verifying file integrity...
|
||||||
|
</span>
|
||||||
|
) : v.unzipping ? (
|
||||||
|
<div className='flex flex-col gap-1 w-full'>
|
||||||
|
<span className='text-center'>
|
||||||
|
Unzipped {v.unzipped} / {v.unzipTotal} files
|
||||||
|
</span>
|
||||||
|
<ProgressBar
|
||||||
|
progress={(v.unzipped / v.unzipTotal) * 100}
|
||||||
|
className='w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col gap-1 w-full'>
|
||||||
|
<span className='text-center'>
|
||||||
|
Downloaded{' '}
|
||||||
|
{prettyBytes(v.progressBytes, {
|
||||||
|
minimumFractionDigits: 1,
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
})}{' '}
|
||||||
|
of{' '}
|
||||||
|
{prettyBytes(v.size, {
|
||||||
|
minimumFractionDigits: 1,
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
})}{' '}
|
||||||
|
(ETA: {formatEtaSmart(v.etaSecs)} • Speed:{' '}
|
||||||
|
{prettyBytes(v.speed, {
|
||||||
|
minimumFractionDigits: 1,
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
})}
|
||||||
|
/s)
|
||||||
|
</span>
|
||||||
|
<ProgressBar progress={v.progress} className='w-full' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faInfo
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
|
export default function GamesDownloadPopup () {
|
||||||
|
const {
|
||||||
|
serverVersionList,
|
||||||
|
setSelectedGame,
|
||||||
|
setManagingGame,
|
||||||
|
setPopupMode,
|
||||||
|
setShowPopup,
|
||||||
|
setFadeOut,
|
||||||
|
setViewingInfoFromDownloads,
|
||||||
|
developerMode
|
||||||
|
} = useGlobal()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className='text-xl text-center'>Select a game to download</p>
|
||||||
|
<div className='popup-content'>
|
||||||
|
{serverVersionList?.games
|
||||||
|
.filter(g => {
|
||||||
|
const gameVersions = serverVersionList.versions.filter(
|
||||||
|
gv => gv.game === g.id
|
||||||
|
)
|
||||||
|
if (gameVersions.length > 0) return true
|
||||||
|
})
|
||||||
|
.map((g, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className='popup-entry grid items-center gap-3'
|
||||||
|
style={{ gridTemplateColumns: 'auto minmax(0, 1fr) auto' }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className='button btntheme3'
|
||||||
|
onClick={() => {
|
||||||
|
setViewingInfoFromDownloads(true)
|
||||||
|
setManagingGame(g.id)
|
||||||
|
setPopupMode(5)
|
||||||
|
setShowPopup(true)
|
||||||
|
setFadeOut(false)
|
||||||
|
}}
|
||||||
|
title='Click to view game info.'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faInfo} /> Info
|
||||||
|
</button>
|
||||||
|
<p className='text-2xl text-center truncate' title={g.name}>
|
||||||
|
{developerMode ? <span className='text-sm'>{g.id} </span> : ''}
|
||||||
|
{g.name}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className='button btntheme3'
|
||||||
|
onClick={() => setSelectedGame(g.id)}
|
||||||
|
title='Click to view versions of the game.'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faArrowUpRightFromSquare} /> View
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
faCheck,
|
||||||
|
faCode,
|
||||||
|
faDownload,
|
||||||
|
faHardDrive,
|
||||||
|
faShieldHalved,
|
||||||
|
faWarning
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import prettyBytes from 'pretty-bytes'
|
||||||
|
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||||
|
|
||||||
|
export default function ManageGamePopup () {
|
||||||
|
const {
|
||||||
|
versionsList,
|
||||||
|
serverVersionList,
|
||||||
|
closePopup,
|
||||||
|
versions,
|
||||||
|
customDataLocation,
|
||||||
|
managingGame
|
||||||
|
} = useGlobal()
|
||||||
|
const [gameSize, setGameSize] = useState<number>(0)
|
||||||
|
|
||||||
|
const versionsInstalled = useMemo(() => {
|
||||||
|
return serverVersionList?.versions.filter(
|
||||||
|
vf => vf.game == managingGame && Object.keys(versionsList).includes(vf.id)
|
||||||
|
)
|
||||||
|
}, [serverVersionList, managingGame, versionsList])
|
||||||
|
const gameInfo = serverVersionList?.games.find(vf => vf.id == managingGame)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!versionsInstalled || versionsInstalled.length == 0) return
|
||||||
|
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
let total = 0
|
||||||
|
setGameSize(0)
|
||||||
|
|
||||||
|
for (const version of versionsInstalled) {
|
||||||
|
if (cancelled) return
|
||||||
|
|
||||||
|
const size = await invoke<string>('folder_size', {
|
||||||
|
version: version.id
|
||||||
|
}).catch(() => '0')
|
||||||
|
|
||||||
|
total += Number(size)
|
||||||
|
setGameSize(total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [versionsInstalled])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className='text-xl text-center'>Viewing {gameInfo?.name}</p>
|
||||||
|
<div className='popup-content h-full w-full flex justify-center items-center flex-col gap-2'>
|
||||||
|
<div className='flex flex-row gap-2 justify-center'>
|
||||||
|
{gameInfo?.official ? (
|
||||||
|
<div className='entry-info-item btntheme2'>
|
||||||
|
<FontAwesomeIcon icon={faCheck} color='#19c84b' />
|
||||||
|
<p>Official</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='entry-info-item btntheme2'>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={gameInfo?.verified ? faShieldHalved : faWarning}
|
||||||
|
color={gameInfo?.verified ? '#19c84b' : '#ffc800'}
|
||||||
|
/>
|
||||||
|
<p>{gameInfo?.verified ? 'Verified' : 'Unverified'}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{gameInfo?.developer != null && (
|
||||||
|
<div className='entry-info-item btntheme2'>
|
||||||
|
<FontAwesomeIcon icon={faCode} color='lightgray' />
|
||||||
|
<p>Developer: {gameInfo?.developer}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-row gap-2 justify-center'>
|
||||||
|
<div className='entry-info-item btntheme2'>
|
||||||
|
<FontAwesomeIcon icon={faDownload} color='lightgray' />
|
||||||
|
<p>
|
||||||
|
{(() => {
|
||||||
|
if (!serverVersionList) return 'N/A'
|
||||||
|
|
||||||
|
const gameVersions = serverVersionList.versions.filter(
|
||||||
|
vf => vf.game === managingGame
|
||||||
|
)
|
||||||
|
const installed = gameVersions.filter(v =>
|
||||||
|
Object.keys(versionsList).includes(v.id)
|
||||||
|
).length
|
||||||
|
return gameVersions.length
|
||||||
|
? `${installed}/${gameVersions.length}`
|
||||||
|
: 'N/A'
|
||||||
|
})()}{' '}
|
||||||
|
versions installed
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{versionsInstalled?.length != 0 && (
|
||||||
|
<>
|
||||||
|
<div className='entry-info-item btntheme2'>
|
||||||
|
<FontAwesomeIcon icon={faHardDrive} color='lightgray' />
|
||||||
|
<p>
|
||||||
|
Size on disk:{' '}
|
||||||
|
{prettyBytes(gameSize, {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='entry-info-item btntheme2'
|
||||||
|
onClick={async () => {
|
||||||
|
closePopup()
|
||||||
|
if (!versionsInstalled) return
|
||||||
|
|
||||||
|
const updatedList = { ...versionsList }
|
||||||
|
|
||||||
|
for (const version of versionsInstalled)
|
||||||
|
delete updatedList[version.id]
|
||||||
|
|
||||||
|
await versions?.set('list', updatedList)
|
||||||
|
|
||||||
|
for (const version of versionsInstalled) {
|
||||||
|
const managingVersion = version.id
|
||||||
|
|
||||||
|
if (
|
||||||
|
await exists(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
'game/' +
|
||||||
|
managingVersion,
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await remove(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
'game/' +
|
||||||
|
managingVersion,
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData,
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title='Click to uninstall this game. This will NOT remove any progress or any save files.'
|
||||||
|
>
|
||||||
|
Uninstall
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faHardDrive
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import prettyBytes from 'pretty-bytes'
|
||||||
|
import { openFolder } from '@/lib/util'
|
||||||
|
|
||||||
|
export default function ManageVersionPopup () {
|
||||||
|
const {
|
||||||
|
versionsList,
|
||||||
|
serverVersionList,
|
||||||
|
managingVersion,
|
||||||
|
viewingInfoFromDownloads,
|
||||||
|
setManagingVersion,
|
||||||
|
closePopup,
|
||||||
|
setPopupMode,
|
||||||
|
setSelectedVersionList,
|
||||||
|
downloadVersions,
|
||||||
|
uninstallVersion
|
||||||
|
} = useGlobal()
|
||||||
|
const [versionSize, setVersionSize] = useState<number>(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewingInfoFromDownloads) return
|
||||||
|
invoke<string>('folder_size', {
|
||||||
|
version: managingVersion
|
||||||
|
}).then(size => {
|
||||||
|
setVersionSize(Number(size))
|
||||||
|
})
|
||||||
|
}, [managingVersion, setVersionSize, viewingInfoFromDownloads])
|
||||||
|
|
||||||
|
if (!managingVersion) return null
|
||||||
|
|
||||||
|
const versionInfo = serverVersionList?.versions.find(
|
||||||
|
vf => vf.id == managingVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className='text-xl text-center'>Viewing {versionInfo?.displayName}</p>
|
||||||
|
<div className='popup-content h-full w-full flex justify-center items-center flex-col gap-2'>
|
||||||
|
<div className='flex flex-row gap-2 justify-center'>
|
||||||
|
{!viewingInfoFromDownloads && (
|
||||||
|
<div className='entry-info-item btntheme2'>
|
||||||
|
<p>
|
||||||
|
Installed{' '}
|
||||||
|
{new Intl.DateTimeFormat(undefined).format(
|
||||||
|
versionsList[managingVersion] ?? 0
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{versionInfo && (
|
||||||
|
<>
|
||||||
|
{versionInfo.releaseDate != 0 && (
|
||||||
|
<div className='entry-info-item btntheme2'>
|
||||||
|
<p>
|
||||||
|
Released{' '}
|
||||||
|
{new Intl.DateTimeFormat(undefined).format(
|
||||||
|
versionInfo?.releaseDate
|
||||||
|
? versionInfo.releaseDate * 1000
|
||||||
|
: 0
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{versionInfo.changelog && (
|
||||||
|
<div
|
||||||
|
className='entry-info-item btntheme2'
|
||||||
|
onClick={() => setPopupMode(4)}
|
||||||
|
>
|
||||||
|
<p>View Changelog</p>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
color='lightgray'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!viewingInfoFromDownloads && (
|
||||||
|
<div className='flex flex-row gap-2 justify-center'>
|
||||||
|
<div className='entry-info-item btntheme2'>
|
||||||
|
<FontAwesomeIcon icon={faHardDrive} color='lightgray' />
|
||||||
|
<p>
|
||||||
|
Size on disk:{' '}
|
||||||
|
{prettyBytes(versionSize, {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='entry-info-item btntheme2'
|
||||||
|
onClick={async () => openFolder(managingVersion)}
|
||||||
|
title="Click to browse the game's files."
|
||||||
|
>
|
||||||
|
Open Folder
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
color='lightgray'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='entry-info-item btntheme2'
|
||||||
|
onClick={async () => {
|
||||||
|
closePopup()
|
||||||
|
uninstallVersion(managingVersion)
|
||||||
|
}}
|
||||||
|
title='Click to uninstall this game. This will NOT remove any progress or any save files.'
|
||||||
|
>
|
||||||
|
Uninstall
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='entry-info-item btntheme2'
|
||||||
|
onClick={async () => {
|
||||||
|
//change popup to downloads
|
||||||
|
setManagingVersion(null)
|
||||||
|
setPopupMode(1)
|
||||||
|
|
||||||
|
//uninstall
|
||||||
|
uninstallVersion(managingVersion)
|
||||||
|
|
||||||
|
//reinstall
|
||||||
|
setSelectedVersionList([managingVersion])
|
||||||
|
await downloadVersions([
|
||||||
|
{
|
||||||
|
id: managingVersion,
|
||||||
|
type: 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}}
|
||||||
|
title="Click to reinstall this game. This will NOT remove any progress or any save files. This WILL uninstall any modifications to the game's executable files."
|
||||||
|
>
|
||||||
|
Reinstall
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { verifySignature } from '@/lib/util'
|
||||||
|
import { Mod } from '@/types/Mod'
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faClock,
|
||||||
|
faCodeBranch,
|
||||||
|
faDownload,
|
||||||
|
faGlobe,
|
||||||
|
faPeopleGroup,
|
||||||
|
faRefresh,
|
||||||
|
faTrash
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { message } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||||
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
|
import { arch, platform } from '@tauri-apps/plugin-os'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { app } from '@tauri-apps/api'
|
||||||
|
|
||||||
|
export default function ModDownloadsPopup () {
|
||||||
|
const {
|
||||||
|
modsList,
|
||||||
|
serverVersionList,
|
||||||
|
managingVersion,
|
||||||
|
showModInfo,
|
||||||
|
setShowModInfo,
|
||||||
|
downloadVersions,
|
||||||
|
downloadProgress,
|
||||||
|
versions,
|
||||||
|
customDataLocation
|
||||||
|
} = useGlobal()
|
||||||
|
|
||||||
|
const [mods, setMods] = useState<Mod[] | 0 | 1>(0)
|
||||||
|
const [tab, setTab] = useState<number>(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!managingVersion) return
|
||||||
|
;(async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://games.lncvrt.xyz/api/launcher/mods?version=' + managingVersion,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Requester: 'LncvrtGamesLauncherClient',
|
||||||
|
ClientVersion: await app.getVersion(),
|
||||||
|
ClientPlatform: platform() + '-' + arch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
|
const data = await response.json()
|
||||||
|
if (
|
||||||
|
response.status == 200 &&
|
||||||
|
(await verifySignature(JSON.stringify(data), signature))
|
||||||
|
) {
|
||||||
|
setMods(data as Mod[])
|
||||||
|
} else {
|
||||||
|
setMods(1)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [managingVersion])
|
||||||
|
|
||||||
|
if (!managingVersion || !serverVersionList) return null
|
||||||
|
|
||||||
|
const versionInfo = serverVersionList.versions.find(
|
||||||
|
v => v.id == managingVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{typeof mods != 'number' && showModInfo == null && (
|
||||||
|
<button
|
||||||
|
className='popup-top-button btntheme1 right-2'
|
||||||
|
onClick={async () => {
|
||||||
|
setMods(0)
|
||||||
|
const response = await fetch(
|
||||||
|
'https://games.lncvrt.xyz/api/launcher/mods?version=' +
|
||||||
|
managingVersion,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Requester: 'LncvrtGamesLauncherClient',
|
||||||
|
ClientVersion: await app.getVersion(),
|
||||||
|
ClientPlatform: platform() + '-' + arch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
|
const data = await response.json()
|
||||||
|
if (
|
||||||
|
response.status == 200 &&
|
||||||
|
(await verifySignature(JSON.stringify(data), signature))
|
||||||
|
) {
|
||||||
|
setMods(data as Mod[])
|
||||||
|
} else {
|
||||||
|
setMods(1)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRefresh} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<p className='text-xl text-center'>
|
||||||
|
{!showModInfo
|
||||||
|
? versionInfo?.displayName + ' Mod Manager'
|
||||||
|
: showModInfo.name +
|
||||||
|
' by ' +
|
||||||
|
showModInfo.creators[0] +
|
||||||
|
(showModInfo.creators.length > 1
|
||||||
|
? ' + ' + (showModInfo.creators.length - 1) + ' more'
|
||||||
|
: '')}
|
||||||
|
</p>
|
||||||
|
{typeof mods != 'number' && !showModInfo ? (
|
||||||
|
<div className='flex flex-row justify-center items-center -mb-2 mt-0.5 gap-1.5'>
|
||||||
|
<button
|
||||||
|
className={`button ${tab == 0 ? 'btntheme3' : 'btntheme2'}`}
|
||||||
|
onClick={() => setTab(0)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faDownload} />
|
||||||
|
Installed
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`button ${tab == 1 ? 'btntheme3' : 'btntheme2'}`}
|
||||||
|
onClick={() => setTab(1)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faGlobe} />
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className='popup-content h-full'>
|
||||||
|
{typeof mods == 'number' ||
|
||||||
|
(!showModInfo &&
|
||||||
|
mods.filter(v =>
|
||||||
|
tab == 0
|
||||||
|
? Object.keys(modsList).includes(
|
||||||
|
String(versionInfo?.game + '-' + v.id)
|
||||||
|
)
|
||||||
|
: !Object.keys(modsList).includes(
|
||||||
|
String(versionInfo?.game + '-' + v.id)
|
||||||
|
)
|
||||||
|
).length == 0) ? (
|
||||||
|
<p className='text-2xl flex justify-center items-center h-full'>
|
||||||
|
{typeof mods == 'number'
|
||||||
|
? mods == 0
|
||||||
|
? 'Loading'
|
||||||
|
: 'Failed to load mods'
|
||||||
|
: tab == 0
|
||||||
|
? 'No mods installed'
|
||||||
|
: 'No mods available to download'}
|
||||||
|
</p>
|
||||||
|
) : !showModInfo ? (
|
||||||
|
<div className='flex flex-col items-center justify-center gap-2 p-2'>
|
||||||
|
{mods
|
||||||
|
.filter(v =>
|
||||||
|
tab == 0
|
||||||
|
? Object.keys(modsList).includes(
|
||||||
|
String(versionInfo?.game + '-' + v.id)
|
||||||
|
)
|
||||||
|
: !Object.keys(modsList).includes(
|
||||||
|
String(versionInfo?.game + '-' + v.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map(v => {
|
||||||
|
const localVersion =
|
||||||
|
tab == 0
|
||||||
|
? Object.keys(
|
||||||
|
modsList[versionInfo?.game + '-' + v.id] ?? {}
|
||||||
|
)[0]
|
||||||
|
: null
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={v.id}
|
||||||
|
className='bg-(--col3) border border-(--col5) rounded-lg w-full h-16 flex flex-row'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col justify-center h-full px-3 w-fit'>
|
||||||
|
<p>
|
||||||
|
<span className='text-lg'>{v.name}</span>{' '}
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
tab != 0 || v.latestVersion == localVersion
|
||||||
|
? 'text-green-300'
|
||||||
|
: 'text-red-300'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
v{tab == 0 ? localVersion : v.latestVersion}
|
||||||
|
</span>
|
||||||
|
{tab == 0 && v.latestVersion != localVersion && (
|
||||||
|
<span className='text-green-300'>
|
||||||
|
{' '}
|
||||||
|
(available: v{v.latestVersion})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className='text-yellow-200'>
|
||||||
|
Made by {v.creators[0]}
|
||||||
|
{v.creators.length > 1
|
||||||
|
? ' + ' + (v.creators.length - 1) + ' more'
|
||||||
|
: null}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-row items-center h-full gap-2 px-3 ml-auto'>
|
||||||
|
{tab != 0 && (
|
||||||
|
<p className='text-green-400'>
|
||||||
|
<FontAwesomeIcon icon={faDownload} />{' '}
|
||||||
|
{v.downloads.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{tab == 0 && v.latestVersion != localVersion && (
|
||||||
|
<button
|
||||||
|
className='button btntheme3'
|
||||||
|
onClick={async () => {
|
||||||
|
const path =
|
||||||
|
'game/' +
|
||||||
|
versionInfo?.id +
|
||||||
|
'/BepInEx/plugins/' +
|
||||||
|
v.id
|
||||||
|
if (
|
||||||
|
await exists(
|
||||||
|
(customDataLocation
|
||||||
|
? customDataLocation + '/'
|
||||||
|
: null) + path,
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await remove(
|
||||||
|
(customDataLocation
|
||||||
|
? customDataLocation + '/'
|
||||||
|
: null) + path,
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData,
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await versions?.set(
|
||||||
|
'mods',
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(modsList).filter(
|
||||||
|
([k]) => k !== versionInfo?.game + '-' + v.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await downloadVersions([
|
||||||
|
{
|
||||||
|
id: managingVersion,
|
||||||
|
type: 2,
|
||||||
|
modDownload: v.latestDownload,
|
||||||
|
gameId: versionInfo?.game,
|
||||||
|
modId: v.id,
|
||||||
|
modVersion: v.latestVersion
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}}
|
||||||
|
disabled={downloadProgress.length != 0}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className='button btntheme3'
|
||||||
|
onClick={() => setShowModInfo(v)}
|
||||||
|
>
|
||||||
|
{tab == 0 ? 'View' : 'Get'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col h-full w-full items-center'>
|
||||||
|
<div className='flex flex-col p-2 h-fit w-fit bg-(--col3) border border-(--col5) rounded-lg m-2 items-center'>
|
||||||
|
<p>
|
||||||
|
<FontAwesomeIcon icon={faDownload} className='text-green-400' />{' '}
|
||||||
|
Downloads {showModInfo.downloads.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FontAwesomeIcon icon={faClock} /> Released{' '}
|
||||||
|
{new Intl.DateTimeFormat(undefined).format(
|
||||||
|
showModInfo.released * 1000
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FontAwesomeIcon icon={faClock} /> Updated{' '}
|
||||||
|
{new Intl.DateTimeFormat(undefined).format(
|
||||||
|
showModInfo.updated * 1000
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FontAwesomeIcon icon={faCodeBranch} /> Version{' '}
|
||||||
|
{showModInfo.latestVersion}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col p-2 bg-(--col3) border border-(--col5) rounded-lg h-full w-[calc(100%-16px)]'>
|
||||||
|
{showModInfo.description
|
||||||
|
? atob(showModInfo.description)
|
||||||
|
: '(No description added)'}
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-row h-fit w-full gap-2 justify-center my-2'>
|
||||||
|
{Object.keys(modsList).includes(
|
||||||
|
String(versionInfo?.game + '-' + showModInfo.id)
|
||||||
|
) ? (
|
||||||
|
<button
|
||||||
|
className='button btntheme2 w-fit'
|
||||||
|
disabled={downloadProgress.length != 0}
|
||||||
|
onClick={async () => {
|
||||||
|
const path =
|
||||||
|
'game/' +
|
||||||
|
versionInfo?.id +
|
||||||
|
'/BepInEx/plugins/' +
|
||||||
|
showModInfo.id
|
||||||
|
if (
|
||||||
|
await exists(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await remove(
|
||||||
|
(customDataLocation ? customDataLocation + '/' : '') +
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
baseDir: customDataLocation
|
||||||
|
? undefined
|
||||||
|
: BaseDirectory.AppLocalData,
|
||||||
|
recursive: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await versions?.set(
|
||||||
|
'mods',
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(modsList).filter(
|
||||||
|
([k]) =>
|
||||||
|
k !== versionInfo?.game + '-' + showModInfo.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} className='text-red-400' />{' '}
|
||||||
|
Uninstall
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className='button btntheme2 w-fit'
|
||||||
|
onClick={async () =>
|
||||||
|
await downloadVersions([
|
||||||
|
{
|
||||||
|
id: managingVersion,
|
||||||
|
type: 2,
|
||||||
|
modDownload: showModInfo.latestDownload,
|
||||||
|
gameId: versionInfo?.game,
|
||||||
|
modId: showModInfo.id,
|
||||||
|
modVersion: showModInfo.latestVersion
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
disabled={downloadProgress.length != 0}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faDownload}
|
||||||
|
className='text-green-400'
|
||||||
|
/>{' '}
|
||||||
|
Install
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className='button btntheme2 w-fit'
|
||||||
|
onClick={async () => {
|
||||||
|
await message(
|
||||||
|
!showModInfo.changelog
|
||||||
|
? '(No changelog provided)'
|
||||||
|
: atob(showModInfo?.changelog ?? ''),
|
||||||
|
{
|
||||||
|
title: 'Changelog for ' + showModInfo.name,
|
||||||
|
kind: 'info'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
color='lightgray'
|
||||||
|
/>{' '}
|
||||||
|
View changelog
|
||||||
|
</button>
|
||||||
|
{showModInfo.creators.length > 2 && (
|
||||||
|
<button
|
||||||
|
className='button btntheme2 w-fit'
|
||||||
|
onClick={async () => {
|
||||||
|
if (!versionInfo) return
|
||||||
|
await message(showModInfo.creators.join(', '), {
|
||||||
|
title: 'Creators for ' + showModInfo.name,
|
||||||
|
kind: 'info'
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPeopleGroup} color='lightgray' />{' '}
|
||||||
|
View all creators
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function VersionChangelogPopup () {
|
||||||
|
const { serverVersionList, managingVersion, developerMode } = useGlobal()
|
||||||
|
const [raw, setRaw] = useState<boolean>(false)
|
||||||
|
|
||||||
|
if (!managingVersion) return null
|
||||||
|
|
||||||
|
const versionInfo = serverVersionList?.versions.find(
|
||||||
|
vf => vf.id == managingVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{developerMode && (
|
||||||
|
<button
|
||||||
|
className='popup-top-button btntheme1 right-2'
|
||||||
|
onClick={() => setRaw(!raw)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={raw ? faEyeSlash : faEye} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<p className='text-xl text-center'>
|
||||||
|
Viewing changelog {versionInfo?.displayName}
|
||||||
|
</p>
|
||||||
|
<div className='popup-content h-full w-full p-2 select-text'>
|
||||||
|
{raw ? (
|
||||||
|
<>
|
||||||
|
<p>Raw: {versionInfo?.changelog}</p>
|
||||||
|
<p
|
||||||
|
className='whitespace-pre-line'
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html:
|
||||||
|
'Decoded: \n' +
|
||||||
|
DOMPurify.sanitize(atob(versionInfo?.changelog ?? ''))
|
||||||
|
}}
|
||||||
|
></p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
className='whitespace-pre-line'
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(
|
||||||
|
atob(versionInfo?.changelog ?? '')
|
||||||
|
.replace(/^- /gm, '• ')
|
||||||
|
.replaceAll(/`([^`]+)`/g, '<code>$1</code>')
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
></p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
import { faAdd, faInfo, faRemove } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
|
export default function VersionsDownloadPopup () {
|
||||||
|
const {
|
||||||
|
serverVersionList,
|
||||||
|
getSpecialVersionsList,
|
||||||
|
selectedVersionList,
|
||||||
|
setSelectedVersionList,
|
||||||
|
setManagingVersion,
|
||||||
|
setPopupMode,
|
||||||
|
selectedGame,
|
||||||
|
setViewingInfoFromDownloads,
|
||||||
|
downloadVersions,
|
||||||
|
developerMode
|
||||||
|
} = useGlobal()
|
||||||
|
|
||||||
|
const game = serverVersionList?.games.find(g => g.id === selectedGame)
|
||||||
|
|
||||||
|
if (!selectedGame || !game) return null
|
||||||
|
|
||||||
|
const list = getSpecialVersionsList(selectedGame)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className='text-xl text-center'>Select versions to download</p>
|
||||||
|
<div className='popup-content'>
|
||||||
|
{list.length != 0 ? (
|
||||||
|
list.map((v, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className='popup-entry grid items-center gap-3'
|
||||||
|
style={{ gridTemplateColumns: 'auto minmax(0, 1fr) auto' }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className='button btntheme3'
|
||||||
|
onClick={() => {
|
||||||
|
setManagingVersion(v.id)
|
||||||
|
setViewingInfoFromDownloads(true)
|
||||||
|
setPopupMode(2)
|
||||||
|
}}
|
||||||
|
title='Click to view version info.'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faInfo} /> Info
|
||||||
|
</button>
|
||||||
|
<p
|
||||||
|
className='text-2xl text-center truncate'
|
||||||
|
title={v.displayName}
|
||||||
|
>
|
||||||
|
{developerMode ? <span className='text-sm'>{v.id} </span> : ''}
|
||||||
|
{v.displayName}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className='button btntheme3'
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedVersionList(prev =>
|
||||||
|
prev.includes(v.id)
|
||||||
|
? prev.filter(i => i !== v.id)
|
||||||
|
: [...prev, v.id]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
selectedVersionList.includes(v.id)
|
||||||
|
? 'This version will be downloaded. Click to remove from the list of versions that will be downloaded.'
|
||||||
|
: 'This version will NOT be downloaded. Click to add from the list of versions that will be downloaded.'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectedVersionList.includes(v.id) ? (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={faRemove} /> Remove
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={faAdd} /> Add
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col gap-2 justify-center text-center w-full h-full'>
|
||||||
|
<p className='text-2xl'>No versions available to download</p>
|
||||||
|
<p>You may already have all versions installed for this game</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
<button
|
||||||
|
className='button btntheme1 w-fit mt-2 -mb-4'
|
||||||
|
onClick={async () => {
|
||||||
|
await downloadVersions(
|
||||||
|
selectedVersionList.map(versionId => ({
|
||||||
|
id: versionId,
|
||||||
|
type: 0
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
setPopupMode(1)
|
||||||
|
}}
|
||||||
|
disabled={selectedVersionList.length == 0}
|
||||||
|
title={`Download ${selectedVersionList.length} version${
|
||||||
|
selectedVersionList.length == 1 ? '' : 's'
|
||||||
|
} of ${game.name}.`}
|
||||||
|
>
|
||||||
|
Download {selectedVersionList.length} version
|
||||||
|
{selectedVersionList.length == 1 ? '' : 's'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='button btntheme1 w-fit mt-2 ml-2 -mb-4'
|
||||||
|
onClick={() => {
|
||||||
|
const allIds = list.map(v => v.id)
|
||||||
|
setSelectedVersionList(prev =>
|
||||||
|
prev.length === allIds.length ? [] : allIds
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
selectedVersionList.length === list.length
|
||||||
|
? 'Click to remove all selected versions for download.'
|
||||||
|
: 'Click to add all selected versions for download.'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectedVersionList.length === list.length
|
||||||
|
? 'Deselect All'
|
||||||
|
: 'Select All'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
ReactNode,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction
|
||||||
|
} from 'react'
|
||||||
|
import { DownloadProgress } from '@/types/DownloadProgress'
|
||||||
|
import { ServerVersionsResponse } from '@/types/ServerVersionsResponse'
|
||||||
|
import { Mod } from '@/types/Mod'
|
||||||
|
import { Store } from '@tauri-apps/plugin-store'
|
||||||
|
import { GameVersion } from '@/types/GameVersion'
|
||||||
|
|
||||||
|
type GlobalCtxType = {
|
||||||
|
serverVersionList: ServerVersionsResponse | null
|
||||||
|
setServerVersionList: Dispatch<SetStateAction<ServerVersionsResponse | null>>
|
||||||
|
selectedVersionList: string[]
|
||||||
|
setSelectedVersionList: (value: SetStateAction<string[]>) => void
|
||||||
|
downloadProgress: DownloadProgress[]
|
||||||
|
setDownloadProgress: Dispatch<SetStateAction<DownloadProgress[]>>
|
||||||
|
showPopup: boolean
|
||||||
|
setShowPopup: Dispatch<SetStateAction<boolean>>
|
||||||
|
popupMode: number | null
|
||||||
|
setPopupMode: Dispatch<SetStateAction<number | null>>
|
||||||
|
fadeOut: boolean
|
||||||
|
setFadeOut: Dispatch<SetStateAction<boolean>>
|
||||||
|
managingVersion: string | null
|
||||||
|
setManagingVersion: Dispatch<SetStateAction<string | null>>
|
||||||
|
setSelectedGame: Dispatch<SetStateAction<number | null>>
|
||||||
|
viewingInfoFromDownloads: boolean
|
||||||
|
version: string | null
|
||||||
|
downloadVersions: (
|
||||||
|
list: Array<{
|
||||||
|
id: string
|
||||||
|
type: 0 | 1 | 2
|
||||||
|
modDownload?: number
|
||||||
|
gameId?: number
|
||||||
|
modId?: number
|
||||||
|
modVersion?: string
|
||||||
|
}>
|
||||||
|
) => Promise<void>
|
||||||
|
category: number
|
||||||
|
setCategory: Dispatch<SetStateAction<number>>
|
||||||
|
downloadQueue: string[]
|
||||||
|
setDownloadQueue: Dispatch<SetStateAction<string[]>>
|
||||||
|
closePopup: () => void
|
||||||
|
selectedGame: number | null
|
||||||
|
setViewingInfoFromDownloads: Dispatch<SetStateAction<boolean>>
|
||||||
|
showModInfo: Mod | null
|
||||||
|
setShowModInfo: Dispatch<SetStateAction<Mod | null>>
|
||||||
|
getSpecialVersionsList: (game?: number) => GameVersion[]
|
||||||
|
settings: Store | null
|
||||||
|
versions: Store | null
|
||||||
|
account: Store | null
|
||||||
|
sidebarShowGames: boolean
|
||||||
|
linuxUseWine: boolean
|
||||||
|
linuxWineCommand: string
|
||||||
|
theme: string
|
||||||
|
customDataLocation: string
|
||||||
|
developerMode: boolean
|
||||||
|
versionsList: Record<string, number>
|
||||||
|
modsList: Record<string, Record<string, number>>
|
||||||
|
launchesList: Record<string, Record<string, number>>
|
||||||
|
accountSession: string | null
|
||||||
|
accountId: string | null
|
||||||
|
accountName: string | null
|
||||||
|
accountAdmin: boolean
|
||||||
|
managingGame: number | null
|
||||||
|
setManagingGame: Dispatch<SetStateAction<number | null>>
|
||||||
|
needsRevisionUpdate: (
|
||||||
|
lastRevision: number | undefined,
|
||||||
|
version: string
|
||||||
|
) => boolean
|
||||||
|
uninstallVersion: (version: string, updateList?: boolean) => Promise<void>
|
||||||
|
launchGame: (versionInfo: GameVersion) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const GlobalCtx = createContext<GlobalCtxType | null>(null)
|
||||||
|
|
||||||
|
export const useGlobal = () => {
|
||||||
|
const ctx = useContext(GlobalCtx)
|
||||||
|
if (!ctx) throw new Error('useGlobal must be inside GlobalProvider')
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalProvider = ({
|
||||||
|
children,
|
||||||
|
value
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
value: GlobalCtxType
|
||||||
|
}) => {
|
||||||
|
return <GlobalCtx.Provider value={value}>{children}</GlobalCtx.Provider>
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
@apply relative w-40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-btn {
|
||||||
|
@apply w-full flex items-center justify-between px-2 py-1 rounded-md cursor-pointer
|
||||||
|
bg-(--col2) hover:bg-(--col3)
|
||||||
|
border border-(--col4) hover:border-(--col5)
|
||||||
|
text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow {
|
||||||
|
@apply w-3 h-3 transition-transform stroke-current stroke-2 fill-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow.open {
|
||||||
|
@apply rotate-180;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
@apply absolute z-50 mt-1 w-full rounded-md shadow-2xl overflow-hidden
|
||||||
|
bg-(--col2) border border-(--col4)
|
||||||
|
origin-top
|
||||||
|
opacity-0 scale-95 -translate-y-1 pointer-events-none
|
||||||
|
transition-[opacity,transform] duration-150 ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu.open {
|
||||||
|
@apply opacity-100 scale-100 translate-y-0 pointer-events-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
@apply px-2 py-1 cursor-pointer text-white transition-colors border-b border-(--col4) last:border-b-0;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover,
|
||||||
|
.dropdown-item.active {
|
||||||
|
@apply bg-(--col3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu.open .dropdown-item {
|
||||||
|
animation: dropIn 220ms ease-out forwards;
|
||||||
|
animation-delay: calc(var(--i) * 40ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
html {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-(--col0) text-white select-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme {
|
||||||
|
--col0: rgb(8, 8, 8);
|
||||||
|
--col1: rgb(16, 16, 16);
|
||||||
|
--col2: rgb(32, 32, 32);
|
||||||
|
--col3: rgb(48, 48, 48);
|
||||||
|
--col4: rgb(64, 64, 64);
|
||||||
|
--col5: rgb(80, 80, 80);
|
||||||
|
--col6: rgb(96, 96, 96);
|
||||||
|
--col7: rgb(112, 112, 112);
|
||||||
|
--col8: rgb(128, 128, 128);
|
||||||
|
--col9: rgb(144, 144, 144);
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-theme {
|
||||||
|
--col0: rgb(32, 8, 8);
|
||||||
|
--col1: rgb(40, 16, 16);
|
||||||
|
--col2: rgb(56, 32, 32);
|
||||||
|
--col3: rgb(72, 48, 48);
|
||||||
|
--col4: rgb(88, 64, 64);
|
||||||
|
--col5: rgb(104, 80, 80);
|
||||||
|
--col6: rgb(120, 96, 96);
|
||||||
|
--col7: rgb(136, 112, 112);
|
||||||
|
--col8: rgb(152, 128, 128);
|
||||||
|
--col9: rgb(168, 144, 144);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue-theme {
|
||||||
|
--col0: rgb(8, 8, 32);
|
||||||
|
--col1: rgb(16, 16, 40);
|
||||||
|
--col2: rgb(32, 32, 56);
|
||||||
|
--col3: rgb(48, 48, 72);
|
||||||
|
--col4: rgb(64, 64, 88);
|
||||||
|
--col5: rgb(80, 80, 104);
|
||||||
|
--col6: rgb(96, 96, 120);
|
||||||
|
--col7: rgb(112, 112, 136);
|
||||||
|
--col8: rgb(128, 128, 152);
|
||||||
|
--col9: rgb(144, 144, 168);
|
||||||
|
}
|
||||||
|
|
||||||
|
.purple-theme {
|
||||||
|
--col0: rgb(20, 8, 32);
|
||||||
|
--col1: rgb(28, 16, 40);
|
||||||
|
--col2: rgb(44, 32, 56);
|
||||||
|
--col3: rgb(60, 48, 72);
|
||||||
|
--col4: rgb(76, 64, 88);
|
||||||
|
--col5: rgb(92, 80, 104);
|
||||||
|
--col6: rgb(108, 96, 120);
|
||||||
|
--col7: rgb(124, 112, 136);
|
||||||
|
--col8: rgb(140, 128, 152);
|
||||||
|
--col9: rgb(156, 144, 168);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
.input-field {
|
||||||
|
@apply rounded-md cursor-pointer text-[16px] py-1 px-2 transition-colors border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btntheme1 {
|
||||||
|
@apply bg-(--col2) border-(--col4) hover:bg-(--col4) hover:border-(--col6) disabled:text-gray-300 disabled:bg-(--col1) disabled:border-(--col5) disabled:hover:text-gray-100 disabled:hover:bg-(--col2) disabled:hover:border-(--col6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btntheme2,
|
||||||
|
.main-container .main-entry .entry-info-item,
|
||||||
|
.main-container .main-entry .button {
|
||||||
|
@apply bg-(--col3) border-(--col5) hover:bg-(--col5) hover:border-(--col7) disabled:text-gray-300 disabled:bg-(--col2) disabled:border-(--col6) disabled:hover:text-gray-100 disabled:hover:bg-(--col3) disabled:hover:border-(--col7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btntheme3,
|
||||||
|
.main-container .main-entry:hover .entry-info-item,
|
||||||
|
.main-container .main-entry:hover .button {
|
||||||
|
@apply bg-(--col4) border-(--col6) hover:bg-(--col6) hover:border-(--col8) disabled:text-gray-300 disabled:bg-(--col2) disabled:border-(--col7) disabled:hover:text-gray-100 disabled:hover:bg-(--col4) disabled:hover:border-(--col8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field.btntheme1 {
|
||||||
|
@apply focus:bg-(--col4) focus:border-(--col6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field.btntheme2 {
|
||||||
|
@apply focus:bg-(--col5) focus:border-(--col7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field.btntheme3 {
|
||||||
|
@apply focus:bg-(--col6) focus:border-(--col8);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
@apply w-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
@apply bg-(--col2) rounded-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-(--col4) w-1 rounded-lg active:bg-(--col5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
@apply bg-(--col1) border border-(--col3) rounded-lg overflow-y-auto w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container.main-flex {
|
||||||
|
@apply flex flex-col gap-2 p-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container .main-entry {
|
||||||
|
@apply flex justify-between items-center p-2 rounded-lg text-gray-200 text-lg transition-colors bg-(--col2) hover:bg-(--col3) border border-(--col4) hover:border-(--col5) cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-overlay {
|
||||||
|
@apply fixed w-screen h-screen z-99999 flex justify-center items-center animate-[fadeIn_0.2s_ease-out_forwards] left-0 top-0 bg-[rgba(0,0,0,0.5)];
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-overlay.fade-out {
|
||||||
|
@apply animate-[fadeOut_0.2s_ease-out_forwards];
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-box {
|
||||||
|
@apply relative w-150 h-120 rounded-lg bg-(--col1) border border-(--col3) flex flex-col p-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
@apply flex-1 overflow-auto bg-(--col2) border border-(--col4) rounded-lg mt-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-entry {
|
||||||
|
@apply relative h-fit bg-(--col3) m-2 p-2 rounded-lg border border-(--col5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-top-button {
|
||||||
|
@apply flex justify-center items-center absolute text-2xl text-gray-200 hover:text-white cursor-pointer h-12 w-12 p-3 rounded-lg top-2 transition-colors border;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
@apply outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-info-item {
|
||||||
|
@apply flex flex-row items-center gap-1 border text-gray-300 py-1 px-2 rounded-md w-fit text-[16px] transition-colors cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
@apply bg-(--col4) border border-(--col6) rounded-lg px-1 py-0.5 text-sm;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
.setting-checkbox-wrapper {
|
||||||
|
@apply relative w-5 h-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-checkbox {
|
||||||
|
@apply appearance-none w-full h-full border border-(--col4) rounded-sm bg-(--col2) transition-colors cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-checkbox:checked {
|
||||||
|
@apply bg-(--col4) border-(--col6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-checkbox-wrapper svg {
|
||||||
|
@apply absolute top-1/2 left-1/2 text-white text-[12px] pointer-events-none -translate-x-2/4 -translate-y-2/4;
|
||||||
|
}
|
||||||
@@ -5,24 +5,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-downloads {
|
.sidebar-downloads {
|
||||||
@apply text-[#bdbdbd] fixed bottom-3 left-2 bg-(--col2) rounded-lg border border-(--col3) w-55 p-4 cursor-pointer transition-colors duration-[0.25s];
|
@apply text-[#bdbdbd] fixed bottom-2 left-2 bg-(--col2) rounded-lg border border-(--col3) px-4 py-3 text-xl cursor-pointer transition-colors opacity-75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-downloads:hover {
|
.sidebar-downloads:hover {
|
||||||
@apply text-white;
|
@apply text-white opacity-100;
|
||||||
@apply bg-(--col3) border-(--col4);
|
@apply bg-(--col3) border-(--col4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
|
||||||
@apply text-2xl font-bold p-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links {
|
.nav-links {
|
||||||
@apply flex flex-col p-4 space-y-1 overflow-y-auto overflow-x-hidden;
|
@apply flex flex-col px-4 space-y-1 pt-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
@apply text-[#bdbdbd] p-2 rounded-md no-underline cursor-pointer transition-colors duration-[0.25s] border border-transparent;
|
@apply text-[#bdbdbd] p-2 rounded-md no-underline cursor-pointer transition-colors border border-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link.active {
|
.link.active {
|
||||||
@@ -37,3 +33,11 @@
|
|||||||
.link.active:hover {
|
.link.active:hover {
|
||||||
@apply bg-(--col3) border-(--col4);
|
@apply bg-(--col3) border-(--col4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar ::-webkit-scrollbar {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.macos-drag {
|
||||||
|
@apply h-7 fixed top-0 left-0 w-60 -z-10;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export class DownloadProgress {
|
||||||
|
constructor (
|
||||||
|
public version: string,
|
||||||
|
public progress: number,
|
||||||
|
public progressBytes: number,
|
||||||
|
public failed: boolean,
|
||||||
|
public downloading: boolean,
|
||||||
|
public paused: boolean,
|
||||||
|
public canceled: boolean,
|
||||||
|
public queued: boolean,
|
||||||
|
public hash_checking: boolean,
|
||||||
|
public speed: number,
|
||||||
|
public size: number,
|
||||||
|
public etaSecs: number,
|
||||||
|
public unzipping: boolean,
|
||||||
|
public unzipTotal: number,
|
||||||
|
public unzipped: number,
|
||||||
|
public url: string | null,
|
||||||
|
public executable: string | null,
|
||||||
|
public hash: string | null,
|
||||||
|
public type: 0 | 1 | 2,
|
||||||
|
public modDownload: number | null,
|
||||||
|
public modGame: number | null,
|
||||||
|
public modId: number | null,
|
||||||
|
public modVersion: string | null
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -4,5 +4,5 @@ export interface Game {
|
|||||||
official: boolean
|
official: boolean
|
||||||
verified: boolean
|
verified: boolean
|
||||||
developer: string | null
|
developer: string | null
|
||||||
cutOff: number | null
|
categoryNames: Record<string, string>
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export interface GameVersion {
|
||||||
|
id: string
|
||||||
|
displayName: string
|
||||||
|
releaseDate: number
|
||||||
|
game: number
|
||||||
|
place: number
|
||||||
|
changelog: string
|
||||||
|
wine: number | undefined
|
||||||
|
category: number
|
||||||
|
lastRevision: number
|
||||||
|
download: number | undefined
|
||||||
|
executable: string
|
||||||
|
modSupportDownload: number | undefined
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export interface Mod {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string | null
|
||||||
|
creators: string[]
|
||||||
|
downloads: number
|
||||||
|
released: number
|
||||||
|
updated: number
|
||||||
|
latestVersion: string
|
||||||
|
latestDownload: number
|
||||||
|
changelog: string | null
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Game } from "./Game"
|
import { Game } from './Game'
|
||||||
import { GameVersion } from "./GameVersion"
|
import { GameVersion } from './GameVersion'
|
||||||
|
|
||||||
export interface ServerVersionsResponse {
|
export interface ServerVersionsResponse {
|
||||||
versions: GameVersion[]
|
versions: GameVersion[]
|
||||||
@@ -3,4 +3,6 @@ export type SettingProps = {
|
|||||||
value: boolean
|
value: boolean
|
||||||
onChange: (val: boolean) => void
|
onChange: (val: boolean) => void
|
||||||
className?: string
|
className?: string
|
||||||
|
title?: string
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "es2024",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -28,7 +28,9 @@
|
|||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"**/*.mts"
|
"**/*.mts",
|
||||||
|
"next.config.ts",
|
||||||
|
"eslint.config.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||