You've already forked AstralRinth
forked from didirus/AstralRinth
Compare commits
624 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45519f5dbb | |||
| 3843ed6690 | |||
|
|
53ec2c5306 | ||
|
|
cace1a54cd | ||
|
|
803c17de31 | ||
|
|
537eadef0c | ||
|
|
39f2b0ecb6 | ||
|
|
1e9e13aebb | ||
|
|
67835b04a8 | ||
|
|
3f93041ca2 | ||
|
|
0663b8adb0 | ||
|
|
1f48f5b5af | ||
|
|
0268600044 | ||
|
|
8fb38ba0f2 | ||
|
|
85c65e697d | ||
|
|
563997e060 | ||
|
|
2d5568ecec | ||
|
|
a64c4201bb | ||
|
|
51d5ed771c | ||
|
|
539132a527 | ||
|
|
9958600121 | ||
|
|
9ad01723a2 | ||
|
|
8448bacae7 | ||
|
|
c21e98a2a8 | ||
|
|
5bbc3872f3 | ||
|
|
8d894541e8 | ||
|
|
dc16a65b62 | ||
|
|
514c6f6e34 | ||
|
|
609e3896eb | ||
|
|
fd08dff1e7 | ||
|
|
6425ab8c57 | ||
|
|
e123e51c66 | ||
|
|
21fad12a21 | ||
|
|
924a77eb3f | ||
|
|
7aaf99a0c8 | ||
|
|
91accd5578 | ||
|
|
147f19f11e | ||
|
|
73ff6df73c | ||
|
|
0de780b7c9 | ||
|
|
f49f889536 | ||
|
|
b3f598aa1d | ||
|
|
cd1b5dcd3d | ||
|
|
79b7d269b0 | ||
|
|
40ac726930 | ||
|
|
ddcc14d99f | ||
|
|
3dd2de5f18 | ||
|
|
0a8f489234 | ||
|
|
1d64b2e22a | ||
|
|
251e89fe5a | ||
|
|
4fbbc2b1cf | ||
|
|
d5b7ac3542 | ||
|
|
fec395a4cf | ||
|
|
16c0dadc4a | ||
|
|
779092c0b7 | ||
|
|
9aa06fbc26 | ||
|
|
cfd2977c21 | ||
|
|
27fc0796a4 | ||
|
|
b1438bd460 | ||
|
|
267e0cb636 | ||
|
|
d471ef6763 | ||
|
|
cea5cfa4ab | ||
|
|
56356e8260 | ||
|
|
41e4086973 | ||
|
|
0f1f27d450 | ||
|
|
a558064f9d | ||
|
|
c421249767 | ||
|
|
8eff939039 | ||
|
|
e3444a3456 | ||
|
|
16a6f7b352 | ||
|
|
79c2633011 | ||
|
|
783aaa6553 | ||
| ddf51c9596 | |||
| a63b6b27d5 | |||
|
|
60e0953616 | ||
|
|
f7c86f9fc9 | ||
| cac3b46652 | |||
|
|
fe684ab903 | ||
|
|
8592761493 | ||
|
|
dfe087df20 | ||
| 82119a9fc9 | |||
| b9ec1b42dc | |||
| 7345fa401b | |||
|
|
be3208c5a1 | ||
|
|
b56f39ce07 | ||
|
|
0178fddc38 | ||
|
|
31417a2aa1 | ||
|
|
f333a75221 | ||
|
|
bcf14a4c51 | ||
|
|
130c2863ab | ||
|
|
e59664426b | ||
|
|
2f0ef07944 | ||
|
|
9af19d01e5 | ||
|
|
e837d9fa30 | ||
|
|
93b79759c7 | ||
|
|
4becb2a822 | ||
|
|
134a621d0d | ||
|
|
089cca60ce | ||
|
|
20484ed7aa | ||
|
|
763a38812f | ||
|
|
7ccc32675b | ||
|
|
26feaf753a | ||
|
|
94c0003c19 | ||
|
|
c27f787c91 | ||
|
|
70e2138248 | ||
|
|
590ba3ce55 | ||
|
|
29671347a0 | ||
|
|
386e6e50da | ||
|
|
880ed21bcd | ||
|
|
bbc31ef077 | ||
|
|
9a13e977a0 | ||
|
|
a5602ff18c | ||
|
|
5901c5a535 | ||
|
|
cca1dd7e37 | ||
|
|
127e01cc96 | ||
|
|
1dcb38cb57 | ||
|
|
98b4970680 | ||
|
|
9706f1597b | ||
|
|
f8a5a77daa | ||
|
|
1efdceacfd | ||
|
|
b998c71337 | ||
|
|
6a6adb3480 | ||
|
|
a694aeed32 | ||
|
|
8182b795de | ||
|
|
608ab988f0 | ||
|
|
a261598e89 | ||
|
|
11a1918a2e | ||
|
|
67fb825937 | ||
|
|
4289f8b52d | ||
|
|
fb1ba51a2b | ||
|
|
cb47bc97c7 | ||
|
|
06e1bc9dd6 | ||
|
|
af39a1769c | ||
|
|
60ffa75653 | ||
|
|
7674433f88 | ||
|
|
7437a833ef | ||
|
|
1bad1a57b0 | ||
|
|
3437387885 | ||
|
|
23d098eee5 | ||
|
|
4636372ff4 | ||
|
|
4592786de8 | ||
|
|
f054f39c5d | ||
|
|
6e47de06bb | ||
|
|
c38751a38a | ||
|
|
2d218d79c6 | ||
|
|
5a41a35716 | ||
|
|
644554f1e9 | ||
|
|
3765a6ded8 | ||
|
|
92698e4bb5 | ||
|
|
17f395ee55 | ||
|
|
b11934054d | ||
|
|
40cbe92dbc | ||
| 9139c23469 | |||
| 932f4ce662 | |||
| 1fbd39c920 | |||
| 27abe2b42f | |||
| ece15a97a0 | |||
| 97a9c24768 | |||
|
|
b7f0988399 | ||
|
|
4c1020d2ba | ||
|
|
00f9cf0e2c | ||
|
|
1dd7e3bcdc | ||
|
|
3ac3122b31 | ||
|
|
6b5f8a41e7 | ||
|
|
8b39ba491a | ||
|
|
c74460fffa | ||
|
|
5000c4067b | ||
|
|
af33950bbe | ||
|
|
075331b26c | ||
|
|
f31b74f7fd | ||
|
|
bcc36362be | ||
|
|
632b27dc21 | ||
|
|
cf6f3736eb | ||
|
|
aaaef8f39e | ||
|
|
3f8dd1a79c | ||
|
|
363f47f269 | ||
|
|
a0f23a2bca | ||
|
|
08e316a2b2 | ||
|
|
9aaf5fb87e | ||
|
|
bcca66b12c | ||
|
|
ccb24ce8eb | ||
|
|
5dd6c804d0 | ||
|
|
ab886a5ea8 | ||
|
|
03b0eba695 | ||
|
|
707ff2146b | ||
|
|
8d80433c2c | ||
|
|
a547f7a9b0 | ||
|
|
f78fbe3215 | ||
|
|
f375913c62 | ||
|
|
a4015d9df3 | ||
|
|
977de0e18a | ||
|
|
c379e4b173 | ||
|
|
eeed4e572d | ||
|
|
79502a19d6 | ||
|
|
3dbfd69bdd | ||
|
|
19393a38bb | ||
|
|
859d7f57cf | ||
|
|
24bec6baba | ||
|
|
63d8f70e20 | ||
|
|
8a30b7978d | ||
|
|
4a9f0b8a0e | ||
|
|
0e17427a58 | ||
|
|
ad3b5aec69 | ||
|
|
4b17eb5d35 | ||
|
|
6a70acef25 | ||
|
|
e58456eed4 | ||
|
|
12940fc207 | ||
|
|
7796273529 | ||
| 7cc9d8183d | |||
| 231e95792e | |||
| 905eae8403 | |||
| 868fda1703 | |||
| 4b86c4ee8a | |||
|
|
752f68124c | ||
|
|
699a049c69 | ||
| 8e720ccef5 | |||
| 03b49284e1 | |||
| ac6c26a5f9 | |||
| cc963cfc40 | |||
|
|
fa7d1d7942 | ||
|
|
d1ffed564d | ||
|
|
e719ae2f42 | ||
|
|
5db5bf4c4c | ||
|
|
b23d3e674f | ||
|
|
75e3994c6e | ||
|
|
71e28e1ea5 | ||
|
|
6dbd1e5236 | ||
|
|
77afdb1cc4 | ||
|
|
7fa442fb28 | ||
|
|
2535156dac | ||
|
|
8972c9a198 | ||
|
|
03ed64c99f | ||
|
|
4cd8ccd319 | ||
|
|
ea594ec27c | ||
|
|
2a61916d1e | ||
|
|
e66b131a5d | ||
|
|
0c66fa3f12 | ||
|
|
aec49cff7c | ||
|
|
c88bdda3e6 | ||
|
|
1a72d55e2d | ||
|
|
55eae7ec7e | ||
|
|
351b3da337 | ||
|
|
9ee0626e8b | ||
|
|
e9735bd9ba | ||
|
|
15a7815ec3 | ||
|
|
6919c8dea9 | ||
|
|
f32558cf97 | ||
|
|
ad705fa66f | ||
|
|
87f8773401 | ||
|
|
3c578108de | ||
|
|
cb5600ad45 | ||
|
|
59e48ea2b1 | ||
|
|
7658e1c653 | ||
|
|
9589e23118 | ||
|
|
dbc64afe48 | ||
|
|
7e682c22bb | ||
|
|
fd80f1217d | ||
|
|
d98394d8d5 | ||
|
|
e303655727 | ||
|
|
95de8977d4 | ||
|
|
92e91a0606 | ||
|
|
98269842f3 | ||
|
|
ab6e9dd5d7 | ||
|
|
a13647b9e2 | ||
|
|
2af7ecc077 | ||
|
|
bea0ba017c | ||
|
|
f874856452 | ||
|
|
b96c5cd5ab | ||
|
|
7e84659249 | ||
|
|
24504cb94d | ||
|
|
6fe4235358 | ||
|
|
04f0f53104 | ||
|
|
c169b48228 | ||
|
|
beff2fcaa9 | ||
|
|
9315af9b20 | ||
|
|
4d11dc821b | ||
|
|
8fd40f46c5 | ||
|
|
28e9f017e3 | ||
|
|
beb1bdb31f | ||
|
|
895b040ad7 | ||
|
|
54747aa628 | ||
|
|
53c9699b46 | ||
|
|
671fd22389 | ||
|
|
bddc40e601 | ||
|
|
324ad65d7c | ||
|
|
7eace32d93 | ||
|
|
a5108ecc5d | ||
|
|
a538b99c18 | ||
|
|
f6f66a313f | ||
|
|
d5f756fd86 | ||
|
|
b4eba5a0d5 | ||
|
|
d418eaee12 | ||
|
|
f466470d06 | ||
|
|
3f55711f9e | ||
|
|
bb9ce52c9d | ||
|
|
14af3d0763 | ||
|
|
d43451e398 | ||
|
|
2492b11ec0 | ||
|
|
4228a193e9 | ||
|
|
47020f34b6 | ||
|
|
5c00cb06f1 | ||
|
|
e6edf07eae | ||
|
|
5d7bd3b177 | ||
|
|
71d63fbe17 | ||
|
|
f33efed91b | ||
|
|
d41b31c775 | ||
|
|
20281c4efc | ||
|
|
afcdb1d0a1 | ||
|
|
f3060cd9b4 | ||
|
|
1a1b9f54df | ||
|
|
716f293e8e | ||
|
|
f5825f1065 | ||
|
|
5b44454e18 | ||
|
|
b425c66832 | ||
|
|
0b8762cd0a | ||
|
|
ff50964f25 | ||
|
|
36d0760a3e | ||
|
|
4def0e8407 | ||
|
|
6da190ed01 | ||
|
|
8149618187 | ||
|
|
902d749293 | ||
|
|
1491642209 | ||
|
|
7bc2c1dd4d | ||
|
|
9f11759292 | ||
|
|
cef425b6be | ||
|
|
3fc55184a7 | ||
|
|
67e090565e | ||
|
|
d8d9720495 | ||
|
|
9361acb78e | ||
|
|
58aac642a9 | ||
|
|
af3b829449 | ||
|
|
567e31401d | ||
|
|
b95ece04c4 | ||
|
|
3dfd035b50 | ||
|
|
01b19424cd | ||
|
|
cb3130f998 | ||
|
|
4d3e1ade67 | ||
|
|
1b33a3619f | ||
|
|
ea607c1a04 | ||
|
|
fda06cfc60 | ||
|
|
0d61945956 | ||
|
|
c017038f71 | ||
|
|
a323bf6c25 | ||
|
|
e2f07a7848 | ||
|
|
0511a14bd9 | ||
|
|
8f8a4af9eb | ||
|
|
9ed094a1e7 | ||
|
|
aa6de3cc80 | ||
|
|
f5aece1fb1 | ||
|
|
79aa41fd7a | ||
|
|
bd918c7616 | ||
|
|
d23b925bb9 | ||
|
|
8aaddb9d8a | ||
|
|
f48eaee336 | ||
|
|
749fd32307 | ||
|
|
2e95a8a117 | ||
|
|
2194ae774c | ||
|
|
052637d402 | ||
|
|
c1a092e55c | ||
|
|
bd3342badf | ||
|
|
d832ca1e5a | ||
|
|
5b7f025094 | ||
|
|
d0c67b368a | ||
|
|
c43d359561 | ||
|
|
8b2a89d4e0 | ||
|
|
8aede4e082 | ||
|
|
3d80201112 | ||
|
|
8d14f34994 | ||
|
|
6f34130633 | ||
|
|
5a699eec22 | ||
|
|
9fa490aa6a | ||
|
|
d119b301d0 | ||
|
|
15c31f04a3 | ||
|
|
48e5319134 | ||
|
|
8058993578 | ||
|
|
28337c88f6 | ||
|
|
a6d08e9d50 | ||
|
|
7943f77655 | ||
|
|
dc4ef332f8 | ||
|
|
652f2e241f | ||
|
|
5fd27bcb65 | ||
|
|
8fa01b937d | ||
|
|
8b98087936 | ||
|
|
7afe35a6cd | ||
|
|
debaf1381c | ||
|
|
697468e910 | ||
|
|
46c325f78a | ||
|
|
0ac42344e7 | ||
|
|
df261dad95 | ||
|
|
d30643b5a0 | ||
|
|
ab95dcf951 | ||
|
|
ab539a313f | ||
|
|
a2c07c92f8 | ||
|
|
0925abfd1c | ||
|
|
8cf42471a3 | ||
|
|
006b19e3c9 | ||
|
|
ca36d11570 | ||
|
|
c612c8b009 | ||
|
|
f9cf3d5ef9 | ||
|
|
e7d933411e | ||
|
|
44cbbd9ed7 | ||
|
|
87dbb6dcbc | ||
|
|
3d1cafdcec | ||
|
|
e114c7466e | ||
|
|
20059e6cf0 | ||
|
|
6b10b4d30b | ||
|
|
a47dde972c | ||
|
|
e8b0c9df4c | ||
|
|
b8bc2c4cb6 | ||
|
|
328500d381 | ||
|
|
f56672fb68 | ||
|
|
d3459e4b12 | ||
|
|
07703e49ef | ||
|
|
08011161c8 | ||
|
|
9b29694907 | ||
|
|
805c0b86a5 | ||
|
|
d19bf82cb1 | ||
|
|
2e6cff7efc | ||
|
|
b2ff2d8737 | ||
|
|
eaa4b44a16 | ||
|
|
76d0ef03e7 | ||
|
|
ee8c47adcb | ||
|
|
5d3ca3ba02 | ||
|
|
235717b01c | ||
|
|
518f7adafb | ||
|
|
490b994d7b | ||
|
|
14eac461be | ||
|
|
9af1391e0e | ||
|
|
bcfa6941e4 | ||
|
|
5ffe14f058 | ||
|
|
166d14e7e1 | ||
|
|
b03d754a57 | ||
|
|
674f29959d | ||
|
|
3e735b99eb | ||
|
|
74d2d85cb5 | ||
| 3a92adfb82 | |||
| af4c627a04 | |||
| 1e725e6d03 | |||
|
|
1454e3351e | ||
|
|
6f59f4c110 | ||
|
|
8e0732bf01 | ||
|
|
0cf3c1a88e | ||
|
|
8a3171d7c4 | ||
|
|
e25d726da4 | ||
|
|
11e99cb9d3 | ||
|
|
632b09ff3f | ||
|
|
713571d50e | ||
|
|
4ad6daa45c | ||
|
|
9b5f172170 | ||
|
|
4f789a0ebc | ||
|
|
ee3ac37967 | ||
|
|
2aabcf36ee | ||
|
|
82697278dc | ||
|
|
0bc6502443 | ||
|
|
5ffcc48d75 | ||
|
|
b81e727204 | ||
|
|
9ea43a12fd | ||
|
|
b279c43069 | ||
|
|
9497ba70a4 | ||
|
|
c02b809601 | ||
| 1d000bb238 | |||
|
|
df1499047c | ||
|
|
80eb297284 | ||
|
|
58645b9ba9 | ||
|
|
544f63512a | ||
|
|
3b8cd661bc | ||
|
|
8af65f58d9 | ||
|
|
ab79e84398 | ||
|
|
cf190d86d5 | ||
|
|
ca0c16b1fe | ||
|
|
17c9e4a721 | ||
|
|
d7f1029b54 | ||
|
|
ad208536b0 | ||
| 553db55c7b | |||
|
|
d22c9e24f4 | ||
|
|
e31197f649 | ||
|
|
0dee21814d | ||
|
|
0657e4466f | ||
|
|
13dbb4c57e | ||
| 4c6290ead6 | |||
|
|
99493b9917 | ||
|
|
72a52eb7b1 | ||
|
|
b33e12c71d | ||
|
|
82d86839c7 | ||
|
|
3a20e15340 | ||
|
|
1c89b84314 | ||
| 8d36c14554 | |||
|
|
6387fb21c6 | ||
|
|
c7d0839bfb | ||
| 2b43e26a85 | |||
|
|
175b90be5a | ||
|
|
13103b4950 | ||
|
|
8804478221 | ||
|
|
b8982a6d17 | ||
|
|
ff88724d01 | ||
|
|
7dffb352d5 | ||
|
|
1df6e29aa1 | ||
|
|
5deb4179ad | ||
|
|
358cf31c87 | ||
| 7cea4b21a8 | |||
| 7846fd00aa | |||
| cebc195fe0 | |||
|
|
6db1d66591 | ||
|
|
8052fda840 | ||
| ae58f3844d | |||
| acd4b1696a | |||
| 5ea78b78c2 | |||
| f90998157d | |||
| 634000cdb6 | |||
|
|
5fd8c38c1c | ||
|
|
15892a88d3 | ||
|
|
32793c50e1 | ||
|
|
0e0ca1971a | ||
|
|
bb9af18eed | ||
|
|
d4516d3527 | ||
|
|
87de47fe5e | ||
|
|
7d76fe1b6a | ||
| 46d30e491a | |||
| 059c0618f1 | |||
| 7ef60fcafe | |||
| ec17e79014 | |||
| e351d674f4 | |||
| f555fa916a | |||
| dbe38cb4e7 | |||
| 2e40e26116 | |||
|
|
ae25a15abd | ||
|
|
0f755b94ce | ||
|
|
bcf46d440b | ||
|
|
526561f2de | ||
|
|
a8caa1afc3 | ||
|
|
98e9a8473d | ||
|
|
936395484e | ||
|
|
0c3e23db96 | ||
|
|
013ba4d86d | ||
|
|
93813c448c | ||
|
|
c20b869e62 | ||
|
|
56c556821b | ||
|
|
44267619b6 | ||
| 10afd673db | |||
|
|
90043fe84d | ||
|
|
a6a98ff63e | ||
|
|
911652133b | ||
|
|
cee1b5f522 | ||
|
|
62f5a23fcb | ||
| 5a10292add | |||
| 3f606a08aa | |||
| 2d5d747202 | |||
|
|
eb595cdc3e | ||
| 7516ff9e47 | |||
|
|
572cd065ed | ||
| df9bbe3ba0 | |||
| 362fd7f32a | |||
|
|
76dc8a0897 | ||
|
|
4723de6269 | ||
|
|
e15fa35bad | ||
| 7716a0c524 | |||
|
|
2cc6bc8ce4 | ||
|
|
5d19d31b2c | ||
|
|
c1b95ede07 | ||
|
|
058185c7fd | ||
|
|
6fb125cf0f | ||
|
|
a945e9b005 | ||
|
|
b943638afb | ||
|
|
207dc0e2bb | ||
|
|
359fbd4738 | ||
| adf831dab9 | |||
| efeac22d14 | |||
| 591d98a9eb | |||
| 77472d9a09 | |||
| 789d666515 | |||
| d917bff6ef | |||
| 4e69cd8bde | |||
| b71e4cc6f9 | |||
| a56ab6adb9 | |||
| f1b67c9584 | |||
| 3d32640b83 | |||
| 6dfb599e14 | |||
| 332a543f66 | |||
| 1ef96c447e | |||
| 1ec92b5f97 | |||
| f0a4532051 | |||
|
|
f7700acce4 | ||
|
|
87a3e2d022 | ||
|
|
5d17663040 | ||
|
|
cff3c72f94 | ||
|
|
fadf475f06 | ||
|
|
7228499737 | ||
|
|
bca467a634 | ||
| 14f6450cf4 | |||
| 14bf06e4bd | |||
|
|
cb72d2ac80 | ||
|
|
3c79607d1f | ||
| 97bd18c7b3 | |||
| 8af0288274 | |||
| 167072de0c | |||
| 2df37be9a7 | |||
| 34d85a03b2 | |||
| 17cf5e3132 | |||
|
|
36ad1f16e4 | ||
|
|
5d4f334505 | ||
|
|
1fdb5ba748 | ||
| c5e67a5c6f | |||
| e2e21c1496 | |||
| 6da942ccbb | |||
|
|
26df6f51ef | ||
|
|
6caf794ae1 | ||
|
|
2692953e31 | ||
|
|
242fd713ab | ||
|
|
7a12c4d5e2 | ||
| 0ab4dec62d | |||
|
|
f256ef43c0 | ||
| 3ecb20afd6 | |||
| 1e10f24efe | |||
| 006fd7c7f5 | |||
| 1e8e001eb8 | |||
| 585935c799 | |||
|
|
e0cde2d6ff | ||
| a64c3360d2 | |||
|
|
e4e77dc0d2 | ||
|
|
8ba6467f21 | ||
| a2b2711204 | |||
|
|
088cb54317 | ||
| ab57926e44 | |||
| 35cd79727a | |||
|
|
c47bcf665d |
@@ -1,6 +1,6 @@
|
|||||||
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
||||||
[target.'cfg(windows)']
|
[target.'cfg(windows)']
|
||||||
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||||
|
|
||||||
[build]
|
[target.x86_64-pc-windows-msvc]
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
linker = "rust-lld"
|
||||||
|
|||||||
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
.gitignore
|
||||||
@@ -3,16 +3,23 @@ root = true
|
|||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = tab
|
||||||
indent_size = 2
|
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
max_line_length = 100
|
max_line_length = 100
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
|
indent_size = 2
|
||||||
max_line_length = off
|
max_line_length = off
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[*.{rs,java,kts}]
|
[*.{toml,json}]
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
|
|
||||||
|
# YAML requires space indentation by spec
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
indent_style = space
|
||||||
|
|||||||
63
.github/ISSUE_TEMPLATE/3-servers-bug.yml
vendored
Normal file
63
.github/ISSUE_TEMPLATE/3-servers-bug.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
name: 👥 Bug with Modrinth Servers
|
||||||
|
description: For issues with a Modrinth Servers product.
|
||||||
|
labels: [servers]
|
||||||
|
type: 'bug'
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Please confirm the following.
|
||||||
|
options:
|
||||||
|
- label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) for duplicate problems
|
||||||
|
required: true
|
||||||
|
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: issue-location
|
||||||
|
attributes:
|
||||||
|
label: Is this an issue in the control panel or with the Minecraft server itself?
|
||||||
|
options:
|
||||||
|
- Control panel (on Modrinth.com)
|
||||||
|
- Minecraft server
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: browsers
|
||||||
|
attributes:
|
||||||
|
label: What browsers are you seeing the problem on? (if a panel issue)
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- N/A
|
||||||
|
- Chrome (including Arc, Brave, Opera, Vivaldi)
|
||||||
|
- Microsoft Edge
|
||||||
|
- Firefox
|
||||||
|
- Safari
|
||||||
|
- Other (please specify)
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: A clear and concise description of what the bug is. Include screenshots if applicable.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Steps to reproduce the behavior.
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: A clear and concise description of what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
14
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,14 +1,8 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 🫶 Support Portal
|
- name: 🫶 Support portal
|
||||||
about: Get support using through our portal.
|
about: Get support using through our support website.
|
||||||
url: https://support.modrinth.com
|
url: https://support.modrinth.com
|
||||||
- name: 💬 Chat
|
- name: 💬 Chat on Discord
|
||||||
about: Join our Discord server to chat about Modrinth.
|
about: Join our Discord server to chat about Modrinth.
|
||||||
url: https://discord.modrinth.com
|
url: https://discord.modrinth.com
|
||||||
- name: 🛣️ Roadmap
|
|
||||||
about: View our Roadmap. Please do not open issues for items on our roadmap.
|
|
||||||
url: https://roadmap.modrinth.com
|
|
||||||
- name: 📚 Documentation
|
|
||||||
about: Useful documentation about Modrinth's API
|
|
||||||
url: https://docs.modrinth.com
|
|
||||||
|
|||||||
72
.github/instructions/i18n-convert.instructions.md
vendored
Normal file
72
.github/instructions/i18n-convert.instructions.md
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
applyTo: '**/*.vue'
|
||||||
|
---
|
||||||
|
|
||||||
|
You are given a Nuxt/Vue single-file component (.vue). Your task is to convert every hard-coded natural-language string in the <template> into our localization system using @vintl/vintl-nuxt (which wraps FormatJS).
|
||||||
|
|
||||||
|
Please follow these rules precisely:
|
||||||
|
|
||||||
|
1. Identify translatable strings
|
||||||
|
|
||||||
|
- Scan the <template> for all user-visible strings (inner text, alt attributes, placeholders, button labels, etc.). Do not extract dynamic expressions (like {{ user.name }}) or HTML tags. Only extract static human-readable text.
|
||||||
|
- There may be strings within the <script> block, e.g dropdown option labels, notifications etc.
|
||||||
|
|
||||||
|
2. Create message definitions
|
||||||
|
|
||||||
|
- In the <script setup> block, import `defineMessage` or `defineMessages` from `@vintl/vintl`.
|
||||||
|
- For each extracted string, define a message with a unique `id` (use a descriptive prefix based on the component path, e.g. `auth.welcome.long-title`) and a `defaultMessage` equal to the original English string.
|
||||||
|
Example:
|
||||||
|
const messages = defineMessages({
|
||||||
|
welcomeTitle: { id: 'auth.welcome.title', defaultMessage: 'Welcome' },
|
||||||
|
welcomeDescription: { id: 'auth.welcome.description', defaultMessage: 'You’re now part of the community…' },
|
||||||
|
})
|
||||||
|
|
||||||
|
3. Handle variables and ICU formats
|
||||||
|
|
||||||
|
- Replace dynamic parts with ICU placeholders: "Hello, ${user.name}!" → `{name}` and defaultMessage: 'Hello, {name}!'
|
||||||
|
- For numbers/dates/times, use ICU/FormatJS options (e.g., currency): `{price, number, ::currency/USD}`
|
||||||
|
- For plurals/selects, use ICU: `'{count, plural, one {# message} other {# messages}}'`
|
||||||
|
|
||||||
|
4. Rich-text messages (links/markup)
|
||||||
|
|
||||||
|
- In `defaultMessage`, wrap link/markup ranges with tags, e.g.:
|
||||||
|
"By creating an account, you agree to our <terms-link>Terms</terms-link> and <privacy-link>Privacy Policy</privacy-link>."
|
||||||
|
- Render rich-text messages with `<IntlFormatted>` from `@vintl/vintl/components` and map tags via `values`:
|
||||||
|
<IntlFormatted
|
||||||
|
:message="messages.tosLabel"
|
||||||
|
:values="{
|
||||||
|
'terms-link': (chunks) => <NuxtLink to='/terms'>{chunks}</NuxtLink>,
|
||||||
|
'privacy-link': (chunks) => <NuxtLink to='/privacy'>{chunks}</NuxtLink>,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
- For simple emphasis: `'Welcome to <strong>Modrinth</strong>!'` and map `'strong': (c) => <strong>{c}</strong>`
|
||||||
|
|
||||||
|
5. Formatting in templates
|
||||||
|
|
||||||
|
- Import and use `useVIntl()`; prefer `formatMessage` for simple strings:
|
||||||
|
`const { formatMessage } = useVIntl()`
|
||||||
|
`<button>{{ formatMessage(messages.welcomeTitle) }}</button>`
|
||||||
|
- Vue methods like `$formatMessage`, `$formatNumber`, `$formatDate` are also available if needed.
|
||||||
|
|
||||||
|
6. Naming conventions and id stability
|
||||||
|
|
||||||
|
- Make `id`s descriptive and stable (e.g., `error.generic.default.title`). Group related messages with `defineMessages`.
|
||||||
|
|
||||||
|
7. Avoid Vue/ICU delimiter collisions
|
||||||
|
|
||||||
|
- If an ICU placeholder would end right before `}}` in a Vue template, insert a space so it becomes `} }` to avoid parsing issues.
|
||||||
|
|
||||||
|
8. Update imports and remove literals
|
||||||
|
|
||||||
|
- Ensure imports for `defineMessage`/`defineMessages`, `useVIntl`, and `<IntlFormatted>` are present. Replace all hard-coded strings with `formatMessage(...)` or `<IntlFormatted>` and remove the literals.
|
||||||
|
|
||||||
|
9. Preserve functionality
|
||||||
|
|
||||||
|
- Do not change logic, layout, reactivity, or bindings—only refactor strings into i18n.
|
||||||
|
|
||||||
|
Use existing patterns from our codebase:
|
||||||
|
|
||||||
|
- Variables/plurals: see `apps/frontend/src/pages/frog.vue`
|
||||||
|
- Rich-text link tags: see `apps/frontend/src/pages/auth/welcome.vue` and `apps/frontend/src/error.vue`
|
||||||
|
|
||||||
|
When you finish, there should be no hard-coded English strings left in the template—everything comes from `formatMessage` or `<IntlFormatted>`.
|
||||||
4
.github/templates/crowdin-pr.md
vendored
Normal file
4
.github/templates/crowdin-pr.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
This pull request is created according to the `.github/workflows/i18n-pull.yml` file.
|
||||||
|
|
||||||
|
- 🌐 [Contribute to translations on Crowdin](https://translate.modrinth.com/)
|
||||||
|
- 🔄 [Dispatch this workflow again to update this PR](https://github.com/Modrinth/code/actions/workflows/i18n-pull.yml)
|
||||||
67
.github/workflows/astralrinth-build.yml
vendored
67
.github/workflows/astralrinth-build.yml
vendored
@@ -4,9 +4,14 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- master
|
||||||
|
- prod
|
||||||
|
- release
|
||||||
|
- beta
|
||||||
- feature*
|
- feature*
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- release-*
|
||||||
|
- beta-*
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/astralrinth-build.yml
|
- .github/workflows/astralrinth-build.yml
|
||||||
- 'apps/app/**'
|
- 'apps/app/**'
|
||||||
@@ -16,6 +21,7 @@ on:
|
|||||||
- 'packages/assets/**'
|
- 'packages/assets/**'
|
||||||
- 'packages/ui/**'
|
- 'packages/ui/**'
|
||||||
- 'packages/utils/**'
|
- 'packages/utils/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -23,10 +29,11 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [macos-latest, windows-latest, ubuntu-latest]
|
# platform: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
platform: [windows-latest, ubuntu-latest]
|
||||||
include:
|
include:
|
||||||
- platform: macos-latest
|
# - platform: macos-latest
|
||||||
artifact-target-name: universal-apple-darwin
|
# artifact-target-name: universal-apple-darwin
|
||||||
- platform: windows-latest
|
- platform: windows-latest
|
||||||
artifact-target-name: x86_64-pc-windows-msvc
|
artifact-target-name: x86_64-pc-windows-msvc
|
||||||
- platform: ubuntu-latest
|
- platform: ubuntu-latest
|
||||||
@@ -40,6 +47,35 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: 🔍 Validate Git config does not introduce CRLF
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "🔍 Checking Git config for CRLF settings..."
|
||||||
|
|
||||||
|
autocrlf=$(git config --get core.autocrlf || echo "unset")
|
||||||
|
eol_setting=$(git config --get core.eol || echo "unset")
|
||||||
|
|
||||||
|
echo "core.autocrlf = $autocrlf"
|
||||||
|
echo "core.eol = $eol_setting"
|
||||||
|
|
||||||
|
if [ "$autocrlf" = "true" ]; then
|
||||||
|
echo "⚠️ WARNING: core.autocrlf is set to 'true'. Consider setting it to 'input' or 'false'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$eol_setting" = "crlf" ]; then
|
||||||
|
echo "⚠️ WARNING: core.eol is set to 'crlf'. Consider unsetting it or setting to 'lf'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 🔍 Check migration files line endings (LF only)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "🔍 Scanning migration SQL files for CR characters (\\r)..."
|
||||||
|
if grep -Iq $'\r' packages/app-lib/migrations/*.sql; then
|
||||||
|
echo "❌ ERROR: Some migration files contain CR (\\r) characters — expected only LF line endings."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ All migration files use LF line endings"
|
||||||
|
|
||||||
- name: 🧰 Setup Rust toolchain
|
- name: 🧰 Setup Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -64,7 +100,12 @@ jobs:
|
|||||||
libayatana-appindicator3-dev \
|
libayatana-appindicator3-dev \
|
||||||
librsvg2-dev \
|
librsvg2-dev \
|
||||||
xdg-utils \
|
xdg-utils \
|
||||||
openjdk-11-jdk
|
openjdk-17-jdk
|
||||||
|
|
||||||
|
- name: ⚙️ Set application environment
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cp packages/app-lib/.env.prod packages/app-lib/.env
|
||||||
|
|
||||||
- name: 💨 Setup Turbo cache
|
- name: 💨 Setup Turbo cache
|
||||||
uses: rharkor/caching-for-turbo@v1.8
|
uses: rharkor/caching-for-turbo@v1.8
|
||||||
@@ -73,7 +114,7 @@ jobs:
|
|||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: ✍️ Set up Windows code signing (jsign)
|
- name: ✍️ Set up Windows code signing (jsign)
|
||||||
if: matrix.platform == 'windows' && env.SIGN_WINDOWS_BINARIES == 'true'
|
if: matrix.platform == 'windows-latest' && env.SIGN_WINDOWS_BINARIES == 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
choco install jsign --ignore-dependencies
|
choco install jsign --ignore-dependencies
|
||||||
@@ -84,12 +125,12 @@ jobs:
|
|||||||
rm -rf target/release/bundle
|
rm -rf target/release/bundle
|
||||||
rm -rf target/*/release/bundle || true
|
rm -rf target/*/release/bundle || true
|
||||||
|
|
||||||
- name: 🔨 Build macOS app
|
# - name: 🔨 Build macOS app
|
||||||
if: matrix.platform == 'macos-latest'
|
# if: matrix.platform == 'macos-latest'
|
||||||
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
# run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
||||||
env:
|
# env:
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
# TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: 🔨 Build Linux app
|
- name: 🔨 Build Linux app
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
@@ -102,7 +143,7 @@ jobs:
|
|||||||
if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
|
$env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
|
||||||
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
|
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
|
||||||
env:
|
env:
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
|||||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -9,7 +9,6 @@ tmp
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/.idea
|
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
.c9/
|
.c9/
|
||||||
@@ -24,6 +23,14 @@ node_modules
|
|||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# IDE - IntelliJ
|
||||||
|
.idea/*
|
||||||
|
!.idea/code.iml
|
||||||
|
!.idea/gradle.xml
|
||||||
|
!.idea/icon.svg
|
||||||
|
!.idea/modules.xml
|
||||||
|
!.idea/vcs.xml
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
/.sass-cache
|
/.sass-cache
|
||||||
/connect.lock
|
/connect.lock
|
||||||
@@ -56,8 +63,8 @@ generated
|
|||||||
# app testing dir
|
# app testing dir
|
||||||
app-playground-data/*
|
app-playground-data/*
|
||||||
|
|
||||||
# soley because i need the PORT to be 3002 due to WSL stuff
|
|
||||||
.env
|
|
||||||
apps/frontend/.env
|
|
||||||
|
|
||||||
.astro
|
.astro
|
||||||
|
.claude
|
||||||
|
|
||||||
|
# labrinth demo fixtures
|
||||||
|
apps/labrinth/fixtures/demo
|
||||||
|
|||||||
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
4
.idea/code.iml
generated
4
.idea/code.iml
generated
@@ -10,11 +10,11 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
|
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/rust-common/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/packages/path-util/src" isTestSource="false" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
7
.idea/discord.xml
generated
7
.idea/discord.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DiscordProjectSettings">
|
|
||||||
<option name="show" value="PROJECT_FILES" />
|
|
||||||
<option name="description" value="" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
17
.idea/gradle.xml
generated
Normal file
17
.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1"/>
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/packages/app-lib/java"/>
|
||||||
|
<option name="gradleJvm" value="#JAVA_HOME"/>
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/packages/app-lib/java"/>
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
4
.idea/icon.svg
generated
Normal file
4
.idea/icon.svg
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="512" height="514" viewBox="0 0 512 514" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M503.16 323.56C514.55 281.47 515.32 235.91 503.2 190.76C466.57 54.2299 326.04 -26.8001 189.33 9.77991C83.8101 38.0199 11.3899 128.07 0.689941 230.47H43.99C54.29 147.33 113.74 74.7298 199.75 51.7098C306.05 23.2598 415.13 80.6699 453.17 181.38L411.03 192.65C391.64 145.8 352.57 111.45 306.3 96.8198L298.56 140.66C335.09 154.13 364.72 184.5 375.56 224.91C391.36 283.8 361.94 344.14 308.56 369.17L320.09 412.16C390.25 383.21 432.4 310.3 422.43 235.14L464.41 223.91C468.91 252.62 467.35 281.16 460.55 308.07L503.16 323.56Z" fill="#00af5c"/>
|
||||||
|
<path d="M321.99 504.22C185.27 540.8 44.7501 459.77 8.11011 323.24C3.84011 307.31 1.17 291.33 0 275.46H43.27C44.36 287.37 46.4699 299.35 49.6799 311.29C53.0399 323.8 57.45 335.75 62.79 347.07L101.38 323.92C98.1299 316.42 95.39 308.6 93.21 300.47C69.17 210.87 122.41 118.77 212.13 94.7601C229.13 90.2101 246.23 88.4401 262.93 89.1501L255.19 133C244.73 133.05 234.11 134.42 223.53 137.25C157.31 154.98 118.01 222.95 135.75 289.09C136.85 293.16 138.13 297.13 139.59 300.99L188.94 271.38L174.07 231.95L220.67 184.08L279.57 171.39L296.62 192.38L269.47 219.88L245.79 227.33L228.87 244.72L237.16 267.79C237.16 267.79 253.95 285.63 253.98 285.64L277.7 279.33L294.58 260.79L331.44 249.12L342.42 273.82L304.39 320.45L240.66 340.63L212.08 308.81L162.26 338.7C187.8 367.78 226.2 383.93 266.01 380.56L277.54 423.55C218.13 431.41 160.1 406.82 124.05 361.64L85.6399 384.68C136.25 451.17 223.84 484.11 309.61 461.16C371.35 444.64 419.4 402.56 445.42 349.38L488.06 364.88C457.17 431.16 398.22 483.82 321.99 504.22Z" fill="#00af5c"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
26
.idea/libraries/KotlinJavaRuntime.xml
generated
26
.idea/libraries/KotlinJavaRuntime.xml
generated
@@ -1,26 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="KotlinJavaRuntime" type="repository">
|
|
||||||
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0" />
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0-javadoc.jar!/" />
|
|
||||||
</JAVADOC>
|
|
||||||
<SOURCES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0-sources.jar!/" />
|
|
||||||
</SOURCES>
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
19
.idea/modules.xml
generated
19
.idea/modules.xml
generated
@@ -1,8 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml"
|
||||||
</modules>
|
filepath="$PROJECT_DIR$/.idea/code.iml"/>
|
||||||
</component>
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/theseus.iml"
|
||||||
</project>
|
filepath="$PROJECT_DIR$/.idea/modules/theseus.iml"/>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/theseus.main.iml"
|
||||||
|
filepath="$PROJECT_DIR$/.idea/modules/theseus.main.iml"/>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/theseus.test.iml"
|
||||||
|
filepath="$PROJECT_DIR$/.idea/modules/theseus.test.iml"/>
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|||||||
22
.idea/vcs.xml
generated
22
.idea/vcs.xml
generated
@@ -1,12 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CommitMessageInspectionProfile">
|
<component name="CommitMessageInspectionProfile">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="CommitFormat" enabled="true" level="WARNING"
|
||||||
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
enabled_by_default="true"/>
|
||||||
</profile>
|
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING"
|
||||||
</component>
|
enabled_by_default="true"/>
|
||||||
<component name="VcsDirectoryMappings">
|
</profile>
|
||||||
<mapping directory="" vcs="Git" />
|
</component>
|
||||||
</component>
|
<component name="VcsDirectoryMappings">
|
||||||
</project>
|
<mapping directory="" vcs="Git"/>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Cargo.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
.github/**/*.png
|
||||||
38
.vscode/settings.json
vendored
38
.vscode/settings.json
vendored
@@ -1,9 +1,33 @@
|
|||||||
{
|
{
|
||||||
"prettier.endOfLine": "lf",
|
"prettier.endOfLine": "lf",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||||
"editor.detectIndentation": true,
|
"editor.detectIndentation": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.insertSpaces": false,
|
||||||
"source.fixAll.eslint": "explicit"
|
"files.eol": "\n",
|
||||||
}
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "always"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[vue]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[scss]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[rust]": {
|
||||||
|
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
CLAUDE.md
Normal file
63
CLAUDE.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
Use TAB instead of spaces.
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
There are two similar frontends in the Modrinth monorepo, the website (apps/frontend) and the app frontend (apps/app-frontend).
|
||||||
|
|
||||||
|
Both use Tailwind v3, and their respective configs can be seen at `tailwind.config.ts` and `tailwind.config.js` respectively.
|
||||||
|
|
||||||
|
Both utilize shared and common components from `@modrinth/ui` which can be found at `packages/ui`, and stylings from `@modrinth/assets` which can be found at `packages/assets`.
|
||||||
|
|
||||||
|
Both can utilize icons from `@modrinth/assets`, which are automatically generated based on what's available within the `icons` folder of the `packages/assets` directory. You can see the generated icons list in `generated-icons.ts`.
|
||||||
|
|
||||||
|
Both have access to our dependency injection framework, examples as seen in `packages/ui/src/providers/`. Ideally any state which is shared between a page and it's subpages should be shared using this dependency injection framework.
|
||||||
|
|
||||||
|
### Website (apps/frontend)
|
||||||
|
|
||||||
|
Before a pull request can be opened for the website, `pnpm web:fix` and `pnpm web:intl:extract` must be run, otherwise CI will fail.
|
||||||
|
|
||||||
|
To run a development version of the frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within the `apps/frontend` folder into `apps/frontend/.env`. Then you can run the frontend by running `pnpm web:dev` in the root folder.
|
||||||
|
|
||||||
|
### App Frontend (apps/app-frontend)
|
||||||
|
|
||||||
|
Before a pull request can be opened for the website, you must CD into the `app-frontend` folder; `pnpm fix` and `pnpm intl:extract` must be run, otherwise CI will fail.
|
||||||
|
|
||||||
|
To run a development version of the app frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within `packages/app-lib` into `packages/app-lib/.env`. Then you must run the app itself by running `pnpm app:dev` in the root folder.
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
Refer to `.github/instructions/i18n-convert.instructions.md` if the user asks you to perform any i18n conversion work on a component, set of components, pages or sets of pages.
|
||||||
|
|
||||||
|
## Labrinth
|
||||||
|
|
||||||
|
Labrinth is the backend API service for Modrinth.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Before a pull request can be opened, run `cargo clippy -p labrinth --all-targets` and make sure there are ZERO warnings, otherwise CI will fail.
|
||||||
|
|
||||||
|
Use `cargo test -p labrinth --all-targets` to test your changes. All tests must pass, otherwise CI will fail.
|
||||||
|
|
||||||
|
To prepare the sqlx cache, cd into `apps/labrinth` and run `cargo sqlx prepare`. Make sure to NEVER run `cargo sqlx prepare --workspace`.
|
||||||
|
|
||||||
|
Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` to access these services.
|
||||||
|
|
||||||
|
When the user refers to "performing pre-PR checks", do the following:
|
||||||
|
|
||||||
|
- Run clippy as described above
|
||||||
|
- DO NOT run tests unless explicitly requested (they take a long time)
|
||||||
|
- Prepare the sqlx cache
|
||||||
|
|
||||||
|
### Clickhouse
|
||||||
|
|
||||||
|
Use `docker exec labrinth-clickhouse clickhouse-client` to access the Clickhouse instance. We use the `staging_ariadne` database to store data in testing.
|
||||||
|
|
||||||
|
### Postgres
|
||||||
|
|
||||||
|
Use `docker exec labrinth-postgres psql -U labrinth -d labrinth -c "SELECT 1"` to access the PostgreSQL instance, replacing the `SELECT 1` with your query.
|
||||||
|
|
||||||
|
# Guidelines
|
||||||
|
|
||||||
|
- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to.
|
||||||
@@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
All packages in this repository are licensed under their respective licenses. For more information, refer to the LICENSE file in each package.
|
All packages in this repository are licensed under their respective licenses. For more information, refer to the LICENSE file in each package.
|
||||||
|
|
||||||
For detailed information, consult each package's COPYING.md file, if available.
|
For detailed information, consult each package's COPYING.md, LICENSE.txt, or LICENSE file, if available.
|
||||||
|
|
||||||
## Modrinth Branding
|
## Modrinth Branding
|
||||||
|
|
||||||
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
|
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
|
||||||
|
|
||||||
All rights reserved. © 2020-2024 Rinth, Inc.
|
> All rights reserved. © 2020-2024 Rinth, Inc.
|
||||||
|
|
||||||
|
This includes, but may not be limited to, the following files:
|
||||||
|
|
||||||
|
- .idea/icon.svg
|
||||||
|
|
||||||
If you fork this repository, you must remove all Modrinth branding assets from your fork.
|
If you fork this repository, you must remove all Modrinth branding assets from your fork.
|
||||||
|
|||||||
4256
Cargo.lock
generated
4256
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
200
Cargo.toml
200
Cargo.toml
@@ -8,113 +8,142 @@ members = [
|
|||||||
"packages/app-lib",
|
"packages/app-lib",
|
||||||
"packages/ariadne",
|
"packages/ariadne",
|
||||||
"packages/daedalus",
|
"packages/daedalus",
|
||||||
|
"packages/modrinth-log",
|
||||||
|
"packages/modrinth-maxmind",
|
||||||
|
"packages/modrinth-util",
|
||||||
|
"packages/path-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
rust-version = "1.90.0"
|
||||||
|
repository = "https://github.com/modrinth/code"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
actix-cors = "0.7.1"
|
actix-cors = "0.7.1"
|
||||||
actix-files = "0.6.6"
|
actix-files = "0.6.8"
|
||||||
actix-http = "3.11.0"
|
actix-http = "3.11.2"
|
||||||
actix-multipart = "0.7.2"
|
actix-multipart = "0.7.2"
|
||||||
actix-rt = "2.10.0"
|
actix-rt = "2.11.0"
|
||||||
actix-web = "4.11.0"
|
actix-web = "4.11.0"
|
||||||
actix-web-prom = "0.10.0"
|
actix-web-prom = "0.10.0"
|
||||||
actix-ws = "0.3.0"
|
actix-ws = "0.3.0"
|
||||||
|
arc-swap = "1.7.1"
|
||||||
argon2 = { version = "0.5.3", features = ["std"] }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
ariadne = { path = "packages/ariadne" }
|
ariadne = { path = "packages/ariadne" }
|
||||||
async_zip = "0.0.17"
|
async-compression = { version = "0.4.32", default-features = false }
|
||||||
async-compression = { version = "0.4.25", default-features = false }
|
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||||
"runtime-tokio-hyper-rustls",
|
"runtime-tokio-hyper-rustls",
|
||||||
] }
|
] }
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.89"
|
||||||
async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
async-tungstenite = { version = "0.31.0", default-features = false, features = [
|
||||||
"futures-03-sink",
|
"futures-03-sink"
|
||||||
] }
|
] }
|
||||||
async-walkdir = "2.1.0"
|
async-walkdir = "2.1.0"
|
||||||
|
async_zip = "0.0.18"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bitflags = "2.9.1"
|
bitflags = "2.9.4"
|
||||||
bytemuck = "1.23.0"
|
bytemuck = "1.24.0"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
censor = "0.3.0"
|
censor = "0.3.0"
|
||||||
chardetng = "0.1.17"
|
chardetng = "0.1.17"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.42"
|
||||||
clap = "4.5.40"
|
cidre = { version = "0.11.3", default-features = false, features = [
|
||||||
clickhouse = "0.13.3"
|
"macos_15_0"
|
||||||
|
] }
|
||||||
|
clap = "4.5.48"
|
||||||
|
clickhouse = "0.14.0"
|
||||||
|
color-eyre = "0.6.5"
|
||||||
color-thief = "0.2.2"
|
color-thief = "0.2.2"
|
||||||
console-subscriber = "0.4.1"
|
const_format = "0.2.34"
|
||||||
daedalus = { path = "packages/daedalus" }
|
daedalus = { path = "packages/daedalus" }
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.2"
|
||||||
deadpool-redis = "0.21.1"
|
deadpool-redis = "0.22.0"
|
||||||
|
derive_more = "2.0.1"
|
||||||
|
directories = "6.0.0"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
discord-rich-presence = "0.2.5"
|
discord-rich-presence = "1.0.0"
|
||||||
dotenv-build = "0.1.1"
|
dotenv-build = "0.1.1"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
either = "1.15.0"
|
either = "1.15.0"
|
||||||
encoding_rs = "0.8.35"
|
encoding_rs = "0.8.35"
|
||||||
enumset = "1.1.6"
|
enumset = "1.1.10"
|
||||||
flate2 = "1.1.2"
|
eyre = "0.6.12"
|
||||||
|
flate2 = "1.1.4"
|
||||||
fs4 = { version = "0.13.1", default-features = false }
|
fs4 = { version = "0.13.1", default-features = false }
|
||||||
futures = { version = "0.3.31", default-features = false }
|
futures = "0.3.31"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
hashlink = "0.10.0"
|
|
||||||
heck = "0.5.0"
|
heck = "0.5.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
hyper-tls = "0.6.0"
|
hyper = "1.7.0"
|
||||||
hyper-util = "0.1.14"
|
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
||||||
iana-time-zone = "0.1.63"
|
"aws-lc-rs",
|
||||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
"http1",
|
||||||
indexmap = "2.9.0"
|
"native-tokio",
|
||||||
indicatif = "0.17.11"
|
"tls12",
|
||||||
|
] }
|
||||||
|
hyper-util = "0.1.17"
|
||||||
|
iana-time-zone = "0.1.64"
|
||||||
|
image = { version = "0.25.8", default-features = false, features = ["rayon"] }
|
||||||
|
indexmap = "2.11.4"
|
||||||
|
indicatif = "0.18.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
jemalloc_pprof = "0.7.0"
|
jemalloc_pprof = "0.8.1"
|
||||||
json-patch = { version = "4.0.0", default-features = false }
|
json-patch = { version = "4.1.0", default-features = false }
|
||||||
lettre = { version = "0.11.17", default-features = false, features = [
|
lettre = { version = "0.11.19", default-features = false, features = [
|
||||||
|
"aws-lc-rs",
|
||||||
"builder",
|
"builder",
|
||||||
"hostname",
|
"hostname",
|
||||||
"pool",
|
"pool",
|
||||||
"ring",
|
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-native-certs",
|
"rustls-native-certs",
|
||||||
"smtp-transport",
|
"smtp-transport",
|
||||||
|
"tokio1",
|
||||||
|
"tokio1-rustls",
|
||||||
] }
|
] }
|
||||||
maxminddb = "0.26.0"
|
maxminddb = "0.26.0"
|
||||||
meilisearch-sdk = { version = "0.28.0", default-features = false }
|
meilisearch-sdk = { version = "0.30.0", default-features = false }
|
||||||
|
modrinth-log = { path = "packages/modrinth-log" }
|
||||||
|
modrinth-maxmind = { path = "packages/modrinth-maxmind" }
|
||||||
|
modrinth-util = { path = "packages/modrinth-util" }
|
||||||
|
muralpay = { path = "packages/muralpay" }
|
||||||
murmur2 = "0.1.0"
|
murmur2 = "0.1.0"
|
||||||
native-dialog = "0.9.0"
|
native-dialog = "0.9.2"
|
||||||
notify = { version = "8.0.0", default-features = false }
|
notify = { version = "8.2.0", default-features = false }
|
||||||
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||||
p256 = "0.13.2"
|
p256 = "0.13.2"
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
png = "0.17.16"
|
path-util = { path = "packages/path-util" }
|
||||||
|
phf = { version = "0.13.1", features = ["macros"] }
|
||||||
|
png = "0.18.0"
|
||||||
prometheus = "0.14.0"
|
prometheus = "0.14.0"
|
||||||
quartz_nbt = "0.2.9"
|
quartz_nbt = "0.2.9"
|
||||||
quick-xml = "0.37.5"
|
quick-xml = "0.38.3"
|
||||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
redis = "0.32.7"
|
||||||
regex = "1.11.1"
|
regex = "1.12.2"
|
||||||
reqwest = { version = "0.12.20", default-features = false }
|
reqwest = { version = "0.12.24", default-features = false }
|
||||||
rgb = "0.8.50"
|
rgb = "0.8.52"
|
||||||
rust_decimal = { version = "1.37.2", features = [
|
rust_decimal = { version = "1.39.0", features = [
|
||||||
"serde-with-float",
|
"serde-with-float",
|
||||||
"serde-with-str",
|
"serde-with-str"
|
||||||
] }
|
] }
|
||||||
rust_iso3166 = "0.1.14"
|
rust_iso3166 = "0.1.14"
|
||||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
rust-s3 = { version = "0.37.0", default-features = false, features = [
|
||||||
"fail-on-err",
|
"fail-on-err",
|
||||||
"tags",
|
"tags",
|
||||||
"tokio-rustls-tls",
|
"tokio-rustls-tls",
|
||||||
] }
|
] }
|
||||||
|
rustls = "0.23.32"
|
||||||
rusty-money = "0.4.1"
|
rusty-money = "0.4.1"
|
||||||
sentry = { version = "0.41.0", default-features = false, features = [
|
secrecy = "0.10.3"
|
||||||
|
sentry = { version = "0.45.0", default-features = false, features = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"contexts",
|
"contexts",
|
||||||
"debug-images",
|
"debug-images",
|
||||||
@@ -122,57 +151,68 @@ sentry = { version = "0.41.0", default-features = false, features = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"rustls",
|
"rustls",
|
||||||
] }
|
] }
|
||||||
sentry-actix = "0.41.0"
|
sentry-actix = "0.45.0"
|
||||||
serde = "1.0.219"
|
serde = "1.0.228"
|
||||||
serde_bytes = "0.11.17"
|
serde_bytes = "0.11.19"
|
||||||
serde_cbor = "0.11.2"
|
serde_cbor = "0.11.2"
|
||||||
serde_ini = "0.2.0"
|
serde_ini = "0.2.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.145"
|
||||||
serde_with = "3.13.0"
|
serde_with = "3.15.0"
|
||||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
spdx = "0.10.8"
|
shlex = "1.3.0"
|
||||||
|
spdx = "0.12.0"
|
||||||
sqlx = { version = "0.8.6", default-features = false }
|
sqlx = { version = "0.8.6", default-features = false }
|
||||||
sysinfo = { version = "0.35.2", default-features = false }
|
strum = "0.27.2"
|
||||||
|
sysinfo = { version = "0.37.2", default-features = false }
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
tauri = "2.6.1"
|
tauri = "2.8.5"
|
||||||
tauri-build = "2.3.0"
|
tauri-build = "2.4.1"
|
||||||
tauri-plugin-deep-link = "2.4.0"
|
tauri-plugin-deep-link = "2.4.3"
|
||||||
tauri-plugin-dialog = "2.3.0"
|
tauri-plugin-dialog = "2.4.0"
|
||||||
tauri-plugin-http = "2.5.0"
|
tauri-plugin-http = "2.5.2"
|
||||||
tauri-plugin-opener = "2.4.0"
|
tauri-plugin-opener = "2.5.0"
|
||||||
tauri-plugin-os = "2.3.0"
|
tauri-plugin-os = "2.3.1"
|
||||||
tauri-plugin-single-instance = "2.3.0"
|
tauri-plugin-single-instance = "2.3.4"
|
||||||
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"zip",
|
"zip",
|
||||||
] }
|
] }
|
||||||
tauri-plugin-window-state = "2.3.0"
|
tauri-plugin-window-state = "2.4.0"
|
||||||
tempfile = "3.20.0"
|
tempfile = "3.23.0"
|
||||||
theseus = { path = "packages/app-lib" }
|
theseus = { path = "packages/app-lib" }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.17"
|
||||||
tikv-jemalloc-ctl = "0.6.0"
|
tikv-jemalloc-ctl = "0.6.0"
|
||||||
tikv-jemallocator = "0.6.0"
|
tikv-jemallocator = "0.6.0"
|
||||||
tokio = "1.45.1"
|
tokio = "1.47.1"
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
tokio-util = "0.7.15"
|
tokio-util = "0.7.16"
|
||||||
totp-rs = "5.7.0"
|
totp-rs = "5.7.0"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-actix-web = "0.7.18"
|
tracing-actix-web = { version = "0.7.19", default-features = false }
|
||||||
|
tracing-ecs = "0.5.0"
|
||||||
tracing-error = "0.2.1"
|
tracing-error = "0.2.1"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.20"
|
||||||
url = "2.5.4"
|
typed-path = "0.12.0"
|
||||||
|
url = "2.5.7"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
uuid = "1.17.0"
|
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
|
||||||
|
utoipa-actix-web = { version = "0.1.2" }
|
||||||
|
utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web", "vendored"] }
|
||||||
|
uuid = "1.18.1"
|
||||||
validator = "0.20.0"
|
validator = "0.20.0"
|
||||||
webp = { version = "0.3.0", default-features = false }
|
webp = { version = "0.3.1", default-features = false }
|
||||||
whoami = "1.6.0"
|
webview2-com = "0.38.0" # Should be updated in lockstep with wry
|
||||||
|
whoami = "1.6.1"
|
||||||
|
windows = "=0.61.3" # Locked on 0.61 until we can update windows-core to 0.62
|
||||||
|
windows-core = "=0.61.2" # Locked on 0.61 until webview2-com updates to 0.62
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
woothee = "0.13.0"
|
woothee = "0.13.0"
|
||||||
yaserde = "0.12.0"
|
yaserde = "0.12.0"
|
||||||
zip = { version = "4.2.0", default-features = false, features = [
|
zbus = "5.11.0"
|
||||||
|
zip = { version = "6.0.0", default-features = false, features = [
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"deflate",
|
"deflate",
|
||||||
"deflate64",
|
"deflate64",
|
||||||
@@ -211,15 +251,13 @@ redundant_clone = "warn"
|
|||||||
redundant_feature_names = "warn"
|
redundant_feature_names = "warn"
|
||||||
redundant_type_annotations = "warn"
|
redundant_type_annotations = "warn"
|
||||||
todo = "warn"
|
todo = "warn"
|
||||||
|
too_many_arguments = "allow"
|
||||||
|
uninlined_format_args = "warn"
|
||||||
unnested_or_patterns = "warn"
|
unnested_or_patterns = "warn"
|
||||||
wildcard_dependencies = "warn"
|
wildcard_dependencies = "warn"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[profile.dev.package.sqlx-macros]
|
||||||
# Turn warnings into errors by default
|
opt-level = 3
|
||||||
warnings = "deny"
|
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
wry = { git = "https://github.com/modrinth/wry", rev = "21db186" }
|
|
||||||
|
|
||||||
# Optimize for speed and reduce size on release builds
|
# Optimize for speed and reduce size on release builds
|
||||||
[profile.release]
|
[profile.release]
|
||||||
@@ -229,5 +267,7 @@ lto = true # Enables link to optimizations
|
|||||||
panic = "abort" # Strip expensive panic clean-up logic
|
panic = "abort" # Strip expensive panic clean-up logic
|
||||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
# Specific profile for labrinth production builds
|
||||||
opt-level = 3
|
[profile.release-labrinth]
|
||||||
|
inherits = "release"
|
||||||
|
panic = "unwind" # Don't exit the whole app on panic in production
|
||||||
|
|||||||
157
README.md
157
README.md
@@ -1,76 +1,119 @@
|
|||||||
# Navigation in this README
|
# 📘 Navigation
|
||||||
- [Install instructions](#install-instructions)
|
|
||||||
- [Features](#features)
|
- [🔧 Install Instructions](#install-instructions)
|
||||||
- [Getting started](#getting-started)
|
- [✨ Features](#features)
|
||||||
- [Disclaimer](#disclaimer)
|
- [🚀 Getting Started](#getting-started)
|
||||||
- [Donate](#support-our-project-crypto-wallets)
|
- [⚠️ Disclaimer](#disclaimer)
|
||||||
|
- [💰 Donate](#support-our-project-crypto-wallets)
|
||||||
|
|
||||||
|
## Other languages
|
||||||
|
> [Русский](readme/ru_ru/README.md)
|
||||||
|
|
||||||
|
## Support channel
|
||||||
|
> [Telegram](https://me.astralium.su/ref/telegram_channel)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# About Project
|
# About Project
|
||||||
|
|
||||||
## AstralRinth • Empowering Your Minecraft Adventure
|
## **AstralRinth • Empowering Your Minecraft Experience**
|
||||||
Welcome to AR • Fork of Modrinth, the ultimate game launcher designed to enhance your Minecraft experience through the Modrinth platform and their API. Whether you're a graphical interface enthusiast, or a developer integrating Modrinth projects, Theseus core is your gateway to a new level of Minecraft gaming.
|
|
||||||
|
|
||||||
## About Software
|
**AstralRinth** — a powerful fork of Modrinth, reimagined to enhance your Minecraft journey. Whether you're a GUI enthusiast or a developer building with Modrinth’s API, **Theseus Core** is your launchpad into a new era of Minecraft gameplay.
|
||||||
Introducing AstralRinth, a specialized variant of Theseus dedicated to implementing offline authorization for an even more flexible and user-centric Minecraft Modrinth experience. Roam the Minecraft realms without the constraints of online authentication, thanks to AstralRinth.
|
|
||||||
|
|
||||||
## AR • Unlocking Minecraft's Boundless Horizon
|
## **About the Software**
|
||||||
Dive into the extraordinary world of AstralRinth, a fork of the original project with a unique focus on providing a free trial experience for Minecraft, all without the need for a license. Currently boasting:
|
|
||||||
|
|
||||||
# Install instructions
|
**AstralRinth** is a dedicated branch of the Modrinth (a.k.a Theseus) project, focused on **offline authentication**, offering you more flexibility and control. Play Minecraft without the need for constant online verification — a user-first approach to modern modded gaming.
|
||||||
- To install our application, you need to download a file for your operating system from our available releases or development builds • [Download variants here](https://git.astralium.su/didirus/AstralRinth/releases)
|
|
||||||
- After you have downloaded the required executable file or archive, then open it
|
|
||||||
|
|
||||||
### Downloadable file extensions
|
---
|
||||||
- `.msi` format for Windows OS system _(Supported popular latest versions of Microsoft Windows)_
|
|
||||||
- `.dmg` format for MacOS system _(Works on Macos Ventura / Sonoma / Sequoia, but it should be works on older OS builds)_
|
|
||||||
- `.deb` format for Linux OS systems _(Since there are quite a few distributions, we do not guarantee
|
|
||||||
|
|
||||||
### Installation subjects
|
# Install Instructions
|
||||||
- Builds in releases that are signed with the following prefixes are not recommended for installation and may contain errors:
|
|
||||||
- `dev`
|
To install the launcher:
|
||||||
- `nightly`
|
|
||||||
- `dirty`
|
1. Visit the [releases page](https://git.astralium.su/didirus/AstralRinth/releases) to download the correct version for your system.
|
||||||
- `dirty-dev`
|
2. Run the downloaded file or extract and launch it, depending on the format.
|
||||||
- `dirty-nightly`
|
|
||||||
- `dirty_dev`
|
### Downloadable File Extensions
|
||||||
- `dirty_nightly`
|
|
||||||
- Auto-updating takes place through parsing special versions from releases, so we also distribute clean types of `.msi, .dmg and .deb`
|
| Extension | OS | Notes |
|
||||||
|
| --------- | ------- | --------------------------------------------------------------------- |
|
||||||
|
| `.msi` | Windows | Supported on all recent Windows versions (10/11) |
|
||||||
|
| `.dmg` | macOS | Works on Ventura, Sonoma, Sequoia, Tahoe _(may also support older versions)_ |
|
||||||
|
| `.deb` | Linux | Basic support; compatibility may vary by distribution |
|
||||||
|
|
||||||
|
### Installation Warnings
|
||||||
|
|
||||||
|
Avoid using builds with these prefixes — they may be unstable or experimental:
|
||||||
|
|
||||||
|
- `dev`
|
||||||
|
- `nightly`
|
||||||
|
- `dirty`
|
||||||
|
- `dirty-dev`
|
||||||
|
- `dirty-nightly`
|
||||||
|
- `dirty_dev`
|
||||||
|
- `dirty_nightly`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
### Featured enhancement in AR
|
> _The launcher provides an opportunity to use the well-known Modrinth, but with an improved user experience._
|
||||||
- AstralRinth offers a range of authorization options, giving users the flexibility to log in with valid licenses or even a pirate account without auth credentials breaks (_Unlike MultiMC Cracked and similar software_). Experience Minecraft on your terms, breaking free from traditional licensing constraints (_Popular in Russian Federation_).
|
|
||||||
|
|
||||||
### Easy to use
|
## Included exclusive features
|
||||||
- Using the launcher is intuitive, any user can figure it out.
|
|
||||||
|
|
||||||
### Update notifies
|
- No ads in the entire launcher.
|
||||||
- We have implemented notifications about the release of new updates on our Git. The launcher can also download them for you and try to install them.
|
- Custom `.svg` vector icons for a distinct UI.
|
||||||
|
- Improved compatibility with both licensed and pirate accounts.
|
||||||
|
- Use **official microsoft accounts** or **offline/pirate accounts**.
|
||||||
|
- Supports license-free access for testing or personal use.
|
||||||
|
- No dependence on official authentication services.
|
||||||
|
- Discord Rich Presence integration:
|
||||||
|
- Dynamic status messages.
|
||||||
|
- In-game timer and AFK counter.
|
||||||
|
- Strict disabling of statistics and other Modrinth metrics.
|
||||||
|
- Optimized archive/package size.
|
||||||
|
- Integrated update fetcher for seamless version management.
|
||||||
|
- Built-in update alerts for new versions posted on Git Astralium.
|
||||||
|
- Automatic download and installation capabilities.
|
||||||
|
- Database migration fixes, when error occurred (Interactive Mode) (Modrinth issue)
|
||||||
|
- Ely.by full integration
|
||||||
|
- The official account skin system is managed by ely.by
|
||||||
|
- Offline accounts must install AuthLib through the instance settings
|
||||||
|
|
||||||
### Enhancements
|
---
|
||||||
- Custom .SVG vectors for a personalized touch.
|
|
||||||
- Improved compatibility for both pirate and licensed accounts.
|
|
||||||
- Beautiful Discord RPC with random messages while playing, along with an in-game timer and AFK counter.
|
|
||||||
- Forced disabling of statistics collection (modrinch metrics) with a hard patch from AstralRinth, ensuring it remains deactivated regardless of the configuration setting.
|
|
||||||
- Removal of advertisements from all launcher views.
|
|
||||||
- Optimization of packages (archives).
|
|
||||||
- Integrated update fetching feature
|
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
To begin your AstralRinth adventure, follow these steps:
|
|
||||||
1. **Download Your OS Version**: Head over to our [releases page](https://git.astralium.su/didirus/AstralRinth/releases/) to find the right file for your operating system.
|
To begin using AstralRinth:
|
||||||
- **Choosing the Correct File**: Ensure you select the file that matches your OS requirements.
|
|
||||||
- [**How select file**](#downloadable-file-extensions)
|
1. **Download Latest Release**
|
||||||
- [**How select release**](#installation-subjects)
|
|
||||||
2. **Authentication**: Log in with a valid license or, for testing, try using a pirate account to see AstralRinth in action.
|
- Go to the [releases page](https://git.astralium.su/didirus/AstralRinth/releases)
|
||||||
3. **Launch Minecraft**: Start your journey by launching Minecraft through AstralRinth and enjoy the adventures that await.
|
- [How to choose a file](#downloadable-file-extensions)
|
||||||
- **Choosing java installation**: The launcher will try to automatically detect the recommended JVM version for running the game, but you can configure everything in the launcher settings.
|
- [How to choose a release](#installation-warnings)
|
||||||
|
|
||||||
|
2. **Log in or create new offline account**
|
||||||
|
|
||||||
|
- Use your official Microsoft account (MSA), or test using a non-licensed account (Offline).
|
||||||
|
|
||||||
|
3. **Launch Minecraft**
|
||||||
|
- Start Minecraft from the launcher.
|
||||||
|
- The launcher will auto-detect the recommended JVM version.
|
||||||
|
- You can also configure Java manually in the settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Disclaimer
|
# Disclaimer
|
||||||
- AstralRinth is a project intended for experimentation and educational purposes only. It does not endorse or support piracy, and users are encouraged to obtain valid licenses for a fully-supported Minecraft experience.
|
|
||||||
- Users are reminded to respect licensing agreements and support the developers of Minecraft.
|
|
||||||
|
|
||||||
# Support our Project (Crypto Wallets)
|
- **AstralRinth** is intended **solely for educational and experimental use**.
|
||||||
- BTC (Telegram): 14g6asNYzcUoaQtB8B2QGKabgEvn55wfLj
|
- We **do not condone piracy** — users are encouraged to purchase a legitimate Minecraft license.
|
||||||
- USDT TRC20 (Telegram): TMSmv1D5Fdf4fipUpwBCdh16WevrV45vGr
|
- Respect all relevant licensing agreements and support Minecraft developers.
|
||||||
- TONCOIN (Telegram): UQAqUJ2_hVBI6k_gPyfp_jd-1K0OS61nIFPZuJWN9BwGAvKe
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Support Our Project (Crypto Wallets)
|
||||||
|
|
||||||
|
If you'd like to support development, you can donate via the following crypto wallets:
|
||||||
|
|
||||||
|
- Toncoin (TON): UQA5pGOJhIz9UAVEOh5t2ur1QVbNr_FC1eq9bOb3GwTgaiqk
|
||||||
|
- USDT (TON): UQA5pGOJhIz9UAVEOh5t2ur1QVbNr_FC1eq9bOb3GwTgaiqk
|
||||||
|
|||||||
19
_typos.toml
Normal file
19
_typos.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[files]
|
||||||
|
extend-exclude = [
|
||||||
|
"**/src/locales/",
|
||||||
|
"apps/frontend/",
|
||||||
|
"patches/",
|
||||||
|
"packages/utils/",
|
||||||
|
"packages/ui/",
|
||||||
|
"packages/blog/",
|
||||||
|
# contains licenses like `CC-BY-ND-4.0`
|
||||||
|
"packages/moderation/src/data/stages/license.ts",
|
||||||
|
# contains payment card IDs like `IY1VMST1MOXS` which are flagged
|
||||||
|
"apps/labrinth/src/queue/payouts/mod.rs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[default.extend-words]
|
||||||
|
# Terms Of Use in `tou-link`
|
||||||
|
tou = "tou"
|
||||||
|
# Google Ad Manager
|
||||||
|
gam = "gam"
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
**/dist
|
**/dist
|
||||||
*.gltf
|
*.gltf
|
||||||
|
src/locales/
|
||||||
|
src/assets/**/*.svg
|
||||||
|
|||||||
@@ -1,22 +1,2 @@
|
|||||||
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
|
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||||
import { fixupPluginRules } from '@eslint/compat'
|
export default config
|
||||||
import turboPlugin from 'eslint-plugin-turbo'
|
|
||||||
|
|
||||||
export default createConfigForNuxt().append([
|
|
||||||
{
|
|
||||||
name: 'turbo',
|
|
||||||
plugins: {
|
|
||||||
turbo: fixupPluginRules(turboPlugin),
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'turbo/no-undeclared-env-vars': 'error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'modrinth',
|
|
||||||
rules: {
|
|
||||||
'vue/html-self-closing': 'off',
|
|
||||||
'vue/multi-word-component-names': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" class="dark-mode">
|
<html lang="en" class="dark-mode">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>AstralRinth App</title>
|
<title>AstralRinth App</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/src/assets/stylesheets/global.scss" />
|
<link rel="stylesheet" href="/src/assets/stylesheets/global.scss" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script src="https://tally.so/widgets/embed.js" async></script>
|
||||||
</body>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,64 +1,66 @@
|
|||||||
{
|
{
|
||||||
"name": "@modrinth/app-frontend",
|
"name": "@modrinth/app-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0-local",
|
"version": "1.0.0-local",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"tsc:check": "vue-tsc --noEmit",
|
"tsc:check": "vue-tsc --noEmit",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"fix": "eslint . --fix && prettier --write .",
|
||||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||||
"test": "vue-tsc --noEmit"
|
"test": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
"@modrinth/api-client": "workspace:^",
|
||||||
"@modrinth/assets": "workspace:*",
|
"@modrinth/assets": "workspace:*",
|
||||||
"@modrinth/ui": "workspace:*",
|
"@modrinth/ui": "workspace:*",
|
||||||
"@modrinth/utils": "workspace:*",
|
"@modrinth/utils": "workspace:*",
|
||||||
"@sentry/vue": "^8.27.0",
|
"@sentry/vue": "^8.27.0",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@sfirew/minecraft-motd-parser": "^1.1.6",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
"@tanstack/vue-query": "^5.90.7",
|
||||||
"@tauri-apps/plugin-http": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
"@tauri-apps/plugin-http": "^2.5.0",
|
||||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@types/three": "^0.172.0",
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
"@vintl/vintl": "^4.4.1",
|
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||||
"@vueuse/core": "^11.1.0",
|
"@types/three": "^0.172.0",
|
||||||
"dayjs": "^1.11.10",
|
"@vintl/vintl": "^4.4.1",
|
||||||
"floating-vue": "^5.2.2",
|
"@vueuse/core": "^11.1.0",
|
||||||
"ofetch": "^1.3.4",
|
"dayjs": "^1.11.10",
|
||||||
"pinia": "^2.1.7",
|
"floating-vue": "^5.2.2",
|
||||||
"posthog-js": "^1.158.2",
|
"ofetch": "^1.3.4",
|
||||||
"three": "^0.172.0",
|
"pinia": "^2.1.7",
|
||||||
"vite-svg-loader": "^5.1.0",
|
"posthog-js": "^1.158.2",
|
||||||
"vue": "^3.5.13",
|
"three": "^0.172.0",
|
||||||
"vue-multiselect": "3.0.0",
|
"vite-svg-loader": "^5.1.0",
|
||||||
"vue-router": "4.3.0",
|
"vue": "^3.5.13",
|
||||||
"vue-virtual-scroller": "v2.0.0-beta.8"
|
"vue-multiselect": "3.0.0",
|
||||||
},
|
"vue-router": "4.3.0",
|
||||||
"devDependencies": {
|
"vue-virtual-scroller": "v2.0.0-beta.8"
|
||||||
"@eslint/compat": "^1.1.1",
|
},
|
||||||
"@formatjs/cli": "^6.2.12",
|
"devDependencies": {
|
||||||
"@nuxt/eslint-config": "^0.5.6",
|
"@eslint/compat": "^1.1.1",
|
||||||
"@taijased/vue-render-tracker": "^1.0.7",
|
"@formatjs/cli": "^6.2.12",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@modrinth/tooling-config": "workspace:*",
|
||||||
"autoprefixer": "^10.4.19",
|
"@nuxt/eslint-config": "^0.5.6",
|
||||||
"eslint": "^9.9.1",
|
"@taijased/vue-render-tracker": "^1.0.7",
|
||||||
"eslint-config-custom": "workspace:*",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"eslint-plugin-turbo": "^2.5.4",
|
"autoprefixer": "^10.4.19",
|
||||||
"postcss": "^8.4.39",
|
"eslint": "^9.9.1",
|
||||||
"prettier": "^3.2.5",
|
"eslint-plugin-turbo": "^2.5.4",
|
||||||
"sass": "^1.74.1",
|
"postcss": "^8.4.39",
|
||||||
"tailwindcss": "^3.4.4",
|
"prettier": "^3.2.5",
|
||||||
"tsconfig": "workspace:*",
|
"sass": "^1.74.1",
|
||||||
"typescript": "^5.5.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"vite": "^5.4.6",
|
"typescript": "^5.5.4",
|
||||||
"vue-tsc": "^2.1.6"
|
"vite": "^5.4.6",
|
||||||
},
|
"vue-component-type-helpers": "^3.1.8",
|
||||||
"packageManager": "pnpm@9.4.0",
|
"vue-tsc": "^2.1.6"
|
||||||
"web-types": "../../web-types.json"
|
},
|
||||||
|
"packageManager": "pnpm@9.4.0",
|
||||||
|
"web-types": "../../web-types.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
24
apps/app-frontend/src/assets/external/index.js
vendored
24
apps/app-frontend/src/assets/external/index.js
vendored
@@ -1,18 +1,18 @@
|
|||||||
|
export { default as ATLauncherIcon } from './atlauncher.svg'
|
||||||
export { default as BuyMeACoffeeIcon } from './bmac.svg'
|
export { default as BuyMeACoffeeIcon } from './bmac.svg'
|
||||||
export { default as DiscordIcon } from './discord.svg'
|
export { default as DiscordIcon } from './discord.svg'
|
||||||
|
export { default as GDLauncherIcon } from './gdlauncher.png'
|
||||||
|
export { default as GithubIcon } from './github.svg'
|
||||||
|
export { default as GitLabIcon } from './gitlab.svg'
|
||||||
|
export { default as GoogleIcon } from './google.svg'
|
||||||
export { default as KoFiIcon } from './kofi.svg'
|
export { default as KoFiIcon } from './kofi.svg'
|
||||||
|
export { default as MastodonIcon } from './mastodon.svg'
|
||||||
|
export { default as MicrosoftIcon } from './microsoft.svg'
|
||||||
|
export { default as MultiMCIcon } from './multimc.webp'
|
||||||
|
export { default as OpenCollectiveIcon } from './opencollective.svg'
|
||||||
export { default as PatreonIcon } from './patreon.svg'
|
export { default as PatreonIcon } from './patreon.svg'
|
||||||
export { default as PaypalIcon } from './paypal.svg'
|
export { default as PaypalIcon } from './paypal.svg'
|
||||||
export { default as OpenCollectiveIcon } from './opencollective.svg'
|
|
||||||
export { default as TwitterIcon } from './twitter.svg'
|
|
||||||
export { default as GithubIcon } from './github.svg'
|
|
||||||
export { default as MastodonIcon } from './mastodon.svg'
|
|
||||||
export { default as RedditIcon } from './reddit.svg'
|
|
||||||
export { default as GoogleIcon } from './google.svg'
|
|
||||||
export { default as MicrosoftIcon } from './microsoft.svg'
|
|
||||||
export { default as SteamIcon } from './steam.svg'
|
|
||||||
export { default as GitLabIcon } from './gitlab.svg'
|
|
||||||
export { default as ATLauncherIcon } from './atlauncher.svg'
|
|
||||||
export { default as GDLauncherIcon } from './gdlauncher.png'
|
|
||||||
export { default as MultiMCIcon } from './multimc.webp'
|
|
||||||
export { default as PrismIcon } from './prism.svg'
|
export { default as PrismIcon } from './prism.svg'
|
||||||
|
export { default as RedditIcon } from './reddit.svg'
|
||||||
|
export { default as SteamIcon } from './steam.svg'
|
||||||
|
export { default as TwitterIcon } from './twitter.svg'
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
export { default as SwapIcon } from './arrow-left-right.svg'
|
|
||||||
export { default as ToggleIcon } from './toggle.svg'
|
|
||||||
export { default as PackageIcon } from './package.svg'
|
|
||||||
export { default as VersionIcon } from './milestone.svg'
|
|
||||||
export { default as TextInputIcon } from './text-cursor-input.svg'
|
|
||||||
export { default as AddProjectImage } from './add-project.svg'
|
export { default as AddProjectImage } from './add-project.svg'
|
||||||
export { default as NewInstanceImage } from './new-instance.svg'
|
export { default as SwapIcon } from './arrow-left-right.svg'
|
||||||
export { default as MenuIcon } from './menu.svg'
|
export { default as MenuIcon } from './menu.svg'
|
||||||
export { default as ChatIcon } from './messages-square.svg'
|
export { default as ChatIcon } from './messages-square.svg'
|
||||||
export { default as Pirate } from './pirate.svg'
|
export { default as Pirate } from './pirate.svg'
|
||||||
export { default as Microsoft } from './microsoft.svg'
|
export { default as Microsoft } from './microsoft.svg'
|
||||||
export { default as PirateShip } from './pirate-ship.svg'
|
export { default as PirateShip } from './pirate-ship.svg'
|
||||||
|
export { default as VersionIcon } from './milestone.svg'
|
||||||
|
export { default as NewInstanceImage } from './new-instance.svg'
|
||||||
|
export { default as PackageIcon } from './package.svg'
|
||||||
|
export { default as TextInputIcon } from './text-cursor-input.svg'
|
||||||
|
export { default as ToggleIcon } from './toggle.svg'
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" version="1.1" viewBox="0 0 1793 199">
|
|
||||||
<g>
|
|
||||||
<g id="Layer_1">
|
|
||||||
<g id="green" fill="var(--color-brand)">
|
|
||||||
<path d="M1184.1,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
|
|
||||||
<path d="M1291.1,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
|
|
||||||
<path d="M1357.2,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
|
|
||||||
<path d="M1460,165.3l-40.8-95.1h23.2l35.1,83.9h-11.4l36.3-83.9h21.4l-40.8,95.1h-23Z"/>
|
|
||||||
<path d="M1579.6,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
|
|
||||||
<path d="M1645.7,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
|
|
||||||
<path d="M1749.9,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
|
|
||||||
<g>
|
|
||||||
<path d="M9.8,143l63.4-38.1-5.8-15.3,18.1-18.6,22.9-4.9,6.6,8.2-10.6,10.7-9.2,2.9-6.6,6.8,3.2,9,6.5,6.9,9.2-2.5,6.6-7.2,14.3-4.5,4.3,9.6-14.8,18.1-24.8,7.8-11.1-12.4-63.6,38.2c-3-3.9-6.5-9.4-8.8-14.7ZM192.8,65.4l-50.4,13.6c2.8,7.4,3.7,11.7,4.5,16.5l50.3-13.6c-.8-5.4-2.2-10.8-4.4-16.5Z" fill-rule="evenodd"/>
|
|
||||||
<path d="M17.3,106.5c3.6,42.1,38.9,75.2,82,75.2s60.7-18.9,74-46.3l16.4,5.7c-15.8,34.1-50.3,57.9-90.4,57.9S3.6,158.2,0,106.5h17.3ZM.3,89.4C5.3,39.2,47.8,0,99.3,0s99.5,44.6,99.5,99.5-1.1,17.4-3.3,25.5l-16.3-5.7c1.6-6.5,2.4-13.1,2.4-19.8,0-45.4-36.9-82.3-82.3-82.3S22.6,48.7,17.6,89.4H.3Z" fill-rule="evenodd"/>
|
|
||||||
<path d="M99,51.6c-26.4,0-47.9,21.5-47.9,48s21.5,48,48,48,2.7,0,4-.2l4.8,16.8c-2.9.4-5.8.6-8.8.6-36,0-65.2-29.2-65.2-65.2S63.1,34.4,99,34.4s1.8,0,2.7,0l-2.7,17.1ZM118.6,37.4c26.4,8.3,45.6,33,45.6,62.2s-16.4,50.2-39.8,60l-4.8-16.7c16.2-7.7,27.4-24.2,27.4-43.3s-13-38.1-31.1-44.9l2.7-17.2Z" fill-rule="evenodd"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g id="black" fill="currentColor">
|
|
||||||
<path d="M354.8,69.2c12,0,21.7,3.4,28.6,10.4,7,7.2,10.6,17.5,10.6,31.5v54.8h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,5-6.8,12.2-6.8,21.3v48.5h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,4.8-6.8,12-6.8,21.3v48.5h-22.4v-95.6h21.3v12.2c3.6-4.3,8.1-7.5,13.4-9.8,5.4-2.3,11.3-3.4,17.9-3.4s13.6,1.3,19.2,3.9c5.5,2.9,9.8,6.8,13.1,12,3.9-5,8.9-8.9,15.2-11.8,6.3-2.7,13.1-4.1,20.6-4.1ZM466,167.2c-9.7,0-18.4-2.1-26.1-6.3-7.6-4-13.8-10.1-18.1-17.5-4.5-7.3-6.6-15.7-6.6-25.2s2.1-17.9,6.6-25.2c4.3-7.4,10.6-13.4,18.1-17.4,7.7-4.1,16.5-6.3,26.1-6.3s18.6,2.1,26.3,6.3c7.7,4.1,13.8,10,18.3,17.4,4.3,7.3,6.4,15.7,6.4,25.2s-2.1,17.9-6.4,25.2c-4.5,7.5-10.6,13.4-18.3,17.5-7.7,4.1-16.5,6.3-26.3,6.3h0ZM466,148c8.2,0,15-2.7,20.4-8.2,5.4-5.5,8.1-12.7,8.1-21.7s-2.7-16.1-8.1-21.7c-5.4-5.5-12.2-8.2-20.4-8.2s-15,2.7-20.2,8.2c-5.4,5.5-8.1,12.7-8.1,21.7s2.7,16.1,8.1,21.7c5.2,5.5,12,8.2,20.2,8.2ZM631.5,33.1v132.8h-21.5v-12.3c-3.7,4.4-8.3,7.9-13.6,10.2-5.5,2.3-11.5,3.4-18.1,3.4s-17.4-2-24.7-6.1c-7.3-4.1-13.2-9.8-17.4-17.4-4.1-7.3-6.3-15.9-6.3-25.6s2.1-18.3,6.3-25.6c4.1-7.3,10-13.1,17.4-17.2,7.3-4.1,15.6-6.1,24.7-6.1s12.2,1.1,17.4,3.2c5.2,2.1,9.8,5.4,13.4,9.7v-49h22.4ZM581.1,148c5.4,0,10.2-1.3,14.5-3.8,4.3-2.3,7.7-5.9,10.2-10.4,2.5-4.5,3.8-9.8,3.8-15.7s-1.3-11.3-3.8-15.7c-2.5-4.5-5.9-8.1-10.2-10.6-4.3-2.3-9.1-3.6-14.5-3.6s-10.2,1.3-14.5,3.6c-4.3,2.5-7.7,6.1-10.2,10.6-2.5,4.5-3.8,9.8-3.8,15.7s1.3,11.3,3.8,15.7c2.5,4.5,5.9,8.1,10.2,10.4,4.3,2.5,9.1,3.8,14.5,3.8ZM681.6,84.3c6.4-10,17.7-15,34-15v21.3c-1.7-.3-3.4-.5-5.2-.5-8.8,0-15.6,2.5-20.4,7.5-4.8,5.2-7.3,12.5-7.3,22v46.4h-22.4v-95.6h21.3v14h0ZM734.1,70.3h22.4v95.6h-22.4v-95.6ZM745.4,54.6c-4.1,0-7.5-1.3-10.2-3.9-2.7-2.4-4.2-5.9-4.1-9.5,0-3.8,1.4-7,4.1-9.7,2.7-2.5,6.1-3.8,10.2-3.8s7.5,1.3,10.2,3.6c2.7,2.5,4.1,5.5,4.1,9.3s-1.3,7.2-3.9,9.8c-2.7,2.7-6.3,4.1-10.4,4.1ZM839.5,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4v-95.6h21.3v12.3c3.8-4.5,8.4-7.7,14-10,5.5-2.3,12-3.4,19-3.4ZM964.8,160.7c-2.8,2.2-6,3.9-9.5,4.8-3.9,1.1-7.9,1.6-12,1.6-10.6,0-18.6-2.7-24.3-8.2-5.7-5.5-8.6-13.4-8.6-24v-46h-15.7v-17.9h15.7v-21.8h22.4v21.8h25.6v17.9h-25.6v45.5c0,4.7,1.1,8.2,3.4,10.6,2.3,2.5,5.5,3.8,9.8,3.8s9.1-1.3,12.5-3.9l6.3,15.9ZM1036.9,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4V33.1h22.4v48.3c3.8-3.9,8.2-7,13.8-9.1,5.4-2,11.5-3,18.1-3Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 7.0 KiB |
@@ -2,159 +2,161 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@import '@modrinth/ui/src/styles/tailwind-utilities.css';
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'bundled-minecraft-font-mrapp';
|
font-family: 'bundled-minecraft-font-mrapp';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-minecraft {
|
.font-minecraft {
|
||||||
font-family: 'bundled-minecraft-font-mrapp', monospace;
|
font-family: 'bundled-minecraft-font-mrapp', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: var(--font-standard, sans-serif), sans-serif;
|
font-family: var(--font-standard, sans-serif), sans-serif;
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--view-width: calc(100% - 5rem);
|
--view-width: calc(100% - 5rem);
|
||||||
--expanded-view-width: calc(100% - 13rem);
|
--expanded-view-width: calc(100% - 13rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-divider {
|
.card-divider {
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--color-button-bg);
|
color: var(--color-button-bg);
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin: var(--gap-sm) 0;
|
margin: var(--gap-sm) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-wrap {
|
.no-wrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-select {
|
.no-select {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
padding-block: var(--gap-sm);
|
padding-block: var(--gap-sm);
|
||||||
padding-inline: var(--gap-lg);
|
padding-inline: var(--gap-lg);
|
||||||
width: min-content;
|
width: min-content;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1.1rem;
|
width: 1.1rem;
|
||||||
height: 1.1rem;
|
height: 1.1rem;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.featured {
|
&.featured {
|
||||||
background-color: var(--color-brand-highlight);
|
background-color: var(--color-brand-highlight);
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: auto;
|
scrollbar-width: auto;
|
||||||
scrollbar-color: var(--color-scrollbar) var(--color-bg);
|
scrollbar-color: var(--color-scrollbar) var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chrome, Edge, and Safari */
|
/* Chrome, Edge, and Safari */
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
border: 3px solid transparent;
|
border: 3px solid transparent;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar:hover {
|
*::-webkit-scrollbar:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
*::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
background-color: var(--color-scrollbar);
|
background-color: var(--color-scrollbar);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
border: 5px solid transparent;
|
border: 5px solid transparent;
|
||||||
background-clip: content-box;
|
background-clip: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlighted {
|
.highlighted {
|
||||||
box-shadow: 0 0 1rem var(--color-brand) !important;
|
box-shadow: 0 0 1rem var(--color-brand) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gecko {
|
.gecko {
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-shadow {
|
.card-shadow {
|
||||||
box-shadow: var(--shadow-card);
|
box-shadow: var(--shadow-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
@import '@modrinth/assets/omorphia.scss';
|
@import '@modrinth/assets/omorphia.scss';
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
img {
|
img {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,39 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
FolderOpenIcon,
|
EyeIcon,
|
||||||
PlayIcon,
|
FolderOpenIcon,
|
||||||
PlusIcon,
|
PlayIcon,
|
||||||
TrashIcon,
|
PlusIcon,
|
||||||
StopCircleIcon,
|
SearchIcon,
|
||||||
EyeIcon,
|
StopCircleIcon,
|
||||||
SearchIcon,
|
TrashIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, DropdownSelect } from '@modrinth/ui'
|
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { formatCategoryHeader } from '@modrinth/utils'
|
import { formatCategoryHeader } from '@modrinth/utils'
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import { useStorage } from '@vueuse/core'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { duplicate, remove } from '@/helpers/profile.js'
|
import { computed, ref } from 'vue'
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { duplicate, remove } from '@/helpers/profile.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instances: {
|
instances: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const instanceOptions = ref(null)
|
const instanceOptions = ref(null)
|
||||||
const instanceComponents = ref(null)
|
const instanceComponents = ref(null)
|
||||||
@@ -39,335 +42,356 @@ const currentDeleteInstance = ref(null)
|
|||||||
const confirmModal = ref(null)
|
const confirmModal = ref(null)
|
||||||
|
|
||||||
async function deleteProfile() {
|
async function deleteProfile() {
|
||||||
if (currentDeleteInstance.value) {
|
if (currentDeleteInstance.value) {
|
||||||
instanceComponents.value = instanceComponents.value.filter(
|
instanceComponents.value = instanceComponents.value.filter(
|
||||||
(x) => x.instance.path !== currentDeleteInstance.value,
|
(x) => x.instance.path !== currentDeleteInstance.value,
|
||||||
)
|
)
|
||||||
await remove(currentDeleteInstance.value).catch(handleError)
|
await remove(currentDeleteInstance.value).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateProfile(p) {
|
async function duplicateProfile(p) {
|
||||||
await duplicate(p).catch(handleError)
|
await duplicate(p).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRightClick = (event, profilePathId) => {
|
const handleRightClick = (event, profilePathId) => {
|
||||||
const item = instanceComponents.value.find((x) => x.instance.path === profilePathId)
|
const item = instanceComponents.value.find((x) => x.instance.path === profilePathId)
|
||||||
const baseOptions = [
|
const baseOptions = [
|
||||||
{ name: 'add_content' },
|
{ name: 'add_content' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ name: 'edit' },
|
{ name: 'edit' },
|
||||||
{ name: 'duplicate' },
|
{ name: 'duplicate' },
|
||||||
{ name: 'open' },
|
{ name: 'open' },
|
||||||
{ name: 'copy' },
|
{ name: 'copy' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
instanceOptions.value.showMenu(
|
instanceOptions.value.showMenu(
|
||||||
event,
|
event,
|
||||||
item,
|
item,
|
||||||
item.playing
|
item.playing
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'stop',
|
name: 'stop',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
name: 'play',
|
name: 'play',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOptionsClick = async (args) => {
|
const handleOptionsClick = async (args) => {
|
||||||
switch (args.option) {
|
switch (args.option) {
|
||||||
case 'play':
|
case 'play':
|
||||||
args.item.play(null, 'InstanceGridContextMenu')
|
args.item.play(null, 'InstanceGridContextMenu')
|
||||||
break
|
break
|
||||||
case 'stop':
|
case 'stop':
|
||||||
args.item.stop(null, 'InstanceGridContextMenu')
|
args.item.stop(null, 'InstanceGridContextMenu')
|
||||||
break
|
break
|
||||||
case 'add_content':
|
case 'add_content':
|
||||||
await args.item.addContent()
|
await args.item.addContent()
|
||||||
break
|
break
|
||||||
case 'edit':
|
case 'edit':
|
||||||
await args.item.seeInstance()
|
await args.item.seeInstance()
|
||||||
break
|
break
|
||||||
case 'duplicate':
|
case 'duplicate':
|
||||||
if (args.item.instance.install_stage == 'installed')
|
if (args.item.instance.install_stage == 'installed')
|
||||||
await duplicateProfile(args.item.instance.path)
|
await duplicateProfile(args.item.instance.path)
|
||||||
break
|
break
|
||||||
case 'open':
|
case 'open':
|
||||||
await args.item.openFolder()
|
await args.item.openFolder()
|
||||||
break
|
break
|
||||||
case 'copy':
|
case 'copy':
|
||||||
await navigator.clipboard.writeText(args.item.instance.path)
|
await navigator.clipboard.writeText(args.item.instance.path)
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
currentDeleteInstance.value = args.item.instance.path
|
currentDeleteInstance.value = args.item.instance.path
|
||||||
confirmModal.value.show()
|
confirmModal.value.show()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const state = useStorage(
|
||||||
|
`${props.label}-grid-display-state`,
|
||||||
|
{
|
||||||
|
group: 'Group',
|
||||||
|
sortBy: 'Name',
|
||||||
|
},
|
||||||
|
localStorage,
|
||||||
|
{ mergeDefaults: true },
|
||||||
|
)
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const group = ref('Group')
|
|
||||||
const sortBy = ref('Name')
|
|
||||||
|
|
||||||
const filteredResults = computed(() => {
|
const filteredResults = computed(() => {
|
||||||
const instances = props.instances.filter((instance) => {
|
const { group = 'Group', sortBy = 'Name' } = state.value
|
||||||
return instance.name.toLowerCase().includes(search.value.toLowerCase())
|
|
||||||
})
|
|
||||||
|
|
||||||
if (sortBy.value === 'Name') {
|
const instances = props.instances.filter((instance) => {
|
||||||
instances.sort((a, b) => {
|
return instance.name.toLowerCase().includes(search.value.toLowerCase())
|
||||||
return a.name.localeCompare(b.name)
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sortBy.value === 'Game version') {
|
if (sortBy === 'Name') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return a.game_version.localeCompare(b.game_version)
|
return a.name.localeCompare(b.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Last played') {
|
if (sortBy === 'Game version') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
|
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Date created') {
|
if (sortBy === 'Last played') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.date_created).diff(dayjs(a.date_created))
|
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Date modified') {
|
if (sortBy === 'Date created') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
|
return dayjs(b.date_created).diff(dayjs(a.date_created))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceMap = new Map()
|
if (sortBy === 'Date modified') {
|
||||||
|
instances.sort((a, b) => {
|
||||||
|
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (group.value === 'Loader') {
|
const instanceMap = new Map()
|
||||||
instances.forEach((instance) => {
|
|
||||||
const loader = formatCategoryHeader(instance.loader)
|
|
||||||
if (!instanceMap.has(loader)) {
|
|
||||||
instanceMap.set(loader, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
instanceMap.get(loader).push(instance)
|
if (group === 'Loader') {
|
||||||
})
|
instances.forEach((instance) => {
|
||||||
} else if (group.value === 'Game version') {
|
const loader = formatCategoryHeader(instance.loader)
|
||||||
instances.forEach((instance) => {
|
if (!instanceMap.has(loader)) {
|
||||||
if (!instanceMap.has(instance.game_version)) {
|
instanceMap.set(loader, [])
|
||||||
instanceMap.set(instance.game_version, [])
|
}
|
||||||
}
|
|
||||||
|
|
||||||
instanceMap.get(instance.game_version).push(instance)
|
instanceMap.get(loader).push(instance)
|
||||||
})
|
})
|
||||||
} else if (group.value === 'Group') {
|
} else if (group === 'Game version') {
|
||||||
instances.forEach((instance) => {
|
instances.forEach((instance) => {
|
||||||
if (instance.groups.length === 0) {
|
if (!instanceMap.has(instance.game_version)) {
|
||||||
instance.groups.push('None')
|
instanceMap.set(instance.game_version, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const category of instance.groups) {
|
instanceMap.get(instance.game_version).push(instance)
|
||||||
if (!instanceMap.has(category)) {
|
})
|
||||||
instanceMap.set(category, [])
|
} else if (group === 'Group') {
|
||||||
}
|
instances.forEach((instance) => {
|
||||||
|
if (instance.groups.length === 0) {
|
||||||
|
instance.groups.push('None')
|
||||||
|
}
|
||||||
|
|
||||||
instanceMap.get(category).push(instance)
|
for (const category of instance.groups) {
|
||||||
}
|
if (!instanceMap.has(category)) {
|
||||||
})
|
instanceMap.set(category, [])
|
||||||
} else {
|
}
|
||||||
return instanceMap.set('None', instances)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
|
instanceMap.get(category).push(instance)
|
||||||
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
|
}
|
||||||
if (sortBy.value === 'Name') {
|
})
|
||||||
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
} else {
|
||||||
// None should always be first
|
return instanceMap.set('None', instances)
|
||||||
if (a[0] === 'None' && b[0] !== 'None') {
|
}
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (a[0] !== 'None' && b[0] === 'None') {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return a[0].localeCompare(b[0])
|
|
||||||
})
|
|
||||||
instanceMap.clear()
|
|
||||||
sortedEntries.forEach((entry) => {
|
|
||||||
instanceMap.set(entry[0], entry[1])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return instanceMap
|
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
|
||||||
|
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
|
||||||
|
if (sortBy === 'Name') {
|
||||||
|
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||||
|
// None should always be first
|
||||||
|
if (a[0] === 'None' && b[0] !== 'None') {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (a[0] !== 'None' && b[0] === 'None') {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return a[0].localeCompare(b[0])
|
||||||
|
})
|
||||||
|
instanceMap.clear()
|
||||||
|
sortedEntries.forEach((entry) => {
|
||||||
|
instanceMap.set(entry[0], entry[1])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
|
||||||
|
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
|
||||||
|
if (group === 'Game version') {
|
||||||
|
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||||
|
return a[0].localeCompare(b[0], undefined, { numeric: true })
|
||||||
|
})
|
||||||
|
instanceMap.clear()
|
||||||
|
sortedEntries.forEach((entry) => {
|
||||||
|
instanceMap.set(entry[0], entry[1])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return instanceMap
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="iconified-input flex-1">
|
<div class="iconified-input flex-1">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
<input v-model="search" type="text" placeholder="Search" />
|
<input v-model="search" type="text" placeholder="Search" />
|
||||||
<Button class="r-btn" @click="() => (search = '')">
|
<Button class="r-btn" @click="() => (search = '')">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<DropdownSelect
|
<DropdownSelect
|
||||||
v-slot="{ selected }"
|
v-slot="{ selected }"
|
||||||
v-model="sortBy"
|
v-model="state.sortBy"
|
||||||
name="Sort Dropdown"
|
name="Sort Dropdown"
|
||||||
class="max-w-[16rem]"
|
class="max-w-[16rem]"
|
||||||
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
||||||
placeholder="Select..."
|
placeholder="Select..."
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-primary">Sort by: </span>
|
<span class="font-semibold text-primary">Sort by: </span>
|
||||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
</DropdownSelect>
|
</DropdownSelect>
|
||||||
<DropdownSelect
|
<DropdownSelect
|
||||||
v-slot="{ selected }"
|
v-slot="{ selected }"
|
||||||
v-model="group"
|
v-model="state.group"
|
||||||
class="max-w-[16rem]"
|
class="max-w-[16rem]"
|
||||||
name="Group Dropdown"
|
name="Group Dropdown"
|
||||||
:options="['Group', 'Loader', 'Game version', 'None']"
|
:options="['Group', 'Loader', 'Game version', 'None']"
|
||||||
placeholder="Select..."
|
placeholder="Select..."
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-primary">Group by: </span>
|
<span class="font-semibold text-primary">Group by: </span>
|
||||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||||
</DropdownSelect>
|
</DropdownSelect>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
|
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
}))"
|
}))"
|
||||||
:key="instanceSection.key"
|
:key="instanceSection.key"
|
||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div v-if="instanceSection.key !== 'None'" class="divider">
|
<div v-if="instanceSection.key !== 'None'" class="divider">
|
||||||
<p>{{ instanceSection.key }}</p>
|
<p>{{ instanceSection.key }}</p>
|
||||||
<hr aria-hidden="true" />
|
<hr aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<section class="instances">
|
<section class="instances">
|
||||||
<Instance
|
<Instance
|
||||||
v-for="instance in instanceSection.value"
|
v-for="instance in instanceSection.value"
|
||||||
ref="instanceComponents"
|
ref="instanceComponents"
|
||||||
:key="instance.path + instance.install_stage"
|
:key="instance.path + instance.install_stage"
|
||||||
:instance="instance"
|
:instance="instance"
|
||||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="confirmModal"
|
ref="confirmModal"
|
||||||
title="Are you sure you want to delete this instance?"
|
title="Are you sure you want to delete this instance?"
|
||||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||||
:has-to-type="false"
|
:has-to-type="false"
|
||||||
proceed-label="Delete"
|
proceed-label="Delete"
|
||||||
@proceed="deleteProfile"
|
@proceed="deleteProfile"
|
||||||
/>
|
/>
|
||||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||||
<template #play> <PlayIcon /> Play </template>
|
<template #play> <PlayIcon /> Play </template>
|
||||||
<template #stop> <StopCircleIcon /> Stop </template>
|
<template #stop> <StopCircleIcon /> Stop </template>
|
||||||
<template #add_content> <PlusIcon /> Add content </template>
|
<template #add_content> <PlusIcon /> Add content </template>
|
||||||
<template #edit> <EyeIcon /> View instance </template>
|
<template #edit> <EyeIcon /> View instance </template>
|
||||||
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
||||||
<template #delete> <TrashIcon /> Delete </template>
|
<template #delete> <TrashIcon /> Delete </template>
|
||||||
<template #open> <FolderOpenIcon /> Open folder </template>
|
<template #open> <FolderOpenIcon /> Open folder </template>
|
||||||
<template #copy> <ClipboardCopyIcon /> Copy path </template>
|
<template #copy> <ClipboardCopyIcon /> Copy path </template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
background-color: var(--color-gray);
|
background-color: var(--color-gray);
|
||||||
height: 1px;
|
height: 1px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: inherit;
|
align-items: inherit;
|
||||||
margin: 1rem 1rem 0 !important;
|
margin: 1rem 1rem 0 !important;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
width: calc(100% - 2rem);
|
width: calc(100% - 2rem);
|
||||||
|
|
||||||
.iconified-input {
|
.iconified-input {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-dropdown {
|
.sort-dropdown {
|
||||||
width: 10rem;
|
width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-dropdown {
|
.filter-dropdown {
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-dropdown {
|
.group-dropdown {
|
||||||
width: 10rem;
|
width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.labeled_button {
|
.labeled_button {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instances {
|
.instances {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { useLoading } from '@/store/state.js'
|
import { useLoading } from '@/store/state.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
throttle: {
|
throttle: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1000,
|
default: 1000,
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 2,
|
default: 2,
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'var(--loading-bar-gradient)',
|
default: 'var(--loading-bar-gradient)',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const indicator = useLoadingIndicator({
|
const indicator = useLoadingIndicator({
|
||||||
duration: props.duration,
|
duration: props.duration,
|
||||||
throttle: props.throttle,
|
throttle: props.throttle,
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => indicator.clear)
|
onBeforeUnmount(() => indicator.clear)
|
||||||
@@ -31,111 +32,111 @@ onBeforeUnmount(() => indicator.clear)
|
|||||||
const loading = useLoading()
|
const loading = useLoading()
|
||||||
|
|
||||||
watch(loading, (newValue) => {
|
watch(loading, (newValue) => {
|
||||||
if (newValue.barEnabled) {
|
if (newValue.barEnabled) {
|
||||||
if (newValue.loading) {
|
if (newValue.loading) {
|
||||||
indicator.start()
|
indicator.start()
|
||||||
} else {
|
} else {
|
||||||
indicator.finish()
|
indicator.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function useLoadingIndicator(opts) {
|
function useLoadingIndicator(opts) {
|
||||||
const progress = ref(0)
|
const progress = ref(0)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const step = computed(() => 10000 / opts.duration)
|
const step = computed(() => 10000 / opts.duration)
|
||||||
|
|
||||||
let _timer = null
|
let _timer = null
|
||||||
let _throttle = null
|
let _throttle = null
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
clear()
|
clear()
|
||||||
progress.value = 0
|
progress.value = 0
|
||||||
if (opts.throttle) {
|
if (opts.throttle) {
|
||||||
_throttle = setTimeout(() => {
|
_throttle = setTimeout(() => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
_startTimer()
|
_startTimer()
|
||||||
}, opts.throttle)
|
}, opts.throttle)
|
||||||
} else {
|
} else {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
_startTimer()
|
_startTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function finish() {
|
function finish() {
|
||||||
progress.value = 100
|
progress.value = 100
|
||||||
_hide()
|
_hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
clearInterval(_timer)
|
clearInterval(_timer)
|
||||||
clearTimeout(_throttle)
|
clearTimeout(_throttle)
|
||||||
_timer = null
|
_timer = null
|
||||||
_throttle = null
|
_throttle = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function _increase(num) {
|
function _increase(num) {
|
||||||
progress.value = Math.min(100, progress.value + num)
|
progress.value = Math.min(100, progress.value + num)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _hide() {
|
function _hide() {
|
||||||
clear()
|
clear()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
progress.value = 0
|
progress.value = 0
|
||||||
}, 400)
|
}, 400)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _startTimer() {
|
function _startTimer() {
|
||||||
_timer = setInterval(() => {
|
_timer = setInterval(() => {
|
||||||
_increase(step.value)
|
_increase(step.value)
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { progress, isLoading, start, finish, clear }
|
return { progress, isLoading, start, finish, clear }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="loading-indicator-bar"
|
class="loading-indicator-bar"
|
||||||
:style="{
|
:style="{
|
||||||
'--_width': `${indicator.progress.value}%`,
|
'--_width': `${indicator.progress.value}%`,
|
||||||
'--_height': `${indicator.isLoading.value ? props.height : 0}px`,
|
'--_height': `${indicator.isLoading.value ? props.height : 0}px`,
|
||||||
'--_opacity': `${indicator.isLoading.value ? 1 : 0}`,
|
'--_opacity': `${indicator.isLoading.value ? 1 : 0}`,
|
||||||
top: `0`,
|
top: `0`,
|
||||||
right: `0`,
|
right: `0`,
|
||||||
left: `${props.offsetWidth}`,
|
left: `${props.offsetWidth}`,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
width: `var(--_width)`,
|
width: `var(--_width)`,
|
||||||
height: `var(--_height)`,
|
height: `var(--_height)`,
|
||||||
borderRadius: `var(--_height)`,
|
borderRadius: `var(--_height)`,
|
||||||
// opacity: `var(--_opacity)`,
|
// opacity: `var(--_opacity)`,
|
||||||
background: `${props.color}`,
|
background: `${props.color}`,
|
||||||
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
||||||
transition: 'width 0.1s ease-in-out, height 0.1s ease-out',
|
transition: 'width 0.1s ease-in-out, height 0.1s ease-out',
|
||||||
zIndex: 6,
|
zIndex: 6,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.loading-indicator-bar::before {
|
.loading-indicator-bar::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: var(--_width);
|
width: var(--_width);
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-image: radial-gradient(80% 100% at 20% 0%, var(--color-brand) 0%, transparent 80%);
|
background-image: radial-gradient(80% 100% at 20% 0%, var(--color-brand) 0%, transparent 80%);
|
||||||
opacity: calc(var(--_opacity) * 0.1);
|
opacity: calc(var(--_opacity) * 0.1);
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
transition:
|
transition:
|
||||||
width 0.1s ease-in-out,
|
width 0.1s ease-in-out,
|
||||||
opacity 0.1s ease-out;
|
opacity 0.1s ease-out;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,52 +1,54 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
FolderOpenIcon,
|
DownloadIcon,
|
||||||
PlayIcon,
|
ExternalIcon,
|
||||||
PlusIcon,
|
EyeIcon,
|
||||||
TrashIcon,
|
FolderOpenIcon,
|
||||||
DownloadIcon,
|
GlobeIcon,
|
||||||
GlobeIcon,
|
PlayIcon,
|
||||||
StopCircleIcon,
|
PlusIcon,
|
||||||
ExternalIcon,
|
StopCircleIcon,
|
||||||
EyeIcon,
|
TrashIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import { HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import ProjectCard from '@/components/ui/ProjectCard.vue'
|
|
||||||
import { get_by_profile_path } from '@/helpers/process.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import ProjectCard from '@/components/ui/ProjectCard.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { get_by_profile_path } from '@/helpers/process.js'
|
||||||
|
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
||||||
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { install as installVersion } from '@/store/install.js'
|
import { install as installVersion } from '@/store/install.js'
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
||||||
import { HeadingLink } from '@modrinth/ui'
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instances: {
|
instances: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
canPaginate: Boolean,
|
canPaginate: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const actualInstances = computed(() =>
|
const actualInstances = computed(() =>
|
||||||
props.instances.filter(
|
props.instances.filter(
|
||||||
(x) => (x && x.instances && x.instances[0] && x.show === undefined) || x.show,
|
(x) => (x && x.instances && x.instances[0] && x.show === undefined) || x.show,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const modsRow = ref(null)
|
const modsRow = ref(null)
|
||||||
@@ -58,124 +60,131 @@ const deleteConfirmModal = ref(null)
|
|||||||
const currentDeleteInstance = ref(null)
|
const currentDeleteInstance = ref(null)
|
||||||
|
|
||||||
async function deleteProfile() {
|
async function deleteProfile() {
|
||||||
if (currentDeleteInstance.value) {
|
if (currentDeleteInstance.value) {
|
||||||
await remove(currentDeleteInstance.value).catch(handleError)
|
await remove(currentDeleteInstance.value).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateProfile(p) {
|
async function duplicateProfile(p) {
|
||||||
await duplicate(p).catch(handleError)
|
await duplicate(p).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInstanceRightClick = async (event, passedInstance) => {
|
const handleInstanceRightClick = async (event, passedInstance) => {
|
||||||
const baseOptions = [
|
const baseOptions = [
|
||||||
{ name: 'add_content' },
|
{ name: 'add_content' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ name: 'edit' },
|
{ name: 'edit' },
|
||||||
{ name: 'duplicate' },
|
{ name: 'duplicate' },
|
||||||
{ name: 'open_folder' },
|
{ name: 'open_folder' },
|
||||||
{ name: 'copy_path' },
|
{ name: 'copy_path' },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const runningProcesses = await get_by_profile_path(passedInstance.path).catch(handleError)
|
const runningProcesses = await get_by_profile_path(passedInstance.path).catch(handleError)
|
||||||
|
|
||||||
const options =
|
const options =
|
||||||
runningProcesses.length > 0
|
runningProcesses.length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'stop',
|
name: 'stop',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
name: 'play',
|
name: 'play',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
]
|
]
|
||||||
|
|
||||||
instanceOptions.value.showMenu(event, passedInstance, options)
|
instanceOptions.value.showMenu(event, passedInstance, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleProjectClick = (event, passedInstance) => {
|
const handleProjectClick = (event, passedInstance) => {
|
||||||
instanceOptions.value.showMenu(event, passedInstance, [
|
instanceOptions.value.showMenu(event, passedInstance, [
|
||||||
{
|
{
|
||||||
name: 'install',
|
name: 'install',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
name: 'open_link',
|
name: 'open_link',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'copy_link',
|
name: 'copy_link',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOptionsClick = async (args) => {
|
const handleOptionsClick = async (args) => {
|
||||||
switch (args.option) {
|
switch (args.option) {
|
||||||
case 'play':
|
case 'play':
|
||||||
await run(args.item.path).catch((err) =>
|
await run(args.item.path).catch((err) =>
|
||||||
handleSevereError(err, { profilePath: args.item.path }),
|
handleSevereError(err, { profilePath: args.item.path }),
|
||||||
)
|
)
|
||||||
trackEvent('InstanceStart', {
|
trackEvent('InstanceStart', {
|
||||||
loader: args.item.loader,
|
loader: args.item.loader,
|
||||||
game_version: args.item.game_version,
|
game_version: args.item.game_version,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'stop':
|
case 'stop':
|
||||||
await kill(args.item.path).catch(handleError)
|
await kill(args.item.path).catch(handleError)
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
loader: args.item.loader,
|
loader: args.item.loader,
|
||||||
game_version: args.item.game_version,
|
game_version: args.item.game_version,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'add_content':
|
case 'add_content':
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/browse/${args.item.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
path: `/browse/${args.item.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||||
query: { i: args.item.path },
|
query: { i: args.item.path },
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'edit':
|
case 'edit':
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/instance/${encodeURIComponent(args.item.path)}/`,
|
path: `/instance/${encodeURIComponent(args.item.path)}/`,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'duplicate':
|
case 'duplicate':
|
||||||
if (args.item.install_stage == 'installed') await duplicateProfile(args.item.path)
|
if (args.item.install_stage == 'installed') await duplicateProfile(args.item.path)
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
currentDeleteInstance.value = args.item.path
|
currentDeleteInstance.value = args.item.path
|
||||||
deleteConfirmModal.value.show()
|
deleteConfirmModal.value.show()
|
||||||
break
|
break
|
||||||
case 'open_folder':
|
case 'open_folder':
|
||||||
await showProfileInFolder(args.item.path)
|
await showProfileInFolder(args.item.path)
|
||||||
break
|
break
|
||||||
case 'copy_path':
|
case 'copy_path':
|
||||||
await navigator.clipboard.writeText(args.item.path)
|
await navigator.clipboard.writeText(args.item.path)
|
||||||
break
|
break
|
||||||
case 'install': {
|
case 'install': {
|
||||||
await installVersion(args.item.project_id, null, null, 'ProjectCardContextMenu')
|
await installVersion(
|
||||||
|
args.item.project_id,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'ProjectCardContextMenu',
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
).catch(handleError)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'open_link':
|
case 'open_link':
|
||||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||||
break
|
break
|
||||||
case 'copy_link':
|
case 'copy_link':
|
||||||
await navigator.clipboard.writeText(
|
await navigator.clipboard.writeText(
|
||||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxInstancesPerCompactRow = ref(1)
|
const maxInstancesPerCompactRow = ref(1)
|
||||||
@@ -183,184 +192,184 @@ const maxInstancesPerRow = ref(1)
|
|||||||
const maxProjectsPerRow = ref(1)
|
const maxProjectsPerRow = ref(1)
|
||||||
|
|
||||||
const calculateCardsPerRow = () => {
|
const calculateCardsPerRow = () => {
|
||||||
if (rows.value.length === 0) {
|
if (rows.value.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate how many cards fit in one row
|
// Calculate how many cards fit in one row
|
||||||
const containerWidth = rows.value[0].clientWidth
|
const containerWidth = rows.value[0].clientWidth
|
||||||
// Convert container width from pixels to rem
|
// Convert container width from pixels to rem
|
||||||
const containerWidthInRem =
|
const containerWidthInRem =
|
||||||
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||||
|
|
||||||
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
||||||
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
|
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
|
||||||
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
|
||||||
|
|
||||||
if (maxInstancesPerRow.value < 5) {
|
if (maxInstancesPerRow.value < 5) {
|
||||||
maxInstancesPerRow.value *= 2
|
maxInstancesPerRow.value *= 2
|
||||||
}
|
}
|
||||||
if (maxInstancesPerCompactRow.value < 5) {
|
if (maxInstancesPerCompactRow.value < 5) {
|
||||||
maxInstancesPerCompactRow.value *= 2
|
maxInstancesPerCompactRow.value *= 2
|
||||||
}
|
}
|
||||||
if (maxProjectsPerRow.value < 3) {
|
if (maxProjectsPerRow.value < 3) {
|
||||||
maxProjectsPerRow.value *= 2
|
maxProjectsPerRow.value *= 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowContainer = ref(null)
|
const rowContainer = ref(null)
|
||||||
const resizeObserver = ref(null)
|
const resizeObserver = ref(null)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
calculateCardsPerRow()
|
calculateCardsPerRow()
|
||||||
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
|
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
|
||||||
if (rowContainer.value) {
|
if (rowContainer.value) {
|
||||||
resizeObserver.value.observe(rowContainer.value)
|
resizeObserver.value.observe(rowContainer.value)
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', calculateCardsPerRow)
|
window.addEventListener('resize', calculateCardsPerRow)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', calculateCardsPerRow)
|
window.removeEventListener('resize', calculateCardsPerRow)
|
||||||
if (rowContainer.value) {
|
if (rowContainer.value) {
|
||||||
resizeObserver.value.unobserve(rowContainer.value)
|
resizeObserver.value.unobserve(rowContainer.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="deleteConfirmModal"
|
ref="deleteConfirmModal"
|
||||||
title="Are you sure you want to delete this instance?"
|
title="Are you sure you want to delete this instance?"
|
||||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||||
:has-to-type="false"
|
:has-to-type="false"
|
||||||
proceed-label="Delete"
|
proceed-label="Delete"
|
||||||
@proceed="deleteProfile"
|
@proceed="deleteProfile"
|
||||||
/>
|
/>
|
||||||
<div ref="rowContainer" class="flex flex-col gap-4">
|
<div ref="rowContainer" class="flex flex-col gap-4">
|
||||||
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
|
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
|
||||||
<HeadingLink class="mt-1" :to="row.route">
|
<HeadingLink class="mt-1" :to="row.route">
|
||||||
{{ row.label }}
|
{{ row.label }}
|
||||||
</HeadingLink>
|
</HeadingLink>
|
||||||
<section
|
<section
|
||||||
v-if="row.instance"
|
v-if="row.instance"
|
||||||
ref="modsRow"
|
ref="modsRow"
|
||||||
class="instances"
|
class="instances"
|
||||||
:class="{ compact: row.compact }"
|
:class="{ compact: row.compact }"
|
||||||
>
|
>
|
||||||
<Instance
|
<Instance
|
||||||
v-for="(instance, instanceIndex) in row.instances.slice(
|
v-for="(instance, instanceIndex) in row.instances.slice(
|
||||||
0,
|
0,
|
||||||
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
|
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
|
||||||
)"
|
)"
|
||||||
:key="row.label + instance.path"
|
:key="row.label + instance.path"
|
||||||
:instance="instance"
|
:instance="instance"
|
||||||
:compact="row.compact"
|
:compact="row.compact"
|
||||||
:first="instanceIndex === 0"
|
:first="instanceIndex === 0"
|
||||||
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
|
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section v-else ref="modsRow" class="projects">
|
<section v-else ref="modsRow" class="projects">
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
v-for="project in row.instances.slice(0, maxProjectsPerRow)"
|
v-for="project in row.instances.slice(0, maxProjectsPerRow)"
|
||||||
:key="project?.project_id"
|
:key="project?.project_id"
|
||||||
ref="instanceComponents"
|
ref="instanceComponents"
|
||||||
class="item"
|
class="item"
|
||||||
:project="project"
|
:project="project"
|
||||||
@contextmenu.prevent.stop="(event) => handleProjectClick(event, project)"
|
@contextmenu.prevent.stop="(event) => handleProjectClick(event, project)"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||||
<template #play> <PlayIcon /> Play </template>
|
<template #play> <PlayIcon /> Play </template>
|
||||||
<template #stop> <StopCircleIcon /> Stop </template>
|
<template #stop> <StopCircleIcon /> Stop </template>
|
||||||
<template #add_content> <PlusIcon /> Add content </template>
|
<template #add_content> <PlusIcon /> Add content </template>
|
||||||
<template #edit> <EyeIcon /> View instance </template>
|
<template #edit> <EyeIcon /> View instance </template>
|
||||||
<template #delete> <TrashIcon /> Delete </template>
|
<template #delete> <TrashIcon /> Delete </template>
|
||||||
<template #open_folder> <FolderOpenIcon /> Open folder </template>
|
<template #open_folder> <FolderOpenIcon /> Open folder </template>
|
||||||
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
|
||||||
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
|
||||||
<template #install> <DownloadIcon /> Install </template>
|
<template #install> <DownloadIcon /> Install </template>
|
||||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
|
||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
gap: var(--gap-xs);
|
gap: var(--gap-xs);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instances {
|
.instances {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||||
grid-gap: 0.75rem;
|
grid-gap: 0.75rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&.compact {
|
&.compact {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects {
|
.projects {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||||
grid-gap: 0.75rem;
|
grid-gap: 0.75rem;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,62 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DropdownIcon, PlusIcon, FolderOpenIcon } from '@modrinth/assets'
|
import { DropdownIcon, FolderOpenIcon, PlusIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth/ui'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { add_project_from_path } from '@/helpers/profile.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { add_project_from_path } from '@/helpers/profile.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const handleAddContentFromFile = async () => {
|
const handleAddContentFromFile = async () => {
|
||||||
const newProject = await open({ multiple: true })
|
const newProject = await open({ multiple: true })
|
||||||
if (!newProject) return
|
if (!newProject) return
|
||||||
|
|
||||||
for (const project of newProject) {
|
for (const project of newProject) {
|
||||||
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
|
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearchContent = async () => {
|
const handleSearchContent = async () => {
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
||||||
query: { i: props.instance.path },
|
query: { i: props.instance.path },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="joined-buttons">
|
<div class="joined-buttons">
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="handleSearchContent">
|
<button @click="handleSearchContent">
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
Install content
|
Install content
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
id: 'from_file',
|
id: 'from_file',
|
||||||
action: handleAddContentFromFile,
|
action: handleAddContentFromFile,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<DropdownIcon />
|
<DropdownIcon />
|
||||||
<template #from_file>
|
<template #from_file>
|
||||||
<FolderOpenIcon />
|
<FolderOpenIcon />
|
||||||
<span class="no-wrap"> Add from file </span>
|
<span class="no-wrap"> Add from file </span>
|
||||||
</template>
|
</template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,63 +1,64 @@
|
|||||||
<template>
|
<template>
|
||||||
<div data-tauri-drag-region class="flex items-center gap-1 pl-3">
|
<div data-tauri-drag-region class="flex items-center gap-1 pl-3">
|
||||||
<Button v-if="false" class="breadcrumbs__back transparent" icon-only @click="$router.back()">
|
<Button v-if="false" class="breadcrumbs__back transparent" icon-only @click="$router.back()">
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="false"
|
v-if="false"
|
||||||
class="breadcrumbs__forward transparent"
|
class="breadcrumbs__forward transparent"
|
||||||
icon-only
|
icon-only
|
||||||
@click="$router.forward()"
|
@click="$router.forward()"
|
||||||
>
|
>
|
||||||
<ChevronRightIcon />
|
<ChevronRightIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||||
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="breadcrumb.link"
|
v-if="breadcrumb.link"
|
||||||
:to="{
|
:to="{
|
||||||
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id)),
|
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id)),
|
||||||
query: breadcrumb.query,
|
query: breadcrumb.query,
|
||||||
}"
|
}"
|
||||||
class="text-primary"
|
class="text-primary"
|
||||||
>{{
|
>{{
|
||||||
breadcrumb.name.charAt(0) === '?'
|
breadcrumb.name.charAt(0) === '?'
|
||||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
||||||
: breadcrumb.name
|
: breadcrumb.name
|
||||||
}}
|
}}
|
||||||
</router-link>
|
</router-link>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
class="text-contrast font-semibold cursor-default select-none"
|
class="text-contrast font-semibold cursor-default select-none"
|
||||||
>{{
|
>{{
|
||||||
breadcrumb.name.charAt(0) === '?'
|
breadcrumb.name.charAt(0) === '?'
|
||||||
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
? breadcrumbData.getName(breadcrumb.name.slice(1))
|
||||||
: breadcrumb.name
|
: breadcrumb.name
|
||||||
}}</span
|
}}</span
|
||||||
>
|
>
|
||||||
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5" />
|
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ChevronRightIcon, ChevronLeftIcon } from '@modrinth/assets'
|
import { ChevronLeftIcon, ChevronRightIcon } from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button } from '@modrinth/ui'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const breadcrumbData = useBreadcrumbs()
|
const breadcrumbData = useBreadcrumbs()
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
const additionalContext =
|
const additionalContext =
|
||||||
route.meta.useContext === true
|
route.meta.useContext === true
|
||||||
? breadcrumbData.context
|
? breadcrumbData.context
|
||||||
: route.meta.useRootContext === true
|
: route.meta.useRootContext === true
|
||||||
? breadcrumbData.rootContext
|
? breadcrumbData.rootContext
|
||||||
: null
|
: null
|
||||||
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
|
return additionalContext ? [additionalContext, ...route.meta.breadcrumb] : route.meta.breadcrumb
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-show="shown" ref="contextMenu" class="context-menu" :style="{
|
<div
|
||||||
left: left,
|
v-show="shown"
|
||||||
top: top,
|
ref="contextMenu"
|
||||||
}">
|
class="context-menu"
|
||||||
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
:style="{
|
||||||
<hr v-if="option.type === 'divider'" class="divider" />
|
left: left,
|
||||||
<div v-else-if="!(isLinkedData(item) && option.name === `add_content`)" class="item clickable"
|
top: top,
|
||||||
:class="[option.color ?? 'base']">
|
}"
|
||||||
<slot :name="option.name" />
|
>
|
||||||
</div>
|
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
||||||
</div>
|
<hr v-if="option.type === 'divider'" class="divider" />
|
||||||
</div>
|
<div
|
||||||
</transition>
|
v-else-if="!(isLinkedData(item) && option.name === `add_content`)"
|
||||||
|
class="item clickable"
|
||||||
|
:class="[option.color ?? 'base']"
|
||||||
|
>
|
||||||
|
<slot :name="option.name" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const emit = defineEmits(['menu-closed', 'option-clicked'])
|
const emit = defineEmits(['menu-closed', 'option-clicked'])
|
||||||
|
|
||||||
@@ -28,141 +36,146 @@ const top = ref('0px')
|
|||||||
const shown = ref(false)
|
const shown = ref(false)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
showMenu: (event, passedItem, passedOptions) => {
|
showMenu: (event, passedItem, passedOptions) => {
|
||||||
item.value = passedItem
|
item.value = passedItem
|
||||||
options.value = passedOptions
|
options.value = passedOptions
|
||||||
|
|
||||||
const menuWidth = contextMenu.value.clientWidth
|
// show to get dimensions
|
||||||
const menuHeight = contextMenu.value.clientHeight
|
shown.value = true
|
||||||
|
|
||||||
if (menuWidth + event.pageX >= window.innerWidth) {
|
// then, adjust position if overflowing
|
||||||
left.value = event.pageX - menuWidth + 2 + 'px'
|
nextTick(() => {
|
||||||
} else {
|
const menuWidth = contextMenu.value?.clientWidth || 200
|
||||||
left.value = event.pageX - 2 + 'px'
|
const menuHeight = contextMenu.value?.clientHeight || 100
|
||||||
}
|
const minFromEdge = 10
|
||||||
|
|
||||||
if (menuHeight + event.pageY >= window.innerHeight) {
|
if (event.pageX + menuWidth + minFromEdge >= window.innerWidth) {
|
||||||
top.value = event.pageY - menuHeight + 2 + 'px'
|
left.value = Math.max(minFromEdge, event.pageX - menuWidth - minFromEdge) + 'px'
|
||||||
} else {
|
} else {
|
||||||
top.value = event.pageY - 2 + 'px'
|
left.value = event.pageX + minFromEdge + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
shown.value = true
|
if (event.pageY + menuHeight + minFromEdge >= window.innerHeight) {
|
||||||
},
|
top.value = Math.max(minFromEdge, event.pageY - menuHeight - minFromEdge) + 'px'
|
||||||
|
} else {
|
||||||
|
top.value = event.pageY + minFromEdge + 'px'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const isLinkedData = (item) => {
|
const isLinkedData = (item) => {
|
||||||
if (item.instance != undefined && item.instance.linked_data) {
|
if (item.instance != undefined && item.instance.linked_data) {
|
||||||
return true
|
return true
|
||||||
} else if (item != undefined && item.linked_data) {
|
} else if (item != undefined && item.linked_data) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideContextMenu = () => {
|
const hideContextMenu = () => {
|
||||||
shown.value = false
|
shown.value = false
|
||||||
emit('menu-closed')
|
emit('menu-closed')
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionClicked = (option) => {
|
const optionClicked = (option) => {
|
||||||
emit('option-clicked', {
|
emit('option-clicked', {
|
||||||
item: item.value,
|
item: item.value,
|
||||||
option: option,
|
option: option,
|
||||||
})
|
})
|
||||||
hideContextMenu()
|
hideContextMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEscKeyRelease = (event) => {
|
const onEscKeyRelease = (event) => {
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
hideContextMenu()
|
hideContextMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||||
if (
|
if (
|
||||||
contextMenu.value &&
|
contextMenu.value &&
|
||||||
contextMenu.value.$el !== event.target &&
|
contextMenu.value.$el !== event.target &&
|
||||||
!elements.includes(contextMenu.value.$el)
|
!elements.includes(contextMenu.value.$el)
|
||||||
) {
|
) {
|
||||||
hideContextMenu()
|
hideContextMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('click', handleClickOutside)
|
window.addEventListener('click', handleClickOutside)
|
||||||
document.body.addEventListener('keyup', onEscKeyRelease)
|
document.body.addEventListener('keyup', onEscKeyRelease)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('click', handleClickOutside)
|
window.removeEventListener('click', handleClickOutside)
|
||||||
document.removeEventListener('keyup', onEscKeyRelease)
|
document.removeEventListener('keyup', onEscKeyRelease)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.context-menu {
|
.context-menu {
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
box-shadow: var(--shadow-floating);
|
box-shadow: var(--shadow-floating);
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-divider);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000000;
|
z-index: 1000000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: var(--gap-sm);
|
padding: var(--gap-sm);
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
padding: var(--gap-sm);
|
padding: var(--gap-sm);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
&.base {
|
&.base {
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background-color: var(--color-brand);
|
background-color: var(--color-brand);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.danger {
|
&.danger {
|
||||||
background-color: var(--color-red);
|
background-color: var(--color-red);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.contrast {
|
&.contrast {
|
||||||
background-color: var(--color-orange);
|
background-color: var(--color-orange);
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-accent-contrast);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-divider);
|
||||||
margin: var(--gap-sm);
|
margin: var(--gap-sm);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,380 @@
|
|||||||
|
<template>
|
||||||
|
<ModalWrapper ref="modal" :header="'Import from CurseForge Profile Code'">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="input-row">
|
||||||
|
<p class="input-label">Profile Code</p>
|
||||||
|
<div class="iconified-input">
|
||||||
|
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||||
|
<input ref="codeInput" v-model="profileCode" autocomplete="off" class="h-12 card-shadow"
|
||||||
|
spellcheck="false" type="text" placeholder="Enter CurseForge profile code" maxlength="20"
|
||||||
|
@keyup.enter="importProfile" />
|
||||||
|
<Button v-if="profileCode" class="r-btn" @click="() => (profileCode = '')">
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="metadata && !importing" class="profile-info">
|
||||||
|
<h3>Profile Information</h3>
|
||||||
|
<p><strong>Name:</strong> {{ metadata.name }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-message">
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="importing && importProgress.visible" class="progress-section">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span class="progress-text">{{ importProgress.message }}</span>
|
||||||
|
<span class="progress-percentage">{{ Math.floor(importProgress.percentage) }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar" :style="{ width: `${importProgress.percentage}%` }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-row">
|
||||||
|
<Button @click="hide" :disabled="importing">
|
||||||
|
<XIcon />
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button v-if="!metadata" @click="fetchMetadata" :disabled="!profileCode.trim() || fetching"
|
||||||
|
color="secondary">
|
||||||
|
<SearchIcon v-if="!fetching" />
|
||||||
|
{{ fetching ? 'Checking...' : 'Check Profile' }}
|
||||||
|
</Button>
|
||||||
|
<Button v-if="metadata" @click="importProfile" :disabled="importing" color="primary">
|
||||||
|
<DownloadIcon v-if="!importing" />
|
||||||
|
{{ importing ? 'Importing...' : 'Import Profile' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { Button } from '@modrinth/ui'
|
||||||
|
import {
|
||||||
|
XIcon,
|
||||||
|
SearchIcon,
|
||||||
|
DownloadIcon
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
fetch_curseforge_profile_metadata,
|
||||||
|
import_curseforge_profile
|
||||||
|
} from '@/helpers/import.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { loading_listener } from '@/helpers/events.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
closeParent: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const modal = ref(null)
|
||||||
|
const codeInput = ref(null)
|
||||||
|
const profileCode = ref('')
|
||||||
|
const metadata = ref(null)
|
||||||
|
const fetching = ref(false)
|
||||||
|
const importing = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const importProgress = ref({
|
||||||
|
visible: false,
|
||||||
|
percentage: 0,
|
||||||
|
message: 'Starting import...',
|
||||||
|
totalMods: 0,
|
||||||
|
downloadedMods: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
let unlistenLoading = null
|
||||||
|
let activeLoadingBarId = null
|
||||||
|
let progressFallbackTimer = null
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show: () => {
|
||||||
|
profileCode.value = ''
|
||||||
|
metadata.value = null
|
||||||
|
fetching.value = false
|
||||||
|
importing.value = false
|
||||||
|
error.value = ''
|
||||||
|
importProgress.value = {
|
||||||
|
visible: false,
|
||||||
|
percentage: 0,
|
||||||
|
message: 'Starting import...',
|
||||||
|
totalMods: 0,
|
||||||
|
downloadedMods: 0
|
||||||
|
}
|
||||||
|
modal.value?.show()
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
codeInput.value?.focus()
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
trackEvent('CurseForgeProfileImportStart', { source: 'ImportModal' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchMetadata = async () => {
|
||||||
|
if (!profileCode.value.trim()) return
|
||||||
|
|
||||||
|
fetching.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fetch_curseforge_profile_metadata(profileCode.value.trim())
|
||||||
|
metadata.value = result
|
||||||
|
trackEvent('CurseForgeProfileMetadataFetched', {
|
||||||
|
profileCode: profileCode.value.trim()
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch CurseForge profile metadata:', err)
|
||||||
|
error.value = 'Failed to fetch profile information. Please check the code and try again.'
|
||||||
|
handleError(err)
|
||||||
|
} finally {
|
||||||
|
fetching.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importProfile = async () => {
|
||||||
|
if (!profileCode.value.trim()) return
|
||||||
|
|
||||||
|
importing.value = true
|
||||||
|
error.value = ''
|
||||||
|
activeLoadingBarId = null // Reset for new import session
|
||||||
|
importProgress.value = {
|
||||||
|
visible: true,
|
||||||
|
percentage: 0,
|
||||||
|
message: 'Starting import...',
|
||||||
|
totalMods: 0,
|
||||||
|
downloadedMods: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback progress timer in case loading events don't work
|
||||||
|
progressFallbackTimer = setInterval(() => {
|
||||||
|
if (importing.value && importProgress.value.percentage < 90) {
|
||||||
|
// Slowly increment progress as a fallback
|
||||||
|
importProgress.value.percentage = Math.min(90, importProgress.value.percentage + 1)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { result, profilePath } = await import_curseforge_profile(profileCode.value.trim())
|
||||||
|
|
||||||
|
trackEvent('CurseForgeProfileImported', {
|
||||||
|
profileCode: profileCode.value.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
hide()
|
||||||
|
|
||||||
|
// Close the parent modal if provided
|
||||||
|
if (props.closeParent) {
|
||||||
|
props.closeParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to the imported profile
|
||||||
|
await router.push(`/instance/${encodeURIComponent(profilePath)}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to import CurseForge profile:', err)
|
||||||
|
error.value = 'Failed to import profile. Please try again.'
|
||||||
|
handleError(err)
|
||||||
|
} finally {
|
||||||
|
importing.value = false
|
||||||
|
importProgress.value.visible = false
|
||||||
|
if (progressFallbackTimer) {
|
||||||
|
clearInterval(progressFallbackTimer)
|
||||||
|
progressFallbackTimer = null
|
||||||
|
}
|
||||||
|
activeLoadingBarId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Listen for loading events to update progress
|
||||||
|
unlistenLoading = await loading_listener((event) => {
|
||||||
|
console.log('Loading event received:', event) // Debug log
|
||||||
|
|
||||||
|
// Handle all loading events that could be related to CurseForge profile import
|
||||||
|
const isCurseForgeEvent = event.event?.type === 'curseforge_profile_download'
|
||||||
|
const hasProfileName = event.event?.profile_name && importing.value
|
||||||
|
|
||||||
|
if ((isCurseForgeEvent || hasProfileName) && importing.value) {
|
||||||
|
// Store the loading bar ID for this import session
|
||||||
|
if (!activeLoadingBarId) {
|
||||||
|
activeLoadingBarId = event.loader_uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process events for our current import session
|
||||||
|
if (event.loader_uuid === activeLoadingBarId) {
|
||||||
|
if (event.fraction !== null && event.fraction !== undefined) {
|
||||||
|
const baseProgress = (event.fraction || 0) * 100
|
||||||
|
|
||||||
|
// Calculate custom progress based on the message
|
||||||
|
let finalProgress = baseProgress
|
||||||
|
const message = event.message || 'Importing profile...'
|
||||||
|
|
||||||
|
// Custom progress calculation for different stages
|
||||||
|
if (message.includes('Fetching') || message.includes('metadata')) {
|
||||||
|
finalProgress = Math.min(10, baseProgress)
|
||||||
|
} else if (message.includes('Downloading profile ZIP') || message.includes('profile ZIP')) {
|
||||||
|
finalProgress = Math.min(15, 10 + (baseProgress - 10) * 0.5)
|
||||||
|
} else if (message.includes('Extracting') || message.includes('ZIP')) {
|
||||||
|
finalProgress = Math.min(20, 15 + (baseProgress - 15) * 0.5)
|
||||||
|
} else if (message.includes('Configuring') || message.includes('profile')) {
|
||||||
|
finalProgress = Math.min(30, 20 + (baseProgress - 20) * 0.5)
|
||||||
|
} else if (message.includes('Copying') || message.includes('files')) {
|
||||||
|
finalProgress = Math.min(40, 30 + (baseProgress - 30) * 0.5)
|
||||||
|
} else if (message.includes('Downloaded mod') && message.includes(' of ')) {
|
||||||
|
// Parse "Downloaded mod X of Y" message
|
||||||
|
const match = message.match(/Downloaded mod (\d+) of (\d+)/)
|
||||||
|
if (match) {
|
||||||
|
const current = parseInt(match[1])
|
||||||
|
const total = parseInt(match[2])
|
||||||
|
// Mods take 40% of progress (from 40% to 80%)
|
||||||
|
const modProgress = (current / total) * 40
|
||||||
|
finalProgress = 40 + modProgress
|
||||||
|
} else {
|
||||||
|
finalProgress = Math.min(80, 40 + (baseProgress - 40) * 0.5)
|
||||||
|
}
|
||||||
|
} else if (message.includes('Downloading mod') || message.includes('mods')) {
|
||||||
|
// General mod downloading stage (40% to 80%)
|
||||||
|
finalProgress = Math.min(80, 40 + (baseProgress - 40) * 0.4)
|
||||||
|
} else if (message.includes('Installing Minecraft') || message.includes('Minecraft')) {
|
||||||
|
finalProgress = Math.min(95, 80 + (baseProgress - 80) * 0.75)
|
||||||
|
} else if (message.includes('Finalizing') || message.includes('completed')) {
|
||||||
|
finalProgress = Math.min(100, 95 + (baseProgress - 95))
|
||||||
|
} else {
|
||||||
|
// Default: use the base progress but ensure minimum progression
|
||||||
|
finalProgress = Math.max(importProgress.value.percentage, baseProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
importProgress.value.percentage = Math.min(100, Math.max(0, finalProgress))
|
||||||
|
importProgress.value.message = message
|
||||||
|
} else {
|
||||||
|
// Loading complete
|
||||||
|
importProgress.value.percentage = 100
|
||||||
|
importProgress.value.message = 'Import completed!'
|
||||||
|
activeLoadingBarId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (unlistenLoading) {
|
||||||
|
unlistenLoading()
|
||||||
|
}
|
||||||
|
if (progressFallbackTimer) {
|
||||||
|
clearInterval(progressFallbackTimer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.modal-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-contrast);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-button);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: var(--color-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
color: var(--color-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: var(--color-red);
|
||||||
|
border: 1px solid var(--color-red);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 0.75rem;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-contrast);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
color: var(--color-base);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
color: var(--color-contrast);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--color-button);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--color-brand);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,28 +1,36 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
DropdownIcon,
|
CopyIcon,
|
||||||
XIcon,
|
DropdownIcon,
|
||||||
HammerIcon,
|
HammerIcon,
|
||||||
LogInIcon,
|
LogInIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
CopyIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
|
import { ButtonStyled, Collapsible, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { ChatIcon } from '@/assets/icons'
|
import { ChatIcon } from '@/assets/icons'
|
||||||
import { ButtonStyled, Collapsible } from '@modrinth/ui'
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import { cancel_directory_change } from '@/helpers/settings.ts'
|
|
||||||
import { install } from '@/helpers/profile.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||||
|
import { install } from '@/helpers/profile.js'
|
||||||
|
import { cancel_directory_change } from '@/helpers/settings.ts'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
|
||||||
|
// [AR] Imports
|
||||||
|
import { applyMigrationFix } from '@/helpers/utils.js'
|
||||||
|
import { restartApp } from '@/helpers/utils.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const errorModal = ref()
|
const errorModal = ref()
|
||||||
const error = ref()
|
const error = ref()
|
||||||
const closable = ref(true)
|
const closable = ref(true)
|
||||||
const errorCollapsed = ref(false)
|
const errorCollapsed = ref(false)
|
||||||
|
const migrationFixSuccess = ref(null) // null | true | false
|
||||||
|
const migrationFixCallbackModel = ref()
|
||||||
|
|
||||||
const title = ref('An error occurred')
|
const title = ref('An error occurred')
|
||||||
const errorType = ref('unknown')
|
const errorType = ref('unknown')
|
||||||
@@ -30,111 +38,111 @@ const supportLink = ref('https://support.modrinth.com')
|
|||||||
const metadata = ref({})
|
const metadata = ref({})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
async show(errorVal, context, canClose = true, source = null) {
|
async show(errorVal, context, canClose = true, source = null) {
|
||||||
closable.value = canClose
|
closable.value = canClose
|
||||||
|
|
||||||
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
||||||
title.value = 'Unable to sign in to Minecraft'
|
title.value = 'Unable to sign in to Minecraft'
|
||||||
errorType.value = 'minecraft_auth'
|
errorType.value = 'minecraft_auth'
|
||||||
supportLink.value =
|
supportLink.value =
|
||||||
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
|
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
|
||||||
|
|
||||||
if (
|
if (
|
||||||
errorVal.message.includes('existing connection was forcibly closed') ||
|
errorVal.message.includes('existing connection was forcibly closed') ||
|
||||||
errorVal.message.includes('error sending request for url')
|
errorVal.message.includes('error sending request for url')
|
||||||
) {
|
) {
|
||||||
metadata.value.network = true
|
metadata.value.network = true
|
||||||
}
|
}
|
||||||
if (errorVal.message.includes('because the target machine actively refused it')) {
|
if (errorVal.message.includes('because the target machine actively refused it')) {
|
||||||
metadata.value.hostsFile = true
|
metadata.value.hostsFile = true
|
||||||
}
|
}
|
||||||
} else if (errorVal.message && errorVal.message.includes('User is not logged in')) {
|
} else if (errorVal.message && errorVal.message.includes('User is not logged in')) {
|
||||||
title.value = 'Sign in to Minecraft'
|
title.value = 'Sign in to Minecraft'
|
||||||
errorType.value = 'minecraft_sign_in'
|
errorType.value = 'minecraft_sign_in'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
} else if (errorVal.message && errorVal.message.includes('Move directory error:')) {
|
} else if (errorVal.message && errorVal.message.includes('Move directory error:')) {
|
||||||
title.value = 'Could not change app directory'
|
title.value = 'Could not change app directory'
|
||||||
errorType.value = 'directory_move'
|
errorType.value = 'directory_move'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
|
|
||||||
if (errorVal.message.includes('directory is not writeable')) {
|
if (errorVal.message.includes('directory is not writeable')) {
|
||||||
metadata.value.readOnly = true
|
metadata.value.readOnly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorVal.message.includes('Not enough space')) {
|
if (errorVal.message.includes('Not enough space')) {
|
||||||
metadata.value.notEnoughSpace = true
|
metadata.value.notEnoughSpace = true
|
||||||
}
|
}
|
||||||
} else if (errorVal.message && errorVal.message.includes('No loader version selected for')) {
|
} else if (errorVal.message && errorVal.message.includes('No loader version selected for')) {
|
||||||
title.value = 'No loader selected'
|
title.value = 'No loader selected'
|
||||||
errorType.value = 'no_loader_version'
|
errorType.value = 'no_loader_version'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
metadata.value.profilePath = context.profilePath
|
metadata.value.profilePath = context.profilePath
|
||||||
} else if (source === 'state_init') {
|
} else if (source === 'state_init') {
|
||||||
title.value = 'Error initializing Modrinth App'
|
title.value = 'Error initializing AstralRinth App'
|
||||||
errorType.value = 'state_init'
|
errorType.value = 'state_init'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
} else {
|
} else {
|
||||||
title.value = 'An error occurred'
|
title.value = 'An error occurred'
|
||||||
errorType.value = 'unknown'
|
errorType.value = 'unknown'
|
||||||
supportLink.value = 'https://support.modrinth.com'
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
metadata.value = {}
|
metadata.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
error.value = errorVal
|
error.value = errorVal
|
||||||
errorModal.value.show()
|
errorModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadingMinecraft = ref(false)
|
const loadingMinecraft = ref(false)
|
||||||
async function loginMinecraft() {
|
async function loginMinecraft() {
|
||||||
try {
|
try {
|
||||||
loadingMinecraft.value = true
|
loadingMinecraft.value = true
|
||||||
const loggedIn = await login_flow()
|
const loggedIn = await login_flow()
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
await set_default_user(loggedIn.profile.id).catch(handleError)
|
await set_default_user(loggedIn.profile.id).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
||||||
loadingMinecraft.value = false
|
loadingMinecraft.value = false
|
||||||
errorModal.value.hide()
|
errorModal.value.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
loadingMinecraft.value = false
|
loadingMinecraft.value = false
|
||||||
handleSevereError(err)
|
handleSevereError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelDirectoryChange() {
|
async function cancelDirectoryChange() {
|
||||||
try {
|
try {
|
||||||
await cancel_directory_change()
|
await cancel_directory_change()
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function retryDirectoryChange() {
|
function retryDirectoryChange() {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadingRepair = ref(false)
|
const loadingRepair = ref(false)
|
||||||
async function repairInstance() {
|
async function repairInstance() {
|
||||||
loadingRepair.value = true
|
loadingRepair.value = true
|
||||||
try {
|
try {
|
||||||
await install(metadata.value.profilePath, false)
|
await install(metadata.value.profilePath, false)
|
||||||
errorModal.value.hide()
|
errorModal.value.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleSevereError(err)
|
handleSevereError(err)
|
||||||
}
|
}
|
||||||
loadingRepair.value = false
|
loadingRepair.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasDebugInfo = computed(
|
const hasDebugInfo = computed(
|
||||||
() =>
|
() =>
|
||||||
errorType.value === 'directory_move' ||
|
errorType.value === 'directory_move' ||
|
||||||
errorType.value === 'minecraft_auth' ||
|
errorType.value === 'minecraft_auth' ||
|
||||||
errorType.value === 'state_init' ||
|
errorType.value === 'state_init' ||
|
||||||
errorType.value === 'no_loader_version',
|
errorType.value === 'no_loader_version',
|
||||||
)
|
)
|
||||||
|
|
||||||
const debugInfo = computed(() => error.value.message ?? error.value ?? 'No error message.')
|
const debugInfo = computed(() => error.value.message ?? error.value ?? 'No error message.')
|
||||||
@@ -142,236 +150,334 @@ const debugInfo = computed(() => error.value.message ?? error.value ?? 'No error
|
|||||||
const copied = ref(false)
|
const copied = ref(false)
|
||||||
|
|
||||||
async function copyToClipboard(text) {
|
async function copyToClipboard(text) {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
copied.value = true
|
copied.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copied.value = false
|
copied.value = false
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onApplyMigrationFix(eol) {
|
||||||
|
console.log(`[AR] • Attempting to apply migration ${eol.toUpperCase()} fix`)
|
||||||
|
try {
|
||||||
|
const result = await applyMigrationFix(eol)
|
||||||
|
migrationFixSuccess.value = result === true
|
||||||
|
console.log(`[AR] • Successfully applied migration ${eol.toUpperCase()} fix`, result)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[AR] • Failed to apply migration fix:`, err)
|
||||||
|
migrationFixSuccess.value = false
|
||||||
|
} finally {
|
||||||
|
migrationFixCallbackModel.value?.show?.()
|
||||||
|
if (migrationFixSuccess.value === true) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await restartApp()
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
|
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="markdown-body">
|
<div class="markdown-body">
|
||||||
<template v-if="errorType === 'minecraft_auth'">
|
<template v-if="errorType === 'minecraft_auth'">
|
||||||
<template v-if="metadata.network">
|
<template v-if="metadata.network">
|
||||||
<h3>Network issues</h3>
|
<h3>Network issues</h3>
|
||||||
<p>
|
<p>
|
||||||
It looks like there were issues with the Modrinth App connecting to Microsoft's
|
It looks like there were issues with the AstralRinth App connecting to Microsoft's
|
||||||
servers. This is often the result of a poor connection, so we recommend trying again
|
servers. This is often the result of a poor connection, so we recommend trying again
|
||||||
to see if it works. If issues continue to persist, follow the steps in
|
to see if it works. If issues continue to persist, follow the steps in
|
||||||
<a
|
<a
|
||||||
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_e71a5f805f"
|
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_e71a5f805f"
|
||||||
>
|
>
|
||||||
our support article
|
our support article
|
||||||
</a>
|
</a>
|
||||||
to troubleshoot.
|
to troubleshoot.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="metadata.hostsFile">
|
<template v-else-if="metadata.hostsFile">
|
||||||
<h3>Network issues</h3>
|
<h3>Network issues</h3>
|
||||||
<p>
|
<p>
|
||||||
The Modrinth App tried to connect to Microsoft / Xbox / Minecraft services, but the
|
The AstralRinth App tried to connect to Microsoft / Xbox / Minecraft services, but the
|
||||||
remote server rejected the connection. This may indicate that these services are
|
remote server rejected the connection. This may indicate that these services are
|
||||||
blocked by the hosts file. Please visit
|
blocked by the hosts file. Please visit
|
||||||
<a
|
<a
|
||||||
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_d694a29256"
|
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_d694a29256"
|
||||||
>
|
>
|
||||||
our support article
|
our support article
|
||||||
</a>
|
</a>
|
||||||
for steps on how to fix the issue.
|
for steps on how to fix the issue.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h3>Try another Microsoft account</h3>
|
<h3>Try another Microsoft account</h3>
|
||||||
<p>
|
<p>
|
||||||
Double check you've signed in with the right account. You may own Minecraft on a
|
Double check you've signed in with the right account. You may own Minecraft on a
|
||||||
different Microsoft account.
|
different Microsoft account.
|
||||||
</p>
|
</p>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
||||||
<LogInIcon /> Try another account
|
<LogInIcon /> Try another account
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3>Using PC Game Pass, coming from Bedrock, or just bought the game?</h3>
|
<h3>Using PC Game Pass, coming from Bedrock, or just bought the game?</h3>
|
||||||
<p>
|
<p>
|
||||||
Try signing in with the
|
Try signing in with the
|
||||||
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>
|
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>
|
||||||
first. Once you're done, come back here and sign in!
|
first. Once you're done, come back here and sign in!
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
||||||
<LogInIcon /> Try signing in again
|
<LogInIcon /> Try signing in again
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="errorType === 'directory_move'">
|
<template v-if="errorType === 'directory_move'">
|
||||||
<template v-if="metadata.readOnly">
|
<template v-if="metadata.readOnly">
|
||||||
<h3>Change directory permissions</h3>
|
<h3>Change directory permissions</h3>
|
||||||
<p>
|
<p>
|
||||||
It looks like the Modrinth App is unable to write to the directory you selected.
|
It looks like the AstralRinth App is unable to write to the directory you selected.
|
||||||
Please adjust the permissions of the directory and try again or cancel the directory
|
Please adjust the permissions of the directory and try again or cancel the directory
|
||||||
change.
|
change.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="metadata.notEnoughSpace">
|
<template v-else-if="metadata.notEnoughSpace">
|
||||||
<h3>Not enough space</h3>
|
<h3>Not enough space</h3>
|
||||||
<p>
|
<p>
|
||||||
It looks like there is not enough space on the disk containing the directory you
|
It looks like there is not enough space on the disk containing the directory you
|
||||||
selected. Please free up some space and try again or cancel the directory change.
|
selected. Please free up some space and try again or cancel the directory change.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p>
|
<p>
|
||||||
The Modrinth App is unable to migrate to the new directory you selected. Please
|
The AstralRinth App is unable to migrate to the new directory you selected. Please
|
||||||
contact support for help or cancel the directory change.
|
contact support for help or cancel the directory change.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn" @click="retryDirectoryChange">
|
<button class="btn" @click="retryDirectoryChange">
|
||||||
<UpdatedIcon /> Retry directory change
|
<UpdatedIcon /> Retry directory change
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" @click="cancelDirectoryChange">
|
<button class="btn btn-danger" @click="cancelDirectoryChange">
|
||||||
<XIcon /> Cancel directory change
|
<XIcon /> Cancel directory change
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="errorType === 'minecraft_sign_in'">
|
<div v-else-if="errorType === 'minecraft_sign_in'">
|
||||||
<p>
|
<p>
|
||||||
To play this instance, you must sign in through Microsoft below. If you don't have a
|
To play this instance, you must sign in through Microsoft below. If you don't have a
|
||||||
Minecraft account, you can purchase the game on the
|
Minecraft account, you can purchase the game on the
|
||||||
<a href="https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc"
|
<a href="https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc"
|
||||||
>Minecraft website</a
|
>Minecraft website</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
|
||||||
<LogInIcon /> Sign in to Minecraft
|
<LogInIcon /> Sign in to Minecraft
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-else-if="errorType === 'state_init'">
|
<template v-else-if="errorType === 'state_init'">
|
||||||
<p>
|
<p>
|
||||||
Modrinth App failed to load correctly. This may be because of a corrupted file, or
|
AstralRinth App failed to load correctly. This may be because of a corrupted file, or
|
||||||
because the app is missing crucial files.
|
because the app is missing crucial files.
|
||||||
</p>
|
</p>
|
||||||
<p>You may be able to fix it through one of the following ways:</p>
|
<p>You may be able to fix it through one of the following ways:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Ensuring you are connected to the internet, then try restarting the app.</li>
|
<li>Ensuring you are connected to the internet, then try restarting the app.</li>
|
||||||
<li>Redownloading the app.</li>
|
<li>Redownloading the app.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="errorType === 'no_loader_version'">
|
<template v-else-if="errorType === 'no_loader_version'">
|
||||||
<p>The Modrinth App failed to find the loader version for this instance.</p>
|
<p>The AstralRinth App failed to find the loader version for this instance.</p>
|
||||||
<p>To resolve this, you need to repair the instance. Click the button below to do so.</p>
|
<p>To resolve this, you need to repair the instance. Click the button below to do so.</p>
|
||||||
<div class="cta-button">
|
<div class="cta-button">
|
||||||
<button class="btn btn-primary" :disabled="loadingRepair" @click="repairInstance">
|
<button class="btn btn-primary" :disabled="loadingRepair" @click="repairInstance">
|
||||||
<HammerIcon /> Repair instance
|
<HammerIcon /> Repair instance
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ debugInfo }}
|
{{ debugInfo }}
|
||||||
</template>
|
</template>
|
||||||
<template v-if="hasDebugInfo">
|
<template v-if="hasDebugInfo">
|
||||||
<hr />
|
<hr />
|
||||||
<p>
|
<p>
|
||||||
If nothing is working and you need help, visit
|
If nothing is working and you need help, visit
|
||||||
<a :href="supportLink">our support page</a>
|
<a :href="supportLink">our support page</a>
|
||||||
and start a chat using the widget in the bottom right and we will be more than happy to
|
and start a chat using the widget in the bottom right and we will be more than happy to
|
||||||
assist! Make sure to provide the following debug information to the agent:
|
assist! Make sure to provide the following debug information to the agent:
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<a :href="supportLink" @click="errorModal.hide()"><ChatIcon /> Get support</a>
|
<a :href="supportLink" @click="errorModal.hide()"><ChatIcon /> Get support</a>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="closable">
|
<ButtonStyled v-if="closable">
|
||||||
<button @click="errorModal.hide()"><XIcon /> Close</button>
|
<button @click="errorModal.hide()"><XIcon /> Close</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="hasDebugInfo">
|
<ButtonStyled v-if="hasDebugInfo">
|
||||||
<button :disabled="copied" @click="copyToClipboard(debugInfo)">
|
<button :disabled="copied" @click="copyToClipboard(debugInfo)">
|
||||||
<template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template>
|
<template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template>
|
||||||
<template v-else> <CopyIcon /> Copy debug info </template>
|
<template v-else> <CopyIcon /> Copy debug info </template>
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="hasDebugInfo">
|
<template v-if="hasDebugInfo">
|
||||||
<div class="bg-button-bg rounded-xl mt-2 overflow-clip">
|
<div class="bg-button-bg rounded-xl mt-2 overflow-clip">
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer"
|
class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer"
|
||||||
@click="errorCollapsed = !errorCollapsed"
|
@click="errorCollapsed = !errorCollapsed"
|
||||||
>
|
>
|
||||||
<span class="text-contrast font-extrabold m-0">Debug information:</span>
|
<span class="text-contrast font-extrabold m-0">Debug information:</span>
|
||||||
<DropdownIcon
|
<DropdownIcon
|
||||||
class="h-5 w-5 text-secondary transition-transform"
|
class="h-5 w-5 text-secondary transition-transform"
|
||||||
:class="{ 'rotate-180': !errorCollapsed }"
|
:class="{ 'rotate-180': !errorCollapsed }"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<Collapsible :collapsed="errorCollapsed">
|
<Collapsible :collapsed="errorCollapsed">
|
||||||
<pre class="m-0 px-4 py-3 bg-bg rounded-none">{{ debugInfo }}</pre>
|
<pre class="m-0 px-4 py-3 bg-bg rounded-none whitespace-pre-wrap break-words overflow-x-auto max-w-full"
|
||||||
</Collapsible>
|
>{{ debugInfo }}</pre>
|
||||||
</div>
|
</Collapsible>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
<template v-if="errorType === 'state_init'">
|
||||||
</ModalWrapper>
|
<h2>⚠️ Migration Issue • Important Notice</h2>
|
||||||
|
<p>We've detected a problem with our database migration system caused by inconsistent line endings between operating systems (Windows vs. macOS/Linux). This may affect app stability.</p>
|
||||||
|
<p><strong>What’s happening?</strong> Our migration validator misreads modified migrations when line endings differ (CRLF ↔ LF), which can make the app unusable.</p>
|
||||||
|
<p><strong>Why?</strong> Git’s automatic line-ending conversions and OS differences can cause these inconsistencies during builds.</p>
|
||||||
|
<p><strong>What’s next?</strong> We’re working on a permanent fix. In the meantime, you can apply one of the quick fixes below depending on your system.</p>
|
||||||
|
<h3>Do I need to apply a fix now?</h3>
|
||||||
|
<div>
|
||||||
|
<p class="notice__text">
|
||||||
|
If you're encountering an error while applying migrations, such as "Error while applying migrations: migration XXXXXXXXXX was previously applied but has been modified", or a similar issue with migration, the following actions might help:
|
||||||
|
</p>
|
||||||
|
<p>If none of the above steps help, you can try saving a copy of the file <code>app.db</code> to a safe location, such as <code>%appdata%\Roaming\AstralRinthApp</code>
|
||||||
|
on Windows or <code>~/Library/Application Support/AstralRinthApp</code> on macOS, then deleting the original file and letting the app re-create the database file.
|
||||||
|
Note that this may cause data loss inside the app, so make sure to back up your launcher data before applying this fixes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<ol class="flex flex-col gap-3">
|
||||||
|
<li>
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button
|
||||||
|
title="Convert all line endings in migration files to LF (Unix-style: \\n)"
|
||||||
|
@click="onApplyMigrationFix('lf')"
|
||||||
|
>
|
||||||
|
Apply fix for Unix like systems (Debian, Ubuntu, macOS and others)
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button
|
||||||
|
title="Convert all line endings in migration files to CRLF (Windows-style: \\r\\n)"
|
||||||
|
@click="onApplyMigrationFix('crlf')"
|
||||||
|
>
|
||||||
|
Apply fix for Windows
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="migrationFixCallbackModel"
|
||||||
|
header="💡 Migration fix report"
|
||||||
|
:closable="closable">
|
||||||
|
<div class="modal-body">
|
||||||
|
<h2 class="text-lg font-bold text-contrast space-y-2">
|
||||||
|
<template v-if="migrationFixSuccess === true">
|
||||||
|
<p class="flex items-center gap-2 neon-text">
|
||||||
|
✅ The migration fix has been applied successfully. Please restart the launcher and try to log in to the game :)
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm neon-text">
|
||||||
|
If the problem persists, please try the other fix.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="migrationFixSuccess === false">
|
||||||
|
<p class="flex items-center gap-2 neon-text">
|
||||||
|
❌ The migration fix failed or had no effect.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm neon-text">
|
||||||
|
If the problem persists, please try the other fix.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.light-mode {
|
.light-mode {
|
||||||
--color-orange-bg: rgba(255, 163, 71, 0.2);
|
--color-orange-bg: rgba(255, 163, 71, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode,
|
.dark-mode,
|
||||||
.oled-mode {
|
.oled-mode {
|
||||||
--color-orange-bg: rgba(224, 131, 37, 0.2);
|
--color-orange-bg: rgba(224, 131, 37, 0.2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@import '../../../../../packages/assets/styles/neon-button.scss';
|
||||||
|
@import '../../../../../packages/assets/styles/neon-text.scss';
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: linear-gradient(90deg, #005eff, #00cfff);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.cta-button {
|
.cta-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-banner {
|
.warning-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: var(--gap-lg);
|
padding: var(--gap-lg);
|
||||||
background-color: var(--color-orange-bg);
|
background-color: var(--color-orange-bg);
|
||||||
border: 2px solid var(--color-orange);
|
border: 2px solid var(--color-orange);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-banner__title {
|
.warning-banner__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: var(--color-orange);
|
color: var(--color-orange);
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { XIcon, PlusIcon } from '@modrinth/assets'
|
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button, Checkbox } from '@modrinth/ui'
|
import { Button, Checkbox, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
exportModal.value.show()
|
exportModal.value.show()
|
||||||
initFiles()
|
initFiles()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const exportModal = ref(null)
|
const exportModal = ref(null)
|
||||||
@@ -31,273 +33,273 @@ const folders = ref([])
|
|||||||
const showingFiles = ref(false)
|
const showingFiles = ref(false)
|
||||||
|
|
||||||
const initFiles = async () => {
|
const initFiles = async () => {
|
||||||
const newFolders = new Map()
|
const newFolders = new Map()
|
||||||
const sep = '/'
|
const sep = '/'
|
||||||
files.value = []
|
files.value = []
|
||||||
await get_pack_export_candidates(props.instance.path).then((filePaths) =>
|
await get_pack_export_candidates(props.instance.path).then((filePaths) =>
|
||||||
filePaths
|
filePaths
|
||||||
.map((folder) => ({
|
.map((folder) => ({
|
||||||
path: folder,
|
path: folder,
|
||||||
name: folder.split(sep).pop(),
|
name: folder.split(sep).pop(),
|
||||||
selected:
|
selected:
|
||||||
folder.startsWith('mods') ||
|
folder.startsWith('mods') ||
|
||||||
folder.startsWith('datapacks') ||
|
folder.startsWith('datapacks') ||
|
||||||
folder.startsWith('resourcepacks') ||
|
folder.startsWith('resourcepacks') ||
|
||||||
folder.startsWith('shaderpacks') ||
|
folder.startsWith('shaderpacks') ||
|
||||||
folder.startsWith('config'),
|
folder.startsWith('config'),
|
||||||
disabled:
|
disabled:
|
||||||
folder === 'profile.json' ||
|
folder === 'profile.json' ||
|
||||||
folder.startsWith('modrinth_logs') ||
|
folder.startsWith('modrinth_logs') ||
|
||||||
folder.startsWith('.fabric'),
|
folder.startsWith('.fabric'),
|
||||||
}))
|
}))
|
||||||
.filter((pathData) => !pathData.path.includes('.DS_Store'))
|
.filter((pathData) => !pathData.path.includes('.DS_Store'))
|
||||||
.forEach((pathData) => {
|
.forEach((pathData) => {
|
||||||
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
||||||
if (parent !== '') {
|
if (parent !== '') {
|
||||||
if (newFolders.has(parent)) {
|
if (newFolders.has(parent)) {
|
||||||
newFolders.get(parent).push(pathData)
|
newFolders.get(parent).push(pathData)
|
||||||
} else {
|
} else {
|
||||||
newFolders.set(parent, [pathData])
|
newFolders.set(parent, [pathData])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
files.value.push(pathData)
|
files.value.push(pathData)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
folders.value = [...newFolders.entries()].map(([name, value]) => [
|
folders.value = [...newFolders.entries()].map(([name, value]) => [
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
showingMore: false,
|
showingMore: false,
|
||||||
},
|
},
|
||||||
value,
|
value,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
await initFiles()
|
await initFiles()
|
||||||
|
|
||||||
const exportPack = async () => {
|
const exportPack = async () => {
|
||||||
const filesToExport = files.value.filter((file) => file.selected).map((file) => file.path)
|
const filesToExport = files.value.filter((file) => file.selected).map((file) => file.path)
|
||||||
folders.value.forEach((args) => {
|
folders.value.forEach((args) => {
|
||||||
args[1].forEach((child) => {
|
args[1].forEach((child) => {
|
||||||
if (child.selected) {
|
if (child.selected) {
|
||||||
filesToExport.push(child.path)
|
filesToExport.push(child.path)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const outputPath = await open({
|
const outputPath = await open({
|
||||||
directory: true,
|
directory: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (outputPath) {
|
if (outputPath) {
|
||||||
export_profile_mrpack(
|
export_profile_mrpack(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
|
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
|
||||||
filesToExport,
|
filesToExport,
|
||||||
versionInput.value,
|
versionInput.value,
|
||||||
exportDescription.value,
|
exportDescription.value,
|
||||||
nameInput.value,
|
nameInput.value,
|
||||||
).catch((err) => handleError(err))
|
).catch((err) => handleError(err))
|
||||||
exportModal.value.hide()
|
exportModal.value.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="exportModal" header="Export modpack">
|
<ModalWrapper ref="exportModal" header="Export modpack">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="labeled_input">
|
<div class="labeled_input">
|
||||||
<p>Modpack Name</p>
|
<p>Modpack Name</p>
|
||||||
<div class="iconified-input">
|
<div class="iconified-input">
|
||||||
<PackageIcon />
|
<PackageIcon />
|
||||||
<input v-model="nameInput" type="text" placeholder="Modpack name" class="input" />
|
<input v-model="nameInput" type="text" placeholder="Modpack name" class="input" />
|
||||||
<Button class="r-btn" @click="nameInput = ''">
|
<Button class="r-btn" @click="nameInput = ''">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="labeled_input">
|
<div class="labeled_input">
|
||||||
<p>Version number</p>
|
<p>Version number</p>
|
||||||
<div class="iconified-input">
|
<div class="iconified-input">
|
||||||
<VersionIcon />
|
<VersionIcon />
|
||||||
<input v-model="versionInput" type="text" placeholder="1.0.0" class="input" />
|
<input v-model="versionInput" type="text" placeholder="1.0.0" class="input" />
|
||||||
<Button class="r-btn" @click="versionInput = ''">
|
<Button class="r-btn" @click="versionInput = ''">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="adjacent-input">
|
<div class="adjacent-input">
|
||||||
<div class="labeled_input">
|
<div class="labeled_input">
|
||||||
<p>Description</p>
|
<p>Description</p>
|
||||||
|
|
||||||
<div class="textarea-wrapper">
|
<div class="textarea-wrapper">
|
||||||
<textarea v-model="exportDescription" placeholder="Enter modpack description..." />
|
<textarea v-model="exportDescription" placeholder="Enter modpack description..." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<div class="table-head">
|
<div class="table-head">
|
||||||
<div class="table-cell row-wise">
|
<div class="table-cell row-wise">
|
||||||
Select files and folders to include in pack
|
Select files and folders to include in pack
|
||||||
<Button
|
<Button
|
||||||
class="sleek-primary collapsed-button"
|
class="sleek-primary collapsed-button"
|
||||||
icon-only
|
icon-only
|
||||||
@click="() => (showingFiles = !showingFiles)"
|
@click="() => (showingFiles = !showingFiles)"
|
||||||
>
|
>
|
||||||
<PlusIcon v-if="!showingFiles" />
|
<PlusIcon v-if="!showingFiles" />
|
||||||
<XIcon v-else />
|
<XIcon v-else />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showingFiles" class="table-content">
|
<div v-if="showingFiles" class="table-content">
|
||||||
<div v-for="[path, children] in folders" :key="path.name" class="table-row">
|
<div v-for="[path, children] in folders" :key="path.name" class="table-row">
|
||||||
<div class="table-cell file-entry">
|
<div class="table-cell file-entry">
|
||||||
<div class="file-primary">
|
<div class="file-primary">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="children.every((child) => child.selected)"
|
:model-value="children.every((child) => child.selected)"
|
||||||
:label="path.name"
|
:label="path.name"
|
||||||
class="select-checkbox"
|
class="select-checkbox"
|
||||||
:disabled="children.every((x) => x.disabled)"
|
:disabled="children.every((x) => x.disabled)"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(newValue) => children.forEach((child) => (child.selected = newValue))
|
(newValue) => children.forEach((child) => (child.selected = newValue))
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="path.showingMore"
|
v-model="path.showingMore"
|
||||||
class="select-checkbox dropdown"
|
class="select-checkbox dropdown"
|
||||||
collapsing-toggle-style
|
collapsing-toggle-style
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="path.showingMore" class="file-secondary">
|
<div v-if="path.showingMore" class="file-secondary">
|
||||||
<div v-for="child in children" :key="child.path" class="file-secondary-row">
|
<div v-for="child in children" :key="child.path" class="file-secondary-row">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="child.selected"
|
v-model="child.selected"
|
||||||
:label="child.name"
|
:label="child.name"
|
||||||
class="select-checkbox"
|
class="select-checkbox"
|
||||||
:disabled="child.disabled"
|
:disabled="child.disabled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="file in files" :key="file.path" class="table-row">
|
<div v-for="file in files" :key="file.path" class="table-row">
|
||||||
<div class="table-cell file-entry">
|
<div class="table-cell file-entry">
|
||||||
<div class="file-primary">
|
<div class="file-primary">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="file.selected"
|
v-model="file.selected"
|
||||||
:label="file.name"
|
:label="file.name"
|
||||||
:disabled="file.disabled"
|
:disabled="file.disabled"
|
||||||
class="select-checkbox"
|
class="select-checkbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row push-right">
|
<div class="button-row push-right">
|
||||||
<Button @click="exportModal.hide">
|
<Button @click="exportModal.hide">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="primary" @click="exportPack">
|
<Button color="primary" @click="exportPack">
|
||||||
<PackageIcon />
|
<PackageIcon />
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.labeled_input {
|
.labeled_input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-checkbox {
|
.select-checkbox {
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
|
|
||||||
button.checkbox {
|
button.checkbox {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dropdown {
|
&.dropdown {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-content {
|
.table-content {
|
||||||
max-height: 18rem;
|
max-height: 18rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
border: 1px solid var(--color-bg);
|
border: 1px solid var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-entry {
|
.file-entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-primary {
|
.file-primary {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-secondary {
|
.file-secondary {
|
||||||
margin-left: var(--gap-xl);
|
margin-left: var(--gap-xl);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
vertical-align: center;
|
vertical-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-secondary-row {
|
.file-secondary-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-wise {
|
.row-wise {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea-wrapper {
|
.textarea-wrapper {
|
||||||
// margin-top: 1rem;
|
// margin-top: 1rem;
|
||||||
height: 12rem;
|
height: 12rem;
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
max-height: 12rem;
|
max-height: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,51 +1,52 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import {
|
||||||
|
DownloadIcon,
|
||||||
|
GameIcon,
|
||||||
|
PlayIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
TimerIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
|
||||||
DownloadIcon,
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
GameIcon,
|
|
||||||
PlayIcon,
|
|
||||||
SpinnerIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
TimerIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { Avatar, ButtonStyled, useRelativeTime } from '@modrinth/ui'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { finish_install, kill, run } from '@/helpers/profile'
|
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
import { handleError } from '@/store/state.js'
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
|
import { finish_install, kill, run } from '@/helpers/profile'
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
first: {
|
first: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const modLoading = computed(
|
const modLoading = computed(
|
||||||
() =>
|
() =>
|
||||||
loading.value ||
|
loading.value ||
|
||||||
currentEvent.value === 'installing' ||
|
currentEvent.value === 'installing' ||
|
||||||
(currentEvent.value === 'launched' && !playing.value),
|
(currentEvent.value === 'launched' && !playing.value),
|
||||||
)
|
)
|
||||||
const installing = computed(() => props.instance.install_stage.includes('installing'))
|
const installing = computed(() => props.instance.install_stage.includes('installing'))
|
||||||
const installed = computed(() => props.instance.install_stage === 'installed')
|
const installed = computed(() => props.instance.install_stage === 'installed')
|
||||||
@@ -53,78 +54,78 @@ const installed = computed(() => props.instance.install_stage === 'installed')
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const seeInstance = async () => {
|
const seeInstance = async () => {
|
||||||
await router.push(`/instance/${encodeURIComponent(props.instance.path)}`)
|
await router.push(`/instance/${encodeURIComponent(props.instance.path)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkProcess = async () => {
|
const checkProcess = async () => {
|
||||||
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
||||||
|
|
||||||
playing.value = runningProcesses.length > 0
|
playing.value = runningProcesses.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const play = async (e, context) => {
|
const play = async (e, context) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await run(props.instance.path)
|
await run(props.instance.path)
|
||||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
trackEvent('InstancePlay', {
|
trackEvent('InstancePlay', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
source: context,
|
source: context,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const stop = async (e, context) => {
|
const stop = async (e, context) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
playing.value = false
|
playing.value = false
|
||||||
|
|
||||||
await kill(props.instance.path).catch(handleError)
|
await kill(props.instance.path).catch(handleError)
|
||||||
|
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
source: context,
|
source: context,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const repair = async (e) => {
|
const repair = async (e) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
|
|
||||||
await finish_install(props.instance)
|
await finish_install(props.instance).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openFolder = async () => {
|
const openFolder = async () => {
|
||||||
await showProfileInFolder(props.instance.path)
|
await showProfileInFolder(props.instance.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const addContent = async () => {
|
const addContent = async () => {
|
||||||
await router.push({
|
await router.push({
|
||||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||||
query: { i: props.instance.path },
|
query: { i: props.instance.path },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
play,
|
play,
|
||||||
stop,
|
stop,
|
||||||
seeInstance,
|
seeInstance,
|
||||||
openFolder,
|
openFolder,
|
||||||
addContent,
|
addContent,
|
||||||
instance: props.instance,
|
instance: props.instance,
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentEvent = ref(null)
|
const currentEvent = ref(null)
|
||||||
|
|
||||||
const unlisten = await process_listener((e) => {
|
const unlisten = await process_listener((e) => {
|
||||||
if (e.profile_path_id === props.instance.path) {
|
if (e.profile_path_id === props.instance.path) {
|
||||||
currentEvent.value = e.event
|
currentEvent.value = e.event
|
||||||
if (e.event === 'finished') {
|
if (e.event === 'finished') {
|
||||||
playing.value = false
|
playing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => checkProcess())
|
onMounted(() => checkProcess())
|
||||||
@@ -132,118 +133,118 @@ onUnmounted(() => unlisten())
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-if="compact">
|
<template v-if="compact">
|
||||||
<div
|
<div
|
||||||
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
|
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
|
||||||
@click="seeInstance"
|
@click="seeInstance"
|
||||||
@mouseenter="checkProcess"
|
@mouseenter="checkProcess"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="48px"
|
size="48px"
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
alt="Mod card"
|
alt="Mod card"
|
||||||
/>
|
/>
|
||||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||||
<span class="line-clamp-2">{{ instance.name }}</span>
|
<span class="line-clamp-2">{{ instance.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
||||||
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else-if="modLoading" color="standard" circular>
|
<ButtonStyled v-else-if="modLoading" color="standard" circular>
|
||||||
<button v-tooltip="'Instance is loading...'" disabled>
|
<button v-tooltip="'Instance is loading...'" disabled>
|
||||||
<SpinnerIcon class="animate-spin" />
|
<SpinnerIcon class="animate-spin" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else :color="first ? 'brand' : 'standard'" circular>
|
<ButtonStyled v-else :color="first ? 'brand' : 'standard'" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Play'"
|
v-tooltip="'Play'"
|
||||||
@click="(e) => play(e, 'InstanceCard')"
|
@click="(e) => play(e, 'InstanceCard')"
|
||||||
@mousehover="checkProcess"
|
@mousehover="checkProcess"
|
||||||
>
|
>
|
||||||
<!-- Translate for optical centering -->
|
<!-- Translate for optical centering -->
|
||||||
<PlayIcon class="translate-x-[1px]" />
|
<PlayIcon class="translate-x-[1px]" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||||
<TimerIcon />
|
<TimerIcon />
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
<template v-if="instance.last_played">
|
<template v-if="instance.last_played">
|
||||||
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
|
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else> Never played </template>
|
<template v-else> Never played </template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
|
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
|
||||||
@click="seeInstance"
|
@click="seeInstance"
|
||||||
@mouseenter="checkProcess"
|
@mouseenter="checkProcess"
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center">
|
<div class="relative flex items-center justify-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="48px"
|
size="48px"
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
alt="Mod card"
|
alt="Mod card"
|
||||||
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||||
/>
|
/>
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
<ButtonStyled v-if="playing" size="large" color="red" circular>
|
<ButtonStyled v-if="playing" size="large" color="red" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Stop'"
|
v-tooltip="'Stop'"
|
||||||
:class="{ 'scale-100 opacity-100': playing }"
|
:class="{ 'scale-100 opacity-100': playing }"
|
||||||
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
||||||
@click="(e) => stop(e, 'InstanceCard')"
|
@click="(e) => stop(e, 'InstanceCard')"
|
||||||
@mousehover="checkProcess"
|
@mousehover="checkProcess"
|
||||||
>
|
>
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<SpinnerIcon
|
<SpinnerIcon
|
||||||
v-else-if="modLoading || installing"
|
v-else-if="modLoading || installing"
|
||||||
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
||||||
class="animate-spin w-8 h-8"
|
class="animate-spin w-8 h-8"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
<ButtonStyled v-else-if="!installed" size="large" color="brand" circular>
|
<ButtonStyled v-else-if="!installed" size="large" color="brand" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Repair'"
|
v-tooltip="'Repair'"
|
||||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||||
@click="(e) => repair(e)"
|
@click="(e) => repair(e)"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else size="large" color="brand" circular>
|
<ButtonStyled v-else size="large" color="brand" circular>
|
||||||
<button
|
<button
|
||||||
v-tooltip="'Play'"
|
v-tooltip="'Play'"
|
||||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||||
@click="(e) => play(e, 'InstanceCard')"
|
@click="(e) => play(e, 'InstanceCard')"
|
||||||
@mousehover="checkProcess"
|
@mousehover="checkProcess"
|
||||||
>
|
>
|
||||||
<PlayIcon class="translate-x-[2px]" />
|
<PlayIcon class="translate-x-[2px]" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
|
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
|
||||||
{{ instance.name }}
|
{{ instance.name }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
||||||
<GameIcon class="shrink-0" />
|
<GameIcon class="shrink-0" />
|
||||||
<span class="text-sm capitalize">
|
<span class="text-sm capitalize">
|
||||||
{{ instance.loader }} {{ instance.game_version }}
|
{{ instance.loader }} {{ instance.game_version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,53 +1,53 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { formatCategory } from '@modrinth/utils'
|
|
||||||
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||||
|
import { formatCategory } from '@modrinth/utils'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
type Instance = {
|
type Instance = {
|
||||||
game_version: string
|
game_version: string
|
||||||
loader: string
|
loader: string
|
||||||
path: string
|
path: string
|
||||||
install_stage: string
|
install_stage: string
|
||||||
icon_path?: string
|
icon_path?: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
instance: Instance
|
instance: Instance
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
|
<div class="flex justify-between items-center border-0 border-b border-solid border-divider pb-4">
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
class="flex flex-col gap-4 text-primary"
|
class="flex flex-col gap-4 text-primary"
|
||||||
>
|
>
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||||
:alt="instance.name"
|
:alt="instance.name"
|
||||||
size="48px"
|
size="48px"
|
||||||
/>
|
/>
|
||||||
<span class="flex flex-col gap-2">
|
<span class="flex flex-col gap-2">
|
||||||
<span class="font-extrabold bold text-contrast">
|
<span class="font-extrabold bold text-contrast">
|
||||||
{{ instance.name }}
|
{{ instance.name }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-secondary flex items-center gap-2 font-semibold">
|
<span class="text-secondary flex items-center gap-2 font-semibold">
|
||||||
<GameIcon class="h-5 w-5 text-secondary" />
|
<GameIcon class="h-5 w-5 text-secondary" />
|
||||||
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<router-link :to="`/instance/${encodeURIComponent(instance.path)}`">
|
<router-link :to="`/instance/${encodeURIComponent(instance.path)}`">
|
||||||
<LeftArrowIcon /> Back to instance
|
<LeftArrowIcon /> Back to instance
|
||||||
</router-link>
|
</router-link>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
@@ -1,95 +1,97 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
|
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
|
||||||
<div class="auto-detect-modal">
|
<div class="auto-detect-modal">
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<div class="table-row table-head">
|
<div class="table-row table-head">
|
||||||
<div class="table-cell table-text">Version</div>
|
<div class="table-cell table-text">Version</div>
|
||||||
<div class="table-cell table-text">Path</div>
|
<div class="table-cell table-text">Path</div>
|
||||||
<div class="table-cell table-text">Actions</div>
|
<div class="table-cell table-text">Actions</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="javaInstall in chosenInstallOptions" :key="javaInstall.path" class="table-row">
|
<div v-for="javaInstall in chosenInstallOptions" :key="javaInstall.path" class="table-row">
|
||||||
<div class="table-cell table-text">
|
<div class="table-cell table-text">
|
||||||
<span>{{ javaInstall.version }}</span>
|
<span>{{ javaInstall.version }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-tooltip="javaInstall.path" class="table-cell table-text">
|
<div v-tooltip="javaInstall.path" class="table-cell table-text">
|
||||||
<span>{{ javaInstall.path }}</span>
|
<span>{{ javaInstall.path }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-cell table-text manage">
|
<div class="table-cell table-text manage">
|
||||||
<Button v-if="currentSelected.path === javaInstall.path" disabled
|
<Button v-if="currentSelected.path === javaInstall.path" disabled
|
||||||
><CheckIcon /> Selected</Button
|
><CheckIcon /> Selected</Button
|
||||||
>
|
>
|
||||||
<Button v-else @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</Button>
|
<Button v-else @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
|
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
|
||||||
<div class="table-cell table-text">No java installations found!</div>
|
<div class="table-cell table-text">No java installations found!</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group push-right">
|
<div class="input-group push-right">
|
||||||
<Button @click="$refs.detectJavaModal.hide()">
|
<Button @click="$refs.detectJavaModal.hide()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets'
|
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { find_filtered_jres } from '@/helpers/jre.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { find_filtered_jres } from '@/helpers/jre.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const chosenInstallOptions = ref([])
|
const chosenInstallOptions = ref([])
|
||||||
const detectJavaModal = ref(null)
|
const detectJavaModal = ref(null)
|
||||||
const currentSelected = ref({})
|
const currentSelected = ref({})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: async (version, currentSelectedJava) => {
|
show: async (version, currentSelectedJava) => {
|
||||||
chosenInstallOptions.value = await find_filtered_jres(version).catch(handleError)
|
chosenInstallOptions.value = await find_filtered_jres(version).catch(handleError)
|
||||||
|
|
||||||
currentSelected.value = currentSelectedJava
|
currentSelected.value = currentSelectedJava
|
||||||
if (!currentSelected.value) {
|
if (!currentSelected.value) {
|
||||||
currentSelected.value = { path: '', version: '' }
|
currentSelected.value = { path: '', version: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
detectJavaModal.value.show()
|
detectJavaModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['submit'])
|
const emit = defineEmits(['submit'])
|
||||||
|
|
||||||
function setJavaInstall(javaInstall) {
|
function setJavaInstall(javaInstall) {
|
||||||
emit('submit', javaInstall)
|
emit('submit', javaInstall)
|
||||||
detectJavaModal.value.hide()
|
detectJavaModal.value.hide()
|
||||||
trackEvent('JavaAutoDetect', {
|
trackEvent('JavaAutoDetect', {
|
||||||
path: javaInstall.path,
|
path: javaInstall.path,
|
||||||
version: javaInstall.version,
|
version: javaInstall.version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.auto-detect-modal {
|
.auto-detect-modal {
|
||||||
.table {
|
.table {
|
||||||
.table-row {
|
.table-row {
|
||||||
grid-template-columns: 1fr 4fr min-content;
|
grid-template-columns: 1fr 4fr min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inherit;
|
display: inherit;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.manage {
|
.manage {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,100 +1,102 @@
|
|||||||
<template>
|
<template>
|
||||||
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
|
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
|
||||||
<div class="toggle-setting" :class="{ compact }">
|
<div class="toggle-setting" :class="{ compact }">
|
||||||
<input
|
<input
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:value="props.modelValue ? props.modelValue.path : ''"
|
:value="props.modelValue ? props.modelValue.path : ''"
|
||||||
type="text"
|
type="text"
|
||||||
class="installation-input"
|
class="installation-input"
|
||||||
:placeholder="placeholder ?? '/path/to/java'"
|
:placeholder="placeholder ?? '/path/to/java'"
|
||||||
@input="
|
@input="
|
||||||
(val) => {
|
(val) => {
|
||||||
emit('update:modelValue', {
|
emit('update:modelValue', {
|
||||||
...props.modelValue,
|
...props.modelValue,
|
||||||
path: val.target.value,
|
path: val.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<span class="installation-buttons">
|
<span class="installation-buttons">
|
||||||
<Button
|
<Button
|
||||||
v-if="props.version"
|
v-if="props.version"
|
||||||
:disabled="props.disabled || installingJava"
|
:disabled="props.disabled || installingJava"
|
||||||
@click="reinstallJava"
|
@click="reinstallJava"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button :disabled="props.disabled" @click="autoDetect">
|
<Button :disabled="props.disabled" @click="autoDetect">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
Detect
|
Detect
|
||||||
</Button>
|
</Button>
|
||||||
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||||
<FolderSearchIcon />
|
<FolderSearchIcon />
|
||||||
Browse
|
Browse
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-if="testingJava" disabled> Testing... </Button>
|
<Button v-if="testingJava" disabled> Testing... </Button>
|
||||||
<Button v-else-if="testingJavaSuccess === true">
|
<Button v-else-if="testingJavaSuccess === true">
|
||||||
<CheckIcon class="test-success" />
|
<CheckIcon class="test-success" />
|
||||||
Success
|
Success
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-else-if="testingJavaSuccess === false">
|
<Button v-else-if="testingJavaSuccess === false">
|
||||||
<XIcon class="test-fail" />
|
<XIcon class="test-fail" />
|
||||||
Failed
|
Failed
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-else :disabled="props.disabled" @click="testJava">
|
<Button v-else :disabled="props.disabled" @click="testJava">
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
Test
|
Test
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
SearchIcon,
|
CheckIcon,
|
||||||
PlayIcon,
|
DownloadIcon,
|
||||||
CheckIcon,
|
FolderSearchIcon,
|
||||||
XIcon,
|
PlayIcon,
|
||||||
FolderSearchIcon,
|
SearchIcon,
|
||||||
DownloadIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
version: {
|
version: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
path: '',
|
path: '',
|
||||||
version: '',
|
version: '',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
compact: {
|
compact: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
@@ -105,116 +107,115 @@ const testingJavaSuccess = ref(null)
|
|||||||
const installingJava = ref(false)
|
const installingJava = ref(false)
|
||||||
|
|
||||||
async function testJava() {
|
async function testJava() {
|
||||||
testingJava.value = true
|
testingJava.value = true
|
||||||
testingJavaSuccess.value = await test_jre(
|
testingJavaSuccess.value = await test_jre(
|
||||||
props.modelValue ? props.modelValue.path : '',
|
props.modelValue ? props.modelValue.path : '',
|
||||||
1,
|
props.version,
|
||||||
props.version,
|
)
|
||||||
)
|
testingJava.value = false
|
||||||
testingJava.value = false
|
|
||||||
|
|
||||||
trackEvent('JavaTest', {
|
trackEvent('JavaTest', {
|
||||||
path: props.modelValue ? props.modelValue.path : '',
|
path: props.modelValue ? props.modelValue.path : '',
|
||||||
success: testingJavaSuccess.value,
|
success: testingJavaSuccess.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
testingJavaSuccess.value = null
|
testingJavaSuccess.value = null
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleJavaFileInput() {
|
async function handleJavaFileInput() {
|
||||||
const filePath = await open()
|
const filePath = await open()
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
|
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
path: filePath.path ?? filePath,
|
path: filePath.path ?? filePath,
|
||||||
version: props.version.toString(),
|
version: props.version.toString(),
|
||||||
architecture: 'x86',
|
architecture: 'x86',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trackEvent('JavaManualSelect', {
|
trackEvent('JavaManualSelect', {
|
||||||
version: props.version,
|
version: props.version,
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('update:modelValue', result)
|
emit('update:modelValue', result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const detectJavaModal = ref(null)
|
const detectJavaModal = ref(null)
|
||||||
async function autoDetect() {
|
async function autoDetect() {
|
||||||
if (!props.compact) {
|
if (!props.compact) {
|
||||||
detectJavaModal.value.show(props.version, props.modelValue)
|
detectJavaModal.value.show(props.version, props.modelValue)
|
||||||
} else {
|
} else {
|
||||||
const versions = await find_filtered_jres(props.version).catch(handleError)
|
const versions = await find_filtered_jres(props.version).catch(handleError)
|
||||||
if (versions.length > 0) {
|
if (versions.length > 0) {
|
||||||
emit('update:modelValue', versions[0])
|
emit('update:modelValue', versions[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reinstallJava() {
|
async function reinstallJava() {
|
||||||
installingJava.value = true
|
installingJava.value = true
|
||||||
const path = await auto_install_java(props.version).catch(handleError)
|
const path = await auto_install_java(props.version).catch(handleError)
|
||||||
let result = await get_jre(path)
|
let result = await get_jre(path)
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
path: path,
|
path: path,
|
||||||
version: props.version.toString(),
|
version: props.version.toString(),
|
||||||
architecture: 'x86',
|
architecture: 'x86',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trackEvent('JavaReInstall', {
|
trackEvent('JavaReInstall', {
|
||||||
path: path,
|
path: path,
|
||||||
version: props.version,
|
version: props.version,
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('update:modelValue', result)
|
emit('update:modelValue', result)
|
||||||
installingJava.value = false
|
installingJava.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.installation-input {
|
.installation-input {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-setting {
|
.toggle-setting {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
&.compact {
|
&.compact {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.installation-buttons {
|
.installation-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-success {
|
.test-success {
|
||||||
color: var(--color-green);
|
color: var(--color-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-fail {
|
.test-fail {
|
||||||
color: var(--color-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { CheckIcon } from '@modrinth/assets'
|
import { CheckIcon } from '@modrinth/assets'
|
||||||
import { Button, Badge } from '@modrinth/ui'
|
import { Badge, Button } from '@modrinth/ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { update_managed_modrinth_version } from '@/helpers/profile'
|
|
||||||
import { releaseColor } from '@/helpers/utils'
|
|
||||||
import { SwapIcon } from '@/assets/icons/index.js'
|
import { SwapIcon } from '@/assets/icons/index.js'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||||
|
import { releaseColor } from '@/helpers/utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
versions: {
|
versions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
modpackVersionModal.value.show()
|
modpackVersionModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['finish-install'])
|
const emit = defineEmits(['finish-install'])
|
||||||
|
|
||||||
const filteredVersions = computed(() => {
|
const filteredVersions = computed(() => {
|
||||||
return props.versions
|
return props.versions
|
||||||
})
|
})
|
||||||
|
|
||||||
const modpackVersionModal = ref(null)
|
const modpackVersionModal = ref(null)
|
||||||
@@ -36,160 +37,160 @@ const installing = computed(() => props.instance.install_stage !== 'installed')
|
|||||||
const inProgress = ref(false)
|
const inProgress = ref(false)
|
||||||
|
|
||||||
const switchVersion = async (versionId) => {
|
const switchVersion = async (versionId) => {
|
||||||
modpackVersionModal.value.hide()
|
modpackVersionModal.value.hide()
|
||||||
inProgress.value = true
|
inProgress.value = true
|
||||||
await update_managed_modrinth_version(props.instance.path, versionId)
|
await update_managed_modrinth_version(props.instance.path, versionId)
|
||||||
inProgress.value = false
|
inProgress.value = false
|
||||||
emit('finish-install')
|
emit('finish-install')
|
||||||
}
|
}
|
||||||
|
|
||||||
const onHide = () => {
|
const onHide = () => {
|
||||||
if (!inProgress.value) {
|
if (!inProgress.value) {
|
||||||
emit('finish-install')
|
emit('finish-install')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper
|
<ModalWrapper
|
||||||
ref="modpackVersionModal"
|
ref="modpackVersionModal"
|
||||||
class="modpack-version-modal"
|
class="modpack-version-modal"
|
||||||
header="Change modpack version"
|
header="Change modpack version"
|
||||||
:on-hide="onHide"
|
:on-hide="onHide"
|
||||||
>
|
>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div v-if="instance.linked_data" class="mod-card">
|
<div v-if="instance.linked_data" class="mod-card">
|
||||||
<div class="table">
|
<div class="table">
|
||||||
<div class="table-row with-columns table-head">
|
<div class="table-row with-columns table-head">
|
||||||
<div class="table-cell table-text download-cell" />
|
<div class="table-cell table-text download-cell" />
|
||||||
<div class="name-cell table-cell table-text">Name</div>
|
<div class="name-cell table-cell table-text">Name</div>
|
||||||
<div class="table-cell table-text">Supports</div>
|
<div class="table-cell table-text">Supports</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scrollable">
|
<div class="scrollable">
|
||||||
<div
|
<div
|
||||||
v-for="version in filteredVersions"
|
v-for="version in filteredVersions"
|
||||||
:key="version.id"
|
:key="version.id"
|
||||||
class="table-row with-columns selectable"
|
class="table-row with-columns selectable"
|
||||||
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
|
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
|
||||||
>
|
>
|
||||||
<div class="table-cell table-text">
|
<div class="table-cell table-text">
|
||||||
<Button
|
<Button
|
||||||
:color="version.id === installedVersion ? '' : 'primary'"
|
:color="version.id === installedVersion ? '' : 'primary'"
|
||||||
icon-only
|
icon-only
|
||||||
:disabled="inProgress || installing || version.id === installedVersion"
|
:disabled="inProgress || installing || version.id === installedVersion"
|
||||||
@click.stop="() => switchVersion(version.id)"
|
@click.stop="() => switchVersion(version.id)"
|
||||||
>
|
>
|
||||||
<SwapIcon v-if="version.id !== installedVersion" />
|
<SwapIcon v-if="version.id !== installedVersion" />
|
||||||
<CheckIcon v-else />
|
<CheckIcon v-else />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="name-cell table-cell table-text">
|
<div class="name-cell table-cell table-text">
|
||||||
<div class="version-link">
|
<div class="version-link">
|
||||||
{{ version.name.charAt(0).toUpperCase() + version.name.slice(1) }}
|
{{ version.name.charAt(0).toUpperCase() + version.name.slice(1) }}
|
||||||
<div class="version-badge">
|
<div class="version-badge">
|
||||||
<div class="channel-indicator">
|
<div class="channel-indicator">
|
||||||
<Badge
|
<Badge
|
||||||
:color="releaseColor(version.version_type)"
|
:color="releaseColor(version.version_type)"
|
||||||
:type="
|
:type="
|
||||||
version.version_type.charAt(0).toUpperCase() +
|
version.version_type.charAt(0).toUpperCase() +
|
||||||
version.version_type.slice(1)
|
version.version_type.slice(1)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ version.version_number }}
|
{{ version.version_number }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-cell table-text stacked-text">
|
<div class="table-cell table-text stacked-text">
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
version.loaders
|
version.loaders
|
||||||
.map((str) => str.charAt(0).toUpperCase() + str.slice(1))
|
.map((str) => str.charAt(0).toUpperCase() + str.slice(1))
|
||||||
.join(', ')
|
.join(', ')
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{{ version.game_versions.join(', ') }}
|
{{ version.game_versions.join(', ') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.filter-header {
|
.filter-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.with-columns {
|
.with-columns {
|
||||||
grid-template-columns: min-content 1fr 1fr;
|
grid-template-columns: min-content 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 25rem;
|
max-height: 25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-row {
|
.card-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-card {
|
.mod-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-link {
|
.version-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
.version-badge {
|
.version-badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.channel-indicator {
|
.channel-indicator {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stacked-text {
|
.stacked-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-cell {
|
.download-cell {
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
border: 1px solid var(--color-bg);
|
border: 1px solid var(--color-bg);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="typeof to === 'string'"
|
v-if="typeof to === 'string'"
|
||||||
:to="to"
|
:to="to"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:class="{
|
:class="{
|
||||||
'router-link-active': isPrimary && isPrimary(route),
|
'router-link-active': isPrimary && isPrimary(route),
|
||||||
'subpage-active': isSubpage && isSubpage(route),
|
'subpage-active': isSubpage && isSubpage(route),
|
||||||
}"
|
disabled: disabled,
|
||||||
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
}"
|
||||||
>
|
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
||||||
<slot />
|
>
|
||||||
</RouterLink>
|
<slot />
|
||||||
<button
|
</RouterLink>
|
||||||
v-else
|
<button
|
||||||
v-bind="$attrs"
|
v-else
|
||||||
class="button-animation border-none text-primary cursor-pointer w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
v-bind="$attrs"
|
||||||
@click="to"
|
class="button-animation border-none text-primary cursor-pointer w-12 h-12 rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast"
|
||||||
>
|
:disabled="disabled"
|
||||||
<slot />
|
@click="to"
|
||||||
</button>
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -29,31 +31,37 @@ const route = useRoute()
|
|||||||
|
|
||||||
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
|
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
|
||||||
|
|
||||||
defineProps<{
|
withDefaults(
|
||||||
to: (() => void) | string
|
defineProps<{
|
||||||
isPrimary?: RouteFunction
|
to: (() => void) | string
|
||||||
isSubpage?: RouteFunction
|
isPrimary?: RouteFunction
|
||||||
highlightOverride?: boolean
|
isSubpage?: RouteFunction
|
||||||
}>()
|
highlightOverride?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.router-link-active,
|
.router-link-active,
|
||||||
.subpage-active {
|
.subpage-active {
|
||||||
svg {
|
svg {
|
||||||
filter: drop-shadow(0 0 0.5rem black);
|
filter: drop-shadow(0 0 0.5rem black);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-link-active {
|
.router-link-active {
|
||||||
@apply text-[--color-button-text-selected] bg-[--color-button-bg-selected];
|
@apply text-[--color-button-text-selected] bg-[--color-button-bg-selected];
|
||||||
}
|
}
|
||||||
|
|
||||||
.subpage-active {
|
.subpage-active {
|
||||||
@apply text-contrast bg-button-bg;
|
@apply text-contrast bg-button-bg;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav
|
<nav
|
||||||
class="card-shadow experimental-styles-within relative flex w-fit overflow-clip rounded-full bg-bg-raised p-1 text-sm font-bold"
|
class="card-shadow experimental-styles-within relative flex w-fit overflow-clip rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||||
>
|
>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-for="(link, index) in filteredLinks"
|
v-for="(link, index) in filteredLinks"
|
||||||
v-show="link.shown === undefined ? true : link.shown"
|
v-show="link.shown === undefined ? true : link.shown"
|
||||||
:key="index"
|
:key="index"
|
||||||
ref="tabLinkElements"
|
ref="tabLinkElements"
|
||||||
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
||||||
:class="`button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full ${activeIndex === index && !subpageSelected ? 'text-button-textSelected' : activeIndex === index && subpageSelected ? 'text-contrast' : 'text-primary'}`"
|
:class="`button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full ${activeIndex === index && !subpageSelected ? 'text-button-textSelected' : activeIndex === index && subpageSelected ? 'text-contrast' : 'text-primary'}`"
|
||||||
>
|
>
|
||||||
<component :is="link.icon" v-if="link.icon" class="size-5" />
|
<component :is="link.icon" v-if="link.icon" class="size-5" />
|
||||||
<span class="text-nowrap">{{ link.label }}</span>
|
<span class="text-nowrap">{{ link.label }}</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div
|
<div
|
||||||
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'}`"
|
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'}`"
|
||||||
:style="{
|
:style="{
|
||||||
left: sliderLeftPx,
|
left: sliderLeftPx,
|
||||||
top: sliderTopPx,
|
top: sliderTopPx,
|
||||||
right: sliderRightPx,
|
right: sliderRightPx,
|
||||||
bottom: sliderBottomPx,
|
bottom: sliderBottomPx,
|
||||||
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
|
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
|
||||||
}"
|
}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></div>
|
></div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
import { useRoute, RouterLink } from 'vue-router'
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
label: string
|
label: string
|
||||||
href: string | RouteLocationRaw
|
href: string | RouteLocationRaw
|
||||||
shown?: boolean
|
shown?: boolean
|
||||||
icon?: unknown
|
icon?: unknown
|
||||||
subpages?: string[]
|
subpages?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
links: Tab[]
|
links: Tab[]
|
||||||
query?: string
|
query?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const sliderLeft = ref(4)
|
const sliderLeft = ref(4)
|
||||||
@@ -56,7 +56,7 @@ const oldIndex = ref(-1)
|
|||||||
const subpageSelected = ref(false)
|
const subpageSelected = ref(false)
|
||||||
|
|
||||||
const filteredLinks = computed(() =>
|
const filteredLinks = computed(() =>
|
||||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
||||||
)
|
)
|
||||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
||||||
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
||||||
@@ -64,97 +64,97 @@ const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
|||||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
||||||
|
|
||||||
function pickLink() {
|
function pickLink() {
|
||||||
let index = -1
|
let index = -1
|
||||||
subpageSelected.value = false
|
subpageSelected.value = false
|
||||||
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
||||||
const link = filteredLinks.value[i]
|
const link = filteredLinks.value[i]
|
||||||
|
|
||||||
if (route.path === (typeof link.href === 'string' ? link.href : link.href.path)) {
|
if (route.path === (typeof link.href === 'string' ? link.href : link.href.path)) {
|
||||||
index = i
|
index = i
|
||||||
break
|
break
|
||||||
} else if (link.subpages && link.subpages.some((subpage) => route.path.includes(subpage))) {
|
} else if (link.subpages && link.subpages.some((subpage) => route.path.includes(subpage))) {
|
||||||
index = i
|
index = i
|
||||||
subpageSelected.value = true
|
subpageSelected.value = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activeIndex.value = index
|
activeIndex.value = index
|
||||||
|
|
||||||
if (activeIndex.value !== -1) {
|
if (activeIndex.value !== -1) {
|
||||||
startAnimation()
|
startAnimation()
|
||||||
} else {
|
} else {
|
||||||
oldIndex.value = -1
|
oldIndex.value = -1
|
||||||
sliderLeft.value = 0
|
sliderLeft.value = 0
|
||||||
sliderRight.value = 0
|
sliderRight.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabLinkElements = ref()
|
const tabLinkElements = ref()
|
||||||
|
|
||||||
function startAnimation() {
|
function startAnimation() {
|
||||||
const el = tabLinkElements.value[activeIndex.value].$el
|
const el = tabLinkElements.value[activeIndex.value].$el
|
||||||
|
|
||||||
if (!el || !el.offsetParent) return
|
if (!el || !el.offsetParent) return
|
||||||
|
|
||||||
const newValues = {
|
const newValues = {
|
||||||
left: el.offsetLeft,
|
left: el.offsetLeft,
|
||||||
top: el.offsetTop,
|
top: el.offsetTop,
|
||||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||||
sliderLeft.value = newValues.left
|
sliderLeft.value = newValues.left
|
||||||
sliderRight.value = newValues.right
|
sliderRight.value = newValues.right
|
||||||
sliderTop.value = newValues.top
|
sliderTop.value = newValues.top
|
||||||
sliderBottom.value = newValues.bottom
|
sliderBottom.value = newValues.bottom
|
||||||
} else {
|
} else {
|
||||||
const delay = 200
|
const delay = 200
|
||||||
|
|
||||||
if (newValues.left < sliderLeft.value) {
|
if (newValues.left < sliderLeft.value) {
|
||||||
sliderLeft.value = newValues.left
|
sliderLeft.value = newValues.left
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderRight.value = newValues.right
|
sliderRight.value = newValues.right
|
||||||
}, delay)
|
}, delay)
|
||||||
} else {
|
} else {
|
||||||
sliderRight.value = newValues.right
|
sliderRight.value = newValues.right
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderLeft.value = newValues.left
|
sliderLeft.value = newValues.left
|
||||||
}, delay)
|
}, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValues.top < sliderTop.value) {
|
if (newValues.top < sliderTop.value) {
|
||||||
sliderTop.value = newValues.top
|
sliderTop.value = newValues.top
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderBottom.value = newValues.bottom
|
sliderBottom.value = newValues.bottom
|
||||||
}, delay)
|
}, delay)
|
||||||
} else {
|
} else {
|
||||||
sliderBottom.value = newValues.bottom
|
sliderBottom.value = newValues.bottom
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sliderTop.value = newValues.top
|
sliderTop.value = newValues.top
|
||||||
}, delay)
|
}, delay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('resize', pickLink)
|
window.addEventListener('resize', pickLink)
|
||||||
pickLink()
|
pickLink()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', pickLink)
|
window.removeEventListener('resize', pickLink)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
pickLink()
|
pickLink()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.navtabs-transition {
|
.navtabs-transition {
|
||||||
/* Delay on opacity is to hide any jankiness as the page loads */
|
/* Delay on opacity is to hide any jankiness as the page loads */
|
||||||
transition:
|
transition:
|
||||||
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
|
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
|
||||||
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,33 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-bar__fill" :style="{ width: `${progress}%` }"></div>
|
<div
|
||||||
</div>
|
class="progress-bar__fill"
|
||||||
|
:style="{
|
||||||
|
width: `${progress}%`,
|
||||||
|
'background-color': error ? 'var(--color-red)' : 'var(--color-brand)',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
defineProps({
|
||||||
progress: {
|
progress: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
validator(value) {
|
validator(value) {
|
||||||
return value >= 0 && value <= 100
|
return value >= 0 && value <= 100
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
error: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
background-color: var(--color-button-bg);
|
background-color: var(--color-button-bg);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar__fill {
|
.progress-bar__fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--color-brand);
|
transition: width 0.3s;
|
||||||
transition: width 0.3s;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Avatar, TagItem } from '@modrinth/ui'
|
|
||||||
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
||||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
import { Avatar, TagItem } from '@modrinth/ui'
|
||||||
import { computed } from 'vue'
|
import { formatCategory, formatNumber } from '@modrinth/utils'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
@@ -12,110 +12,107 @@ dayjs.extend(relativeTime)
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
project: {
|
project: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const featuredCategory = computed(() => {
|
const featuredCategory = computed(() => {
|
||||||
if (props.project.categories.includes('optimization')) {
|
if (props.project.display_categories.includes('optimization')) {
|
||||||
return 'optimization'
|
return 'optimization'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.project.categories.length > 0) {
|
return props.project.display_categories[0] ?? props.project.categories[0]
|
||||||
return props.project.categories[0]
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const toColor = computed(() => {
|
const toColor = computed(() => {
|
||||||
let color = props.project.color
|
let color = props.project.color
|
||||||
|
|
||||||
color >>>= 0
|
color >>>= 0
|
||||||
const b = color & 0xff
|
const b = color & 0xff
|
||||||
const g = (color >>> 8) & 0xff
|
const g = (color >>> 8) & 0xff
|
||||||
const r = (color >>> 16) & 0xff
|
const r = (color >>> 16) & 0xff
|
||||||
return 'rgba(' + [r, g, b, 1].join(',') + ')'
|
return 'rgba(' + [r, g, b, 1].join(',') + ')'
|
||||||
})
|
})
|
||||||
|
|
||||||
const toTransparent = computed(() => {
|
const toTransparent = computed(() => {
|
||||||
let color = props.project.color
|
let color = props.project.color
|
||||||
|
|
||||||
color >>>= 0
|
color >>>= 0
|
||||||
const b = color & 0xff
|
const b = color & 0xff
|
||||||
const g = (color >>> 8) & 0xff
|
const g = (color >>> 8) & 0xff
|
||||||
const r = (color >>> 16) & 0xff
|
const r = (color >>> 16) & 0xff
|
||||||
return (
|
return (
|
||||||
'linear-gradient(rgba(' +
|
'linear-gradient(rgba(' +
|
||||||
[r, g, b, 0.03].join(',') +
|
[r, g, b, 0.03].join(',') +
|
||||||
'), 65%, rgba(' +
|
'), 65%, rgba(' +
|
||||||
[r, g, b, 0.3].join(',') +
|
[r, g, b, 0.3].join(',') +
|
||||||
'))'
|
'))'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
|
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
|
||||||
@click="router.push(`/project/${project.slug}`)"
|
@click="router.push(`/project/${project.slug}`)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
|
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
|
||||||
:style="{
|
:style="{
|
||||||
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
|
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
|
||||||
'background-image': `url(${
|
'background-image': `url(${
|
||||||
project.featured_gallery ??
|
project.featured_gallery ??
|
||||||
project.gallery[0] ??
|
project.gallery[0] ??
|
||||||
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
'https://launcher-files.modrinth.com/assets/maze-bg.png'
|
||||||
})`,
|
})`,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="badges-wrapper"
|
class="badges-wrapper"
|
||||||
:class="{
|
:class="{
|
||||||
'no-image': !project.featured_gallery && !project.gallery[0],
|
'no-image': !project.featured_gallery && !project.gallery[0],
|
||||||
}"
|
}"
|
||||||
:style="{
|
:style="{
|
||||||
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
|
background: !project.featured_gallery && !project.gallery[0] ? toTransparent : null,
|
||||||
}"
|
}"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center gap-2 px-4 py-3">
|
<div class="flex flex-col justify-center gap-2 px-4 py-3">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<Avatar size="48px" :src="project.icon_url" />
|
<Avatar size="48px" :src="project.icon_url" />
|
||||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||||
<span class="line-clamp-2">{{ project.title }}</span>
|
<span class="line-clamp-2">{{ project.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="m-0 text-sm font-medium line-clamp-3 leading-tight h-[3.25rem]">
|
<p class="m-0 text-sm font-medium line-clamp-3 leading-tight h-[3.25rem]">
|
||||||
{{ project.description }}
|
{{ project.description }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-2 text-sm text-secondary font-semibold mt-auto">
|
<div class="flex items-center gap-2 text-sm text-secondary font-semibold mt-auto">
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
{{ formatNumber(project.downloads) }}
|
{{ formatNumber(project.downloads) }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||||
>
|
>
|
||||||
<HeartIcon />
|
<HeartIcon />
|
||||||
{{ formatNumber(project.follows) }}
|
{{ formatNumber(project.follows) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 pr-2">
|
<div class="flex items-center gap-1 pr-2">
|
||||||
<TagIcon />
|
<TagIcon />
|
||||||
<TagItem>
|
<TagItem>
|
||||||
{{ formatCategory(featuredCategory) }}
|
{{ formatCategory(featuredCategory) }}
|
||||||
</TagItem>
|
</TagItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
@@ -1,73 +1,75 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { list } from '@/helpers/profile'
|
import { SpinnerIcon } from '@modrinth/assets'
|
||||||
import { handleError } from '@/store/notifications'
|
import { Avatar, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { onUnmounted, ref } from 'vue'
|
import { onUnmounted, ref } from 'vue'
|
||||||
import { profile_listener } from '@/helpers/events.js'
|
|
||||||
import NavButton from '@/components/ui/NavButton.vue'
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
import { Avatar } from '@modrinth/ui'
|
import { profile_listener } from '@/helpers/events.js'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { list } from '@/helpers/profile'
|
||||||
import { SpinnerIcon } from '@modrinth/assets'
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const recentInstances = ref([])
|
const recentInstances = ref([])
|
||||||
const getInstances = async () => {
|
const getInstances = async () => {
|
||||||
const profiles = await list().catch(handleError)
|
const profiles = await list().catch(handleError)
|
||||||
|
|
||||||
recentInstances.value = profiles
|
recentInstances.value = profiles
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const dateACreated = dayjs(a.created)
|
const dateACreated = dayjs(a.created)
|
||||||
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
||||||
|
|
||||||
const dateBCreated = dayjs(b.created)
|
const dateBCreated = dayjs(b.created)
|
||||||
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
|
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
|
||||||
|
|
||||||
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
|
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
|
||||||
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
|
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
|
||||||
|
|
||||||
if (dateA.isSame(dateB)) {
|
if (dateA.isSame(dateB)) {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dateB - dateA
|
return dateB - dateA
|
||||||
})
|
})
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
await getInstances()
|
await getInstances()
|
||||||
|
|
||||||
const unlistenProfile = await profile_listener(async (event) => {
|
const unlistenProfile = await profile_listener(async (event) => {
|
||||||
if (event.event !== 'synced') {
|
if (event.event !== 'synced') {
|
||||||
await getInstances()
|
await getInstances()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlistenProfile()
|
unlistenProfile()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NavButton
|
<NavButton
|
||||||
v-for="instance in recentInstances"
|
v-for="instance in recentInstances"
|
||||||
:key="instance.id"
|
:key="instance.id"
|
||||||
v-tooltip.right="instance.name"
|
v-tooltip.right="instance.name"
|
||||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||||
class="relative"
|
class="relative"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||||
size="28px"
|
size="28px"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="instance.install_stage !== 'installed'"
|
v-if="instance.install_stage !== 'installed'"
|
||||||
class="absolute inset-0 flex items-center justify-center z-10"
|
class="absolute inset-0 flex items-center justify-center z-10"
|
||||||
>
|
>
|
||||||
<SpinnerIcon class="animate-spin w-4 h-4" />
|
<SpinnerIcon class="animate-spin w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-divider"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
@@ -1,146 +1,119 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="action-groups">
|
<div class="action-groups">
|
||||||
<ButtonStyled v-if="currentLoadingBars.length > 0" color="brand" type="transparent" circular>
|
<ButtonStyled v-if="currentLoadingBars.length > 0" color="brand" type="transparent" circular>
|
||||||
<button ref="infoButton" @click="toggleCard()">
|
<button ref="infoButton" @click="toggleCard()">
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<div v-if="offline" class="status">
|
<div v-if="offline" class="status">
|
||||||
<UnplugIcon />
|
<UnplugIcon />
|
||||||
<div class="running-text">
|
<div class="running-text">
|
||||||
<span> Offline </span>
|
<span> Offline </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedProcess" class="status">
|
<div v-if="selectedProcess" class="status">
|
||||||
<span class="circle running" />
|
<span class="circle running" />
|
||||||
<div ref="profileButton" class="running-text">
|
<div ref="profileButton" class="running-text">
|
||||||
<router-link
|
<router-link
|
||||||
class="text-primary"
|
class="text-primary"
|
||||||
:to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`"
|
:to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`"
|
||||||
>
|
>
|
||||||
{{ selectedProcess.profile.name }}
|
{{ selectedProcess.profile.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div v-if="currentProcesses.length > 1" class="arrow button-base" :class="{ rotate: showProfiles }"
|
<div
|
||||||
@click="toggleProfiles()">
|
v-if="currentProcesses.length > 1"
|
||||||
<DropdownIcon />
|
class="arrow button-base"
|
||||||
</div>
|
:class="{ rotate: showProfiles }"
|
||||||
</div>
|
@click="toggleProfiles()"
|
||||||
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click="stop(selectedProcess)">
|
>
|
||||||
<StopCircleIcon />
|
<DropdownIcon />
|
||||||
</Button>
|
</div>
|
||||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
</div>
|
||||||
<TerminalSquareIcon />
|
<Button
|
||||||
</Button>
|
v-tooltip="'Stop instance'"
|
||||||
</div>
|
icon-only
|
||||||
<div v-else class="status">
|
class="icon-button stop"
|
||||||
<span class="circle stopped" />
|
@click="stop(selectedProcess)"
|
||||||
<span class="running-text"> No instances running </span>
|
>
|
||||||
</div>
|
<StopCircleIcon />
|
||||||
<div v-if="updateState">
|
</Button>
|
||||||
<a>
|
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
||||||
<Button class="download" :disabled="installState" @click="confirmUpdating(), getRemote(false, false)">
|
<TerminalSquareIcon />
|
||||||
<DownloadIcon />
|
</Button>
|
||||||
{{
|
</div>
|
||||||
installState
|
<div v-else class="status">
|
||||||
? "Downloading new update..."
|
<span class="circle stopped" />
|
||||||
: "Download new update"
|
<span class="running-text"> No instances running </span>
|
||||||
}}
|
</div>
|
||||||
</Button>
|
</div>
|
||||||
</a>
|
<transition name="download">
|
||||||
</div>
|
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
||||||
<ModalWrapper ref="confirmUpdate" :has-to-type="false" header="Request to update the AstralRinth launcher">
|
<div v-for="loadingBar in currentLoadingBars" :key="loadingBar.id" class="info-text">
|
||||||
<div class="modal-body">
|
<h3 class="info-title">
|
||||||
<div class="markdown-body">
|
{{ loadingBar.title }}
|
||||||
<p>The new version of the AstralRinth launcher is available.</p>
|
</h3>
|
||||||
<p>Your version is outdated. We recommend that you update to the latest version.</p>
|
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
||||||
<p><strong>⚠️ Warning ⚠️</strong></p>
|
<div class="row">
|
||||||
<p>
|
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}%
|
||||||
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
|
{{ loadingBar.message }}
|
||||||
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
|
</div>
|
||||||
your files, so you should always make copies of them and keep them in a safe place.
|
</div>
|
||||||
</p>
|
</Card>
|
||||||
</div>
|
</transition>
|
||||||
<span>Source • Git Astralium</span>
|
<transition name="download">
|
||||||
<span>Version on remote server • <p id="releaseData" class="cosmic inline-fix"></p></span>
|
<Card
|
||||||
<span>Version on local device •
|
v-if="showProfiles === true && currentProcesses.length > 0"
|
||||||
<p class="cosmic inline-fix">v{{ version }}</p>
|
ref="profiles"
|
||||||
</span>
|
class="profile-card"
|
||||||
<div class="button-group push-right">
|
>
|
||||||
<Button class="download-modal" @click="confirmUpdate.hide()">
|
<Button
|
||||||
Decline</Button>
|
v-for="process in currentProcesses"
|
||||||
<Button class="download-modal" @click="approveUpdate()">
|
:key="process.uuid"
|
||||||
Accept
|
class="profile-button"
|
||||||
</Button>
|
@click="selectProcess(process)"
|
||||||
</div>
|
>
|
||||||
</div>
|
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
|
||||||
</ModalWrapper>
|
<Button
|
||||||
</div>
|
v-tooltip="'Stop instance'"
|
||||||
<transition name="download">
|
icon-only
|
||||||
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
class="icon-button stop"
|
||||||
<div v-for="loadingBar in currentLoadingBars" :key="loadingBar.id" class="info-text">
|
@click.stop="stop(process)"
|
||||||
<h3 class="info-title">
|
>
|
||||||
{{ loadingBar.title }}
|
<StopCircleIcon />
|
||||||
</h3>
|
</Button>
|
||||||
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
<Button
|
||||||
<div class="row">
|
v-tooltip="'View logs'"
|
||||||
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}% {{ loadingBar.message }}
|
icon-only
|
||||||
</div>
|
class="icon-button"
|
||||||
</div>
|
@click.stop="goToTerminal(process.profile.path)"
|
||||||
</Card>
|
>
|
||||||
</transition>
|
<TerminalSquareIcon />
|
||||||
<transition name="download">
|
</Button>
|
||||||
<Card v-if="showProfiles === true && currentProcesses.length > 0" ref="profiles" class="profile-card">
|
</Button>
|
||||||
<Button v-for="process in currentProcesses" :key="process.uuid" class="profile-button"
|
</Card>
|
||||||
@click="selectProcess(process)">
|
</transition>
|
||||||
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
|
|
||||||
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click.stop="stop(process)">
|
|
||||||
<StopCircleIcon />
|
|
||||||
</Button>
|
|
||||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click.stop="goToTerminal(process.profile.path)">
|
|
||||||
<TerminalSquareIcon />
|
|
||||||
</Button>
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
</transition>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
StopCircleIcon,
|
DropdownIcon,
|
||||||
TerminalSquareIcon,
|
StopCircleIcon,
|
||||||
DropdownIcon,
|
TerminalSquareIcon,
|
||||||
UnplugIcon,
|
UnplugIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, ButtonStyled, Card } from '@modrinth/ui'
|
import { Button, ButtonStyled, Card, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
|
||||||
import { loading_listener, process_listener } from '@/helpers/events'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { progress_bars_list } from '@/helpers/state.js'
|
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { get_many } from '@/helpers/profile.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { loading_listener, process_listener } from '@/helpers/events'
|
||||||
|
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||||
|
import { get_many } from '@/helpers/profile.js'
|
||||||
|
import { progress_bars_list } from '@/helpers/state.js'
|
||||||
|
|
||||||
const version = await getVersion()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
import { installState, getRemote, updateState } from '@/helpers/update.js'
|
|
||||||
import ModalWrapper from './modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
const confirmUpdate = ref(null)
|
|
||||||
|
|
||||||
const confirmUpdating = async () => {
|
|
||||||
confirmUpdate.value.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const approveUpdate = async () => {
|
|
||||||
confirmUpdate.value.hide()
|
|
||||||
await getRemote(true, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
await getRemote(true, false)
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const card = ref(null)
|
const card = ref(null)
|
||||||
@@ -155,445 +128,347 @@ const currentProcesses = ref([])
|
|||||||
const selectedProcess = ref()
|
const selectedProcess = ref()
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
const processes = await getRunningProcesses().catch(handleError)
|
const processes = await getRunningProcesses().catch(handleError)
|
||||||
const profiles = await get_many(processes.map((x) => x.profile_path)).catch(handleError)
|
const profiles = await get_many(processes.map((x) => x.profile_path)).catch(handleError)
|
||||||
|
|
||||||
currentProcesses.value = processes.map((x) => ({
|
currentProcesses.value = processes.map((x) => ({
|
||||||
profile: profiles.find((prof) => x.profile_path === prof.path),
|
profile: profiles.find((prof) => x.profile_path === prof.path),
|
||||||
...x,
|
...x,
|
||||||
}))
|
}))
|
||||||
if (!selectedProcess.value || !currentProcesses.value.includes(selectedProcess.value)) {
|
if (!selectedProcess.value || !currentProcesses.value.includes(selectedProcess.value)) {
|
||||||
selectedProcess.value = currentProcesses.value[0]
|
selectedProcess.value = currentProcesses.value[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await refresh()
|
await refresh()
|
||||||
|
|
||||||
const offline = ref(!navigator.onLine)
|
const offline = ref(!navigator.onLine)
|
||||||
window.addEventListener('offline', () => {
|
window.addEventListener('offline', () => {
|
||||||
offline.value = true
|
offline.value = true
|
||||||
})
|
})
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
offline.value = false
|
offline.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
const unlistenProcess = await process_listener(async () => {
|
const unlistenProcess = await process_listener(async () => {
|
||||||
await refresh()
|
await refresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
const stop = async (process) => {
|
const stop = async (process) => {
|
||||||
try {
|
try {
|
||||||
await killProcess(process.uuid).catch(handleError)
|
await killProcess(process.uuid).catch(handleError)
|
||||||
|
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
loader: process.profile.loader,
|
loader: process.profile.loader,
|
||||||
game_version: process.profile.game_version,
|
game_version: process.profile.game_version,
|
||||||
source: 'AppBar',
|
source: 'AppBar',
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
await refresh()
|
await refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToTerminal = (path) => {
|
const goToTerminal = (path) => {
|
||||||
router.push(`/instance/${encodeURIComponent(path ?? selectedProcess.value.profile.path)}/logs`)
|
router.push(`/instance/${encodeURIComponent(path ?? selectedProcess.value.profile.path)}/logs`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLoadingBars = ref([])
|
const currentLoadingBars = ref([])
|
||||||
|
|
||||||
const refreshInfo = async () => {
|
const refreshInfo = async () => {
|
||||||
const currentLoadingBarCount = currentLoadingBars.value.length
|
const currentLoadingBarCount = currentLoadingBars.value.length
|
||||||
currentLoadingBars.value = Object.values(await progress_bars_list().catch(handleError)).map(
|
currentLoadingBars.value = Object.values(await progress_bars_list().catch(handleError))
|
||||||
(x) => {
|
.map((x) => {
|
||||||
if (x.bar_type.type === 'java_download') {
|
if (x.bar_type.type === 'java_download') {
|
||||||
x.title = 'Downloading Java ' + x.bar_type.version
|
x.title = 'Downloading Java ' + x.bar_type.version
|
||||||
}
|
}
|
||||||
if (x.bar_type.profile_path) {
|
if (x.bar_type.profile_path) {
|
||||||
x.title = x.bar_type.profile_path
|
x.title = x.bar_type.profile_path
|
||||||
}
|
}
|
||||||
if (x.bar_type.pack_name) {
|
if (x.bar_type.pack_name) {
|
||||||
x.title = x.bar_type.pack_name
|
x.title = x.bar_type.pack_name
|
||||||
}
|
}
|
||||||
|
|
||||||
return x
|
return x
|
||||||
},
|
})
|
||||||
)
|
.filter((bar) => bar?.bar_type?.type !== 'launcher_update')
|
||||||
|
|
||||||
currentLoadingBars.value.sort((a, b) => {
|
currentLoadingBars.value.sort((a, b) => {
|
||||||
if (a.loading_bar_uuid < b.loading_bar_uuid) {
|
if (a.loading_bar_uuid < b.loading_bar_uuid) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
if (a.loading_bar_uuid > b.loading_bar_uuid) {
|
if (a.loading_bar_uuid > b.loading_bar_uuid) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
|
|
||||||
if (currentLoadingBars.value.length === 0) {
|
if (currentLoadingBars.value.length === 0) {
|
||||||
showCard.value = false
|
showCard.value = false
|
||||||
} else if (currentLoadingBarCount < currentLoadingBars.value.length) {
|
} else if (currentLoadingBarCount < currentLoadingBars.value.length) {
|
||||||
showCard.value = true
|
showCard.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshInfo()
|
await refreshInfo()
|
||||||
const unlistenLoading = await loading_listener(async () => {
|
const unlistenLoading = await loading_listener(async () => {
|
||||||
await refreshInfo()
|
await refreshInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectProcess = (process) => {
|
const selectProcess = (process) => {
|
||||||
selectedProcess.value = process
|
selectedProcess.value = process
|
||||||
showProfiles.value = false
|
showProfiles.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClickOutsideCard = (event) => {
|
const handleClickOutsideCard = (event) => {
|
||||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||||
if (
|
if (
|
||||||
card.value &&
|
card.value &&
|
||||||
card.value.$el !== event.target &&
|
card.value.$el !== event.target &&
|
||||||
!elements.includes(card.value.$el) &&
|
!elements.includes(card.value.$el) &&
|
||||||
infoButton.value &&
|
infoButton.value &&
|
||||||
!infoButton.value.contains(event.target)
|
!infoButton.value.contains(event.target)
|
||||||
) {
|
) {
|
||||||
showCard.value = false
|
showCard.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClickOutsideProfile = (event) => {
|
const handleClickOutsideProfile = (event) => {
|
||||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||||
if (
|
if (
|
||||||
profiles.value &&
|
profiles.value &&
|
||||||
profiles.value.$el !== event.target &&
|
profiles.value.$el !== event.target &&
|
||||||
!elements.includes(profiles.value.$el) &&
|
!elements.includes(profiles.value.$el) &&
|
||||||
!profileButton.value.contains(event.target)
|
!profileButton.value.contains(event.target)
|
||||||
) {
|
) {
|
||||||
showProfiles.value = false
|
showProfiles.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleCard = async () => {
|
const toggleCard = async () => {
|
||||||
showCard.value = !showCard.value
|
showCard.value = !showCard.value
|
||||||
showProfiles.value = false
|
showProfiles.value = false
|
||||||
await refreshInfo()
|
await refreshInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleProfiles = async () => {
|
const toggleProfiles = async () => {
|
||||||
if (currentProcesses.value.length === 1) return
|
if (currentProcesses.value.length === 1) return
|
||||||
showProfiles.value = !showProfiles.value
|
showProfiles.value = !showProfiles.value
|
||||||
showCard.value = false
|
showCard.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('click', handleClickOutsideCard)
|
window.addEventListener('click', handleClickOutsideCard)
|
||||||
window.addEventListener('click', handleClickOutsideProfile)
|
window.addEventListener('click', handleClickOutsideProfile)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('click', handleClickOutsideCard)
|
window.removeEventListener('click', handleClickOutsideCard)
|
||||||
window.removeEventListener('click', handleClickOutsideProfile)
|
window.removeEventListener('click', handleClickOutsideProfile)
|
||||||
unlistenProcess()
|
unlistenProcess()
|
||||||
unlistenLoading()
|
unlistenLoading()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.inline-fix {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-top: -2rem;
|
|
||||||
margin-bottom: -2rem;
|
|
||||||
//margin-left: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cosmic {
|
|
||||||
color: #3e8cde;
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
:deep(table) {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(hr),
|
|
||||||
:deep(h1),
|
|
||||||
:deep(h2) {
|
|
||||||
max-width: max(60rem, 90%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(ul),
|
|
||||||
:deep(ol) {
|
|
||||||
margin-left: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: var(--gap-lg);
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download {
|
|
||||||
color: #3e8cde;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
// padding: var(--gap-sm) var(--gap-lg);
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download:hover,
|
|
||||||
.download:focus,
|
|
||||||
.download:active {
|
|
||||||
color: #10fae5;
|
|
||||||
text-shadow: #26065e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-modal {
|
|
||||||
color: #3e8cde;
|
|
||||||
padding: var(--gap-sm) var(--gap-lg);
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-modal:hover,
|
|
||||||
.download-modal:focus,
|
|
||||||
.download-modal:active {
|
|
||||||
color: #10fae5;
|
|
||||||
text-shadow: #26065e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-groups {
|
.action-groups {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
&.rotate {
|
||||||
&.rotate {
|
transform: rotate(180deg);
|
||||||
transform: rotate(180deg);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-divider);
|
||||||
padding: var(--gap-sm) var(--gap-lg);
|
padding: var(--gap-sm) var(--gap-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.running-text {
|
.running-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--gap-xs);
|
gap: var(--gap-xs);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none; /* Safari */
|
||||||
/* Safari */
|
-ms-user-select: none; /* IE 10 and IE 11 */
|
||||||
-ms-user-select: none;
|
user-select: none;
|
||||||
/* IE 10 and IE 11 */
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&.clickable:hover {
|
&.clickable:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
|
|
||||||
&.running {
|
&.running {
|
||||||
background-color: var(--color-brand);
|
background-color: var(--color-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stopped {
|
&.stopped {
|
||||||
background-color: var(--color-base);
|
background-color: var(--color-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
width: 1.25rem !important;
|
width: 1.25rem !important;
|
||||||
height: 1.25rem !important;
|
height: 1.25rem !important;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
min-width: 1.25rem;
|
min-width: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stop {
|
&.stop {
|
||||||
color: var(--color-red);
|
color: var(--color-red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card {
|
.info-card {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3.5rem;
|
top: 3.5rem;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
box-shadow: var(--shadow-raised);
|
box-shadow: var(--shadow-raised);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-divider);
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-option {
|
.loading-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
background-color: var(--color-raised-bg-hover);
|
background-color: var(--color-raised-bg-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-text {
|
.loading-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-icon {
|
.loading-icon {
|
||||||
width: 2.25rem;
|
width: 2.25rem;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
:deep(svg) {
|
:deep(svg) {
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
width: 2.25rem;
|
width: 2.25rem;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-enter-active,
|
.download-enter-active,
|
||||||
.download-leave-active {
|
.download-leave-active {
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-enter-from,
|
.download-enter-from,
|
||||||
.download-leave-to {
|
.download-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-text {
|
.info-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-title {
|
.info-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-button {
|
.profile-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-card {
|
.profile-card {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3.5rem;
|
top: 3.5rem;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
background-color: var(--color-raised-bg);
|
background-color: var(--color-raised-bg);
|
||||||
box-shadow: var(--shadow-raised);
|
box-shadow: var(--shadow-raised);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-divider);
|
||||||
padding: var(--gap-md);
|
padding: var(--gap-md);
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,159 +1,162 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
|
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
emit('open')
|
emit('open')
|
||||||
$router.push({
|
$router.push({
|
||||||
path: `/project/${project.project_id ?? project.id}`,
|
path: `/project/${project.project_id ?? project.id}`,
|
||||||
query: { i: props.instance ? props.instance.path : undefined },
|
query: { i: props.instance ? props.instance.path : undefined },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="icon w-[96px] h-[96px] relative">
|
<div class="icon w-[96px] h-[96px] relative">
|
||||||
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
|
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 overflow-hidden">
|
<div class="flex flex-col gap-2 overflow-hidden">
|
||||||
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
|
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
|
||||||
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
|
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
|
||||||
{{ project.title }}
|
{{ project.title }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
|
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-0 line-clamp-2">
|
<div class="m-0 line-clamp-2">
|
||||||
{{ project.description }}
|
{{ project.description }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
|
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
|
||||||
<TagsIcon class="h-4 w-4 shrink-0" />
|
<TagsIcon class="h-4 w-4 shrink-0" />
|
||||||
<div
|
<div
|
||||||
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
|
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
|
||||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
||||||
>
|
>
|
||||||
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
|
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
|
||||||
Client or server
|
Client or server
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-else-if="
|
v-else-if="
|
||||||
(project.client_side === 'optional' || project.client_side === 'required') &&
|
(project.client_side === 'optional' || project.client_side === 'required') &&
|
||||||
(project.server_side === 'optional' || project.server_side === 'unsupported')
|
(project.server_side === 'optional' || project.server_side === 'unsupported')
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Client
|
Client
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-else-if="
|
v-else-if="
|
||||||
(project.server_side === 'optional' || project.server_side === 'required') &&
|
(project.server_side === 'optional' || project.server_side === 'required') &&
|
||||||
(project.client_side === 'optional' || project.client_side === 'unsupported')
|
(project.client_side === 'optional' || project.client_side === 'unsupported')
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Server
|
Server
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-else-if="
|
v-else-if="
|
||||||
project.client_side === 'unsupported' && project.server_side === 'unsupported'
|
project.client_side === 'unsupported' && project.server_side === 'unsupported'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Unsupported
|
Unsupported
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
|
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
|
||||||
>
|
>
|
||||||
Client and server
|
Client and server
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="tag in categories"
|
v-for="tag in categories"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
||||||
>
|
>
|
||||||
{{ formatCategory(tag.name) }}
|
{{ formatCategory(tag.name) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
|
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<DownloadIcon class="shrink-0" />
|
<DownloadIcon class="shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{{ formatNumber(project.downloads) }}
|
{{ formatNumber(project.downloads) }}
|
||||||
<span class="text-secondary">downloads</span>
|
<span class="text-secondary">downloads</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<HeartIcon class="shrink-0" />
|
<HeartIcon class="shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{{ formatNumber(project.follows ?? project.followers) }}
|
{{ formatNumber(project.follows ?? project.followers) }}
|
||||||
<span class="text-secondary">followers</span>
|
<span class="text-secondary">followers</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-auto relative">
|
<div class="mt-auto relative">
|
||||||
<div class="absolute bottom-0 right-0 w-fit">
|
<div class="absolute bottom-0 right-0 w-fit">
|
||||||
<ButtonStyled color="brand" type="outlined">
|
<ButtonStyled color="brand" type="outlined">
|
||||||
<button
|
<button
|
||||||
:disabled="installed || installing"
|
:disabled="installed || installing"
|
||||||
class="shrink-0 no-wrap"
|
class="shrink-0 no-wrap"
|
||||||
@click.stop="install()"
|
@click.stop="install()"
|
||||||
>
|
>
|
||||||
<template v-if="!installed">
|
<template v-if="!installed">
|
||||||
<DownloadIcon v-if="modpack || instance" />
|
<DownloadIcon v-if="modpack || instance" />
|
||||||
<PlusIcon v-else />
|
<PlusIcon v-else />
|
||||||
</template>
|
</template>
|
||||||
<CheckIcon v-else />
|
<CheckIcon v-else />
|
||||||
{{
|
{{
|
||||||
installing
|
installing
|
||||||
? 'Installing'
|
? 'Installing'
|
||||||
: installed
|
: installed
|
||||||
? 'Installed'
|
? 'Installed'
|
||||||
: modpack || instance
|
: modpack || instance
|
||||||
? 'Install'
|
? 'Install'
|
||||||
: 'Add to an instance'
|
: 'Add to an instance'
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { TagsIcon, DownloadIcon, HeartIcon, PlusIcon, CheckIcon } from '@modrinth/assets'
|
import { CheckIcon, DownloadIcon, HeartIcon, PlusIcon, TagsIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
import { formatCategory, formatNumber } from '@modrinth/utils'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { ref, computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { install as installVersion } from '@/store/install.js'
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
project: {
|
project: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
featured: {
|
featured: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
installed: {
|
installed: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['open', 'install'])
|
const emit = defineEmits(['open', 'install'])
|
||||||
@@ -161,20 +164,20 @@ const emit = defineEmits(['open', 'install'])
|
|||||||
const installing = ref(false)
|
const installing = ref(false)
|
||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
installing.value = true
|
installing.value = true
|
||||||
await installVersion(
|
await installVersion(
|
||||||
props.project.project_id ?? props.project.id,
|
props.project.project_id ?? props.project.id,
|
||||||
null,
|
null,
|
||||||
props.instance ? props.instance.path : null,
|
props.instance ? props.instance.path : null,
|
||||||
'SearchCard',
|
'SearchCard',
|
||||||
() => {
|
() => {
|
||||||
installing.value = false
|
installing.value = false
|
||||||
emit('install', props.project.project_id ?? props.project.id)
|
emit('install', props.project.project_id ?? props.project.id)
|
||||||
},
|
},
|
||||||
(profile) => {
|
(profile) => {
|
||||||
router.push(`/instance/${profile}`)
|
router.push(`/instance/${profile}`)
|
||||||
},
|
},
|
||||||
)
|
).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const modpack = computed(() => props.project.project_type === 'modpack')
|
const modpack = computed(() => props.project.project_type === 'modpack')
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,12 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import SearchCard from '@/components/ui/SearchCard.vue'
|
|
||||||
import { get_categories } from '@/helpers/tags.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { get_version, get_project } from '@/helpers/cache.js'
|
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import SearchCard from '@/components/ui/SearchCard.vue'
|
||||||
|
import { get_project, get_version } from '@/helpers/cache.js'
|
||||||
|
import { get_categories } from '@/helpers/tags.js'
|
||||||
|
import { install as installVersion } from '@/store/install.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const confirmModal = ref(null)
|
const confirmModal = ref(null)
|
||||||
const project = ref(null)
|
const project = ref(null)
|
||||||
@@ -15,86 +17,93 @@ const categories = ref(null)
|
|||||||
const installing = ref(false)
|
const installing = ref(false)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
async show(event) {
|
async show(event) {
|
||||||
if (event.event === 'InstallVersion') {
|
if (event.event === 'InstallVersion') {
|
||||||
version.value = await get_version(event.id, 'must_revalidate').catch(handleError)
|
version.value = await get_version(event.id, 'must_revalidate').catch(handleError)
|
||||||
project.value = await get_project(version.value.project_id, 'must_revalidate').catch(
|
project.value = await get_project(version.value.project_id, 'must_revalidate').catch(
|
||||||
handleError,
|
handleError,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
project.value = await get_project(event.id, 'must_revalidate').catch(handleError)
|
project.value = await get_project(event.id, 'must_revalidate').catch(handleError)
|
||||||
version.value = await get_version(
|
version.value = await get_version(
|
||||||
project.value.versions[project.value.versions.length - 1],
|
project.value.versions[project.value.versions.length - 1],
|
||||||
'must_revalidate',
|
'must_revalidate',
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
}
|
}
|
||||||
categories.value = (await get_categories().catch(handleError)).filter(
|
categories.value = (await get_categories().catch(handleError)).filter(
|
||||||
(cat) => project.value.categories.includes(cat.name) && cat.project_type === 'mod',
|
(cat) => project.value.categories.includes(cat.name) && cat.project_type === 'mod',
|
||||||
)
|
)
|
||||||
confirmModal.value.show()
|
confirmModal.value.show()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
confirmModal.value.hide()
|
confirmModal.value.hide()
|
||||||
await installVersion(project.value.id, version.value.id, null, 'URLConfirmModal')
|
await installVersion(
|
||||||
|
project.value.id,
|
||||||
|
version.value.id,
|
||||||
|
null,
|
||||||
|
'URLConfirmModal',
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
).catch(handleError)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`">
|
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<SearchCard
|
<SearchCard
|
||||||
:project="project"
|
:project="project"
|
||||||
class="project-card"
|
class="project-card"
|
||||||
:categories="categories"
|
:categories="categories"
|
||||||
@open="confirmModal.hide()"
|
@open="confirmModal.hide()"
|
||||||
/>
|
/>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<div class="markdown-body">
|
<div class="markdown-body">
|
||||||
<p>
|
<p>
|
||||||
Installing <code>{{ version.id }}</code> from Modrinth
|
Installing <code>{{ version.id }}</code> from Modrinth
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<Button :loading="installing" color="primary" @click="install">Install</Button>
|
<Button :loading="installing" color="primary" @click="install">Install</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-md);
|
gap: var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card {
|
.project-card {
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
:deep(.badge) {
|
:deep(.badge) {
|
||||||
border: 1px solid var(--color-raised-bg);
|
border: 1px solid var(--color-raised-bg);
|
||||||
background-color: var(--color-accent-contrast);
|
background-color: var(--color-accent-contrast);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
131
apps/app-frontend/src/components/ui/UpdateToast.vue
Normal file
131
apps/app-frontend/src/components/ui/UpdateToast.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DownloadIcon, ExternalIcon, RefreshCwIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import { ButtonStyled, commonMessages, ProgressBar } from '@modrinth/ui'
|
||||||
|
import { formatBytes } from '@modrinth/utils'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'close' | 'restart' | 'download'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
version: string
|
||||||
|
size: number | null
|
||||||
|
metered: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const downloading = ref(false)
|
||||||
|
const { progress } = injectAppUpdateDownloadProgress()
|
||||||
|
|
||||||
|
function download() {
|
||||||
|
emit('download')
|
||||||
|
downloading.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: {
|
||||||
|
id: 'app.update-toast.title',
|
||||||
|
defaultMessage: 'Update available',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
id: 'app.update-toast.body',
|
||||||
|
defaultMessage:
|
||||||
|
'Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App.',
|
||||||
|
},
|
||||||
|
reload: {
|
||||||
|
id: 'app.update-toast.reload',
|
||||||
|
defaultMessage: 'Reload',
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
id: 'app.update-toast.download',
|
||||||
|
defaultMessage: 'Download ({size})',
|
||||||
|
},
|
||||||
|
downloading: {
|
||||||
|
id: 'app.update-toast.downloading',
|
||||||
|
defaultMessage: 'Downloading...',
|
||||||
|
},
|
||||||
|
changelog: {
|
||||||
|
id: 'app.update-toast.changelog',
|
||||||
|
defaultMessage: 'Changelog',
|
||||||
|
},
|
||||||
|
meteredBody: {
|
||||||
|
id: 'app.update-toast.body.metered',
|
||||||
|
defaultMessage: `Modrinth App v{version} is available now! Since you're on a metered network, we didn't automatically download it.`,
|
||||||
|
},
|
||||||
|
downloadCompleteTitle: {
|
||||||
|
id: 'app.update-toast.title.download-complete',
|
||||||
|
defaultMessage: 'Download complete',
|
||||||
|
},
|
||||||
|
downloadedBody: {
|
||||||
|
id: 'app.update-toast.body.download-complete',
|
||||||
|
defaultMessage: `Modrinth App v{version} has finished downloading. Reload to update now, or automatically when you close Modrinth App.`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-[min-content] fixed card-shadow rounded-2xl top-[--top-bar-height] mt-6 right-6 p-4 z-10 bg-bg-raised border-divider border-solid border-[2px]"
|
||||||
|
:class="{
|
||||||
|
'download-complete': progress === 1,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex min-w-[25rem] gap-4">
|
||||||
|
<h2 class="whitespace-nowrap text-base text-contrast font-semibold m-0 grow">
|
||||||
|
{{
|
||||||
|
formatMessage(metered && progress === 1 ? messages.downloadCompleteTitle : messages.title)
|
||||||
|
}}
|
||||||
|
</h2>
|
||||||
|
<ButtonStyled size="small" circular>
|
||||||
|
<button v-tooltip="formatMessage(commonMessages.closeButton)" @click="emit('close')">
|
||||||
|
<XIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm mt-2 mb-0">
|
||||||
|
{{
|
||||||
|
formatMessage(
|
||||||
|
metered
|
||||||
|
? progress === 1
|
||||||
|
? messages.downloadedBody
|
||||||
|
: messages.meteredBody
|
||||||
|
: messages.body,
|
||||||
|
{ version },
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-if="metered && progress < 1"
|
||||||
|
class="text-sm text-secondary mt-2 mb-0 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<template v-if="progress > 0">
|
||||||
|
<ProgressBar :progress="progress" class="max-w-[unset]" />
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button v-if="metered && progress < 1" :disabled="downloading" @click="download">
|
||||||
|
<SpinnerIcon v-if="downloading" class="animate-spin" />
|
||||||
|
<DownloadIcon v-else />
|
||||||
|
{{
|
||||||
|
formatMessage(downloading ? messages.downloading : messages.download, {
|
||||||
|
size: formatBytes(size ?? 0),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
<button v-else @click="emit('restart')">
|
||||||
|
<RefreshCwIcon /> {{ formatMessage(messages.reload) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled>
|
||||||
|
<a href="https://modrinth.com/news/changelog?filter=app">
|
||||||
|
{{ formatMessage(messages.changelog) }} <ExternalIcon />
|
||||||
|
</a>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,361 +1,427 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Avatar, ButtonStyled, OverflowMenu, useRelativeTime } from '@modrinth/ui'
|
import { MailIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
UserPlusIcon,
|
Avatar,
|
||||||
MoreVerticalIcon,
|
ButtonStyled,
|
||||||
MailIcon,
|
commonMessages,
|
||||||
SettingsIcon,
|
injectNotificationManager,
|
||||||
TrashIcon,
|
useRelativeTime,
|
||||||
XIcon,
|
} from '@modrinth/ui'
|
||||||
} from '@modrinth/assets'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { ref, onUnmounted, watch, computed } from 'vue'
|
import { IntlFormatted } from '@vintl/vintl/components'
|
||||||
import { friend_listener } from '@/helpers/events'
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||||
import { friends, friend_statuses, add_friend, remove_friend } from '@/helpers/friends'
|
|
||||||
import { get_user_many } from '@/helpers/cache'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import type { Dayjs } from 'dayjs'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
|
import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { friend_listener } from '@/helpers/events'
|
||||||
|
import {
|
||||||
|
add_friend,
|
||||||
|
friends,
|
||||||
|
type FriendWithUserData,
|
||||||
|
remove_friend,
|
||||||
|
transformFriends,
|
||||||
|
} from '@/helpers/friends.ts'
|
||||||
|
import type { ModrinthCredentials } from '@/helpers/mr_auth'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
credentials: unknown | null
|
credentials: ModrinthCredentials | null
|
||||||
signIn: () => void
|
signIn: () => void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const userCredentials = computed(() => props.credentials)
|
const userCredentials = computed(() => props.credentials)
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const manageFriendsModal = ref()
|
|
||||||
const friendInvitesModal = ref()
|
const friendInvitesModal = ref()
|
||||||
|
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
const addFriendModal = ref()
|
const addFriendModal = ref()
|
||||||
async function addFriendFromModal() {
|
async function addFriendFromModal() {
|
||||||
addFriendModal.value.hide()
|
addFriendModal.value.hide()
|
||||||
await add_friend(username.value).catch(handleError)
|
await add_friend(username.value).catch(handleError)
|
||||||
username.value = ''
|
username.value = ''
|
||||||
await loadFriends()
|
await loadFriends()
|
||||||
}
|
}
|
||||||
|
|
||||||
const friendOptions = ref()
|
async function addFriend(friend: FriendWithUserData) {
|
||||||
async function handleFriendOptions(args) {
|
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
|
||||||
switch (args.option) {
|
if (id) {
|
||||||
case 'remove-friend':
|
await add_friend(id).catch(handleError)
|
||||||
await removeFriend(args.item)
|
await loadFriends()
|
||||||
break
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addFriend(friend: Friend) {
|
async function removeFriend(friend: FriendWithUserData) {
|
||||||
await add_friend(
|
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
|
||||||
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
if (id) {
|
||||||
).catch(handleError)
|
await remove_friend(id).catch(handleError)
|
||||||
await loadFriends()
|
await loadFriends()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFriend(friend: Friend) {
|
const userFriends = ref<FriendWithUserData[]>([])
|
||||||
await remove_friend(
|
const sortedFriends = computed<FriendWithUserData[]>(() =>
|
||||||
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
userFriends.value.slice().sort((a, b) => {
|
||||||
).catch(handleError)
|
if (a.last_updated === null && b.last_updated === null) {
|
||||||
await loadFriends()
|
return 0 // Both are null, equal in sorting
|
||||||
}
|
}
|
||||||
|
if (a.last_updated === null) {
|
||||||
|
return 1 // `a` is null, move it after `b`
|
||||||
|
}
|
||||||
|
if (b.last_updated === null) {
|
||||||
|
return -1 // `b` is null, move it after `a`
|
||||||
|
}
|
||||||
|
// Both are non-null, sort by date
|
||||||
|
return b.last_updated.diff(a.last_updated)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const filteredFriends = computed<FriendWithUserData[]>(() =>
|
||||||
|
sortedFriends.value.filter((x) =>
|
||||||
|
x.username.trim().toLowerCase().includes(search.value.trim().toLowerCase()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
type Friend = {
|
const activeFriends = computed<FriendWithUserData[]>(() =>
|
||||||
id: string
|
filteredFriends.value.filter((x) => !!x.status && x.online && x.accepted),
|
||||||
friend_id: string | null
|
)
|
||||||
status: string | null
|
const onlineFriends = computed<FriendWithUserData[]>(() =>
|
||||||
last_updated: Dayjs | null
|
filteredFriends.value.filter((x) => x.online && !x.status && x.accepted),
|
||||||
created: Dayjs
|
)
|
||||||
username: string
|
const offlineFriends = computed<FriendWithUserData[]>(() =>
|
||||||
accepted: boolean
|
filteredFriends.value.filter((x) => !x.online && x.accepted),
|
||||||
online: boolean
|
|
||||||
avatar: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const userFriends = ref<Friend[]>([])
|
|
||||||
const acceptedFriends = computed(() =>
|
|
||||||
userFriends.value
|
|
||||||
.filter((x) => x.accepted)
|
|
||||||
.toSorted((a, b) => {
|
|
||||||
if (a.last_updated === null && b.last_updated === null) {
|
|
||||||
return 0 // Both are null, equal in sorting
|
|
||||||
}
|
|
||||||
if (a.last_updated === null) {
|
|
||||||
return 1 // `a` is null, move it after `b`
|
|
||||||
}
|
|
||||||
if (b.last_updated === null) {
|
|
||||||
return -1 // `b` is null, move it after `a`
|
|
||||||
}
|
|
||||||
// Both are non-null, sort by date
|
|
||||||
return b.last_updated.diff(a.last_updated)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
const pendingFriends = computed(() =>
|
const pendingFriends = computed(() =>
|
||||||
userFriends.value.filter((x) => !x.accepted).toSorted((a, b) => b.created.diff(a.created)),
|
filteredFriends.value
|
||||||
|
.filter((x) => !x.accepted && x.id !== userCredentials.value?.user_id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.created.diff(a.created)),
|
||||||
|
)
|
||||||
|
const incomingRequests = computed(() =>
|
||||||
|
userFriends.value
|
||||||
|
.filter((x) => !x.accepted && x.id === userCredentials.value?.user_id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.created.diff(a.created)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
async function loadFriends(timeout = false) {
|
async function loadFriends(timeout = false) {
|
||||||
loading.value = timeout
|
loading.value = timeout
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const friendsList = await friends()
|
const friendsList = await friends()
|
||||||
|
userFriends.value = await transformFriends(friendsList, userCredentials.value)
|
||||||
if (friendsList.length === 0) {
|
loading.value = false
|
||||||
userFriends.value = []
|
} catch (e) {
|
||||||
} else {
|
console.error('Error loading friends', e)
|
||||||
const friendStatuses = await friend_statuses()
|
if (timeout) {
|
||||||
const users = await get_user_many(
|
setTimeout(() => loadFriends(), 15 * 1000)
|
||||||
friendsList.map((x) => (x.id === userCredentials.value.user_id ? x.friend_id : x.id)),
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
userFriends.value = friendsList.map((friend) => {
|
|
||||||
const user = users.find((x) => x.id === friend.id || x.id === friend.friend_id)
|
|
||||||
const status = friendStatuses.find(
|
|
||||||
(x) => x.user_id === friend.id || x.user_id === friend.friend_id,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
id: friend.id,
|
|
||||||
friend_id: friend.friend_id,
|
|
||||||
status: status?.profile_name,
|
|
||||||
last_updated: status && status.last_update ? dayjs(status.last_update) : null,
|
|
||||||
created: dayjs(friend.created),
|
|
||||||
avatar: user?.avatar_url,
|
|
||||||
username: user?.username,
|
|
||||||
online: !!status,
|
|
||||||
accepted: friend.accepted,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = false
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error loading friends', e)
|
|
||||||
if (timeout) {
|
|
||||||
setTimeout(() => loadFriends(), 15 * 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
userCredentials,
|
userCredentials,
|
||||||
() => {
|
() => {
|
||||||
if (userCredentials.value === undefined) {
|
if (userCredentials.value === undefined) {
|
||||||
userFriends.value = []
|
userFriends.value = []
|
||||||
} else if (userCredentials.value === null) {
|
loading.value = false
|
||||||
userFriends.value = []
|
} else if (userCredentials.value === null) {
|
||||||
loading.value = false
|
userFriends.value = []
|
||||||
} else {
|
loading.value = false
|
||||||
loadFriends(true)
|
} else {
|
||||||
}
|
loadFriends(true)
|
||||||
},
|
}
|
||||||
{ immediate: true },
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const unlisten = await friend_listener(() => loadFriends())
|
const unlisten = await friend_listener(() => loadFriends())
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlisten()
|
unlisten()
|
||||||
|
})
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
addFriend: {
|
||||||
|
id: 'friends.action.add-friend',
|
||||||
|
defaultMessage: 'Add a friend',
|
||||||
|
},
|
||||||
|
addingAFriend: {
|
||||||
|
id: 'friends.add-friend.title',
|
||||||
|
defaultMessage: 'Adding a friend',
|
||||||
|
},
|
||||||
|
usernameTitle: {
|
||||||
|
id: 'friends.add-friend.username.title',
|
||||||
|
defaultMessage: "What's your friend's Modrinth username?",
|
||||||
|
},
|
||||||
|
usernameDescription: {
|
||||||
|
id: 'friends.add-friend.username.description',
|
||||||
|
defaultMessage: 'It may be different from their Minecraft username!',
|
||||||
|
},
|
||||||
|
usernamePlaceholder: {
|
||||||
|
id: 'friends.add-friend.username.placeholder',
|
||||||
|
defaultMessage: 'Enter Modrinth username...',
|
||||||
|
},
|
||||||
|
sendFriendRequest: {
|
||||||
|
id: 'friends.add-friend.submit',
|
||||||
|
defaultMessage: 'Send friend request',
|
||||||
|
},
|
||||||
|
viewFriendRequests: {
|
||||||
|
id: 'friends.action.view-friend-requests',
|
||||||
|
defaultMessage: '{count} friend {count, plural, one {request} other {requests}}',
|
||||||
|
},
|
||||||
|
searchFriends: {
|
||||||
|
id: 'friends.search-friends-placeholder',
|
||||||
|
defaultMessage: 'Search friends...',
|
||||||
|
},
|
||||||
|
friends: {
|
||||||
|
id: 'friends.heading',
|
||||||
|
defaultMessage: 'Friends',
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
id: 'friends.heading.pending',
|
||||||
|
defaultMessage: 'Pending',
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
id: 'friends.heading.active',
|
||||||
|
defaultMessage: 'Active',
|
||||||
|
},
|
||||||
|
online: {
|
||||||
|
id: 'friends.heading.online',
|
||||||
|
defaultMessage: 'Online',
|
||||||
|
},
|
||||||
|
offline: {
|
||||||
|
id: 'friends.heading.offline',
|
||||||
|
defaultMessage: 'Offline',
|
||||||
|
},
|
||||||
|
noFriendsMatch: {
|
||||||
|
id: 'friends.no-friends-match',
|
||||||
|
defaultMessage: `No friends matching ''{query}''`,
|
||||||
|
},
|
||||||
|
signInToAddFriends: {
|
||||||
|
id: 'friends.sign-in-to-add-friends',
|
||||||
|
defaultMessage:
|
||||||
|
"<link>Sign in to a Modrinth account</link> to add friends and see what they're playing!",
|
||||||
|
},
|
||||||
|
addFriendsToShare: {
|
||||||
|
id: 'friends.add-friends-to-share',
|
||||||
|
defaultMessage: "<link>Add friends</link> to see what they're playing!",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="manageFriendsModal" header="Manage friends">
|
<ModalWrapper ref="friendInvitesModal" header="View friend requests">
|
||||||
<p v-if="acceptedFriends.length === 0">Add friends to share what you're playing!</p>
|
<p v-if="incomingRequests.length === 0">You have no pending friend requests :C</p>
|
||||||
<div v-else class="flex flex-col gap-4 min-w-[20rem]">
|
<div v-else class="flex flex-col gap-4 min-w-[40rem]">
|
||||||
<input v-model="search" type="text" placeholder="Search friends..." class="w-full" />
|
<div v-for="friend in incomingRequests" :key="friend.username" class="flex gap-2">
|
||||||
<div
|
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||||
v-for="friend in acceptedFriends.filter(
|
<div class="grid grid-cols-[1fr_auto] w-full gap-4">
|
||||||
(x) => !search || x.username.toLowerCase().includes(search),
|
<div>
|
||||||
)"
|
<p class="m-0">
|
||||||
:key="friend.username"
|
<template v-if="friend.id === userCredentials?.user_id">
|
||||||
class="flex gap-2 items-center"
|
<span class="text-contrast">{{ friend.username }}</span> sent you a friend request
|
||||||
>
|
</template>
|
||||||
<div class="relative">
|
<template v-else>
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
||||||
<span
|
</template>
|
||||||
v-if="friend.online"
|
</p>
|
||||||
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
<p class="m-0 text-sm text-secondary">
|
||||||
/>
|
{{ formatRelativeTime(friend.created.toISOString()) }}
|
||||||
</div>
|
</p>
|
||||||
<div>{{ friend.username }}</div>
|
</div>
|
||||||
<div class="ml-auto">
|
<div class="flex gap-2">
|
||||||
<ButtonStyled>
|
<template v-if="friend.id === userCredentials?.user_id">
|
||||||
<button @click="removeFriend(friend)">
|
<ButtonStyled color="brand">
|
||||||
<XIcon />
|
<button @click="addFriend(friend)">
|
||||||
Remove
|
<UserPlusIcon />
|
||||||
</button>
|
Accept
|
||||||
</ButtonStyled>
|
</button>
|
||||||
</div>
|
</ButtonStyled>
|
||||||
</div>
|
<ButtonStyled>
|
||||||
</div>
|
<button @click="removeFriend(friend)">
|
||||||
</ModalWrapper>
|
<XIcon />
|
||||||
<ModalWrapper ref="friendInvitesModal" header="View friend requests">
|
Ignore
|
||||||
<p v-if="pendingFriends.length === 0">You have no pending friend requests :C</p>
|
</button>
|
||||||
<div v-else class="flex flex-col gap-4">
|
</ButtonStyled>
|
||||||
<div v-for="friend in pendingFriends" :key="friend.username" class="flex gap-2">
|
</template>
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
<template v-else>
|
||||||
<div class="flex flex-col gap-2">
|
<ButtonStyled>
|
||||||
<div>
|
<button @click="removeFriend(friend)">
|
||||||
<p class="m-0">
|
<XIcon />
|
||||||
<template v-if="friend.id === userCredentials.user_id">
|
Cancel
|
||||||
<span class="font-bold">{{ friend.username }}</span> sent you a friend request
|
</button>
|
||||||
</template>
|
</ButtonStyled>
|
||||||
<template v-else>
|
</template>
|
||||||
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
<p class="m-0 text-sm text-secondary">
|
</div>
|
||||||
{{ formatRelativeTime(friend.created.toISOString()) }}
|
</ModalWrapper>
|
||||||
</p>
|
<ModalWrapper ref="addFriendModal" :header="formatMessage(messages.addingAFriend)">
|
||||||
</div>
|
<div class="min-w-[30rem]">
|
||||||
<div class="flex gap-2">
|
<h2 class="m-0 text-base font-medium text-primary">
|
||||||
<template v-if="friend.id === userCredentials.user_id">
|
{{ formatMessage(messages.usernameTitle) }}
|
||||||
<ButtonStyled color="brand">
|
</h2>
|
||||||
<button @click="addFriend(friend)">
|
<p class="m-0 mt-1 text-sm text-secondary leading-tight">
|
||||||
<UserPlusIcon />
|
{{ formatMessage(messages.usernameDescription) }}
|
||||||
Accept
|
</p>
|
||||||
</button>
|
<div class="flex items-center gap-2 mt-4">
|
||||||
</ButtonStyled>
|
<div class="iconified-input flex-1">
|
||||||
<ButtonStyled>
|
<UserIcon aria-hidden="true" />
|
||||||
<button @click="removeFriend(friend)">
|
<input
|
||||||
<XIcon />
|
v-model="username"
|
||||||
Ignore
|
type="text"
|
||||||
</button>
|
:placeholder="formatMessage(messages.usernamePlaceholder)"
|
||||||
</ButtonStyled>
|
@keyup.enter="addFriendFromModal"
|
||||||
</template>
|
/>
|
||||||
<template v-else>
|
</div>
|
||||||
<ButtonStyled>
|
<ButtonStyled color="brand">
|
||||||
<button @click="removeFriend(friend)">
|
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
||||||
<XIcon />
|
<SendIcon />
|
||||||
Cancel
|
{{ formatMessage(messages.sendFriendRequest) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ModalWrapper>
|
||||||
</div>
|
<div v-if="userCredentials && !loading" class="flex gap-1 items-center mb-3 ml-2 mr-1">
|
||||||
</div>
|
<template v-if="sortedFriends.length > 0">
|
||||||
</ModalWrapper>
|
<ButtonStyled circular type="transparent">
|
||||||
<ModalWrapper ref="addFriendModal" header="Add a friend">
|
<button
|
||||||
<div class="mb-4">
|
v-tooltip="formatMessage(messages.addFriend)"
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Username</h2>
|
:aria-label="formatMessage(messages.addFriend)"
|
||||||
<p class="m-0 mt-1 leading-tight">You can add friends with their Modrinth username.</p>
|
@click="addFriendModal.show"
|
||||||
<input v-model="username" class="mt-2 w-full" type="text" placeholder="Enter username..." />
|
>
|
||||||
</div>
|
<UserPlusIcon />
|
||||||
<ButtonStyled color="brand">
|
</button>
|
||||||
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
</ButtonStyled>
|
||||||
<UserPlusIcon />
|
<div class="iconified-input flex-1">
|
||||||
Add friend
|
<input
|
||||||
</button>
|
v-model="search"
|
||||||
</ButtonStyled>
|
type="text"
|
||||||
</ModalWrapper>
|
class="friends-search-bar flex w-full"
|
||||||
<div class="flex justify-between items-center">
|
:placeholder="formatMessage(messages.searchFriends)"
|
||||||
<h3 class="text-lg m-0">Friends</h3>
|
@keyup.esc="search = ''"
|
||||||
<ButtonStyled v-if="userCredentials" type="transparent" circular>
|
/>
|
||||||
<OverflowMenu
|
<button
|
||||||
:options="[
|
v-if="search"
|
||||||
{
|
v-tooltip="formatMessage(commonMessages.clearButton)"
|
||||||
id: 'add-friend',
|
class="r-btn flex items-center justify-center bg-transparent button-animation p-2 cursor-pointer appearance-none border-none"
|
||||||
action: () => addFriendModal.show(),
|
@click="search = ''"
|
||||||
},
|
>
|
||||||
{
|
<XIcon />
|
||||||
id: 'manage-friends',
|
</button>
|
||||||
action: () => manageFriendsModal.show(),
|
</div>
|
||||||
shown: acceptedFriends.length > 0,
|
</template>
|
||||||
},
|
<h3 v-else class="ml-2 w-full text-base text-primary font-medium m-0">
|
||||||
{
|
{{ formatMessage(messages.friends) }}
|
||||||
id: 'view-requests',
|
</h3>
|
||||||
action: () => friendInvitesModal.show(),
|
<ButtonStyled v-if="incomingRequests.length > 0" circular type="transparent">
|
||||||
shown: pendingFriends.length > 0,
|
<button
|
||||||
},
|
v-tooltip="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||||
]"
|
class="relative"
|
||||||
aria-label="More options"
|
:aria-label="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||||
>
|
@click="friendInvitesModal.show"
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
>
|
||||||
<template #add-friend>
|
<MailIcon />
|
||||||
<UserPlusIcon aria-hidden="true" />
|
<span
|
||||||
Add friend
|
v-if="incomingRequests.length > 0"
|
||||||
</template>
|
aria-hidden="true"
|
||||||
<template #manage-friends>
|
class="absolute bg-brand text-brand-inverted text-[8px] top-0.5 px-1 right-0.5 min-w-3 h-3 rounded-full flex items-center justify-center font-bold"
|
||||||
<SettingsIcon aria-hidden="true" />
|
>
|
||||||
Manage friends
|
{{ incomingRequests.length }}
|
||||||
<div
|
</span>
|
||||||
v-if="acceptedFriends.length > 0"
|
</button>
|
||||||
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
</ButtonStyled>
|
||||||
>
|
</div>
|
||||||
{{ acceptedFriends.length }}
|
<div class="flex flex-col gap-3">
|
||||||
</div>
|
<h3 v-if="loading" class="ml-4 mr-1 text-base text-primary font-medium m-0">
|
||||||
</template>
|
{{ formatMessage(messages.friends) }}
|
||||||
<template #view-requests>
|
</h3>
|
||||||
<MailIcon aria-hidden="true" />
|
<template v-if="loading">
|
||||||
View friend requests
|
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse ml-4 mr-1">
|
||||||
<div
|
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
||||||
v-if="pendingFriends.length > 0"
|
<div class="flex flex-col w-full">
|
||||||
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
||||||
>
|
<div class="h-2.5 bg-button-bg rounded-full w-3/4"></div>
|
||||||
{{ pendingFriends.length }}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</OverflowMenu>
|
<template v-else-if="sortedFriends.length === 0">
|
||||||
</ButtonStyled>
|
<div class="text-sm ml-4 mr-1">
|
||||||
</div>
|
<div v-if="!userCredentials">
|
||||||
<div class="flex flex-col gap-2 mt-2">
|
<IntlFormatted :message-id="messages.signInToAddFriends">
|
||||||
<template v-if="loading">
|
<template #link="{ children }">
|
||||||
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
|
<span class="font-semibold text-brand cursor-pointer" @click="signIn">
|
||||||
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
<component :is="() => children" />
|
||||||
<div class="flex flex-col w-full">
|
</span>
|
||||||
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
</template>
|
||||||
<div class="h-2.5 bg-button-bg rounded-full w-3/4"></div>
|
</IntlFormatted>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else>
|
||||||
</template>
|
<IntlFormatted :message-id="messages.addFriendsToShare">
|
||||||
<template v-else-if="acceptedFriends.length === 0">
|
<template #link="{ children }">
|
||||||
<div class="text-sm">
|
<span class="font-semibold text-brand cursor-pointer" @click="addFriendModal.show">
|
||||||
<div v-if="!userCredentials">
|
<component :is="() => children" />
|
||||||
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
|
</span>
|
||||||
</div>
|
</template>
|
||||||
<div v-else>
|
</IntlFormatted>
|
||||||
<span class="text-link cursor-pointer" @click="addFriendModal.show()">Add friends</span>
|
</div>
|
||||||
to share what you're playing!
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
<template v-else>
|
||||||
</template>
|
<FriendsSection
|
||||||
<template v-else>
|
v-if="activeFriends.length > 0"
|
||||||
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
|
:is-searching="!!search"
|
||||||
<template #remove-friend> <TrashIcon /> Remove friend </template>
|
open-by-default
|
||||||
</ContextMenu>
|
:friends="activeFriends"
|
||||||
<div
|
:heading="formatMessage(messages.active)"
|
||||||
v-for="friend in acceptedFriends.slice(0, 5)"
|
:remove-friend="removeFriend"
|
||||||
:key="friend.username"
|
/>
|
||||||
class="flex gap-2 items-center"
|
<FriendsSection
|
||||||
:class="{ grayscale: !friend.online }"
|
v-if="onlineFriends.length > 0"
|
||||||
@contextmenu.prevent.stop="
|
:is-searching="!!search"
|
||||||
(event) =>
|
open-by-default
|
||||||
friendOptions.showMenu(event, friend, [
|
:friends="onlineFriends"
|
||||||
{
|
:heading="formatMessage(messages.online)"
|
||||||
name: 'remove-friend',
|
:remove-friend="removeFriend"
|
||||||
color: 'danger',
|
/>
|
||||||
},
|
<FriendsSection
|
||||||
])
|
v-if="offlineFriends.length > 0"
|
||||||
"
|
:is-searching="!!search"
|
||||||
>
|
:open-by-default="activeFriends.length + onlineFriends.length < 3"
|
||||||
<div class="relative">
|
:friends="offlineFriends"
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
:heading="formatMessage(messages.offline)"
|
||||||
<span
|
:remove-friend="removeFriend"
|
||||||
v-if="friend.online"
|
/>
|
||||||
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
<FriendsSection
|
||||||
/>
|
v-if="pendingFriends.length > 0"
|
||||||
</div>
|
:is-searching="!!search"
|
||||||
<div class="flex flex-col">
|
open-by-default
|
||||||
<span class="text-md m-0 font-medium" :class="{ 'text-secondary': !friend.online }">
|
:friends="pendingFriends"
|
||||||
{{ friend.username }}
|
:heading="formatMessage(messages.pending)"
|
||||||
</span>
|
:remove-friend="removeFriend"
|
||||||
<span v-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
/>
|
||||||
</div>
|
<p v-if="filteredFriends.length === 0 && search" class="text-sm text-secondary my-1 mx-4">
|
||||||
</div>
|
{{ formatMessage(messages.noFriendsMatch, { query: search }) }}
|
||||||
</template>
|
</p>
|
||||||
</div>
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.friends-search-bar {
|
||||||
|
background: none;
|
||||||
|
border: 2px solid var(--color-button-bg) !important;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friends-search-bar::placeholder {
|
||||||
|
@apply text-sm font-normal;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
185
apps/app-frontend/src/components/ui/friends/FriendsSection.vue
Normal file
185
apps/app-frontend/src/components/ui/friends/FriendsSection.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { MoreVerticalIcon, TrashIcon, UserIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import { Accordion, Avatar, ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import type { FriendWithUserData } from '@/helpers/friends.ts'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
friends: FriendWithUserData[]
|
||||||
|
heading: string
|
||||||
|
removeFriend: (friend: FriendWithUserData) => Promise<void>
|
||||||
|
isSearching?: boolean
|
||||||
|
openByDefault?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
isSearching: false,
|
||||||
|
openByDefault: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function createContextMenuOptions(friend: FriendWithUserData) {
|
||||||
|
if (friend.accepted) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'view-profile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'remove-friend',
|
||||||
|
color: 'danger',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'view-profile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancel-request',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openProfile(username: string) {
|
||||||
|
openUrl('https://modrinth.com/user/' + username)
|
||||||
|
}
|
||||||
|
|
||||||
|
const friendOptions = useTemplateRef('friendOptions')
|
||||||
|
async function handleFriendOptions(args: { item: FriendWithUserData; option: string }) {
|
||||||
|
switch (args.option) {
|
||||||
|
case 'remove-friend':
|
||||||
|
case 'cancel-request':
|
||||||
|
await props.removeFriend(args.item)
|
||||||
|
break
|
||||||
|
case 'view-profile':
|
||||||
|
openProfile(args.item.username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
removeFriend: {
|
||||||
|
id: 'friends.friend.remove-friend',
|
||||||
|
defaultMessage: 'Remove friend',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
id: 'friends.section.heading',
|
||||||
|
defaultMessage: '{title} - {count}',
|
||||||
|
},
|
||||||
|
friendRequestSent: {
|
||||||
|
id: 'friends.friend.request-sent',
|
||||||
|
defaultMessage: 'Friend request sent',
|
||||||
|
},
|
||||||
|
cancelRequest: {
|
||||||
|
id: 'friends.friend.cancel-request',
|
||||||
|
defaultMessage: 'Cancel request',
|
||||||
|
},
|
||||||
|
viewProfile: {
|
||||||
|
id: 'friends.friend.view-profile',
|
||||||
|
defaultMessage: 'View profile',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
|
||||||
|
<template #view-profile>
|
||||||
|
<UserIcon />
|
||||||
|
{{ formatMessage(messages.viewProfile) }}
|
||||||
|
</template>
|
||||||
|
<template #remove-friend> <TrashIcon /> {{ formatMessage(messages.removeFriend) }} </template>
|
||||||
|
<template #cancel-request> <XIcon /> {{ formatMessage(messages.cancelRequest) }} </template>
|
||||||
|
</ContextMenu>
|
||||||
|
<Accordion
|
||||||
|
:open-by-default="openByDefault"
|
||||||
|
:force-open="isSearching"
|
||||||
|
:button-class="
|
||||||
|
'pl-4 pr-3 flex w-full items-center bg-transparent border-0 p-0' +
|
||||||
|
(isSearching
|
||||||
|
? ''
|
||||||
|
: ' cursor-pointer hover:brightness-[--hover-brightness] active:scale-[0.98] transition-all')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<h3 class="text-base text-primary font-medium m-0">
|
||||||
|
{{ formatMessage(messages.heading, { title: heading, count: friends.length }) }}
|
||||||
|
</h3>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<div class="pt-3 flex flex-col gap-1">
|
||||||
|
<div
|
||||||
|
v-for="friend in friends"
|
||||||
|
:key="friend.username"
|
||||||
|
class="group grid items-center grid-cols-[auto_1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full ml-4 mr-1"
|
||||||
|
@contextmenu.prevent.stop="
|
||||||
|
(event) => friendOptions?.showMenu(event, friend, createContextMenuOptions(friend))
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<Avatar
|
||||||
|
:src="friend.avatar"
|
||||||
|
:class="{ grayscale: !friend.online && friend.accepted }"
|
||||||
|
class="w-12 h-12 rounded-full"
|
||||||
|
size="32px"
|
||||||
|
circle
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="friend.online"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="bottom-[2px] right-[-2px] absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span
|
||||||
|
class="text-sm m-0"
|
||||||
|
:class="friend.online || !friend.accepted ? 'text-contrast' : 'text-primary'"
|
||||||
|
>
|
||||||
|
{{ friend.username }}
|
||||||
|
</span>
|
||||||
|
<span v-if="!friend.accepted" class="m-0 text-xs">
|
||||||
|
{{ formatMessage(messages.friendRequestSent) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
||||||
|
</div>
|
||||||
|
<ButtonStyled v-if="friend.accepted" circular type="transparent">
|
||||||
|
<OverflowMenu
|
||||||
|
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'view-profile',
|
||||||
|
action: () => openProfile(friend.username),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remove-friend',
|
||||||
|
action: () => removeFriend(friend),
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon />
|
||||||
|
<template #view-profile>
|
||||||
|
<UserIcon />
|
||||||
|
{{ formatMessage(messages.viewProfile) }}
|
||||||
|
</template>
|
||||||
|
<template #remove-friend>
|
||||||
|
<TrashIcon />
|
||||||
|
{{ formatMessage(messages.removeFriend) }}
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled v-else type="transparent" circular>
|
||||||
|
<button v-tooltip="formatMessage(messages.cancelRequest)" @click="removeFriend(friend)">
|
||||||
|
<XIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Accordion>
|
||||||
|
</template>
|
||||||
@@ -1,71 +1,73 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
|
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>
|
<p>
|
||||||
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
|
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
|
||||||
you're trying to install it on. Are you sure you want to continue? Dependencies will not be
|
you're trying to install it on. Are you sure you want to continue? Dependencies will not be
|
||||||
installed.
|
installed.
|
||||||
</p>
|
</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="header">
|
<tr class="header">
|
||||||
<th>{{ instance?.name }}</th>
|
<th>{{ instance?.name }}</th>
|
||||||
<th>{{ project.title }}</th>
|
<th>{{ project.title }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="content">
|
<tr class="content">
|
||||||
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
|
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
|
||||||
<td>
|
<td>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-if="versions?.length > 1"
|
v-if="versions?.length > 1"
|
||||||
v-model="selectedVersion"
|
v-model="selectedVersion"
|
||||||
:options="versions"
|
:options="versions"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
placeholder="Select version"
|
placeholder="Select version"
|
||||||
open-direction="top"
|
open-direction="top"
|
||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
:custom-label="
|
:custom-label="
|
||||||
(version) =>
|
(version) =>
|
||||||
`${version?.name} (${version?.loaders
|
`${version?.name} (${version?.loaders
|
||||||
.map((name) => formatCategory(name))
|
.map((name) => formatCategory(name))
|
||||||
.join(', ')} - ${version?.game_versions.join(', ')})`
|
.join(', ')} - ${version?.game_versions.join(', ')})`
|
||||||
"
|
"
|
||||||
:max-height="150"
|
:max-height="150"
|
||||||
/>
|
/>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<span>
|
<span>
|
||||||
{{ selectedVersion?.name }} ({{
|
{{ selectedVersion?.name }} ({{
|
||||||
selectedVersion?.loaders.map((name) => formatCategory(name)).join(', ')
|
selectedVersion?.loaders.map((name) => formatCategory(name)).join(', ')
|
||||||
}}
|
}}
|
||||||
- {{ selectedVersion?.game_versions.join(', ') }})
|
- {{ selectedVersion?.game_versions.join(', ') }})
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<Button @click="() => incompatibleModal.hide()"><XIcon />Cancel</Button>
|
<Button @click="() => incompatibleModal.hide()"><XIcon />Cancel</Button>
|
||||||
<Button color="primary" :disabled="installing" @click="install()">
|
<Button color="primary" :disabled="installing" @click="install()">
|
||||||
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
|
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||||
import { XIcon, DownloadIcon } from '@modrinth/assets'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { Button } from '@modrinth/ui'
|
|
||||||
import { formatCategory } from '@modrinth/utils'
|
import { formatCategory } from '@modrinth/utils'
|
||||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { add_project_from_version as installMod } from '@/helpers/profile'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const instance = ref(null)
|
const instance = ref(null)
|
||||||
const project = ref(null)
|
const project = ref(null)
|
||||||
const versions = ref(null)
|
const versions = ref(null)
|
||||||
@@ -76,91 +78,91 @@ const installing = ref(false)
|
|||||||
const onInstall = ref(() => {})
|
const onInstall = ref(() => {})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (instanceVal, projectVal, projectVersions, callback) => {
|
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
|
||||||
instance.value = instanceVal
|
instance.value = instanceVal
|
||||||
versions.value = projectVersions
|
versions.value = projectVersions
|
||||||
selectedVersion.value = projectVersions[0]
|
selectedVersion.value = selected ?? projectVersions[0]
|
||||||
|
|
||||||
project.value = projectVal
|
project.value = projectVal
|
||||||
|
|
||||||
onInstall.value = callback
|
onInstall.value = callback
|
||||||
installing.value = false
|
installing.value = false
|
||||||
|
|
||||||
incompatibleModal.value.show()
|
incompatibleModal.value.show()
|
||||||
|
|
||||||
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
|
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const install = async () => {
|
const install = async () => {
|
||||||
installing.value = true
|
installing.value = true
|
||||||
await installMod(instance.value.path, selectedVersion.value.id).catch(handleError)
|
await installMod(instance.value.path, selectedVersion.value.id).catch(handleError)
|
||||||
installing.value = false
|
installing.value = false
|
||||||
onInstall.value(selectedVersion.value.id)
|
onInstall.value(selectedVersion.value.id)
|
||||||
incompatibleModal.value.hide()
|
incompatibleModal.value.hide()
|
||||||
|
|
||||||
trackEvent('ProjectInstall', {
|
trackEvent('ProjectInstall', {
|
||||||
loader: instance.value.loader,
|
loader: instance.value.loader,
|
||||||
game_version: instance.value.game_version,
|
game_version: instance.value.game_version,
|
||||||
id: project.value,
|
id: project.value,
|
||||||
version_id: selectedVersion.value.id,
|
version_id: selectedVersion.value.id,
|
||||||
project_type: project.value.project_type,
|
project_type: project.value.project_type,
|
||||||
title: project.value.title,
|
title: project.value.title,
|
||||||
source: 'ProjectIncompatibilityWarningModal',
|
source: 'ProjectIncompatibilityWarningModal',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.data {
|
.data {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
box-shadow: 0 0 0 1px var(--color-button-bg);
|
box-shadow: 0 0 0 1px var(--color-button-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-bottom: 1px solid var(--color-button-bg);
|
border-bottom: 1px solid var(--color-button-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
th:first-child {
|
th:first-child {
|
||||||
border-top-left-radius: var(--radius-lg);
|
border-top-left-radius: var(--radius-lg);
|
||||||
border-right: 1px solid var(--color-button-bg);
|
border-right: 1px solid var(--color-button-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
th:last-child {
|
th:last-child {
|
||||||
border-top-right-radius: var(--radius-lg);
|
border-top-right-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
td:first-child {
|
td:first-child {
|
||||||
border-right: 1px solid var(--color-button-bg);
|
border-right: 1px solid var(--color-button-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
:deep(.animated-dropdown .options) {
|
:deep(.animated-dropdown .options) {
|
||||||
max-height: 13.375rem;
|
max-height: 13.375rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { create_profile_and_install as pack_install } from '@/helpers/pack'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { create_profile_and_install as pack_install } from '@/helpers/pack'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const versionId = ref()
|
const versionId = ref()
|
||||||
const project = ref()
|
const project = ref()
|
||||||
@@ -16,60 +18,60 @@ const onInstall = ref(() => {})
|
|||||||
const onCreateInstance = ref(() => {})
|
const onCreateInstance = ref(() => {})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
|
show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
|
||||||
project.value = projectVal
|
project.value = projectVal
|
||||||
versionId.value = versionIdVal
|
versionId.value = versionIdVal
|
||||||
installing.value = false
|
installing.value = false
|
||||||
confirmModal.value.show()
|
confirmModal.value.show()
|
||||||
|
|
||||||
onInstall.value = callback
|
onInstall.value = callback
|
||||||
onCreateInstance.value = createInstanceCallback
|
onCreateInstance.value = createInstanceCallback
|
||||||
|
|
||||||
trackEvent('PackInstallStart')
|
trackEvent('PackInstallStart')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
installing.value = true
|
installing.value = true
|
||||||
confirmModal.value.hide()
|
confirmModal.value.hide()
|
||||||
|
|
||||||
await pack_install(
|
await pack_install(
|
||||||
project.value.id,
|
project.value.id,
|
||||||
versionId.value,
|
versionId.value,
|
||||||
project.value.title,
|
project.value.title,
|
||||||
project.value.icon_url,
|
project.value.icon_url,
|
||||||
onCreateInstance.value,
|
onCreateInstance.value,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
trackEvent('PackInstall', {
|
trackEvent('PackInstall', {
|
||||||
id: project.value.id,
|
id: project.value.id,
|
||||||
version_id: versionId.value,
|
version_id: versionId.value,
|
||||||
title: project.value.title,
|
title: project.value.title,
|
||||||
source: 'ConfirmModal',
|
source: 'ConfirmModal',
|
||||||
})
|
})
|
||||||
|
|
||||||
onInstall.value(versionId.value)
|
onInstall.value(versionId.value)
|
||||||
installing.value = false
|
installing.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall">
|
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
|
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
|
||||||
<div class="input-group push-right">
|
<div class="input-group push-right">
|
||||||
<Button @click="() => $refs.confirmModal.hide()"><XIcon />Cancel</Button>
|
<Button @click="() => $refs.confirmModal.hide()"><XIcon />Cancel</Button>
|
||||||
<Button color="primary" :disabled="installing" @click="install()"
|
<Button color="primary" :disabled="installing" @click="install()"
|
||||||
><DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}</Button
|
><DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,29 +1,34 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
CheckIcon,
|
||||||
PlusIcon,
|
DownloadIcon,
|
||||||
UploadIcon,
|
PlusIcon,
|
||||||
XIcon,
|
RightArrowIcon,
|
||||||
RightArrowIcon,
|
UploadIcon,
|
||||||
CheckIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import {
|
|
||||||
add_project_from_version as installMod,
|
|
||||||
check_installed,
|
|
||||||
get,
|
|
||||||
list,
|
|
||||||
create,
|
|
||||||
} from '@/helpers/profile'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { installVersionDependencies } from '@/store/install.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import {
|
||||||
|
add_project_from_version as installMod,
|
||||||
|
check_installed,
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
list,
|
||||||
|
} from '@/helpers/profile'
|
||||||
|
import {
|
||||||
|
findPreferredVersion,
|
||||||
|
installVersionDependencies,
|
||||||
|
isVersionCompatible,
|
||||||
|
} from '@/store/install.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const versions = ref()
|
const versions = ref()
|
||||||
@@ -43,361 +48,351 @@ const creatingInstance = ref(false)
|
|||||||
const profiles = ref([])
|
const profiles = ref([])
|
||||||
|
|
||||||
const shownProfiles = computed(() =>
|
const shownProfiles = computed(() =>
|
||||||
profiles.value
|
profiles.value
|
||||||
.filter((profile) => {
|
.filter((profile) => {
|
||||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||||
})
|
})
|
||||||
.filter((profile) => {
|
.filter((profile) => {
|
||||||
const loaders = versions.value.flatMap((v) => v.loaders)
|
const version = {
|
||||||
|
game_versions: versions.value.flatMap((v) => v.game_versions),
|
||||||
return (
|
loaders: versions.value.flatMap((v) => v.loaders),
|
||||||
versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) &&
|
}
|
||||||
(project.value.project_type === 'mod'
|
return isVersionCompatible(version, project.value, profile)
|
||||||
? loaders.includes(profile.loader) || loaders.includes('minecraft')
|
}),
|
||||||
: true)
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onInstall = ref(() => {})
|
const onInstall = ref(() => {})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: async (projectVal, versionsVal, callback) => {
|
show: async (projectVal, versionsVal, callback) => {
|
||||||
project.value = projectVal
|
project.value = projectVal
|
||||||
versions.value = versionsVal
|
versions.value = versionsVal
|
||||||
searchFilter.value = ''
|
searchFilter.value = ''
|
||||||
|
|
||||||
showCreation.value = false
|
showCreation.value = false
|
||||||
name.value = null
|
name.value = null
|
||||||
icon.value = null
|
icon.value = null
|
||||||
display_icon.value = null
|
display_icon.value = null
|
||||||
gameVersion.value = null
|
gameVersion.value = null
|
||||||
loader.value = null
|
loader.value = null
|
||||||
|
|
||||||
onInstall.value = callback
|
onInstall.value = callback
|
||||||
|
|
||||||
const profilesVal = await list().catch(handleError)
|
const profilesVal = await list().catch(handleError)
|
||||||
for (const profile of profilesVal) {
|
for (const profile of profilesVal) {
|
||||||
profile.installing = false
|
profile.installing = false
|
||||||
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
|
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
|
||||||
handleError,
|
handleError,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
profiles.value = profilesVal
|
profiles.value = profilesVal
|
||||||
|
|
||||||
installModal.value.show()
|
installModal.value.show()
|
||||||
|
|
||||||
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
|
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function install(instance) {
|
async function install(instance) {
|
||||||
instance.installing = true
|
instance.installing = true
|
||||||
const version = versions.value.find((v) => {
|
const version = findPreferredVersion(versions.value, project.value, instance)
|
||||||
return (
|
|
||||||
v.game_versions.includes(instance.game_version) &&
|
|
||||||
(project.value.project_type === 'mod'
|
|
||||||
? v.loaders.includes(instance.loader) || v.loaders.includes('minecraft')
|
|
||||||
: true)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!version) {
|
if (!version) {
|
||||||
instance.installing = false
|
instance.installing = false
|
||||||
handleError('No compatible version found')
|
handleError('No compatible version found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await installMod(instance.path, version.id).catch(handleError)
|
await installMod(instance.path, version.id).catch(handleError)
|
||||||
await installVersionDependencies(instance, version)
|
await installVersionDependencies(instance, version).catch(handleError)
|
||||||
|
|
||||||
instance.installedMod = true
|
instance.installedMod = true
|
||||||
instance.installing = false
|
instance.installing = false
|
||||||
|
|
||||||
trackEvent('ProjectInstall', {
|
trackEvent('ProjectInstall', {
|
||||||
loader: instance.loader,
|
loader: instance.loader,
|
||||||
game_version: instance.game_version,
|
game_version: instance.game_version,
|
||||||
id: project.value.id,
|
id: project.value.id,
|
||||||
version_id: version.id,
|
version_id: version.id,
|
||||||
project_type: project.value.project_type,
|
project_type: project.value.project_type,
|
||||||
title: project.value.title,
|
title: project.value.title,
|
||||||
source: 'ProjectInstallModal',
|
source: 'ProjectInstallModal',
|
||||||
})
|
})
|
||||||
|
|
||||||
onInstall.value(version.id)
|
onInstall.value(version.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleCreation = () => {
|
const toggleCreation = () => {
|
||||||
showCreation.value = !showCreation.value
|
showCreation.value = !showCreation.value
|
||||||
name.value = null
|
name.value = null
|
||||||
icon.value = null
|
icon.value = null
|
||||||
display_icon.value = null
|
display_icon.value = null
|
||||||
gameVersion.value = null
|
gameVersion.value = null
|
||||||
loader.value = null
|
loader.value = null
|
||||||
|
|
||||||
if (showCreation.value) {
|
if (showCreation.value) {
|
||||||
trackEvent('InstanceCreateStart', { source: 'ProjectInstallModal' })
|
trackEvent('InstanceCreateStart', { source: 'ProjectInstallModal' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const upload_icon = async () => {
|
const upload_icon = async () => {
|
||||||
const res = await open({
|
const res = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Image',
|
name: 'Image',
|
||||||
extensions: ['png', 'jpeg'],
|
extensions: ['png', 'jpeg'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
icon.value = res.path ?? res
|
icon.value = res.path ?? res
|
||||||
|
|
||||||
if (!icon.value) return
|
if (!icon.value) return
|
||||||
display_icon.value = convertFileSrc(icon.value)
|
display_icon.value = convertFileSrc(icon.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const reset_icon = () => {
|
const reset_icon = () => {
|
||||||
icon.value = null
|
icon.value = null
|
||||||
display_icon.value = null
|
display_icon.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const createInstance = async () => {
|
const createInstance = async () => {
|
||||||
creatingInstance.value = true
|
creatingInstance.value = true
|
||||||
|
|
||||||
const loader =
|
const loader =
|
||||||
versions.value[0].loaders[0] !== 'forge' &&
|
versions.value[0].loaders[0] !== 'forge' &&
|
||||||
versions.value[0].loaders[0] !== 'fabric' &&
|
versions.value[0].loaders[0] !== 'fabric' &&
|
||||||
versions.value[0].loaders[0] !== 'quilt'
|
versions.value[0].loaders[0] !== 'quilt'
|
||||||
? 'vanilla'
|
? 'vanilla'
|
||||||
: versions.value[0].loaders[0]
|
: versions.value[0].loaders[0]
|
||||||
|
|
||||||
const id = await create(
|
const id = await create(
|
||||||
name.value,
|
name.value,
|
||||||
versions.value[0].game_versions[0],
|
versions.value[0].game_versions[0],
|
||||||
loader,
|
loader,
|
||||||
'latest',
|
'latest',
|
||||||
icon.value,
|
icon.value,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
|
|
||||||
await installMod(id, versions.value[0].id).catch(handleError)
|
await installMod(id, versions.value[0].id).catch(handleError)
|
||||||
|
|
||||||
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||||
|
|
||||||
const instance = await get(id, true)
|
const instance = await get(id, true)
|
||||||
await installVersionDependencies(instance, versions.value[0])
|
await installVersionDependencies(instance, versions.value[0]).catch(handleError)
|
||||||
|
|
||||||
trackEvent('InstanceCreate', {
|
trackEvent('InstanceCreate', {
|
||||||
profile_name: name.value,
|
profile_name: name.value,
|
||||||
game_version: versions.value[0].game_versions[0],
|
game_version: versions.value[0].game_versions[0],
|
||||||
loader: loader,
|
loader: loader,
|
||||||
loader_version: 'latest',
|
loader_version: 'latest',
|
||||||
has_icon: !!icon.value,
|
has_icon: !!icon.value,
|
||||||
source: 'ProjectInstallModal',
|
source: 'ProjectInstallModal',
|
||||||
})
|
})
|
||||||
|
|
||||||
trackEvent('ProjectInstall', {
|
trackEvent('ProjectInstall', {
|
||||||
loader: loader,
|
loader: loader,
|
||||||
game_version: versions.value[0].game_versions[0],
|
game_version: versions.value[0].game_versions[0],
|
||||||
id: project.value,
|
id: project.value,
|
||||||
version_id: versions.value[0].id,
|
version_id: versions.value[0].id,
|
||||||
project_type: project.value.project_type,
|
project_type: project.value.project_type,
|
||||||
title: project.value.title,
|
title: project.value.title,
|
||||||
source: 'ProjectInstallModal',
|
source: 'ProjectInstallModal',
|
||||||
})
|
})
|
||||||
|
|
||||||
onInstall.value(versions.value[0].id)
|
onInstall.value(versions.value[0].id)
|
||||||
|
|
||||||
if (installModal.value) installModal.value.hide()
|
if (installModal.value) installModal.value.hide()
|
||||||
creatingInstance.value = false
|
creatingInstance.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall">
|
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input
|
<input
|
||||||
v-model="searchFilter"
|
v-model="searchFilter"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
class="search"
|
class="search"
|
||||||
placeholder="Search for an instance"
|
placeholder="Search for an instance"
|
||||||
/>
|
/>
|
||||||
<div class="profiles" :class="{ 'hide-creation': !showCreation }">
|
<div class="profiles" :class="{ 'hide-creation': !showCreation }">
|
||||||
<div v-for="profile in shownProfiles" :key="profile.name" class="option">
|
<div v-for="profile in shownProfiles" :key="profile.name" class="option">
|
||||||
<router-link
|
<router-link
|
||||||
class="btn btn-transparent profile-button"
|
class="btn btn-transparent profile-button"
|
||||||
:to="`/instance/${encodeURIComponent(profile.path)}`"
|
:to="`/instance/${encodeURIComponent(profile.path)}`"
|
||||||
@click="installModal.hide()"
|
@click="installModal.hide()"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
|
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
|
||||||
class="profile-image"
|
class="profile-image"
|
||||||
/>
|
/>
|
||||||
{{ profile.name }}
|
{{ profile.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div
|
<div
|
||||||
v-tooltip="
|
v-tooltip="
|
||||||
profile.linked_data?.locked && !profile.installedMod
|
profile.linked_data?.locked && !profile.installedMod
|
||||||
? 'Unpair or unlock an instance to add mods.'
|
? 'Unpair or unlock an instance to add mods.'
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
:disabled="profile.installedMod || profile.installing"
|
:disabled="profile.installedMod || profile.installing"
|
||||||
@click="install(profile)"
|
@click="install(profile)"
|
||||||
>
|
>
|
||||||
<DownloadIcon v-if="!profile.installedMod && !profile.installing" />
|
<DownloadIcon v-if="!profile.installedMod && !profile.installing" />
|
||||||
<CheckIcon v-else-if="profile.installedMod" />
|
<CheckIcon v-else-if="profile.installedMod" />
|
||||||
{{
|
{{
|
||||||
profile.installing
|
profile.installing
|
||||||
? 'Installing...'
|
? 'Installing...'
|
||||||
: profile.installedMod
|
: profile.installedMod
|
||||||
? 'Installed'
|
? 'Installed'
|
||||||
: 'Install'
|
: 'Install'
|
||||||
}}
|
}}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card v-if="showCreation" class="creation-card">
|
<Card v-if="showCreation" class="creation-card">
|
||||||
<div class="creation-container">
|
<div class="creation-container">
|
||||||
<div class="creation-icon">
|
<div class="creation-icon">
|
||||||
<Avatar size="md" class="icon" :src="display_icon" />
|
<Avatar size="md" class="icon" :src="display_icon" />
|
||||||
<div class="creation-icon__description">
|
<div class="creation-icon__description">
|
||||||
<Button @click="upload_icon()">
|
<Button @click="upload_icon()">
|
||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
<span class="no-wrap"> Select icon </span>
|
<span class="no-wrap"> Select icon </span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button :disabled="!display_icon" @click="reset_icon()">
|
<Button :disabled="!display_icon" @click="reset_icon()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span class="no-wrap"> Remove icon </span>
|
<span class="no-wrap"> Remove icon </span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="creation-settings">
|
<div class="creation-settings">
|
||||||
<input
|
<input
|
||||||
v-model="name"
|
v-model="name"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
class="creation-input"
|
class="creation-input"
|
||||||
/>
|
/>
|
||||||
<Button :disabled="creatingInstance === true || !name" @click="createInstance()">
|
<Button :disabled="creatingInstance === true || !name" @click="createInstance()">
|
||||||
<RightArrowIcon />
|
<RightArrowIcon />
|
||||||
{{ creatingInstance ? 'Creating...' : 'Create' }}
|
{{ creatingInstance ? 'Creating...' : 'Create' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div class="input-group push-right">
|
<div class="input-group push-right">
|
||||||
<Button :color="showCreation ? '' : 'primary'" @click="toggleCreation()">
|
<Button :color="showCreation ? '' : 'primary'" @click="toggleCreation()">
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
{{ showCreation ? 'Hide New Instance' : 'Create new instance' }}
|
{{ showCreation ? 'Hide New Instance' : 'Create new instance' }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="installModal.hide()">Cancel</Button>
|
<Button @click="installModal.hide()">Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.creation-card {
|
.creation-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-container {
|
.creation-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-icon {
|
.creation-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
.creation-icon__description {
|
.creation-icon__description {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-input {
|
.creation-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-wrap {
|
.no-wrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-dropdown {
|
.creation-dropdown {
|
||||||
width: min-content !important;
|
width: min-content !important;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creation-settings {
|
.creation-settings {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profiles {
|
.profiles {
|
||||||
max-height: 12rem;
|
max-height: 12rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
&.hide-creation {
|
&.hide-creation {
|
||||||
max-height: 21rem;
|
max-height: 21rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
width: calc(100%);
|
width: calc(100%);
|
||||||
background: var(--color-raised-bg);
|
background: var(--color-raised-bg);
|
||||||
color: var(--color-base);
|
color: var(--color-base);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-button {
|
.profile-button {
|
||||||
align-content: start;
|
align-content: start;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-image {
|
.profile-image {
|
||||||
--size: 2rem !important;
|
--size: 2rem !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { CopyIcon, EditIcon, PlusIcon, SpinnerIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
Checkbox,
|
||||||
|
injectNotificationManager,
|
||||||
|
OverflowMenu,
|
||||||
|
} from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { SpinnerIcon, TrashIcon, UploadIcon, PlusIcon, EditIcon, CopyIcon } from '@modrinth/assets'
|
|
||||||
import { Avatar, ButtonStyled, OverflowMenu, Checkbox } from '@modrinth/ui'
|
|
||||||
import { computed, ref, type Ref, watch } from 'vue'
|
|
||||||
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { computed, type Ref, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
|
||||||
import type { InstanceSettingsTabProps, GameInstance } from '../../../helpers/types'
|
|
||||||
|
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
||||||
|
|
||||||
|
import type { GameInstance, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -28,299 +36,299 @@ const newCategoryInput = ref('')
|
|||||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||||
|
|
||||||
async function duplicateProfile() {
|
async function duplicateProfile() {
|
||||||
await duplicate(props.instance.path).catch(handleError)
|
await duplicate(props.instance.path).catch(handleError)
|
||||||
trackEvent('InstanceDuplicate', {
|
trackEvent('InstanceDuplicate', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const allInstances = ref((await list()) as GameInstance[])
|
const allInstances = ref((await list()) as GameInstance[])
|
||||||
const availableGroups = computed(() => [
|
const availableGroups = computed(() => [
|
||||||
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
|
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
|
||||||
])
|
])
|
||||||
|
|
||||||
async function resetIcon() {
|
async function resetIcon() {
|
||||||
icon.value = undefined
|
icon.value = undefined
|
||||||
await edit_icon(props.instance.path, null).catch(handleError)
|
await edit_icon(props.instance.path, null).catch(handleError)
|
||||||
trackEvent('InstanceRemoveIcon')
|
trackEvent('InstanceRemoveIcon')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setIcon() {
|
async function setIcon() {
|
||||||
const value = await open({
|
const value = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Image',
|
name: 'Image',
|
||||||
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
extensions: ['png', 'jpeg', 'svg', 'webp', 'gif', 'jpg'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!value) return
|
if (!value) return
|
||||||
|
|
||||||
icon.value = value
|
icon.value = value
|
||||||
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
||||||
|
|
||||||
trackEvent('InstanceSetIcon')
|
trackEvent('InstanceSetIcon')
|
||||||
}
|
}
|
||||||
|
|
||||||
const editProfileObject = computed(() => ({
|
const editProfileObject = computed(() => ({
|
||||||
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
||||||
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const toggleGroup = (group: string) => {
|
const toggleGroup = (group: string) => {
|
||||||
if (groups.value.includes(group)) {
|
if (groups.value.includes(group)) {
|
||||||
groups.value = groups.value.filter((x) => x !== group)
|
groups.value = groups.value.filter((x) => x !== group)
|
||||||
} else {
|
} else {
|
||||||
groups.value.push(group)
|
groups.value.push(group)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addCategory = () => {
|
const addCategory = () => {
|
||||||
const text = newCategoryInput.value.trim()
|
const text = newCategoryInput.value.trim()
|
||||||
|
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
groups.value.push(text.substring(0, 32))
|
groups.value.push(text.substring(0, 32))
|
||||||
newCategoryInput.value = ''
|
newCategoryInput.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[title, groups, groups],
|
[title, groups, groups],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const removing = ref(false)
|
const removing = ref(false)
|
||||||
async function removeProfile() {
|
async function removeProfile() {
|
||||||
removing.value = true
|
removing.value = true
|
||||||
await remove(props.instance.path).catch(handleError)
|
await remove(props.instance.path).catch(handleError)
|
||||||
removing.value = false
|
removing.value = false
|
||||||
|
|
||||||
trackEvent('InstanceRemove', {
|
trackEvent('InstanceRemove', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
})
|
})
|
||||||
|
|
||||||
await router.push({ path: '/' })
|
await router.push({ path: '/' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
name: {
|
name: {
|
||||||
id: 'instance.settings.tabs.general.name',
|
id: 'instance.settings.tabs.general.name',
|
||||||
defaultMessage: 'Name',
|
defaultMessage: 'Name',
|
||||||
},
|
},
|
||||||
libraryGroups: {
|
libraryGroups: {
|
||||||
id: 'instance.settings.tabs.general.library-groups',
|
id: 'instance.settings.tabs.general.library-groups',
|
||||||
defaultMessage: 'Library groups',
|
defaultMessage: 'Library groups',
|
||||||
},
|
},
|
||||||
libraryGroupsDescription: {
|
libraryGroupsDescription: {
|
||||||
id: 'instance.settings.tabs.general.library-groups.description',
|
id: 'instance.settings.tabs.general.library-groups.description',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Library groups allow you to organize your instances into different sections in your library.',
|
'Library groups allow you to organize your instances into different sections in your library.',
|
||||||
},
|
},
|
||||||
libraryGroupsEnterName: {
|
libraryGroupsEnterName: {
|
||||||
id: 'instance.settings.tabs.general.library-groups.enter-name',
|
id: 'instance.settings.tabs.general.library-groups.enter-name',
|
||||||
defaultMessage: 'Enter group name',
|
defaultMessage: 'Enter group name',
|
||||||
},
|
},
|
||||||
libraryGroupsCreate: {
|
libraryGroupsCreate: {
|
||||||
id: 'instance.settings.tabs.general.library-groups.create',
|
id: 'instance.settings.tabs.general.library-groups.create',
|
||||||
defaultMessage: 'Create new group',
|
defaultMessage: 'Create new group',
|
||||||
},
|
},
|
||||||
editIcon: {
|
editIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon',
|
id: 'instance.settings.tabs.general.edit-icon',
|
||||||
defaultMessage: 'Edit icon',
|
defaultMessage: 'Edit icon',
|
||||||
},
|
},
|
||||||
selectIcon: {
|
selectIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon.select',
|
id: 'instance.settings.tabs.general.edit-icon.select',
|
||||||
defaultMessage: 'Select icon',
|
defaultMessage: 'Select icon',
|
||||||
},
|
},
|
||||||
replaceIcon: {
|
replaceIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon.replace',
|
id: 'instance.settings.tabs.general.edit-icon.replace',
|
||||||
defaultMessage: 'Replace icon',
|
defaultMessage: 'Replace icon',
|
||||||
},
|
},
|
||||||
removeIcon: {
|
removeIcon: {
|
||||||
id: 'instance.settings.tabs.general.edit-icon.remove',
|
id: 'instance.settings.tabs.general.edit-icon.remove',
|
||||||
defaultMessage: 'Remove icon',
|
defaultMessage: 'Remove icon',
|
||||||
},
|
},
|
||||||
duplicateInstance: {
|
duplicateInstance: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-instance',
|
id: 'instance.settings.tabs.general.duplicate-instance',
|
||||||
defaultMessage: 'Duplicate instance',
|
defaultMessage: 'Duplicate instance',
|
||||||
},
|
},
|
||||||
duplicateInstanceDescription: {
|
duplicateInstanceDescription: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-instance.description',
|
id: 'instance.settings.tabs.general.duplicate-instance.description',
|
||||||
defaultMessage: 'Creates a copy of this instance, including worlds, configs, mods, etc.',
|
defaultMessage: 'Creates a copy of this instance, including worlds, configs, mods, etc.',
|
||||||
},
|
},
|
||||||
duplicateButtonTooltipInstalling: {
|
duplicateButtonTooltipInstalling: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-button.tooltip.installing',
|
id: 'instance.settings.tabs.general.duplicate-button.tooltip.installing',
|
||||||
defaultMessage: 'Cannot duplicate while installing.',
|
defaultMessage: 'Cannot duplicate while installing.',
|
||||||
},
|
},
|
||||||
duplicateButton: {
|
duplicateButton: {
|
||||||
id: 'instance.settings.tabs.general.duplicate-button',
|
id: 'instance.settings.tabs.general.duplicate-button',
|
||||||
defaultMessage: 'Duplicate',
|
defaultMessage: 'Duplicate',
|
||||||
},
|
},
|
||||||
deleteInstance: {
|
deleteInstance: {
|
||||||
id: 'instance.settings.tabs.general.delete',
|
id: 'instance.settings.tabs.general.delete',
|
||||||
defaultMessage: 'Delete instance',
|
defaultMessage: 'Delete instance',
|
||||||
},
|
},
|
||||||
deleteInstanceDescription: {
|
deleteInstanceDescription: {
|
||||||
id: 'instance.settings.tabs.general.delete.description',
|
id: 'instance.settings.tabs.general.delete.description',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it.',
|
'Permanently deletes an instance from your device, including your worlds, configs, and all installed content. Be careful, as once you delete a instance there is no way to recover it.',
|
||||||
},
|
},
|
||||||
deleteInstanceButton: {
|
deleteInstanceButton: {
|
||||||
id: 'instance.settings.tabs.general.delete.button',
|
id: 'instance.settings.tabs.general.delete.button',
|
||||||
defaultMessage: 'Delete instance',
|
defaultMessage: 'Delete instance',
|
||||||
},
|
},
|
||||||
deletingInstanceButton: {
|
deletingInstanceButton: {
|
||||||
id: 'instance.settings.tabs.general.deleting.button',
|
id: 'instance.settings.tabs.general.deleting.button',
|
||||||
defaultMessage: 'Deleting...',
|
defaultMessage: 'Deleting...',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="deleteConfirmModal"
|
ref="deleteConfirmModal"
|
||||||
title="Are you sure you want to delete this instance?"
|
title="Are you sure you want to delete this instance?"
|
||||||
description="If you proceed, all data for your instance will be permanently erased, including your worlds. You will not be able to recover it."
|
description="If you proceed, all data for your instance will be permanently erased, including your worlds. You will not be able to recover it."
|
||||||
:has-to-type="false"
|
:has-to-type="false"
|
||||||
proceed-label="Delete"
|
proceed-label="Delete"
|
||||||
:show-ad-on-close="false"
|
:show-ad-on-close="false"
|
||||||
@proceed="removeProfile"
|
@proceed="removeProfile"
|
||||||
/>
|
/>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="float-end ml-4 relative group">
|
<div class="float-end ml-4 relative group">
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
v-tooltip="formatMessage(messages.editIcon)"
|
v-tooltip="formatMessage(messages.editIcon)"
|
||||||
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
id: 'select',
|
id: 'select',
|
||||||
action: () => setIcon(),
|
action: () => setIcon(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'remove',
|
id: 'remove',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
action: () => resetIcon(),
|
action: () => resetIcon(),
|
||||||
shown: !!icon,
|
shown: !!icon,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="icon ? convertFileSrc(icon) : icon"
|
:src="icon ? convertFileSrc(icon) : icon"
|
||||||
size="108px"
|
size="108px"
|
||||||
class="!border-4 group-hover:brightness-75"
|
class="!border-4 group-hover:brightness-75"
|
||||||
:tint-by="props.instance.path"
|
:tint-by="props.instance.path"
|
||||||
no-shadow
|
no-shadow
|
||||||
/>
|
/>
|
||||||
<div class="absolute top-0 right-0 m-2">
|
<div class="absolute top-0 right-0 m-2">
|
||||||
<div
|
<div
|
||||||
class="p-2 m-0 text-primary flex items-center justify-center aspect-square bg-button-bg rounded-full border-button-border border-solid border-[1px] hovering-icon-shadow"
|
class="p-2 m-0 text-primary flex items-center justify-center aspect-square bg-button-bg rounded-full border-button-border border-solid border-[1px] hovering-icon-shadow"
|
||||||
>
|
>
|
||||||
<EditIcon aria-hidden="true" class="h-4 w-4 text-primary" />
|
<EditIcon aria-hidden="true" class="h-4 w-4 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #select>
|
<template #select>
|
||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
||||||
</template>
|
</template>
|
||||||
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</div>
|
</div>
|
||||||
<label for="instance-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
<label for="instance-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.name) }}
|
{{ formatMessage(messages.name) }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<input
|
<input
|
||||||
id="instance-name"
|
id="instance-name"
|
||||||
v-model="title"
|
v-model="title"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
maxlength="80"
|
maxlength="80"
|
||||||
class="flex-grow"
|
class="flex-grow"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="instance.install_stage == 'installed'">
|
<template v-if="instance.install_stage == 'installed'">
|
||||||
<div>
|
<div>
|
||||||
<h2
|
<h2
|
||||||
id="duplicate-instance-label"
|
id="duplicate-instance-label"
|
||||||
class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block"
|
class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block"
|
||||||
>
|
>
|
||||||
{{ formatMessage(messages.duplicateInstance) }}
|
{{ formatMessage(messages.duplicateInstance) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0 mb-2">
|
<p class="m-0 mb-2">
|
||||||
{{ formatMessage(messages.duplicateInstanceDescription) }}
|
{{ formatMessage(messages.duplicateInstanceDescription) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button
|
<button
|
||||||
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
||||||
aria-labelledby="duplicate-instance-label"
|
aria-labelledby="duplicate-instance-label"
|
||||||
:disabled="installing"
|
:disabled="installing"
|
||||||
@click="duplicateProfile"
|
@click="duplicateProfile"
|
||||||
>
|
>
|
||||||
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
<h2 class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.libraryGroups) }}
|
{{ formatMessage(messages.libraryGroups) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0 mb-2">
|
<p class="m-0 mb-2">
|
||||||
{{ formatMessage(messages.libraryGroupsDescription) }}
|
{{ formatMessage(messages.libraryGroupsDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-for="group in availableGroups"
|
v-for="group in availableGroups"
|
||||||
:key="group"
|
:key="group"
|
||||||
:model-value="groups.includes(group)"
|
:model-value="groups.includes(group)"
|
||||||
:label="group"
|
:label="group"
|
||||||
@click="toggleGroup(group)"
|
@click="toggleGroup(group)"
|
||||||
/>
|
/>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
v-model="newCategoryInput"
|
v-model="newCategoryInput"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
||||||
@submit="() => addCategory"
|
@submit="() => addCategory"
|
||||||
/>
|
/>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button class="w-fit" @click="() => addCategory()">
|
<button class="w-fit" @click="() => addCategory()">
|
||||||
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 id="delete-instance-label" class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 id="delete-instance-label" class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.deleteInstance) }}
|
{{ formatMessage(messages.deleteInstance) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0 mb-2">
|
<p class="m-0 mb-2">
|
||||||
{{ formatMessage(messages.deleteInstanceDescription) }}
|
{{ formatMessage(messages.deleteInstanceDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<ButtonStyled color="red">
|
<ButtonStyled color="red">
|
||||||
<button
|
<button
|
||||||
aria-labelledby="delete-instance-label"
|
aria-labelledby="delete-instance-label"
|
||||||
:disabled="removing"
|
:disabled="removing"
|
||||||
@click="deleteConfirmModal.show()"
|
@click="deleteConfirmModal.show()"
|
||||||
>
|
>
|
||||||
<SpinnerIcon v-if="removing" class="animate-spin" />
|
<SpinnerIcon v-if="removing" class="animate-spin" />
|
||||||
<TrashIcon v-else />
|
<TrashIcon v-else />
|
||||||
{{
|
{{
|
||||||
removing
|
removing
|
||||||
? formatMessage(messages.deletingInstanceButton)
|
? formatMessage(messages.deletingInstanceButton)
|
||||||
: formatMessage(messages.deleteInstanceButton)
|
: formatMessage(messages.deleteInstanceButton)
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.hovering-icon-shadow {
|
.hovering-icon-shadow {
|
||||||
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
|
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Checkbox } from '@modrinth/ui'
|
import { Checkbox, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { get } from '@/helpers/settings.ts'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { edit } from '@/helpers/profile'
|
|
||||||
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
|
||||||
|
|
||||||
|
import { edit } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
|
import type { AppSettings, Hooks, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
@@ -14,139 +16,139 @@ const props = defineProps<InstanceSettingsTabProps>()
|
|||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
const overrideHooks = ref(
|
const overrideHooks = ref(
|
||||||
!!props.instance.hooks.pre_launch ||
|
!!props.instance.hooks.pre_launch ||
|
||||||
!!props.instance.hooks.wrapper ||
|
!!props.instance.hooks.wrapper ||
|
||||||
!!props.instance.hooks.post_exit,
|
!!props.instance.hooks.post_exit,
|
||||||
)
|
)
|
||||||
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
|
const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
const editProfile: {
|
||||||
hooks?: Hooks
|
hooks?: Hooks
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
// When hooks are not overridden per-instance, we want to clear them
|
// When hooks are not overridden per-instance, we want to clear them
|
||||||
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
||||||
|
|
||||||
return editProfile
|
return editProfile
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[overrideHooks, hooks],
|
[overrideHooks, hooks],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
hooks: {
|
hooks: {
|
||||||
id: 'instance.settings.tabs.hooks.title',
|
id: 'instance.settings.tabs.hooks.title',
|
||||||
defaultMessage: 'Game launch hooks',
|
defaultMessage: 'Game launch hooks',
|
||||||
},
|
},
|
||||||
hooksDescription: {
|
hooksDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.description',
|
id: 'instance.settings.tabs.hooks.description',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Hooks allow advanced users to run certain system commands before and after launching the game.',
|
'Hooks allow advanced users to run certain system commands before and after launching the game.',
|
||||||
},
|
},
|
||||||
customHooks: {
|
customHooks: {
|
||||||
id: 'instance.settings.tabs.hooks.custom-hooks',
|
id: 'instance.settings.tabs.hooks.custom-hooks',
|
||||||
defaultMessage: 'Custom launch hooks',
|
defaultMessage: 'Custom launch hooks',
|
||||||
},
|
},
|
||||||
preLaunch: {
|
preLaunch: {
|
||||||
id: 'instance.settings.tabs.hooks.pre-launch',
|
id: 'instance.settings.tabs.hooks.pre-launch',
|
||||||
defaultMessage: 'Pre-launch',
|
defaultMessage: 'Pre-launch',
|
||||||
},
|
},
|
||||||
preLaunchDescription: {
|
preLaunchDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.pre-launch.description',
|
id: 'instance.settings.tabs.hooks.pre-launch.description',
|
||||||
defaultMessage: 'Ran before the instance is launched.',
|
defaultMessage: 'Ran before the instance is launched.',
|
||||||
},
|
},
|
||||||
preLaunchEnter: {
|
preLaunchEnter: {
|
||||||
id: 'instance.settings.tabs.hooks.pre-launch.enter',
|
id: 'instance.settings.tabs.hooks.pre-launch.enter',
|
||||||
defaultMessage: 'Enter pre-launch command...',
|
defaultMessage: 'Enter pre-launch command...',
|
||||||
},
|
},
|
||||||
wrapper: {
|
wrapper: {
|
||||||
id: 'instance.settings.tabs.hooks.wrapper',
|
id: 'instance.settings.tabs.hooks.wrapper',
|
||||||
defaultMessage: 'Wrapper',
|
defaultMessage: 'Wrapper',
|
||||||
},
|
},
|
||||||
wrapperDescription: {
|
wrapperDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.wrapper.description',
|
id: 'instance.settings.tabs.hooks.wrapper.description',
|
||||||
defaultMessage: 'Wrapper command for launching Minecraft.',
|
defaultMessage: 'Wrapper command for launching Minecraft.',
|
||||||
},
|
},
|
||||||
wrapperEnter: {
|
wrapperEnter: {
|
||||||
id: 'instance.settings.tabs.hooks.wrapper.enter',
|
id: 'instance.settings.tabs.hooks.wrapper.enter',
|
||||||
defaultMessage: 'Enter wrapper command...',
|
defaultMessage: 'Enter wrapper command...',
|
||||||
},
|
},
|
||||||
postExit: {
|
postExit: {
|
||||||
id: 'instance.settings.tabs.hooks.post-exit',
|
id: 'instance.settings.tabs.hooks.post-exit',
|
||||||
defaultMessage: 'Post-exit',
|
defaultMessage: 'Post-exit',
|
||||||
},
|
},
|
||||||
postExitDescription: {
|
postExitDescription: {
|
||||||
id: 'instance.settings.tabs.hooks.post-exit.description',
|
id: 'instance.settings.tabs.hooks.post-exit.description',
|
||||||
defaultMessage: 'Ran after the game closes.',
|
defaultMessage: 'Ran after the game closes.',
|
||||||
},
|
},
|
||||||
postExitEnter: {
|
postExitEnter: {
|
||||||
id: 'instance.settings.tabs.hooks.post-exit.enter',
|
id: 'instance.settings.tabs.hooks.post-exit.enter',
|
||||||
defaultMessage: 'Enter post-exit command...',
|
defaultMessage: 'Enter post-exit command...',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.hooks) }}
|
{{ formatMessage(messages.hooks) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.hooksDescription) }}
|
{{ formatMessage(messages.hooksDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="mt-2" />
|
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="mt-2" />
|
||||||
|
|
||||||
<h2 class="mt-2 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="mt-2 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.preLaunch) }}
|
{{ formatMessage(messages.preLaunch) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.preLaunchDescription) }}
|
{{ formatMessage(messages.preLaunchDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="pre-launch"
|
id="pre-launch"
|
||||||
v-model="hooks.pre_launch"
|
v-model="hooks.pre_launch"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideHooks"
|
:disabled="!overrideHooks"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.preLaunchEnter)"
|
:placeholder="formatMessage(messages.preLaunchEnter)"
|
||||||
class="w-full mt-2"
|
class="w-full mt-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.wrapper) }}
|
{{ formatMessage(messages.wrapper) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.wrapperDescription) }}
|
{{ formatMessage(messages.wrapperDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="wrapper"
|
id="wrapper"
|
||||||
v-model="hooks.wrapper"
|
v-model="hooks.wrapper"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideHooks"
|
:disabled="!overrideHooks"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.wrapperEnter)"
|
:placeholder="formatMessage(messages.wrapperEnter)"
|
||||||
class="w-full mt-2"
|
class="w-full mt-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.postExit) }}
|
{{ formatMessage(messages.postExit) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.postExitDescription) }}
|
{{ formatMessage(messages.postExitDescription) }}
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="post-exit"
|
id="post-exit"
|
||||||
v-model="hooks.post_exit"
|
v-model="hooks.post_exit"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideHooks"
|
:disabled="!overrideHooks"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.postExitEnter)"
|
:placeholder="formatMessage(messages.postExitEnter)"
|
||||||
class="w-full mt-2"
|
class="w-full mt-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,188 +1,182 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Checkbox, Slider } from '@modrinth/ui'
|
|
||||||
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
||||||
import { computed, readonly, ref, watch } from 'vue'
|
import { Checkbox, injectNotificationManager, Slider } from '@modrinth/ui'
|
||||||
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
import { computed, readonly, ref, watch } from 'vue'
|
||||||
import { get_max_memory } from '@/helpers/jre'
|
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
|
||||||
|
|
||||||
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
|
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
|
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
|
|
||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
|
||||||
|
|
||||||
const overrideJavaInstall = ref(!!props.instance.java_path)
|
const overrideJavaInstall = ref(!!props.instance.java_path)
|
||||||
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
||||||
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
|
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
|
||||||
|
|
||||||
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
|
const overrideJavaArgs = ref((props.instance.extra_launch_args?.length ?? 0) > 0)
|
||||||
const javaArgs = ref(
|
const javaArgs = ref(
|
||||||
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
||||||
)
|
)
|
||||||
|
|
||||||
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
|
const overrideEnvVars = ref((props.instance.custom_env_vars?.length ?? 0) > 0)
|
||||||
const envVars = ref(
|
const envVars = ref(
|
||||||
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
||||||
.map((x) => x.join('='))
|
.map((x) => x.join('='))
|
||||||
.join(' '),
|
.join(' '),
|
||||||
)
|
)
|
||||||
|
|
||||||
const overrideMemorySettings = ref(!!props.instance.memory)
|
const overrideMemorySettings = ref(!!props.instance.memory)
|
||||||
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
||||||
const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
|
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
|
||||||
|
maxMemory: number
|
||||||
|
snapPoints: number[]
|
||||||
|
}
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
return {
|
||||||
java_path?: string
|
java_path:
|
||||||
extra_launch_args?: string[]
|
overrideJavaInstall.value && javaInstall.value.path !== ''
|
||||||
custom_env_vars?: string[][]
|
? javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
||||||
memory?: MemorySettings
|
: null,
|
||||||
} = {}
|
extra_launch_args: overrideJavaArgs.value
|
||||||
|
? javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||||
if (overrideJavaInstall.value) {
|
: null,
|
||||||
if (javaInstall.value.path !== '') {
|
custom_env_vars: overrideEnvVars.value
|
||||||
editProfile.java_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
? envVars.value
|
||||||
}
|
.trim()
|
||||||
}
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
if (overrideJavaArgs.value) {
|
.map((x) => x.split('=').filter(Boolean))
|
||||||
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
: null,
|
||||||
}
|
memory: overrideMemorySettings.value ? memory.value : null,
|
||||||
|
}
|
||||||
if (overrideEnvVars.value) {
|
|
||||||
editProfile.custom_env_vars = envVars.value
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((x) => x.split('=').filter(Boolean))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overrideMemorySettings.value) {
|
|
||||||
editProfile.memory = memory.value
|
|
||||||
}
|
|
||||||
|
|
||||||
return editProfile
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[
|
[
|
||||||
overrideJavaInstall,
|
overrideJavaInstall,
|
||||||
javaInstall,
|
javaInstall,
|
||||||
overrideJavaArgs,
|
overrideJavaArgs,
|
||||||
javaArgs,
|
javaArgs,
|
||||||
overrideEnvVars,
|
overrideEnvVars,
|
||||||
envVars,
|
envVars,
|
||||||
overrideMemorySettings,
|
overrideMemorySettings,
|
||||||
memory,
|
memory,
|
||||||
],
|
],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
javaInstallation: {
|
javaInstallation: {
|
||||||
id: 'instance.settings.tabs.java.java-installation',
|
id: 'instance.settings.tabs.java.java-installation',
|
||||||
defaultMessage: 'Java installation',
|
defaultMessage: 'Java installation',
|
||||||
},
|
},
|
||||||
javaArguments: {
|
javaArguments: {
|
||||||
id: 'instance.settings.tabs.java.java-arguments',
|
id: 'instance.settings.tabs.java.java-arguments',
|
||||||
defaultMessage: 'Java arguments',
|
defaultMessage: 'Java arguments',
|
||||||
},
|
},
|
||||||
javaEnvironmentVariables: {
|
javaEnvironmentVariables: {
|
||||||
id: 'instance.settings.tabs.java.environment-variables',
|
id: 'instance.settings.tabs.java.environment-variables',
|
||||||
defaultMessage: 'Environment variables',
|
defaultMessage: 'Environment variables',
|
||||||
},
|
},
|
||||||
javaMemory: {
|
javaMemory: {
|
||||||
id: 'instance.settings.tabs.java.java-memory',
|
id: 'instance.settings.tabs.java.java-memory',
|
||||||
defaultMessage: 'Memory allocated',
|
defaultMessage: 'Memory allocated',
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
id: 'instance.settings.tabs.java.hooks',
|
id: 'instance.settings.tabs.java.hooks',
|
||||||
defaultMessage: 'Hooks',
|
defaultMessage: 'Hooks',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.javaInstallation) }}
|
{{ formatMessage(messages.javaInstallation) }}
|
||||||
</h2>
|
</h2>
|
||||||
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2" />
|
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2" />
|
||||||
<template v-if="!overrideJavaInstall">
|
<template v-if="!overrideJavaInstall">
|
||||||
<div class="flex my-2 items-center gap-2 font-semibold">
|
<div class="flex my-2 items-center gap-2 font-semibold">
|
||||||
<template v-if="javaInstall">
|
<template v-if="javaInstall">
|
||||||
<CheckCircleIcon class="text-brand-green h-4 w-4" />
|
<CheckCircleIcon class="text-brand-green h-4 w-4" />
|
||||||
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
|
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="optimalJava">
|
<template v-else-if="optimalJava">
|
||||||
<XCircleIcon class="text-brand-red h-5 w-5" />
|
<XCircleIcon class="text-brand-red h-5 w-5" />
|
||||||
<span
|
<span
|
||||||
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
|
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
|
||||||
one below:</span
|
one below:</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<XCircleIcon class="text-brand-red h-5 w-5" />
|
<XCircleIcon class="text-brand-red h-5 w-5" />
|
||||||
<span
|
<span
|
||||||
>Could not automatically determine a Java installation to use. Please set one
|
>Could not automatically determine a Java installation to use. Please set one
|
||||||
below:</span
|
below:</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="javaInstall && !overrideJavaInstall"
|
v-if="javaInstall && !overrideJavaInstall"
|
||||||
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
|
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
|
||||||
>
|
>
|
||||||
{{ javaInstall.path }}
|
{{ javaInstall.path }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
|
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.javaMemory) }}
|
{{ formatMessage(messages.javaMemory) }}
|
||||||
</h2>
|
</h2>
|
||||||
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2" />
|
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2" />
|
||||||
<Slider
|
<Slider
|
||||||
id="max-memory"
|
id="max-memory"
|
||||||
v-model="memory.maximum"
|
v-model="memory.maximum"
|
||||||
:disabled="!overrideMemorySettings"
|
:disabled="!overrideMemorySettings"
|
||||||
:min="512"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
unit="MB"
|
:snap-points="snapPoints"
|
||||||
/>
|
:snap-range="512"
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
unit="MB"
|
||||||
{{ formatMessage(messages.javaArguments) }}
|
/>
|
||||||
</h2>
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
|
{{ formatMessage(messages.javaArguments) }}
|
||||||
<input
|
</h2>
|
||||||
id="java-args"
|
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
|
||||||
v-model="javaArgs"
|
<input
|
||||||
autocomplete="off"
|
id="java-args"
|
||||||
:disabled="!overrideJavaArgs"
|
v-model="javaArgs"
|
||||||
type="text"
|
autocomplete="off"
|
||||||
class="w-full"
|
:disabled="!overrideJavaArgs"
|
||||||
placeholder="Enter java arguments..."
|
type="text"
|
||||||
/>
|
class="w-full"
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
placeholder="Enter java arguments..."
|
||||||
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
/>
|
||||||
</h2>
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2" />
|
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
||||||
<input
|
</h2>
|
||||||
id="env-vars"
|
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2" />
|
||||||
v-model="envVars"
|
<input
|
||||||
autocomplete="off"
|
id="env-vars"
|
||||||
:disabled="!overrideEnvVars"
|
v-model="envVars"
|
||||||
type="text"
|
autocomplete="off"
|
||||||
class="w-full"
|
:disabled="!overrideEnvVars"
|
||||||
placeholder="Enter environmental variables..."
|
type="text"
|
||||||
/>
|
class="w-full"
|
||||||
</div>
|
placeholder="Enter environmental variables..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Checkbox, Toggle } from '@modrinth/ui'
|
import { Checkbox, injectNotificationManager, Toggle } from '@modrinth/ui'
|
||||||
import { computed, ref, type Ref, watch } from 'vue'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { get } from '@/helpers/settings.ts'
|
import { computed, type Ref, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { edit } from '@/helpers/profile'
|
import { edit } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
@@ -14,151 +16,139 @@ const props = defineProps<InstanceSettingsTabProps>()
|
|||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as AppSettings
|
||||||
|
|
||||||
const overrideWindowSettings = ref(
|
const overrideWindowSettings = ref(
|
||||||
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
|
!!props.instance.game_resolution || !!props.instance.force_fullscreen,
|
||||||
)
|
)
|
||||||
const resolution: Ref<[number, number]> = ref(
|
const resolution: Ref<[number, number]> = ref(
|
||||||
props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
|
props.instance.game_resolution ?? (globalSettings.game_resolution.slice() as [number, number]),
|
||||||
)
|
)
|
||||||
const fullscreenSetting: Ref<boolean> = ref(
|
const fullscreenSetting: Ref<boolean> = ref(
|
||||||
props.instance.force_fullscreen ?? globalSettings.force_fullscreen,
|
props.instance.force_fullscreen ?? globalSettings.force_fullscreen,
|
||||||
)
|
)
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
if (!overrideWindowSettings.value) {
|
||||||
force_fullscreen?: boolean
|
return {
|
||||||
game_resolution?: [number, number]
|
force_fullscreen: null,
|
||||||
} = {}
|
game_resolution: null,
|
||||||
|
}
|
||||||
if (overrideWindowSettings.value) {
|
}
|
||||||
editProfile.force_fullscreen = fullscreenSetting.value
|
return {
|
||||||
|
force_fullscreen: fullscreenSetting.value,
|
||||||
if (!fullscreenSetting.value) {
|
game_resolution: fullscreenSetting.value ? null : resolution.value,
|
||||||
editProfile.game_resolution = resolution.value
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return editProfile
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[overrideWindowSettings, resolution, fullscreenSetting],
|
[overrideWindowSettings, resolution, fullscreenSetting],
|
||||||
async () => {
|
async () => {
|
||||||
await edit(props.instance.path, editProfileObject.value)
|
await edit(props.instance.path, editProfileObject.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
customWindowSettings: {
|
customWindowSettings: {
|
||||||
id: 'instance.settings.tabs.window.custom-window-settings',
|
id: 'instance.settings.tabs.window.custom-window-settings',
|
||||||
defaultMessage: 'Custom window settings',
|
defaultMessage: 'Custom window settings',
|
||||||
},
|
},
|
||||||
fullscreen: {
|
fullscreen: {
|
||||||
id: 'instance.settings.tabs.window.fullscreen',
|
id: 'instance.settings.tabs.window.fullscreen',
|
||||||
defaultMessage: 'Fullscreen',
|
defaultMessage: 'Fullscreen',
|
||||||
},
|
},
|
||||||
fullscreenDescription: {
|
fullscreenDescription: {
|
||||||
id: 'instance.settings.tabs.window.fullscreen.description',
|
id: 'instance.settings.tabs.window.fullscreen.description',
|
||||||
defaultMessage: 'Make the game start in full screen when launched (using options.txt).',
|
defaultMessage: 'Make the game start in full screen when launched (using options.txt).',
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
id: 'instance.settings.tabs.window.width',
|
id: 'instance.settings.tabs.window.width',
|
||||||
defaultMessage: 'Width',
|
defaultMessage: 'Width',
|
||||||
},
|
},
|
||||||
widthDescription: {
|
widthDescription: {
|
||||||
id: 'instance.settings.tabs.window.width.description',
|
id: 'instance.settings.tabs.window.width.description',
|
||||||
defaultMessage: 'The width of the game window when launched.',
|
defaultMessage: 'The width of the game window when launched.',
|
||||||
},
|
},
|
||||||
enterWidth: {
|
enterWidth: {
|
||||||
id: 'instance.settings.tabs.window.width.enter',
|
id: 'instance.settings.tabs.window.width.enter',
|
||||||
defaultMessage: 'Enter width...',
|
defaultMessage: 'Enter width...',
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
id: 'instance.settings.tabs.window.height',
|
id: 'instance.settings.tabs.window.height',
|
||||||
defaultMessage: 'Height',
|
defaultMessage: 'Height',
|
||||||
},
|
},
|
||||||
heightDescription: {
|
heightDescription: {
|
||||||
id: 'instance.settings.tabs.window.height.description',
|
id: 'instance.settings.tabs.window.height.description',
|
||||||
defaultMessage: 'The height of the game window when launched.',
|
defaultMessage: 'The height of the game window when launched.',
|
||||||
},
|
},
|
||||||
enterHeight: {
|
enterHeight: {
|
||||||
id: 'instance.settings.tabs.window.height.enter',
|
id: 'instance.settings.tabs.window.height.enter',
|
||||||
defaultMessage: 'Enter height...',
|
defaultMessage: 'Enter height...',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="overrideWindowSettings"
|
v-model="overrideWindowSettings"
|
||||||
:label="formatMessage(messages.customWindowSettings)"
|
:label="formatMessage(messages.customWindowSettings)"
|
||||||
@update:model-value="
|
/>
|
||||||
(value) => {
|
<div class="mt-2 flex items-center gap-4 justify-between">
|
||||||
if (!value) {
|
<div>
|
||||||
resolution = globalSettings.game_resolution
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
fullscreenSetting = globalSettings.force_fullscreen
|
{{ formatMessage(messages.fullscreen) }}
|
||||||
}
|
</h2>
|
||||||
}
|
<p class="m-0">
|
||||||
"
|
{{ formatMessage(messages.fullscreenDescription) }}
|
||||||
/>
|
</p>
|
||||||
<div class="mt-2 flex items-center gap-4 justify-between">
|
</div>
|
||||||
<div>
|
<Toggle
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
id="fullscreen"
|
||||||
{{ formatMessage(messages.fullscreen) }}
|
:model-value="overrideWindowSettings ? fullscreenSetting : globalSettings.force_fullscreen"
|
||||||
</h2>
|
:disabled="!overrideWindowSettings"
|
||||||
<p class="m-0">
|
@update:model-value="
|
||||||
{{ formatMessage(messages.fullscreenDescription) }}
|
(e) => {
|
||||||
</p>
|
fullscreenSetting = e
|
||||||
</div>
|
}
|
||||||
<Toggle
|
"
|
||||||
id="fullscreen"
|
/>
|
||||||
:model-value="overrideWindowSettings ? fullscreenSetting : globalSettings.force_fullscreen"
|
</div>
|
||||||
:disabled="!overrideWindowSettings"
|
|
||||||
@update:model-value="
|
|
||||||
(e) => {
|
|
||||||
fullscreenSetting = e
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-4 justify-between">
|
<div class="mt-4 flex items-center gap-4 justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.width) }}
|
{{ formatMessage(messages.width) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.widthDescription) }}
|
{{ formatMessage(messages.widthDescription) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
id="width"
|
id="width"
|
||||||
v-model="resolution[0]"
|
v-model="resolution[0]"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||||
type="number"
|
type="number"
|
||||||
:placeholder="formatMessage(messages.enterWidth)"
|
:placeholder="formatMessage(messages.enterWidth)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-4 justify-between">
|
<div class="mt-4 flex items-center gap-4 justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||||
{{ formatMessage(messages.height) }}
|
{{ formatMessage(messages.height) }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.heightDescription) }}
|
{{ formatMessage(messages.heightDescription) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
id="height"
|
id="height"
|
||||||
v-model="resolution[1]"
|
v-model="resolution[1]"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:disabled="!overrideWindowSettings || fullscreenSetting"
|
:disabled="!overrideWindowSettings || fullscreenSetting"
|
||||||
type="number"
|
type="number"
|
||||||
:placeholder="formatMessage(messages.enterHeight)"
|
:placeholder="formatMessage(messages.enterHeight)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,29 +1,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ReportIcon,
|
CoffeeIcon,
|
||||||
AstralRinthLogo,
|
GameIcon,
|
||||||
ShieldIcon,
|
GaugeIcon,
|
||||||
SettingsIcon,
|
AstralRinthLogo,
|
||||||
GaugeIcon,
|
DownloadIcon,
|
||||||
PaintbrushIcon,
|
SpinnerIcon,
|
||||||
GameIcon,
|
PaintbrushIcon,
|
||||||
CoffeeIcon,
|
ReportIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
ShieldIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { TabbedModal } from '@modrinth/ui'
|
import { ProgressBar, TabbedModal } from '@modrinth/ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
|
||||||
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
|
||||||
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
|
||||||
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
|
||||||
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
|
||||||
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/plugin-os'
|
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
|
||||||
import { useTheming } from '@/store/state'
|
import { defineMessage, defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||||
|
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
||||||
|
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||||
|
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
||||||
|
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
||||||
|
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
|
// [AR] Imports
|
||||||
|
import { installState, getRemote, updateState } from '@/helpers/update.js'
|
||||||
|
|
||||||
|
const updateModalView = ref(null)
|
||||||
|
const updateRequestFailView = ref(null)
|
||||||
|
|
||||||
|
const initUpdateModal = async () => {
|
||||||
|
updateModalView.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const initDownload = async () => {
|
||||||
|
updateModalView.value.hide()
|
||||||
|
const result = await getRemote(true);
|
||||||
|
if (!result) {
|
||||||
|
updateRequestFailView.value.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
@@ -31,131 +53,235 @@ const { formatMessage } = useVIntl()
|
|||||||
const devModeCounter = ref(0)
|
const devModeCounter = ref(0)
|
||||||
|
|
||||||
const developerModeEnabled = defineMessage({
|
const developerModeEnabled = defineMessage({
|
||||||
id: 'app.settings.developer-mode-enabled',
|
id: 'app.settings.developer-mode-enabled',
|
||||||
defaultMessage: 'Developer mode enabled.',
|
defaultMessage: 'Developer mode enabled.',
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.appearance',
|
id: 'app.settings.tabs.appearance',
|
||||||
defaultMessage: 'Appearance',
|
defaultMessage: 'Appearance',
|
||||||
}),
|
}),
|
||||||
icon: PaintbrushIcon,
|
icon: PaintbrushIcon,
|
||||||
content: AppearanceSettings,
|
content: AppearanceSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.privacy',
|
id: 'app.settings.tabs.privacy',
|
||||||
defaultMessage: 'Privacy',
|
defaultMessage: 'Privacy',
|
||||||
}),
|
}),
|
||||||
icon: ShieldIcon,
|
icon: ShieldIcon,
|
||||||
content: PrivacySettings,
|
content: PrivacySettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.java-installations',
|
id: 'app.settings.tabs.java-installations',
|
||||||
defaultMessage: 'Java installations',
|
defaultMessage: 'Java installations',
|
||||||
}),
|
}),
|
||||||
icon: CoffeeIcon,
|
icon: CoffeeIcon,
|
||||||
content: JavaSettings,
|
content: JavaSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.default-instance-options',
|
id: 'app.settings.tabs.default-instance-options',
|
||||||
defaultMessage: 'Default instance options',
|
defaultMessage: 'Default instance options',
|
||||||
}),
|
}),
|
||||||
icon: GameIcon,
|
icon: GameIcon,
|
||||||
content: DefaultInstanceSettings,
|
content: DefaultInstanceSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.resource-management',
|
id: 'app.settings.tabs.resource-management',
|
||||||
defaultMessage: 'Resource management',
|
defaultMessage: 'Resource management',
|
||||||
}),
|
}),
|
||||||
icon: GaugeIcon,
|
icon: GaugeIcon,
|
||||||
content: ResourceManagementSettings,
|
content: ResourceManagementSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'app.settings.tabs.feature-flags',
|
id: 'app.settings.tabs.feature-flags',
|
||||||
defaultMessage: 'Feature flags',
|
defaultMessage: 'Feature flags',
|
||||||
}),
|
}),
|
||||||
icon: ReportIcon,
|
icon: ReportIcon,
|
||||||
content: FeatureFlagSettings,
|
content: FeatureFlagSettings,
|
||||||
developerOnly: true,
|
developerOnly: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOpen = computed(() => modal.value?.isOpen)
|
const isOpen = computed(() => modal.value?.isOpen)
|
||||||
|
|
||||||
defineExpose({ show, isOpen })
|
defineExpose({ show, isOpen })
|
||||||
|
|
||||||
|
const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress()
|
||||||
|
|
||||||
const version = await getVersion()
|
const version = await getVersion()
|
||||||
const osPlatform = getOsPlatform()
|
const osPlatform = getOsPlatform()
|
||||||
const osVersion = getOsVersion()
|
const osVersion = getOsVersion()
|
||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
await set(settings.value)
|
await set(settings.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
function devModeCount() {
|
function devModeCount() {
|
||||||
devModeCounter.value++
|
devModeCounter.value++
|
||||||
if (devModeCounter.value > 5) {
|
if (devModeCounter.value > 5) {
|
||||||
themeStore.devMode = !themeStore.devMode
|
themeStore.devMode = !themeStore.devMode
|
||||||
settings.value.developer_mode = !!themeStore.devMode
|
settings.value.developer_mode = !!themeStore.devMode
|
||||||
devModeCounter.value = 0
|
devModeCounter.value = 0
|
||||||
|
|
||||||
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
|
if (!themeStore.devMode && tabs[modal.value.selectedTab].developerOnly) {
|
||||||
modal.value.setTab(0)
|
modal.value.setTab(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
downloading: {
|
||||||
|
id: 'app.settings.downloading',
|
||||||
|
defaultMessage: 'Downloading v{version}',
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
<SettingsIcon /> Settings
|
<SettingsIcon /> Settings
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
<TabbedModal :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="mt-auto text-secondary text-sm">
|
<div class="mt-auto text-secondary text-sm">
|
||||||
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
<div class="mb-3">
|
||||||
{{ formatMessage(developerModeEnabled) }}
|
<template v-if="progress > 0 && progress < 1">
|
||||||
</p>
|
<p class="m-0 mb-2">
|
||||||
<div class="flex items-center gap-3">
|
{{ formatMessage(messages.downloading, { version: downloadingVersion }) }}
|
||||||
<button
|
</p>
|
||||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
<ProgressBar :progress="progress" />
|
||||||
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
</template>
|
||||||
@click="devModeCount"
|
</div>
|
||||||
>
|
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
||||||
<AstralRinthLogo class="w-6 h-6" />
|
{{ formatMessage(developerModeEnabled) }}
|
||||||
</button>
|
</p>
|
||||||
<div>
|
<div class="flex items-center gap-3">
|
||||||
<p class="m-0">AstralRinth App {{ version }}</p>
|
<button
|
||||||
<p class="m-0">
|
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||||
<span v-if="osPlatform === 'macos'">MacOS</span>
|
:class="{
|
||||||
<span v-else class="capitalize">{{ osPlatform }}</span>
|
'text-brand': themeStore.devMode,
|
||||||
{{ osVersion }}
|
'text-secondary': !themeStore.devMode,
|
||||||
</p>
|
}"
|
||||||
</div>
|
@click="devModeCount"
|
||||||
</div>
|
>
|
||||||
</div>
|
<AstralRinthLogo class="w-6 h-6" />
|
||||||
</template>
|
</button>
|
||||||
</TabbedModal>
|
<div>
|
||||||
</ModalWrapper>
|
<p class="m-0">AstralRinth App {{ version }}</p>
|
||||||
|
<p class="m-0">
|
||||||
|
<span v-if="osPlatform === 'macos'">macOS</span>
|
||||||
|
<span v-else class="capitalize">{{ osPlatform }}</span>
|
||||||
|
{{ osVersion }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="updateState" class="w-8 h-8 cursor-pointer hover:brightness-75 neon-icon pulse">
|
||||||
|
<template v-if="installState">
|
||||||
|
<SpinnerIcon class="size-6 animate-spin" v-tooltip.bottom="'Installing in process...'" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<DownloadIcon class="size-6" v-tooltip.bottom="'View update info'" @click="!installState && (initUpdateModal(), getRemote(false))" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TabbedModal>
|
||||||
|
<!-- [AR] Feature -->
|
||||||
|
<ModalWrapper ref="updateModalView" :has-to-type="false" header="Request to update the AstralRinth launcher">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<strong>The new version of the AstralRinth launcher is available!</strong>
|
||||||
|
<p>Your version is outdated. We recommend that you update to the latest version.</p>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<p><strong>⚠️ Please, read this notice before initialize update process</strong></p>
|
||||||
|
<p>
|
||||||
|
Before updating, make sure that you have saved and closed all running instances and made a backup copy of the launcher data such as
|
||||||
|
<code>%appdata%\Roaming\AstralRinthApp</code> on Windows or <code>~/Library/Application Support/AstralRinthApp</code> on macOS.
|
||||||
|
Remember that the authors of the product are not responsible for the breakdown of
|
||||||
|
your files, so you should always make back up copies of them and keep them in a safe place.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-secondary space-y-1">
|
||||||
|
<p>
|
||||||
|
<strong>☁️ Latest release tag:</strong>
|
||||||
|
<span id="releaseTag" class="neon-text"></span>
|
||||||
|
<br/>
|
||||||
|
<strong>☁️ Latest release title:</strong>
|
||||||
|
<span id="releaseTitle" class="neon-text"></span>
|
||||||
|
<br/>
|
||||||
|
<strong>💾 Installed & Running version:</strong>
|
||||||
|
<span class="neon-text">v{{ version }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a class="neon-text" href="https://me.astralium.su/get/ar" target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
Checkout our git repository
|
||||||
|
</a>
|
||||||
|
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
|
||||||
|
<Button class="bordered" @click="updateModalView.hide()">Cancel</Button>
|
||||||
|
<Button class="bordered" @click="initDownload()">Download file</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="updateRequestFailView" :has-to-type="false" header="Failed to request a file from the server :(">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p><strong>Error occurred</strong></p>
|
||||||
|
<p>Unfortunately, the program was unable to download the file from our servers.</p>
|
||||||
|
<p>
|
||||||
|
Please try downloading it yourself from
|
||||||
|
<a class="neon-text" href="https://me.astralium.su/get/ar" target="_blank" rel="noopener noreferrer">Git
|
||||||
|
Astralium</a>
|
||||||
|
if there are any updates available.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-secondary">
|
||||||
|
<p>
|
||||||
|
<strong>Local AstralRinth:</strong>
|
||||||
|
<span class="neon-text">v{{ version }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
|
||||||
|
<Button class="bordered" @click="updateRequestFailView.hide()">Close</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-icon.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-button.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-text.scss';
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: linear-gradient(90deg, #005eff, #00cfff);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
onFlowCancel: {
|
||||||
|
type: Function,
|
||||||
|
default() {
|
||||||
|
return async () => {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const modal = ref()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
modal.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show, hide })
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ModalWrapper ref="modal" @hide="onFlowCancel">
|
||||||
|
<template #title>
|
||||||
|
<span class="items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
|
<LogInIcon /> Sign in
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-2">
|
||||||
|
<SpinnerIcon class="w-12 h-12 animate-spin" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-secondary">
|
||||||
|
Please sign in at the browser window that just opened to continue.
|
||||||
|
</p>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
@@ -1,89 +1,91 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
|
||||||
import { ConfirmModal } from '@modrinth/ui'
|
import { ConfirmModal } from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import { useTheming } from '@/store/theme.js'
|
// import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||||
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
confirmationText: {
|
confirmationText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
hasToType: {
|
hasToType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'No title defined',
|
default: 'No title defined',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'No description defined',
|
default: 'No description defined',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
proceedIcon: {
|
proceedIcon: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
proceedLabel: {
|
proceedLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Proceed',
|
default: 'Proceed',
|
||||||
},
|
},
|
||||||
danger: {
|
danger: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
// showAdOnClose: {
|
// showAdOnClose: {
|
||||||
// type: Boolean,
|
// type: Boolean,
|
||||||
// default: true,
|
// default: true,
|
||||||
// },
|
// },
|
||||||
markdown: {
|
markdown: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['proceed'])
|
const emit = defineEmits(['proceed'])
|
||||||
const modal = ref(null)
|
const modal = ref(null)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
modal.value.show()
|
// hide_ads_window()
|
||||||
},
|
modal.value.show()
|
||||||
hide: () => {
|
},
|
||||||
// onModalHide()
|
hide: () => {
|
||||||
modal.value.hide()
|
onModalHide()
|
||||||
},
|
modal.value.hide()
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// function onModalHide() {
|
// function onModalHide() {
|
||||||
// if (props.showAdOnClose) {
|
// if (props.showAdOnClose) {
|
||||||
// show_ads_window()
|
// show_ads_window()
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
function proceed() {
|
function proceed() {
|
||||||
emit('proceed')
|
emit('proceed')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:confirmation-text="confirmationText"
|
:confirmation-text="confirmationText"
|
||||||
:has-to-type="hasToType"
|
:has-to-type="hasToType"
|
||||||
:title="title"
|
:title="title"
|
||||||
:description="description"
|
:description="description"
|
||||||
:proceed-icon="proceedIcon"
|
:proceed-icon="proceedIcon"
|
||||||
:proceed-label="proceedLabel"
|
:proceed-label="proceedLabel"
|
||||||
:on-hide="onModalHide"
|
:on-hide="onModalHide"
|
||||||
:noblur="!themeStore.advancedRendering"
|
:noblur="!themeStore.advancedRendering"
|
||||||
:danger="danger"
|
:danger="danger"
|
||||||
:markdown="markdown"
|
:markdown="markdown"
|
||||||
@proceed="proceed"
|
@proceed="proceed"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,19 +2,20 @@
|
|||||||
import { ChevronRightIcon } from '@modrinth/assets'
|
import { ChevronRightIcon } from '@modrinth/assets'
|
||||||
import { Avatar } from '@modrinth/ui'
|
import { Avatar } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||||
size="24px"
|
size="24px"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
/>
|
/>
|
||||||
{{ instance.name }} <ChevronRightIcon />
|
{{ instance.name }} <ChevronRightIcon />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
CoffeeIcon,
|
CodeIcon,
|
||||||
InfoIcon,
|
CoffeeIcon,
|
||||||
WrenchIcon,
|
InfoIcon,
|
||||||
MonitorIcon,
|
MonitorIcon,
|
||||||
CodeIcon,
|
WrenchIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, TabbedModal, type TabbedModalTab } from '@modrinth/ui'
|
import { Avatar, TabbedModal, type TabbedModalTab } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
|
||||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
||||||
|
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
||||||
import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue'
|
import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue'
|
||||||
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
||||||
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
||||||
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
@@ -24,75 +26,75 @@ const { formatMessage } = useVIntl()
|
|||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
|
|
||||||
const tabs: TabbedModalTab<InstanceSettingsTabProps>[] = [
|
const tabs: TabbedModalTab<InstanceSettingsTabProps>[] = [
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.general',
|
id: 'instance.settings.tabs.general',
|
||||||
defaultMessage: 'General',
|
defaultMessage: 'General',
|
||||||
}),
|
}),
|
||||||
icon: InfoIcon,
|
icon: InfoIcon,
|
||||||
content: GeneralSettings,
|
content: GeneralSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.installation',
|
id: 'instance.settings.tabs.installation',
|
||||||
defaultMessage: 'Installation',
|
defaultMessage: 'Installation',
|
||||||
}),
|
}),
|
||||||
icon: WrenchIcon,
|
icon: WrenchIcon,
|
||||||
content: InstallationSettings,
|
content: InstallationSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.window',
|
id: 'instance.settings.tabs.window',
|
||||||
defaultMessage: 'Window',
|
defaultMessage: 'Window',
|
||||||
}),
|
}),
|
||||||
icon: MonitorIcon,
|
icon: MonitorIcon,
|
||||||
content: WindowSettings,
|
content: WindowSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.java',
|
id: 'instance.settings.tabs.java',
|
||||||
defaultMessage: 'Java and memory',
|
defaultMessage: 'Java and memory',
|
||||||
}),
|
}),
|
||||||
icon: CoffeeIcon,
|
icon: CoffeeIcon,
|
||||||
content: JavaSettings,
|
content: JavaSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: defineMessage({
|
name: defineMessage({
|
||||||
id: 'instance.settings.tabs.hooks',
|
id: 'instance.settings.tabs.hooks',
|
||||||
defaultMessage: 'Launch hooks',
|
defaultMessage: 'Launch hooks',
|
||||||
}),
|
}),
|
||||||
icon: CodeIcon,
|
icon: CodeIcon,
|
||||||
content: HooksSettings,
|
content: HooksSettings,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ show })
|
defineExpose({ show })
|
||||||
|
|
||||||
const titleMessage = defineMessage({
|
const titleMessage = defineMessage({
|
||||||
id: 'instance.settings.title',
|
id: 'instance.settings.title',
|
||||||
defaultMessage: 'Settings',
|
defaultMessage: 'Settings',
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||||
size="24px"
|
size="24px"
|
||||||
:tint-by="props.instance.path"
|
:tint-by="props.instance.path"
|
||||||
/>
|
/>
|
||||||
{{ instance.name }} <ChevronRightIcon />
|
{{ instance.name }} <ChevronRightIcon />
|
||||||
<span class="font-extrabold text-contrast">{{ formatMessage(titleMessage) }}</span>
|
<span class="font-extrabold text-contrast">{{ formatMessage(titleMessage) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<TabbedModal :tabs="tabs.map((tab) => ({ ...tab, props }))" />
|
<TabbedModal :tabs="tabs.map((tab) => ({ ...tab, props }))" />
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,57 +1,69 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTemplateRef } from 'vue'
|
|
||||||
import { NewModal as Modal } from '@modrinth/ui'
|
import { NewModal as Modal } from '@modrinth/ui'
|
||||||
// import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
import { useTemplateRef } from 'vue'
|
||||||
import { useTheming } from '@/store/theme.js'
|
|
||||||
|
// import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||||
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
header: {
|
header: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
closable: {
|
hideHeader: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: false,
|
||||||
},
|
},
|
||||||
onHide: {
|
closable: {
|
||||||
type: Function,
|
type: Boolean,
|
||||||
default() {
|
default: true,
|
||||||
return () => { }
|
},
|
||||||
},
|
onHide: {
|
||||||
},
|
type: Function,
|
||||||
// showAdOnClose: {
|
default() {
|
||||||
// type: Boolean,
|
return () => {}
|
||||||
// default: true,
|
},
|
||||||
// },
|
},
|
||||||
|
// showAdOnClose: {
|
||||||
|
// type: Boolean,
|
||||||
|
// default: true,
|
||||||
|
// },
|
||||||
})
|
})
|
||||||
const modal = useTemplateRef('modal')
|
const modal = useTemplateRef('modal')
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (e: MouseEvent) => {
|
show: (e: MouseEvent) => {
|
||||||
// hide_ads_window()
|
// hide_ads_window()
|
||||||
modal.value?.show(e)
|
modal.value?.show(e)
|
||||||
},
|
},
|
||||||
hide: () => {
|
hide: () => {
|
||||||
onModalHide()
|
onModalHide()
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function onModalHide() {
|
function onModalHide() {
|
||||||
// if (props.showAdOnClose) {
|
// if (props.showAdOnClose) {
|
||||||
// show_ads_window()
|
// show_ads_window()
|
||||||
// }
|
// }
|
||||||
props.onHide?.()
|
props.onHide?.()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
|
<Modal
|
||||||
<template #title>
|
ref="modal"
|
||||||
<slot name="title" />
|
:header="header"
|
||||||
</template>
|
:noblur="!themeStore.advancedRendering"
|
||||||
<slot />
|
:closable="closable"
|
||||||
</Modal>
|
:hide-header="hideHeader"
|
||||||
|
@hide="onModalHide"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<slot name="title" />
|
||||||
|
</template>
|
||||||
|
<slot />
|
||||||
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,48 +1,62 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
|
||||||
import { ShareModal } from '@modrinth/ui'
|
import { ShareModal } from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import { useTheming } from '@/store/theme.js'
|
// import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||||
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
header: {
|
header: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Share',
|
default: 'Share',
|
||||||
},
|
},
|
||||||
shareTitle: {
|
shareTitle: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Modrinth',
|
default: 'Modrinth',
|
||||||
},
|
},
|
||||||
shareText: {
|
shareText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
openInNewTab: {
|
openInNewTab: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const modal = ref(null)
|
const modal = ref(null)
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (passedContent) => {
|
show: (passedContent) => {
|
||||||
modal.value.show(passedContent)
|
// hide_ads_window()
|
||||||
},
|
modal.value.show(passedContent)
|
||||||
hide: () => {
|
},
|
||||||
onModalHide()
|
hide: () => {
|
||||||
modal.value.hide()
|
onModalHide()
|
||||||
},
|
modal.value.hide()
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// function onModalHide() {
|
||||||
|
// show_ads_window()
|
||||||
|
// }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ShareModal ref="modal" :header="header" :share-title="shareTitle" :share-text="shareText" :link="link"
|
<ShareModal
|
||||||
:open-in-new-tab="openInNewTab" :on-hide="onModalHide" :noblur="!themeStore.advancedRendering" />
|
ref="modal"
|
||||||
|
:header="header"
|
||||||
|
:share-title="shareTitle"
|
||||||
|
:share-text="shareText"
|
||||||
|
:link="link"
|
||||||
|
:open-in-new-tab="openInNewTab"
|
||||||
|
:on-hide="onModalHide"
|
||||||
|
:noblur="!themeStore.advancedRendering"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
|
import { Combobox, ThemeSelector, Toggle } from '@modrinth/ui'
|
||||||
import { useTheming } from '@/store/state'
|
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
import { getOS } from '@/helpers/utils'
|
import { getOS } from '@/helpers/utils'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
import type { ColorTheme } from '@/store/theme.ts'
|
import type { ColorTheme } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
@@ -12,119 +13,120 @@ const os = ref(await getOS())
|
|||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
await set(settings.value)
|
await set(settings.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Color theme</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Color theme</h2>
|
||||||
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
||||||
|
|
||||||
<ThemeSelector
|
<ThemeSelector
|
||||||
:update-color-theme="
|
:update-color-theme="
|
||||||
(theme: ColorTheme) => {
|
(theme: ColorTheme) => {
|
||||||
themeStore.setThemeState(theme)
|
themeStore.setThemeState(theme)
|
||||||
settings.theme = theme
|
settings.theme = theme
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
:current-theme="settings.theme"
|
:current-theme="settings.theme"
|
||||||
:theme-options="themeStore.getThemeOptions()"
|
:theme-options="themeStore.getThemeOptions()"
|
||||||
system-theme-color="system"
|
system-theme-color="system"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Advanced rendering</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Advanced rendering</h2>
|
||||||
<p class="m-0 mt-1">
|
<p class="m-0 mt-1">
|
||||||
Enables advanced rendering such as blur effects that may cause performance issues without
|
Enables advanced rendering such as blur effects that may cause performance issues without
|
||||||
hardware-accelerated rendering.
|
hardware-accelerated rendering.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
id="advanced-rendering"
|
id="advanced-rendering"
|
||||||
:model-value="themeStore.advancedRendering"
|
:model-value="themeStore.advancedRendering"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(e) => {
|
(e) => {
|
||||||
themeStore.advancedRendering = e
|
themeStore.advancedRendering = !!e
|
||||||
settings.advanced_rendering = themeStore.advancedRendering
|
settings.advanced_rendering = themeStore.advancedRendering
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide nametag</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide nametag</h2>
|
||||||
<p class="m-0 mt-1">Disables the nametag above your player on the skins page.</p>
|
<p class="m-0 mt-1">Disables the nametag above your player on the skins page.</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
|
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Native decorations</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Native decorations</h2>
|
||||||
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle id="native-decorations" v-model="settings.native_decorations" />
|
<Toggle id="native-decorations" v-model="settings.native_decorations" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Minimize launcher</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Minimize launcher</h2>
|
||||||
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
|
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
|
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
|
||||||
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
|
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
|
||||||
</div>
|
</div>
|
||||||
<TeleportDropdownMenu
|
<Combobox
|
||||||
id="opening-page"
|
id="opening-page"
|
||||||
v-model="settings.default_page"
|
v-model="settings.default_page"
|
||||||
name="Opening page dropdown"
|
name="Opening page dropdown"
|
||||||
class="w-40"
|
class="w-40"
|
||||||
:options="['Home', 'Library']"
|
:options="['Home', 'Library'].map((v) => ({ value: v, label: v }))"
|
||||||
/>
|
:display-value="settings.default_page ?? 'Select an option'"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Jump back into worlds</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Jump back into worlds</h2>
|
||||||
<p class="m-0 mt-1">Includes recent worlds in the "Jump back in" section on the Home page.</p>
|
<p class="m-0 mt-1">Includes recent worlds in the "Jump back in" section on the Home page.</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
:model-value="themeStore.getFeatureFlag('worlds_in_home')"
|
:model-value="themeStore.getFeatureFlag('worlds_in_home')"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
() => {
|
() => {
|
||||||
const newValue = !themeStore.getFeatureFlag('worlds_in_home')
|
const newValue = !themeStore.getFeatureFlag('worlds_in_home')
|
||||||
themeStore.featureFlags['worlds_in_home'] = newValue
|
themeStore.featureFlags['worlds_in_home'] = newValue
|
||||||
settings.feature_flags['worlds_in_home'] = newValue
|
settings.feature_flags['worlds_in_home'] = newValue
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
||||||
<p class="m-0 mt-1">Enables the ability to toggle the sidebar.</p>
|
<p class="m-0 mt-1">Enables the ability to toggle the sidebar.</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
id="toggle-sidebar"
|
id="toggle-sidebar"
|
||||||
:model-value="settings.toggle_sidebar"
|
:model-value="settings.toggle_sidebar"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(e) => {
|
(e) => {
|
||||||
settings.toggle_sidebar = e
|
settings.toggle_sidebar = !!e
|
||||||
themeStore.toggleSidebar = settings.toggle_sidebar
|
themeStore.toggleSidebar = settings.toggle_sidebar
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
import { injectNotificationManager, Slider, Toggle } from '@modrinth/ui'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { get_max_memory } from '@/helpers/jre'
|
|
||||||
import { handleError } from '@/store/notifications'
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
import { Slider, Toggle } from '@modrinth/ui'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const fetchSettings = await get()
|
const fetchSettings = await get()
|
||||||
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
||||||
@@ -11,162 +13,167 @@ fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).jo
|
|||||||
|
|
||||||
const settings = ref(fetchSettings)
|
const settings = ref(fetchSettings)
|
||||||
|
|
||||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
|
||||||
|
maxMemory: number
|
||||||
|
snapPoints: number[]
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||||
|
|
||||||
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
||||||
setSettings.custom_env_vars = setSettings.envVars
|
setSettings.custom_env_vars = setSettings.envVars
|
||||||
.trim()
|
.trim()
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((x) => x.split('=').filter(Boolean))
|
.map((x) => x.split('=').filter(Boolean))
|
||||||
|
|
||||||
if (!setSettings.hooks.pre_launch) {
|
if (!setSettings.hooks.pre_launch) {
|
||||||
setSettings.hooks.pre_launch = null
|
setSettings.hooks.pre_launch = null
|
||||||
}
|
}
|
||||||
if (!setSettings.hooks.wrapper) {
|
if (!setSettings.hooks.wrapper) {
|
||||||
setSettings.hooks.wrapper = null
|
setSettings.hooks.wrapper = null
|
||||||
}
|
}
|
||||||
if (!setSettings.hooks.post_exit) {
|
if (!setSettings.hooks.post_exit) {
|
||||||
setSettings.hooks.post_exit = null
|
setSettings.hooks.post_exit = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!setSettings.custom_dir) {
|
if (!setSettings.custom_dir) {
|
||||||
setSettings.custom_dir = null
|
setSettings.custom_dir = null
|
||||||
}
|
}
|
||||||
|
|
||||||
await set(setSettings)
|
await set(setSettings)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Window size</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Window size</h2>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Fullscreen</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Fullscreen</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
Overwrites the options.txt file to start in full screen when launched.
|
Overwrites the options.txt file to start in full screen when launched.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
|
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Width</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Width</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The width of the game window when launched.
|
The width of the game window when launched.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
id="width"
|
id="width"
|
||||||
v-model="settings.game_resolution[0]"
|
v-model="settings.game_resolution[0]"
|
||||||
:disabled="settings.force_fullscreen"
|
:disabled="settings.force_fullscreen"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Enter width..."
|
placeholder="Enter width..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Height</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Height</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The height of the game window when launched.
|
The height of the game window when launched.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
id="height"
|
id="height"
|
||||||
v-model="settings.game_resolution[1]"
|
v-model="settings.game_resolution[1]"
|
||||||
:disabled="settings.force_fullscreen"
|
:disabled="settings.force_fullscreen"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="number"
|
type="number"
|
||||||
class="input"
|
class="input"
|
||||||
placeholder="Enter height..."
|
placeholder="Enter height..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||||
|
|
||||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Memory allocated</h2>
|
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Memory allocated</h2>
|
||||||
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
||||||
<Slider
|
<Slider
|
||||||
id="max-memory"
|
id="max-memory"
|
||||||
v-model="settings.memory.maximum"
|
v-model="settings.memory.maximum"
|
||||||
:min="512"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
unit="MB"
|
:snap-points="snapPoints"
|
||||||
/>
|
:snap-range="512"
|
||||||
|
unit="MB"
|
||||||
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Java arguments</h2>
|
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Java arguments</h2>
|
||||||
<input
|
<input
|
||||||
id="java-args"
|
id="java-args"
|
||||||
v-model="settings.launchArgs"
|
v-model="settings.launchArgs"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter java arguments..."
|
placeholder="Enter java arguments..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Environmental variables</h2>
|
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Environmental variables</h2>
|
||||||
<input
|
<input
|
||||||
id="env-vars"
|
id="env-vars"
|
||||||
v-model="settings.envVars"
|
v-model="settings.envVars"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter environmental variables..."
|
placeholder="Enter environmental variables..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||||
|
|
||||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Hooks</h2>
|
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Hooks</h2>
|
||||||
|
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Pre launch</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Pre launch</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran before the instance is launched.</p>
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran before the instance is launched.</p>
|
||||||
<input
|
<input
|
||||||
id="pre-launch"
|
id="pre-launch"
|
||||||
v-model="settings.hooks.pre_launch"
|
v-model="settings.hooks.pre_launch"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter pre-launch command..."
|
placeholder="Enter pre-launch command..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Wrapper</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Wrapper</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
Wrapper command for launching Minecraft.
|
Wrapper command for launching Minecraft.
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
id="wrapper"
|
id="wrapper"
|
||||||
v-model="settings.hooks.wrapper"
|
v-model="settings.hooks.wrapper"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter wrapper command..."
|
placeholder="Enter wrapper command..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Post exit</h3>
|
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Post exit</h3>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran after the game closes.</p>
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran after the game closes.</p>
|
||||||
<input
|
<input
|
||||||
id="post-exit"
|
id="post-exit"
|
||||||
v-model="settings.hooks.post_exit"
|
v-model="settings.hooks.post_exit"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter post-exit command..."
|
placeholder="Enter post-exit command..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toggle } from '@modrinth/ui'
|
import { Toggle } from '@modrinth/ui'
|
||||||
import { useTheming } from '@/store/state'
|
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
import { DEFAULT_FEATURE_FLAGS, type FeatureFlag } from '@/store/theme.ts'
|
import { DEFAULT_FEATURE_FLAGS, type FeatureFlag } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
@@ -11,30 +12,30 @@ const settings = ref(await getSettings())
|
|||||||
const options = ref<FeatureFlag[]>(Object.keys(DEFAULT_FEATURE_FLAGS))
|
const options = ref<FeatureFlag[]>(Object.keys(DEFAULT_FEATURE_FLAGS))
|
||||||
|
|
||||||
function setFeatureFlag(key: string, value: boolean) {
|
function setFeatureFlag(key: string, value: boolean) {
|
||||||
themeStore.featureFlags[key] = value
|
themeStore.featureFlags[key] = value
|
||||||
settings.value.feature_flags[key] = value
|
settings.value.feature_flags[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
await setSettings(settings.value)
|
await setSettings(settings.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
|
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
|
||||||
{{ option.replaceAll('_', ' ') }}
|
{{ option.replaceAll('_', ' ') }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
id="advanced-rendering"
|
id="advanced-rendering"
|
||||||
:model-value="themeStore.getFeatureFlag(option)"
|
:model-value="themeStore.getFeatureFlag(option)"
|
||||||
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const javaVersions = ref(await get_java_versions().catch(handleError))
|
const javaVersions = ref(await get_java_versions().catch(handleError))
|
||||||
async function updateJavaVersion(version) {
|
async function updateJavaVersion(version) {
|
||||||
if (version?.path === '') {
|
if (version?.path === '') {
|
||||||
version.path = undefined
|
version.path = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version?.path) {
|
if (version?.path) {
|
||||||
version.path = version.path.replace('java.exe', 'javaw.exe')
|
version.path = version.path.replace('java.exe', 'javaw.exe')
|
||||||
}
|
}
|
||||||
|
|
||||||
await set_java_version(version).catch(handleError)
|
await set_java_version(version).catch(handleError)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-for="(javaVersion, index) in [21, 17, 8]" :key="`java-${javaVersion}`">
|
<div v-for="(javaVersion, index) in [25, 21, 17, 8]" :key="`java-${javaVersion}`">
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast" :class="{ 'mt-4': index !== 0 }">
|
<h2 class="m-0 text-lg font-extrabold text-contrast" :class="{ 'mt-4': index !== 0 }">
|
||||||
Java {{ javaVersion }} location
|
Java {{ javaVersion }} location
|
||||||
</h2>
|
</h2>
|
||||||
<JavaSelector
|
<JavaSelector
|
||||||
:id="'java-selector-' + javaVersion"
|
:id="'java-selector-' + javaVersion"
|
||||||
v-model="javaVersions[javaVersion]"
|
v-model="javaVersions[javaVersion]"
|
||||||
:version="javaVersion"
|
:version="javaVersion"
|
||||||
@update:model-value="updateJavaVersion"
|
@update:model-value="updateJavaVersion"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,64 +1,65 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
|
||||||
import { Toggle } from '@modrinth/ui'
|
import { Toggle } from '@modrinth/ui'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
||||||
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
if (settings.value.telemetry) {
|
if (settings.value.telemetry) {
|
||||||
optInAnalytics()
|
optInAnalytics()
|
||||||
} else {
|
} else {
|
||||||
optOutAnalytics()
|
optOutAnalytics()
|
||||||
}
|
}
|
||||||
|
|
||||||
await set(settings.value)
|
await set(settings.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Personalized ads</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Personalized ads</h2>
|
||||||
<p class="m-0 text-sm">
|
<p class="m-0 text-sm">
|
||||||
(Hard disabled by AR) • Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
||||||
option, you opt out and ads will no longer be shown based on your interests.
|
option, you opt out and ads will no longer be shown based on your interests.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- AstralRinth disabled element by default -->
|
<!-- [AR] Patch. Disabled element by default -->
|
||||||
<Toggle id="personalized-ads" v-model="settings.personalized_ads" :disabled="!settings.personalized_ads" />
|
<Toggle id="personalized-ads" v-model="settings.personalized_ads" :disabled="!settings.personalized_ads" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between gap-4">
|
<div class="mt-4 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Telemetry</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Telemetry</h2>
|
||||||
<p class="m-0 text-sm">
|
<p class="m-0 text-sm">
|
||||||
(Hard disabled by AR) • Modrinth collects anonymized analytics and usage data to improve our user experience and
|
Modrinth collects anonymized analytics and usage data to improve our user experience and
|
||||||
customize your experience. By disabling this option, you opt out and your data will no
|
customize your experience. By disabling this option, you opt out and your data will no
|
||||||
longer be collected.
|
longer be collected.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- AstralRinth disabled element by default -->
|
<!-- [AR] Patch. Disabled element by default -->
|
||||||
<Toggle id="opt-out-analytics" v-model="settings.telemetry" :disabled="!settings.telemetry" />
|
<Toggle id="opt-out-analytics" v-model="settings.telemetry" :disabled="!settings.telemetry" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between gap-4">
|
<div class="mt-4 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Discord RPC</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Discord RPC</h2>
|
||||||
<p class="m-0 text-sm">
|
<p class="m-0 text-sm">
|
||||||
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
|
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
|
||||||
longer show up as a game or app you are using on your Discord profile.
|
longer show up as a game or app you are using on your Discord profile.
|
||||||
</p>
|
</p>
|
||||||
<p class="m-0 mt-2 text-sm">
|
<p class="m-0 mt-2 text-sm">
|
||||||
Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
|
Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
|
||||||
as those added by mods. (app restart required to take effect)
|
as those added by mods. (app restart required to take effect)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" />
|
<Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,117 +1,118 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Button, Slider } from '@modrinth/ui'
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
|
||||||
import { purge_cache_types } from '@/helpers/cache.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import { Button, injectNotificationManager, Slider } from '@modrinth/ui'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { purge_cache_types } from '@/helpers/cache.js'
|
||||||
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||||
|
|
||||||
if (!setSettings.custom_dir) {
|
if (!setSettings.custom_dir) {
|
||||||
setSettings.custom_dir = null
|
setSettings.custom_dir = null
|
||||||
}
|
}
|
||||||
|
|
||||||
await set(setSettings)
|
await set(setSettings)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
async function purgeCache() {
|
async function purgeCache() {
|
||||||
await purge_cache_types([
|
await purge_cache_types([
|
||||||
'project',
|
'project',
|
||||||
'version',
|
'version',
|
||||||
'user',
|
'user',
|
||||||
'team',
|
'team',
|
||||||
'organization',
|
'organization',
|
||||||
'loader_manifest',
|
'loader_manifest',
|
||||||
'minecraft_manifest',
|
'minecraft_manifest',
|
||||||
'categories',
|
'categories',
|
||||||
'report_types',
|
'report_types',
|
||||||
'loaders',
|
'loaders',
|
||||||
'game_versions',
|
'game_versions',
|
||||||
'donation_platforms',
|
'donation_platforms',
|
||||||
'file_update',
|
'file_update',
|
||||||
'search_results',
|
'search_results',
|
||||||
]).catch(handleError)
|
]).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findLauncherDir() {
|
async function findLauncherDir() {
|
||||||
const newDir = await open({
|
const newDir = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
directory: true,
|
directory: true,
|
||||||
title: 'Select a new app directory',
|
title: 'Select a new app directory',
|
||||||
})
|
})
|
||||||
|
|
||||||
if (newDir) {
|
if (newDir) {
|
||||||
settings.value.custom_dir = newDir
|
settings.value.custom_dir = newDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">App directory</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">App directory</h2>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The directory where the launcher stores all of its files. Changes will be applied after
|
The directory where the launcher stores all of its files. Changes will be applied after
|
||||||
restarting the launcher.
|
restarting the launcher.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="m-1 my-2">
|
<div class="m-1 my-2">
|
||||||
<div class="iconified-input w-full">
|
<div class="iconified-input w-full">
|
||||||
<BoxIcon />
|
<BoxIcon />
|
||||||
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
||||||
<Button class="r-btn" @click="findLauncherDir">
|
<Button class="r-btn" @click="findLauncherDir">
|
||||||
<FolderSearchIcon />
|
<FolderSearchIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="purgeCacheConfirmModal"
|
ref="purgeCacheConfirmModal"
|
||||||
title="Are you sure you want to purge the cache?"
|
title="Are you sure you want to purge the cache?"
|
||||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||||
:has-to-type="false"
|
:has-to-type="false"
|
||||||
proceed-label="Purge cache"
|
proceed-label="Purge cache"
|
||||||
:show-ad-on-close="false"
|
:show-ad-on-close="false"
|
||||||
@proceed="purgeCache"
|
@proceed="purgeCache"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">App cache</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">App cache</h2>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
|
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
|
||||||
app to reload data. This may slow down the app temporarily.
|
app to reload data. This may slow down the app temporarily.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
Purge cache
|
Purge cache
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast mt-4">Maximum concurrent downloads</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast mt-4">Maximum concurrent downloads</h2>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The maximum amount of files the launcher can download at the same time. Set this to a lower
|
The maximum amount of files the launcher can download at the same time. Set this to a lower
|
||||||
value if you have a poor internet connection. (app restart required to take effect)
|
value if you have a poor internet connection. (app restart required to take effect)
|
||||||
</p>
|
</p>
|
||||||
<Slider
|
<Slider
|
||||||
id="max-downloads"
|
id="max-downloads"
|
||||||
v-model="settings.max_concurrent_downloads"
|
v-model="settings.max_concurrent_downloads"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="10"
|
:max="10"
|
||||||
:step="1"
|
:step="1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Maximum concurrent writes</h2>
|
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Maximum concurrent writes</h2>
|
||||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||||
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
|
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
|
||||||
value if you are frequently getting I/O errors. (app restart required to take effect)
|
value if you are frequently getting I/O errors. (app restart required to take effect)
|
||||||
</p>
|
</p>
|
||||||
<Slider id="max-writes" v-model="settings.max_concurrent_writes" :min="1" :max="50" :step="1" />
|
<Slider id="max-writes" v-model="settings.max_concurrent_writes" :min="1" :max="50" :step="1" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,135 +1,140 @@
|
|||||||
<template>
|
<template>
|
||||||
<UploadSkinModal ref="uploadModal" />
|
<UploadSkinModal ref="uploadModal" />
|
||||||
<ModalWrapper ref="modal" @on-hide="resetState">
|
<ModalWrapper ref="modal" @on-hide="resetState">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="text-lg font-extrabold text-contrast">
|
<span class="text-lg font-extrabold text-contrast">
|
||||||
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
|
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row gap-6">
|
<div class="flex flex-col md:flex-row gap-6">
|
||||||
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||||
<SkinPreviewRenderer
|
<SkinPreviewRenderer
|
||||||
:variant="variant"
|
:variant="variant"
|
||||||
:texture-src="previewSkin || ''"
|
:texture-src="previewSkin || ''"
|
||||||
:cape-src="selectedCapeTexture"
|
:cape-src="selectedCapeTexture"
|
||||||
:scale="1.4"
|
:scale="1.4"
|
||||||
:fov="50"
|
:fov="50"
|
||||||
:initial-rotation="Math.PI / 8"
|
:initial-rotation="Math.PI / 8"
|
||||||
class="h-full w-full"
|
class="h-full w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
|
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-base font-semibold mb-2">Texture</h2>
|
<h2 class="text-base font-semibold mb-2">Texture</h2>
|
||||||
<Button @click="openUploadSkinModal"> <UploadIcon /> Replace texture </Button>
|
<Button @click="openUploadSkinModal"> <UploadIcon /> Replace texture </Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-base font-semibold mb-2">Arm style</h2>
|
<h2 class="text-base font-semibold mb-2">Arm style</h2>
|
||||||
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
|
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
|
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
|
||||||
</template>
|
</template>
|
||||||
</RadioButtons>
|
</RadioButtons>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-base font-semibold mb-2">Cape</h2>
|
<h2 class="text-base font-semibold mb-2">Cape</h2>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<CapeButton
|
<CapeButton
|
||||||
v-if="defaultCape"
|
v-if="defaultCape"
|
||||||
:id="defaultCape.id"
|
:id="defaultCape.id"
|
||||||
:texture="defaultCape.texture"
|
:texture="defaultCape.texture"
|
||||||
:name="undefined"
|
:name="undefined"
|
||||||
:selected="!selectedCape"
|
:selected="!selectedCape"
|
||||||
faded
|
faded
|
||||||
@select="selectCape(undefined)"
|
@select="selectCape(undefined)"
|
||||||
>
|
>
|
||||||
<span>Use default cape</span>
|
<span>Use default cape</span>
|
||||||
</CapeButton>
|
</CapeButton>
|
||||||
<CapeLikeTextButton v-else :highlighted="!selectedCape" @click="selectCape(undefined)">
|
<CapeLikeTextButton v-else :highlighted="!selectedCape" @click="selectCape(undefined)">
|
||||||
<span>Use default cape</span>
|
<span>Use default cape</span>
|
||||||
</CapeLikeTextButton>
|
</CapeLikeTextButton>
|
||||||
|
|
||||||
<CapeButton
|
<CapeButton
|
||||||
v-for="cape in visibleCapeList"
|
v-for="cape in visibleCapeList"
|
||||||
:id="cape.id"
|
:id="cape.id"
|
||||||
:key="cape.id"
|
:key="cape.id"
|
||||||
:texture="cape.texture"
|
:texture="cape.texture"
|
||||||
:name="cape.name || 'Cape'"
|
:name="cape.name || 'Cape'"
|
||||||
:selected="selectedCape?.id === cape.id"
|
:selected="selectedCape?.id === cape.id"
|
||||||
@select="selectCape(cape)"
|
@select="selectCape(cape)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CapeLikeTextButton
|
<CapeLikeTextButton
|
||||||
v-if="(capes?.length ?? 0) > 2"
|
v-if="(capes?.length ?? 0) > 2"
|
||||||
tooltip="View more capes"
|
tooltip="View more capes"
|
||||||
@mouseup="openSelectCapeModal"
|
@mouseup="openSelectCapeModal"
|
||||||
>
|
>
|
||||||
<template #icon><ChevronRightIcon /></template>
|
<template #icon><ChevronRightIcon /></template>
|
||||||
<span>More</span>
|
<span>More</span>
|
||||||
</CapeLikeTextButton>
|
</CapeLikeTextButton>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 mt-12">
|
<div class="flex gap-2 mt-12">
|
||||||
<ButtonStyled color="brand" :disabled="disableSave || isSaving">
|
<ButtonStyled color="brand" :disabled="disableSave || isSaving">
|
||||||
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
||||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||||
<CheckIcon v-else-if="mode === 'new'" />
|
<CheckIcon v-else-if="mode === 'new'" />
|
||||||
<SaveIcon v-else />
|
<SaveIcon v-else />
|
||||||
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
|
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<Button :disabled="isSaving" @click="hide"><XIcon />Cancel</Button>
|
<Button :disabled="isSaving" @click="hide"><XIcon />Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
|
|
||||||
<SelectCapeModal
|
<SelectCapeModal
|
||||||
ref="selectCapeModal"
|
ref="selectCapeModal"
|
||||||
:capes="capes || []"
|
:capes="capes || []"
|
||||||
@select="handleCapeSelected"
|
@select="handleCapeSelected"
|
||||||
@cancel="handleCapeCancel"
|
@cancel="handleCapeCancel"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, useTemplateRef } from 'vue'
|
|
||||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
|
||||||
import {
|
import {
|
||||||
SkinPreviewRenderer,
|
CheckIcon,
|
||||||
Button,
|
ChevronRightIcon,
|
||||||
RadioButtons,
|
SaveIcon,
|
||||||
CapeButton,
|
SpinnerIcon,
|
||||||
CapeLikeTextButton,
|
UploadIcon,
|
||||||
ButtonStyled,
|
XIcon,
|
||||||
} from '@modrinth/ui'
|
|
||||||
import {
|
|
||||||
add_and_equip_custom_skin,
|
|
||||||
remove_custom_skin,
|
|
||||||
unequip_skin,
|
|
||||||
type Skin,
|
|
||||||
type Cape,
|
|
||||||
type SkinModel,
|
|
||||||
get_normalized_skin_texture,
|
|
||||||
} from '@/helpers/skins.ts'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import {
|
|
||||||
UploadIcon,
|
|
||||||
CheckIcon,
|
|
||||||
SaveIcon,
|
|
||||||
XIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
SpinnerIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonStyled,
|
||||||
|
CapeButton,
|
||||||
|
CapeLikeTextButton,
|
||||||
|
injectNotificationManager,
|
||||||
|
RadioButtons,
|
||||||
|
SkinPreviewRenderer,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { computed, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||||
|
import {
|
||||||
|
add_and_equip_custom_skin,
|
||||||
|
type Cape,
|
||||||
|
determineModelType,
|
||||||
|
get_normalized_skin_texture,
|
||||||
|
remove_custom_skin,
|
||||||
|
type Skin,
|
||||||
|
type SkinModel,
|
||||||
|
type SkinTextureUrl,
|
||||||
|
unequip_skin,
|
||||||
|
} from '@/helpers/skins.ts'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const modal = useTemplateRef('modal')
|
const modal = useTemplateRef('modal')
|
||||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||||
@@ -138,7 +143,7 @@ const currentSkin = ref<Skin | null>(null)
|
|||||||
const shouldRestoreModal = ref(false)
|
const shouldRestoreModal = ref(false)
|
||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
|
|
||||||
const uploadedTextureUrl = ref<string | null>(null)
|
const uploadedTextureUrl = ref<SkinTextureUrl | null>(null)
|
||||||
const previewSkin = ref<string>('')
|
const previewSkin = ref<string>('')
|
||||||
|
|
||||||
const variant = ref<SkinModel>('CLASSIC')
|
const variant = ref<SkinModel>('CLASSIC')
|
||||||
@@ -149,264 +154,264 @@ const selectedCapeTexture = computed(() => selectedCape.value?.texture)
|
|||||||
const visibleCapeList = ref<Cape[]>([])
|
const visibleCapeList = ref<Cape[]>([])
|
||||||
|
|
||||||
const sortedCapes = computed(() => {
|
const sortedCapes = computed(() => {
|
||||||
return [...(props.capes || [])].sort((a, b) => {
|
return [...(props.capes || [])].sort((a, b) => {
|
||||||
const nameA = (a.name || '').toLowerCase()
|
const nameA = (a.name || '').toLowerCase()
|
||||||
const nameB = (b.name || '').toLowerCase()
|
const nameB = (b.name || '').toLowerCase()
|
||||||
return nameA.localeCompare(nameB)
|
return nameA.localeCompare(nameB)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function initVisibleCapeList() {
|
function initVisibleCapeList() {
|
||||||
if (!props.capes || props.capes.length === 0) {
|
if (!props.capes || props.capes.length === 0) {
|
||||||
visibleCapeList.value = []
|
visibleCapeList.value = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visibleCapeList.value.length === 0) {
|
if (visibleCapeList.value.length === 0) {
|
||||||
if (selectedCape.value) {
|
if (selectedCape.value) {
|
||||||
const otherCape = getSortedCapeExcluding(selectedCape.value.id)
|
const otherCape = getSortedCapeExcluding(selectedCape.value.id)
|
||||||
visibleCapeList.value = otherCape ? [selectedCape.value, otherCape] : [selectedCape.value]
|
visibleCapeList.value = otherCape ? [selectedCape.value, otherCape] : [selectedCape.value]
|
||||||
} else {
|
} else {
|
||||||
visibleCapeList.value = getSortedCapes(2)
|
visibleCapeList.value = getSortedCapes(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortedCapes(count: number): Cape[] {
|
function getSortedCapes(count: number): Cape[] {
|
||||||
if (!sortedCapes.value || sortedCapes.value.length === 0) return []
|
if (!sortedCapes.value || sortedCapes.value.length === 0) return []
|
||||||
return sortedCapes.value.slice(0, count)
|
return sortedCapes.value.slice(0, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortedCapeExcluding(excludeId: string): Cape | undefined {
|
function getSortedCapeExcluding(excludeId: string): Cape | undefined {
|
||||||
if (!sortedCapes.value || sortedCapes.value.length <= 1) return undefined
|
if (!sortedCapes.value || sortedCapes.value.length <= 1) return undefined
|
||||||
return sortedCapes.value.find((cape) => cape.id !== excludeId)
|
return sortedCapes.value.find((cape) => cape.id !== excludeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPreviewSkin() {
|
async function loadPreviewSkin() {
|
||||||
if (uploadedTextureUrl.value) {
|
if (uploadedTextureUrl.value) {
|
||||||
previewSkin.value = uploadedTextureUrl.value
|
previewSkin.value = uploadedTextureUrl.value.normalized
|
||||||
} else if (currentSkin.value) {
|
} else if (currentSkin.value) {
|
||||||
try {
|
try {
|
||||||
previewSkin.value = await get_normalized_skin_texture(currentSkin.value)
|
previewSkin.value = await get_normalized_skin_texture(currentSkin.value)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load skin texture:', error)
|
console.error('Failed to load skin texture:', error)
|
||||||
previewSkin.value = '/src/assets/skins/steve.png'
|
previewSkin.value = '/src/assets/skins/steve.png'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
previewSkin.value = '/src/assets/skins/steve.png'
|
previewSkin.value = '/src/assets/skins/steve.png'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasEdits = computed(() => {
|
const hasEdits = computed(() => {
|
||||||
if (mode.value !== 'edit') return true
|
if (mode.value !== 'edit') return true
|
||||||
if (uploadedTextureUrl.value) return true
|
if (uploadedTextureUrl.value) return true
|
||||||
if (!currentSkin.value) return false
|
if (!currentSkin.value) return false
|
||||||
if (variant.value !== currentSkin.value.variant) return true
|
if (variant.value !== currentSkin.value.variant) return true
|
||||||
if ((selectedCape.value?.id || null) !== (currentSkin.value.cape_id || null)) return true
|
if ((selectedCape.value?.id || null) !== (currentSkin.value.cape_id || null)) return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const disableSave = computed(
|
const disableSave = computed(
|
||||||
() =>
|
() =>
|
||||||
(mode.value === 'new' && !uploadedTextureUrl.value) ||
|
(mode.value === 'new' && !uploadedTextureUrl.value) ||
|
||||||
(mode.value === 'edit' && !hasEdits.value),
|
(mode.value === 'edit' && !hasEdits.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveTooltip = computed(() => {
|
const saveTooltip = computed(() => {
|
||||||
if (isSaving.value) return 'Saving...'
|
if (isSaving.value) return 'Saving...'
|
||||||
if (mode.value === 'new' && !uploadedTextureUrl.value) return 'Upload a skin first!'
|
if (mode.value === 'new' && !uploadedTextureUrl.value) return 'Upload a skin first!'
|
||||||
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
|
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
mode.value = 'new'
|
mode.value = 'new'
|
||||||
currentSkin.value = null
|
currentSkin.value = null
|
||||||
uploadedTextureUrl.value = null
|
uploadedTextureUrl.value = null
|
||||||
previewSkin.value = ''
|
previewSkin.value = ''
|
||||||
variant.value = 'CLASSIC'
|
variant.value = 'CLASSIC'
|
||||||
selectedCape.value = undefined
|
selectedCape.value = undefined
|
||||||
visibleCapeList.value = []
|
visibleCapeList.value = []
|
||||||
shouldRestoreModal.value = false
|
shouldRestoreModal.value = false
|
||||||
isSaving.value = false
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function show(e: MouseEvent, skin?: Skin) {
|
async function show(e: MouseEvent, skin?: Skin) {
|
||||||
mode.value = skin ? 'edit' : 'new'
|
mode.value = skin ? 'edit' : 'new'
|
||||||
currentSkin.value = skin ?? null
|
currentSkin.value = skin ?? null
|
||||||
if (skin) {
|
if (skin) {
|
||||||
variant.value = skin.variant
|
variant.value = skin.variant
|
||||||
selectedCape.value = props.capes?.find((c) => c.id === skin.cape_id)
|
selectedCape.value = props.capes?.find((c) => c.id === skin.cape_id)
|
||||||
} else {
|
} else {
|
||||||
variant.value = 'CLASSIC'
|
variant.value = 'CLASSIC'
|
||||||
selectedCape.value = undefined
|
selectedCape.value = undefined
|
||||||
}
|
}
|
||||||
visibleCapeList.value = []
|
visibleCapeList.value = []
|
||||||
initVisibleCapeList()
|
initVisibleCapeList()
|
||||||
|
|
||||||
await loadPreviewSkin()
|
await loadPreviewSkin()
|
||||||
|
|
||||||
modal.value?.show(e)
|
modal.value?.show(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showNew(e: MouseEvent, skinTextureUrl: string) {
|
async function showNew(e: MouseEvent, skinTextureUrl: SkinTextureUrl) {
|
||||||
mode.value = 'new'
|
mode.value = 'new'
|
||||||
currentSkin.value = null
|
currentSkin.value = null
|
||||||
uploadedTextureUrl.value = skinTextureUrl
|
uploadedTextureUrl.value = skinTextureUrl
|
||||||
variant.value = 'CLASSIC'
|
variant.value = await determineModelType(skinTextureUrl.original)
|
||||||
selectedCape.value = undefined
|
selectedCape.value = undefined
|
||||||
visibleCapeList.value = []
|
visibleCapeList.value = []
|
||||||
initVisibleCapeList()
|
initVisibleCapeList()
|
||||||
|
|
||||||
await loadPreviewSkin()
|
await loadPreviewSkin()
|
||||||
|
|
||||||
modal.value?.show(e)
|
modal.value?.show(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreWithNewTexture(skinTextureUrl: string) {
|
async function restoreWithNewTexture(skinTextureUrl: SkinTextureUrl) {
|
||||||
uploadedTextureUrl.value = skinTextureUrl
|
uploadedTextureUrl.value = skinTextureUrl
|
||||||
await loadPreviewSkin()
|
await loadPreviewSkin()
|
||||||
|
|
||||||
if (shouldRestoreModal.value) {
|
if (shouldRestoreModal.value) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
modal.value?.show()
|
modal.value?.show()
|
||||||
shouldRestoreModal.value = false
|
shouldRestoreModal.value = false
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
setTimeout(() => resetState(), 250)
|
setTimeout(() => resetState(), 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectCape(cape: Cape | undefined) {
|
function selectCape(cape: Cape | undefined) {
|
||||||
if (cape && selectedCape.value?.id !== cape.id) {
|
if (cape && selectedCape.value?.id !== cape.id) {
|
||||||
const isInVisibleList = visibleCapeList.value.some((c) => c.id === cape.id)
|
const isInVisibleList = visibleCapeList.value.some((c) => c.id === cape.id)
|
||||||
if (!isInVisibleList && visibleCapeList.value.length > 0) {
|
if (!isInVisibleList && visibleCapeList.value.length > 0) {
|
||||||
visibleCapeList.value.splice(0, 1, cape)
|
visibleCapeList.value.splice(0, 1, cape)
|
||||||
|
|
||||||
if (visibleCapeList.value.length > 1 && visibleCapeList.value[1].id === cape.id) {
|
if (visibleCapeList.value.length > 1 && visibleCapeList.value[1].id === cape.id) {
|
||||||
const otherCape = getSortedCapeExcluding(cape.id)
|
const otherCape = getSortedCapeExcluding(cape.id)
|
||||||
if (otherCape) {
|
if (otherCape) {
|
||||||
visibleCapeList.value.splice(1, 1, otherCape)
|
visibleCapeList.value.splice(1, 1, otherCape)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectedCape.value = cape
|
selectedCape.value = cape
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCapeSelected(cape: Cape | undefined) {
|
function handleCapeSelected(cape: Cape | undefined) {
|
||||||
selectCape(cape)
|
selectCape(cape)
|
||||||
|
|
||||||
if (shouldRestoreModal.value) {
|
if (shouldRestoreModal.value) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
modal.value?.show()
|
modal.value?.show()
|
||||||
shouldRestoreModal.value = false
|
shouldRestoreModal.value = false
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCapeCancel() {
|
function handleCapeCancel() {
|
||||||
if (shouldRestoreModal.value) {
|
if (shouldRestoreModal.value) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
modal.value?.show()
|
modal.value?.show()
|
||||||
shouldRestoreModal.value = false
|
shouldRestoreModal.value = false
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSelectCapeModal(e: MouseEvent) {
|
function openSelectCapeModal(e: MouseEvent) {
|
||||||
if (!selectCapeModal.value) return
|
if (!selectCapeModal.value) return
|
||||||
|
|
||||||
shouldRestoreModal.value = true
|
shouldRestoreModal.value = true
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
selectCapeModal.value?.show(
|
selectCapeModal.value?.show(
|
||||||
e,
|
e,
|
||||||
currentSkin.value?.texture_key,
|
currentSkin.value?.texture_key,
|
||||||
selectedCape.value,
|
selectedCape.value,
|
||||||
previewSkin.value,
|
previewSkin.value,
|
||||||
variant.value,
|
variant.value,
|
||||||
)
|
)
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openUploadSkinModal(e: MouseEvent) {
|
function openUploadSkinModal(e: MouseEvent) {
|
||||||
shouldRestoreModal.value = true
|
shouldRestoreModal.value = true
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
emit('open-upload-modal', e)
|
emit('open-upload-modal', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreModal() {
|
function restoreModal() {
|
||||||
if (shouldRestoreModal.value) {
|
if (shouldRestoreModal.value) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const fakeEvent = new MouseEvent('click')
|
const fakeEvent = new MouseEvent('click')
|
||||||
modal.value?.show(fakeEvent)
|
modal.value?.show(fakeEvent)
|
||||||
shouldRestoreModal.value = false
|
shouldRestoreModal.value = false
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
isSaving.value = true
|
isSaving.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let textureUrl: string
|
let textureUrl: string
|
||||||
|
|
||||||
if (uploadedTextureUrl.value) {
|
if (uploadedTextureUrl.value) {
|
||||||
textureUrl = uploadedTextureUrl.value
|
textureUrl = uploadedTextureUrl.value.original
|
||||||
} else {
|
} else {
|
||||||
textureUrl = currentSkin.value!.texture
|
textureUrl = currentSkin.value!.texture
|
||||||
}
|
}
|
||||||
|
|
||||||
await unequip_skin()
|
await unequip_skin()
|
||||||
|
|
||||||
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
|
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
|
||||||
|
|
||||||
if (mode.value === 'new') {
|
if (mode.value === 'new') {
|
||||||
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||||
emit('saved')
|
emit('saved')
|
||||||
} else {
|
} else {
|
||||||
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||||
await remove_custom_skin(currentSkin.value!)
|
await remove_custom_skin(currentSkin.value!)
|
||||||
emit('saved')
|
emit('saved')
|
||||||
}
|
}
|
||||||
|
|
||||||
hide()
|
hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false
|
isSaving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([uploadedTextureUrl, currentSkin], async () => {
|
watch([uploadedTextureUrl, currentSkin], async () => {
|
||||||
await loadPreviewSkin()
|
await loadPreviewSkin()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.capes,
|
() => props.capes,
|
||||||
() => {
|
() => {
|
||||||
initVisibleCapeList()
|
initVisibleCapeList()
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'saved'): void
|
(event: 'saved'): void
|
||||||
(event: 'deleted', skin: Skin): void
|
(event: 'deleted', skin: Skin): void
|
||||||
(event: 'open-upload-modal', mouseEvent: MouseEvent): void
|
(event: 'open-upload-modal', mouseEvent: MouseEvent): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
showNew,
|
showNew,
|
||||||
restoreWithNewTexture,
|
restoreWithNewTexture,
|
||||||
hide,
|
hide,
|
||||||
shouldRestoreModal,
|
shouldRestoreModal,
|
||||||
restoreModal,
|
restoreModal,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTemplateRef, ref, computed } from 'vue'
|
|
||||||
import type { Cape, SkinModel } from '@/helpers/skins.ts'
|
|
||||||
import {
|
|
||||||
ButtonStyled,
|
|
||||||
ScrollablePanel,
|
|
||||||
CapeButton,
|
|
||||||
CapeLikeTextButton,
|
|
||||||
SkinPreviewRenderer,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
ButtonStyled,
|
||||||
|
CapeButton,
|
||||||
|
CapeLikeTextButton,
|
||||||
|
ScrollablePanel,
|
||||||
|
SkinPreviewRenderer,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import type { Cape, SkinModel } from '@/helpers/skins.ts'
|
||||||
|
|
||||||
const modal = useTemplateRef('modal')
|
const modal = useTemplateRef('modal')
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'select', cape: Cape | undefined): void
|
(e: 'select', cape: Cape | undefined): void
|
||||||
(e: 'cancel'): void
|
(e: 'cancel'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
capes: Cape[]
|
capes: Cape[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const sortedCapes = computed(() => {
|
const sortedCapes = computed(() => {
|
||||||
return [...props.capes].sort((a, b) => {
|
return [...props.capes].sort((a, b) => {
|
||||||
const nameA = (a.name || '').toLowerCase()
|
const nameA = (a.name || '').toLowerCase()
|
||||||
const nameB = (b.name || '').toLowerCase()
|
const nameB = (b.name || '').toLowerCase()
|
||||||
return nameA.localeCompare(nameB)
|
return nameA.localeCompare(nameB)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentSkinId = ref<string | undefined>()
|
const currentSkinId = ref<string | undefined>()
|
||||||
@@ -37,104 +38,104 @@ const currentCapeTexture = computed<string | undefined>(() => currentCape.value?
|
|||||||
const currentCape = ref<Cape | undefined>()
|
const currentCape = ref<Cape | undefined>()
|
||||||
|
|
||||||
function show(
|
function show(
|
||||||
e: MouseEvent,
|
e: MouseEvent,
|
||||||
skinId?: string,
|
skinId?: string,
|
||||||
selected?: Cape,
|
selected?: Cape,
|
||||||
skinTexture?: string,
|
skinTexture?: string,
|
||||||
variant?: SkinModel,
|
variant?: SkinModel,
|
||||||
) {
|
) {
|
||||||
currentSkinId.value = skinId
|
currentSkinId.value = skinId
|
||||||
currentSkinTexture.value = skinTexture
|
currentSkinTexture.value = skinTexture
|
||||||
currentSkinVariant.value = variant || 'CLASSIC'
|
currentSkinVariant.value = variant || 'CLASSIC'
|
||||||
currentCape.value = selected
|
currentCape.value = selected
|
||||||
modal.value?.show(e)
|
modal.value?.show(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
function select() {
|
function select() {
|
||||||
emit('select', currentCape.value)
|
emit('select', currentCape.value)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
emit('cancel')
|
emit('cancel')
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelectedCape(cape: Cape | undefined) {
|
function updateSelectedCape(cape: Cape | undefined) {
|
||||||
currentCape.value = cape
|
currentCape.value = cape
|
||||||
}
|
}
|
||||||
|
|
||||||
function onModalHide() {
|
function onModalHide() {
|
||||||
emit('cancel')
|
emit('cancel')
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
hide,
|
hide,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal" @on-hide="onModalHide">
|
<ModalWrapper ref="modal" @on-hide="onModalHide">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-lg font-extrabold text-heading">Change cape</span>
|
<span class="text-lg font-extrabold text-heading">Change cape</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex flex-col md:flex-row gap-6">
|
<div class="flex flex-col md:flex-row gap-6">
|
||||||
<div class="max-h-[25rem] h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
<div class="max-h-[25rem] h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||||
<SkinPreviewRenderer
|
<SkinPreviewRenderer
|
||||||
v-if="currentSkinTexture"
|
v-if="currentSkinTexture"
|
||||||
:cape-src="currentCapeTexture"
|
:cape-src="currentCapeTexture"
|
||||||
:texture-src="currentSkinTexture"
|
:texture-src="currentSkinTexture"
|
||||||
:variant="currentSkinVariant"
|
:variant="currentSkinVariant"
|
||||||
:scale="1.4"
|
:scale="1.4"
|
||||||
:fov="50"
|
:fov="50"
|
||||||
:initial-rotation="Math.PI + Math.PI / 8"
|
:initial-rotation="Math.PI + Math.PI / 8"
|
||||||
class="h-full w-full"
|
class="h-full w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 w-full my-auto">
|
<div class="flex flex-col gap-4 w-full my-auto">
|
||||||
<ScrollablePanel class="max-h-[20rem] max-w-[30rem] mb-5 h-full">
|
<ScrollablePanel class="max-h-[20rem] max-w-[30rem] mb-5 h-full">
|
||||||
<div class="flex flex-wrap gap-2 justify-center content-start overflow-y-auto h-full">
|
<div class="flex flex-wrap gap-2 justify-center content-start overflow-y-auto h-full">
|
||||||
<CapeLikeTextButton
|
<CapeLikeTextButton
|
||||||
tooltip="No Cape"
|
tooltip="No Cape"
|
||||||
:highlighted="!currentCape"
|
:highlighted="!currentCape"
|
||||||
@click="updateSelectedCape(undefined)"
|
@click="updateSelectedCape(undefined)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</template>
|
</template>
|
||||||
<span>None</span>
|
<span>None</span>
|
||||||
</CapeLikeTextButton>
|
</CapeLikeTextButton>
|
||||||
<CapeButton
|
<CapeButton
|
||||||
v-for="cape in sortedCapes"
|
v-for="cape in sortedCapes"
|
||||||
:id="cape.id"
|
:id="cape.id"
|
||||||
:key="cape.id"
|
:key="cape.id"
|
||||||
:name="cape.name"
|
:name="cape.name"
|
||||||
:texture="cape.texture"
|
:texture="cape.texture"
|
||||||
:selected="currentCape?.id === cape.id"
|
:selected="currentCape?.id === cape.id"
|
||||||
@select="updateSelectedCape(cape)"
|
@select="updateSelectedCape(cape)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ScrollablePanel>
|
</ScrollablePanel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button @click="select">
|
<button @click="select">
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
Select
|
Select
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="hide">
|
<button @click="hide">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,40 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal" @on-hide="hide(true)">
|
<ModalWrapper ref="modal" @on-hide="hide(true)">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
|
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
|
||||||
</template>
|
</template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div
|
<div
|
||||||
class="border-2 border-dashed border-highlight-gray rounded-xl h-[173px] flex flex-col items-center justify-center p-8 cursor-pointer bg-button-bg hover:bg-button-hover transition-colors relative"
|
class="border-2 border-dashed border-highlight-gray rounded-xl h-[173px] flex flex-col items-center justify-center p-8 cursor-pointer bg-button-bg hover:bg-button-hover transition-colors relative"
|
||||||
@click="triggerFileInput"
|
@click="triggerFileInput"
|
||||||
>
|
>
|
||||||
<p class="mx-auto mb-0 text-primary font-bold text-lg text-center flex items-center gap-2">
|
<p class="mx-auto mb-0 text-primary font-bold text-lg text-center flex items-center gap-2">
|
||||||
<UploadIcon /> Select skin texture file
|
<UploadIcon /> Select skin texture file
|
||||||
</p>
|
</p>
|
||||||
<p class="mx-auto mt-0 text-secondary text-sm text-center">
|
<p class="mx-auto mt-0 text-secondary text-sm text-center">
|
||||||
Drag and drop or click here to browse
|
Drag and drop or click here to browse
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/png"
|
accept="image/png"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@change="handleInputFileChange"
|
@change="handleInputFileChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onBeforeUnmount, watch } from 'vue'
|
|
||||||
import { UploadIcon } from '@modrinth/assets'
|
import { UploadIcon } from '@modrinth/assets'
|
||||||
import { useNotifications } from '@/store/state'
|
import { injectNotificationManager } from '@modrinth/ui'
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
|
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { get_dragged_skin_data } from '@/helpers/skins'
|
import { get_dragged_skin_data } from '@/helpers/skins'
|
||||||
|
|
||||||
const notifications = useNotifications()
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
const fileInput = ref<HTMLInputElement>()
|
const fileInput = ref<HTMLInputElement>()
|
||||||
@@ -42,98 +43,98 @@ const unlisten = ref<() => void>()
|
|||||||
const modalVisible = ref(false)
|
const modalVisible = ref(false)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'uploaded', data: ArrayBuffer): void
|
(e: 'uploaded', data: ArrayBuffer): void
|
||||||
(e: 'canceled'): void
|
(e: 'canceled'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function show(e?: MouseEvent) {
|
function show(e?: MouseEvent) {
|
||||||
modal.value?.show(e)
|
modal.value?.show(e)
|
||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
setupDragDropListener()
|
setupDragDropListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide(emitCanceled = false) {
|
function hide(emitCanceled = false) {
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
modalVisible.value = false
|
modalVisible.value = false
|
||||||
cleanupDragDropListener()
|
cleanupDragDropListener()
|
||||||
resetState()
|
resetState()
|
||||||
if (emitCanceled) {
|
if (emitCanceled) {
|
||||||
emit('canceled')
|
emit('canceled')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
if (fileInput.value) fileInput.value.value = ''
|
if (fileInput.value) fileInput.value.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerFileInput() {
|
function triggerFileInput() {
|
||||||
fileInput.value?.click()
|
fileInput.value?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleInputFileChange(e: Event) {
|
async function handleInputFileChange(e: Event) {
|
||||||
const files = (e.target as HTMLInputElement).files
|
const files = (e.target as HTMLInputElement).files
|
||||||
if (!files || files.length === 0) {
|
if (!files || files.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const file = files[0]
|
const file = files[0]
|
||||||
const buffer = await file.arrayBuffer()
|
const buffer = await file.arrayBuffer()
|
||||||
await processData(buffer)
|
await processData(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupDragDropListener() {
|
async function setupDragDropListener() {
|
||||||
try {
|
try {
|
||||||
if (modalVisible.value) {
|
if (modalVisible.value) {
|
||||||
await cleanupDragDropListener()
|
await cleanupDragDropListener()
|
||||||
unlisten.value = await getCurrentWebview().onDragDropEvent(async (event) => {
|
unlisten.value = await getCurrentWebview().onDragDropEvent(async (event) => {
|
||||||
if (event.payload.type !== 'drop') {
|
if (event.payload.type !== 'drop') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!event.payload.paths || event.payload.paths.length === 0) {
|
if (!event.payload.paths || event.payload.paths.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = event.payload.paths[0]
|
const filePath = event.payload.paths[0]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await get_dragged_skin_data(filePath)
|
const data = await get_dragged_skin_data(filePath)
|
||||||
await processData(data.buffer)
|
await processData(data.buffer)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.addNotification({
|
addNotification({
|
||||||
title: 'Error processing file',
|
title: 'Error processing file',
|
||||||
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to set up drag and drop listener:', error)
|
console.error('Failed to set up drag and drop listener:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupDragDropListener() {
|
async function cleanupDragDropListener() {
|
||||||
if (unlisten.value) {
|
if (unlisten.value) {
|
||||||
unlisten.value()
|
unlisten.value()
|
||||||
unlisten.value = undefined
|
unlisten.value = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processData(buffer: ArrayBuffer) {
|
async function processData(buffer: ArrayBuffer) {
|
||||||
emit('uploaded', buffer)
|
emit('uploaded', buffer)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(modalVisible, (isVisible) => {
|
watch(modalVisible, (isVisible) => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
setupDragDropListener()
|
setupDragDropListener()
|
||||||
} else {
|
} else {
|
||||||
cleanupDragDropListener()
|
cleanupDragDropListener()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
cleanupDragDropListener()
|
cleanupDragDropListener()
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({ show, hide })
|
defineExpose({ show, hide })
|
||||||
|
|||||||
@@ -1,49 +1,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Dayjs } from 'dayjs'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import {
|
import {
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
OverflowMenu,
|
injectNotificationManager,
|
||||||
SmartClickable,
|
OverflowMenu,
|
||||||
useRelativeTime,
|
SmartClickable,
|
||||||
|
useRelativeTime,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { useVIntl } from '@vintl/vintl'
|
|
||||||
import { computed, nextTick, ref, onMounted, onUnmounted } from 'vue'
|
|
||||||
import { showProfileInFolder } from '@/helpers/utils'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import { get_project } from '@/helpers/cache'
|
|
||||||
import { capitalizeString } from '@modrinth/utils'
|
import { capitalizeString } from '@modrinth/utils'
|
||||||
import { kill, run } from '@/helpers/profile'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { handleSevereError } from '@/store/error'
|
import { useVIntl } from '@vintl/vintl'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import type { Dayjs } from 'dayjs'
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
import dayjs from 'dayjs'
|
||||||
import { handleError } from '@/store/notifications'
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { get_project } from '@/helpers/cache'
|
||||||
|
import { process_listener } from '@/helpers/events'
|
||||||
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
|
import { kill, run } from '@/helpers/profile'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import { showProfileInFolder } from '@/helpers/utils'
|
||||||
|
import { handleSevereError } from '@/store/error'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'play' | 'stop'): void
|
(e: 'play' | 'stop'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
last_played: Dayjs
|
last_played: Dayjs
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const loadingModpack = ref(!!props.instance.linked_data)
|
const loadingModpack = ref(!!props.instance.linked_data)
|
||||||
@@ -51,180 +53,180 @@ const loadingModpack = ref(!!props.instance.linked_data)
|
|||||||
const modpack = ref()
|
const modpack = ref()
|
||||||
|
|
||||||
if (props.instance.linked_data) {
|
if (props.instance.linked_data) {
|
||||||
nextTick().then(async () => {
|
nextTick().then(async () => {
|
||||||
modpack.value = await get_project(props.instance.linked_data?.project_id, 'must_revalidate')
|
modpack.value = await get_project(props.instance.linked_data?.project_id, 'must_revalidate')
|
||||||
loadingModpack.value = false
|
loadingModpack.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceIcon = computed(() => props.instance.icon_path)
|
const instanceIcon = computed(() => props.instance.icon_path)
|
||||||
|
|
||||||
const loader = computed(() => {
|
const loader = computed(() => {
|
||||||
if (props.instance.loader === 'vanilla') {
|
if (props.instance.loader === 'vanilla') {
|
||||||
return 'Minecraft'
|
return 'Minecraft'
|
||||||
} else if (props.instance.loader === 'neoforge') {
|
} else if (props.instance.loader === 'neoforge') {
|
||||||
return 'NeoForge'
|
return 'NeoForge'
|
||||||
} else {
|
} else {
|
||||||
return capitalizeString(props.instance.loader)
|
return capitalizeString(props.instance.loader)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
|
|
||||||
const play = async (event: MouseEvent) => {
|
const play = async (event: MouseEvent) => {
|
||||||
event?.stopPropagation()
|
event?.stopPropagation()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await run(props.instance.path)
|
await run(props.instance.path)
|
||||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
trackEvent('InstancePlay', {
|
trackEvent('InstancePlay', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
source: 'InstanceItem',
|
source: 'InstanceItem',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
emit('play')
|
emit('play')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const stop = async (event: MouseEvent) => {
|
const stop = async (event: MouseEvent) => {
|
||||||
event?.stopPropagation()
|
event?.stopPropagation()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await kill(props.instance.path).catch(handleError)
|
await kill(props.instance.path).catch(handleError)
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
source: 'InstanceItem',
|
source: 'InstanceItem',
|
||||||
})
|
})
|
||||||
emit('stop')
|
emit('stop')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlistenProcesses = await process_listener(async () => {
|
const unlistenProcesses = await process_listener(async () => {
|
||||||
await checkProcess()
|
await checkProcess()
|
||||||
})
|
})
|
||||||
|
|
||||||
const checkProcess = async () => {
|
const checkProcess = async () => {
|
||||||
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
||||||
|
|
||||||
playing.value = runningProcesses.length > 0
|
playing.value = runningProcesses.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkProcess()
|
checkProcess()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlistenProcesses()
|
unlistenProcesses()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<SmartClickable>
|
<SmartClickable>
|
||||||
<template #clickable>
|
<template #clickable>
|
||||||
<router-link
|
<router-link
|
||||||
class="no-click-animation"
|
class="no-click-animation"
|
||||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised rounded-xl smart-clickable:highlight-on-hover"
|
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised card-shadow rounded-xl smart-clickable:highlight-on-hover"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
||||||
:tint-by="instance.path"
|
:tint-by="instance.path"
|
||||||
size="48px"
|
size="48px"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col col-span-2 justify-between h-full">
|
<div class="flex flex-col col-span-2 justify-between h-full">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-lg text-contrast font-bold truncate smart-clickable:underline-on-hover">
|
<div class="text-lg text-contrast font-bold truncate smart-clickable:underline-on-hover">
|
||||||
{{ instance.name }}
|
{{ instance.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-sm text-secondary">
|
<div class="flex items-center gap-2 text-sm text-secondary">
|
||||||
<div
|
<div
|
||||||
v-tooltip="
|
v-tooltip="
|
||||||
instance.last_played
|
instance.last_played
|
||||||
? dayjs(instance.last_played).format('MMMM D, YYYY [at] h:mm A')
|
? dayjs(instance.last_played).format('MMMM D, YYYY [at] h:mm A')
|
||||||
: null
|
: null
|
||||||
"
|
"
|
||||||
class="w-fit shrink-0"
|
class="w-fit shrink-0"
|
||||||
:class="{ 'cursor-help smart-clickable:allow-pointer-events': last_played }"
|
:class="{ 'cursor-help smart-clickable:allow-pointer-events': last_played }"
|
||||||
>
|
>
|
||||||
<template v-if="last_played">
|
<template v-if="last_played">
|
||||||
{{
|
{{
|
||||||
formatMessage(commonMessages.playedLabel, {
|
formatMessage(commonMessages.playedLabel, {
|
||||||
time: formatRelativeTime(last_played.toISOString?.()),
|
time: formatRelativeTime(last_played.toISOString?.()),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</template>
|
</template>
|
||||||
<template v-else> Not played yet </template>
|
<template v-else> Not played yet </template>
|
||||||
</div>
|
</div>
|
||||||
•
|
•
|
||||||
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
|
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
|
||||||
<router-link
|
<router-link
|
||||||
class="inline-flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
class="inline-flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
||||||
:to="`/project/${modpack.id}`"
|
:to="`/project/${modpack.id}`"
|
||||||
>
|
>
|
||||||
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />
|
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />
|
||||||
<span class="truncate">{{ modpack.title }}</span>
|
<span class="truncate">{{ modpack.title }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
({{ loader }} {{ instance.game_version }})
|
({{ loader }} {{ instance.game_version }})
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="loadingModpack" class="flex items-center gap-1 truncate text-secondary">
|
<span v-else-if="loadingModpack" class="flex items-center gap-1 truncate text-secondary">
|
||||||
<SpinnerIcon class="animate-spin shrink-0" />
|
<SpinnerIcon class="animate-spin shrink-0" />
|
||||||
<span class="truncate">Loading modpack...</span>
|
<span class="truncate">Loading modpack...</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="flex items-center gap-1 truncate text-secondary">
|
<span v-else class="flex items-center gap-1 truncate text-secondary">
|
||||||
{{ loader }}
|
{{ loader }}
|
||||||
{{ instance.game_version }}
|
{{ instance.game_version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||||
<ButtonStyled v-if="playing && !loading" color="red">
|
<ButtonStyled v-if="playing && !loading" color="red">
|
||||||
<button @click="stop">
|
<button @click="stop">
|
||||||
<StopCircleIcon aria-hidden="true" />
|
<StopCircleIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.stopButton) }}
|
{{ formatMessage(commonMessages.stopButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-else>
|
<ButtonStyled v-else>
|
||||||
<button
|
<button
|
||||||
v-tooltip="playing ? 'Instance is already open' : null"
|
v-tooltip="playing ? 'Instance is already open' : null"
|
||||||
:disabled="playing || loading"
|
:disabled="playing || loading"
|
||||||
@click="play"
|
@click="play"
|
||||||
>
|
>
|
||||||
<SpinnerIcon v-if="loading" class="animate-spin" />
|
<SpinnerIcon v-if="loading" class="animate-spin" />
|
||||||
<PlayIcon v-else aria-hidden="true" />
|
<PlayIcon v-else aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.playButton) }}
|
{{ formatMessage(commonMessages.playButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled circular type="transparent">
|
<ButtonStyled circular type="transparent">
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
id: 'open-instance',
|
id: 'open-instance',
|
||||||
shown: !!instance.path,
|
shown: !!instance.path,
|
||||||
action: () => router.push(encodeURI(`/instance/${instance.path}`)),
|
action: () => router.push(encodeURI(`/instance/${instance.path}`)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'open-folder',
|
id: 'open-folder',
|
||||||
action: () => showProfileInFolder(instance.path),
|
action: () => showProfileInFolder(instance.path),
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
<template #open-instance>
|
<template #open-instance>
|
||||||
<EyeIcon aria-hidden="true" />
|
<EyeIcon aria-hidden="true" />
|
||||||
View instance
|
View instance
|
||||||
</template>
|
</template>
|
||||||
<template #open-folder>
|
<template #open-folder>
|
||||||
<FolderOpenIcon aria-hidden="true" />
|
<FolderOpenIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||||
</template>
|
</template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SmartClickable>
|
</SmartClickable>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,56 +1,66 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { LoaderCircleIcon } from '@modrinth/assets'
|
||||||
type ServerWorld,
|
import type { GameVersion } from '@modrinth/ui'
|
||||||
type ServerData,
|
import { GAME_MODES, HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
||||||
type WorldWithProfile,
|
|
||||||
get_recent_worlds,
|
|
||||||
getWorldIdentifier,
|
|
||||||
get_profile_protocol_version,
|
|
||||||
refreshServerData,
|
|
||||||
start_join_server,
|
|
||||||
start_join_singleplayer_world,
|
|
||||||
} from '@/helpers/worlds.ts'
|
|
||||||
import { HeadingLink, GAME_MODES } from '@modrinth/ui'
|
|
||||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
|
||||||
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
|
||||||
import { watch, onMounted, onUnmounted, ref, computed } from 'vue'
|
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useTheming } from '@/store/theme.ts'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { kill, run } from '@/helpers/profile'
|
|
||||||
import { handleError } from '@/store/notifications'
|
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
||||||
|
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { process_listener, profile_listener } from '@/helpers/events'
|
import { process_listener, profile_listener } from '@/helpers/events'
|
||||||
import { get_all } from '@/helpers/process'
|
import { get_all } from '@/helpers/process'
|
||||||
|
import { kill, run } from '@/helpers/profile'
|
||||||
|
import { get_game_versions } from '@/helpers/tags'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import {
|
||||||
|
get_profile_protocol_version,
|
||||||
|
get_recent_worlds,
|
||||||
|
getWorldIdentifier,
|
||||||
|
hasServerQuickPlaySupport,
|
||||||
|
hasWorldQuickPlaySupport,
|
||||||
|
type ProtocolVersion,
|
||||||
|
refreshServerData,
|
||||||
|
type ServerData,
|
||||||
|
type ServerWorld,
|
||||||
|
start_join_server,
|
||||||
|
start_join_singleplayer_world,
|
||||||
|
type WorldWithProfile,
|
||||||
|
} from '@/helpers/worlds.ts'
|
||||||
import { handleSevereError } from '@/store/error'
|
import { handleSevereError } from '@/store/error'
|
||||||
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
recentInstances: GameInstance[]
|
recentInstances: GameInstance[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const theme = useTheming()
|
const theme = useTheming()
|
||||||
|
|
||||||
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
const serverData = ref<Record<string, ServerData>>({})
|
const serverData = ref<Record<string, ServerData>>({})
|
||||||
const protocolVersions = ref<Record<string, number | null>>({})
|
const protocolVersions = ref<Record<string, ProtocolVersion | null>>({})
|
||||||
|
const gameVersions = ref<GameVersion[]>(await get_game_versions().catch(() => []))
|
||||||
|
|
||||||
const MIN_JUMP_BACK_IN = 3
|
const MIN_JUMP_BACK_IN = 3
|
||||||
const MAX_JUMP_BACK_IN = 6
|
const MAX_JUMP_BACK_IN = 6
|
||||||
const TWO_WEEKS_AGO = dayjs().subtract(14, 'day')
|
const TWO_WEEKS_AGO = dayjs().subtract(14, 'day')
|
||||||
|
|
||||||
type BaseJumpBackInItem = {
|
type BaseJumpBackInItem = {
|
||||||
last_played: Dayjs
|
last_played: Dayjs
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstanceJumpBackInItem = BaseJumpBackInItem & {
|
type InstanceJumpBackInItem = BaseJumpBackInItem & {
|
||||||
type: 'instance'
|
type: 'instance'
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorldJumpBackInItem = BaseJumpBackInItem & {
|
type WorldJumpBackInItem = BaseJumpBackInItem & {
|
||||||
type: 'world'
|
type: 'world'
|
||||||
world: WorldWithProfile
|
world: WorldWithProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
||||||
@@ -58,247 +68,263 @@ type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
|||||||
const showWorlds = computed(() => theme.getFeatureFlag('worlds_in_home'))
|
const showWorlds = computed(() => theme.getFeatureFlag('worlds_in_home'))
|
||||||
|
|
||||||
watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
||||||
await populateJumpBackIn().catch(() => {
|
await populateJumpBackIn().catch(() => {
|
||||||
console.error('Failed to populate jump back in')
|
console.error('Failed to populate jump back in')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await populateJumpBackIn().catch(() => {
|
populateJumpBackIn()
|
||||||
console.error('Failed to populate jump back in')
|
.catch(() => {
|
||||||
})
|
console.error('Failed to populate jump back in')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
async function populateJumpBackIn() {
|
async function populateJumpBackIn() {
|
||||||
console.info('Repopulating jump back in...')
|
console.info('Repopulating jump back in...')
|
||||||
|
|
||||||
const worldItems: WorldJumpBackInItem[] = []
|
const worldItems: WorldJumpBackInItem[] = []
|
||||||
|
|
||||||
if (showWorlds.value) {
|
if (showWorlds.value) {
|
||||||
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
|
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
|
||||||
|
|
||||||
worlds.forEach((world) => {
|
worlds.forEach((world) => {
|
||||||
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
||||||
|
|
||||||
if (!instance || !world.last_played) {
|
if (!instance || !world.last_played) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
worldItems.push({
|
worldItems.push({
|
||||||
type: 'world',
|
type: 'world',
|
||||||
last_played: dayjs(world.last_played ?? 0),
|
last_played: dayjs(world.last_played ?? 0),
|
||||||
world: world,
|
world: world,
|
||||||
instance: instance,
|
instance: instance,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const servers: {
|
const servers: {
|
||||||
instancePath: string
|
instancePath: string
|
||||||
address: string
|
address: string
|
||||||
}[] = worldItems
|
}[] = worldItems
|
||||||
.filter((item) => item.world.type === 'server' && item.instance)
|
.filter((item) => item.world.type === 'server' && item.instance)
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
instancePath: item.instance.path,
|
instancePath: item.instance.path,
|
||||||
address: (item.world as ServerWorld).address,
|
address: (item.world as ServerWorld).address,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// fetch protocol versions for all unique MC versions with server worlds
|
// fetch protocol versions for all unique MC versions with server worlds
|
||||||
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[...uniqueServerInstances].map((path) =>
|
[...uniqueServerInstances].map((path) =>
|
||||||
get_profile_protocol_version(path)
|
get_profile_protocol_version(path)
|
||||||
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.error(`Failed to get profile protocol for: ${path} `)
|
console.error(`Failed to get profile protocol for: ${path} `)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// initialize server data
|
// initialize server data
|
||||||
servers.forEach(({ address }) => {
|
servers.forEach(({ address }) => {
|
||||||
if (!serverData.value[address]) {
|
if (!serverData.value[address]) {
|
||||||
serverData.value[address] = {
|
serverData.value[address] = {
|
||||||
refreshing: true,
|
refreshing: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// fetch each server's data
|
servers.forEach(({ instancePath, address }) =>
|
||||||
Promise.all(
|
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||||
servers.map(({ instancePath, address }) =>
|
)
|
||||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
}
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const instanceItems: InstanceJumpBackInItem[] = []
|
const instanceItems: InstanceJumpBackInItem[] = []
|
||||||
for (const instance of props.recentInstances) {
|
for (const instance of props.recentInstances) {
|
||||||
const worldItem = worldItems.find((item) => item.instance.path === instance.path)
|
const worldItem = worldItems.find((item) => item.instance.path === instance.path)
|
||||||
if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
|
if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceItems.push({
|
instanceItems.push({
|
||||||
type: 'instance',
|
type: 'instance',
|
||||||
last_played: dayjs(instance.last_played ?? 0),
|
last_played: dayjs(instance.last_played ?? 0),
|
||||||
instance: instance,
|
instance: instance,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
|
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
|
||||||
items.sort((a, b) => dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0)))
|
items.sort((a, b) => dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0)))
|
||||||
jumpBackInItems.value = items
|
jumpBackInItems.value = items
|
||||||
.filter((item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO))
|
.filter((item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO))
|
||||||
.slice(0, MAX_JUMP_BACK_IN)
|
.slice(0, MAX_JUMP_BACK_IN)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshServer(address: string, instancePath: string) {
|
function refreshServer(address: string, instancePath: string) {
|
||||||
await refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinWorld(world: WorldWithProfile) {
|
async function joinWorld(world: WorldWithProfile) {
|
||||||
console.log(`Joining world ${getWorldIdentifier(world)}`)
|
console.log(`Joining world ${getWorldIdentifier(world)}`)
|
||||||
if (world.type === 'server') {
|
if (world.type === 'server') {
|
||||||
await start_join_server(world.profile, world.address).catch(handleError)
|
await start_join_server(world.profile, world.address).catch(handleError)
|
||||||
} else if (world.type === 'singleplayer') {
|
} else if (world.type === 'singleplayer') {
|
||||||
await start_join_singleplayer_world(world.profile, world.path).catch(handleError)
|
await start_join_singleplayer_world(world.profile, world.path).catch(handleError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function playInstance(instance: GameInstance) {
|
async function playInstance(instance: GameInstance) {
|
||||||
await run(instance.path)
|
await run(instance.path)
|
||||||
.catch((err) => handleSevereError(err, { profilePath: instance.path }))
|
.catch((err) => handleSevereError(err, { profilePath: instance.path }))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
trackEvent('InstancePlay', {
|
trackEvent('InstancePlay', {
|
||||||
loader: instance.loader,
|
loader: instance.loader,
|
||||||
game_version: instance.game_version,
|
game_version: instance.game_version,
|
||||||
source: 'WorldItem',
|
source: 'WorldItem',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopInstance(path: string) {
|
async function stopInstance(path: string) {
|
||||||
await kill(path).catch(handleError)
|
await kill(path).catch(handleError)
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
source: 'RecentWorldsList',
|
source: 'RecentWorldsList',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentProfile = ref<string>()
|
const currentProfile = ref<string>()
|
||||||
const currentWorld = ref<string>()
|
const currentWorld = ref<string>()
|
||||||
|
|
||||||
const unlistenProcesses = await process_listener(async () => {
|
const unlistenProcesses = await process_listener(async () => {
|
||||||
await checkProcesses()
|
await checkProcesses()
|
||||||
})
|
})
|
||||||
|
|
||||||
const unlistenProfiles = await profile_listener(async () => {
|
const unlistenProfiles = await profile_listener(async () => {
|
||||||
await populateJumpBackIn().catch(() => {
|
await populateJumpBackIn().catch(() => {
|
||||||
console.error('Failed to populate jump back in')
|
console.error('Failed to populate jump back in')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const runningInstances = ref<string[]>([])
|
const runningInstances = ref<string[]>([])
|
||||||
|
|
||||||
type ProcessMetadata = {
|
type ProcessMetadata = {
|
||||||
uuid: string
|
uuid: string
|
||||||
profile_path: string
|
profile_path: string
|
||||||
start_time: string
|
start_time: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkProcesses = async () => {
|
const checkProcesses = async () => {
|
||||||
const runningProcesses: ProcessMetadata[] = await get_all().catch(handleError)
|
const runningProcesses: ProcessMetadata[] = await get_all().catch(handleError)
|
||||||
|
|
||||||
const runningPaths = runningProcesses.map((x) => x.profile_path)
|
const runningPaths = runningProcesses.map((x) => x.profile_path)
|
||||||
|
|
||||||
const stoppedInstances = runningInstances.value.filter((x) => !runningPaths.includes(x))
|
const stoppedInstances = runningInstances.value.filter((x) => !runningPaths.includes(x))
|
||||||
if (currentProfile.value && stoppedInstances.includes(currentProfile.value)) {
|
if (currentProfile.value && stoppedInstances.includes(currentProfile.value)) {
|
||||||
currentProfile.value = undefined
|
currentProfile.value = undefined
|
||||||
currentWorld.value = undefined
|
currentWorld.value = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
runningInstances.value = runningPaths
|
runningInstances.value = runningPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkProcesses()
|
checkProcesses()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlistenProcesses()
|
unlistenProcesses()
|
||||||
unlistenProfiles()
|
unlistenProfiles()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
<div v-if="loading" class="flex flex-col gap-2">
|
||||||
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
<span class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold">
|
||||||
Jump back in
|
Jump back in
|
||||||
</HeadingLink>
|
</span>
|
||||||
<span
|
<div class="text-center py-4">
|
||||||
v-else
|
<LoaderCircleIcon class="mx-auto size-8 animate-spin text-contrast" />
|
||||||
class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold"
|
</div>
|
||||||
>
|
</div>
|
||||||
Jump back in
|
<div v-else-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||||
</span>
|
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
||||||
<div class="grid-when-huge flex flex-col w-full gap-2">
|
Jump back in
|
||||||
<template
|
</HeadingLink>
|
||||||
v-for="item in jumpBackInItems"
|
<span
|
||||||
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
v-else
|
||||||
>
|
class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold"
|
||||||
<WorldItem
|
>
|
||||||
v-if="item.type === 'world'"
|
Jump back in
|
||||||
:world="item.world"
|
</span>
|
||||||
:playing-instance="runningInstances.includes(item.instance.path)"
|
<div class="grid-when-huge flex flex-col w-full gap-2">
|
||||||
:playing-world="
|
<template
|
||||||
currentProfile === item.instance.path && currentWorld === getWorldIdentifier(item.world)
|
v-for="item in jumpBackInItems"
|
||||||
"
|
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
||||||
:refreshing="
|
>
|
||||||
item.world.type === 'server'
|
<WorldItem
|
||||||
? serverData[item.world.address].refreshing && !serverData[item.world.address].status
|
v-if="item.type === 'world'"
|
||||||
: undefined
|
:world="item.world"
|
||||||
"
|
:playing-instance="runningInstances.includes(item.instance.path)"
|
||||||
supports-quick-play
|
:playing-world="
|
||||||
:server-status="
|
currentProfile === item.instance.path && currentWorld === getWorldIdentifier(item.world)
|
||||||
item.world.type === 'server' ? serverData[item.world.address].status : undefined
|
"
|
||||||
"
|
:refreshing="
|
||||||
:rendered-motd="
|
item.world.type === 'server'
|
||||||
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
? serverData[item.world.address].refreshing && !serverData[item.world.address].status
|
||||||
"
|
: undefined
|
||||||
:current-protocol="protocolVersions[item.instance.path]"
|
"
|
||||||
:game-mode="
|
:supports-server-quick-play="
|
||||||
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
item.world.type === 'server' &&
|
||||||
"
|
hasServerQuickPlaySupport(gameVersions, item.instance.game_version || '')
|
||||||
:instance-path="item.instance.path"
|
"
|
||||||
:instance-name="item.instance.name"
|
:supports-world-quick-play="
|
||||||
:instance-icon="item.instance.icon_path"
|
item.world.type === 'singleplayer' &&
|
||||||
@refresh="
|
hasWorldQuickPlaySupport(gameVersions, item.instance.game_version || '')
|
||||||
() =>
|
"
|
||||||
item.world.type === 'server'
|
:server-status="
|
||||||
? refreshServer(item.world.address, item.instance.path)
|
item.world.type === 'server' ? serverData[item.world.address].status : undefined
|
||||||
: {}
|
"
|
||||||
"
|
:rendered-motd="
|
||||||
@update="() => populateJumpBackIn()"
|
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
||||||
@play="
|
"
|
||||||
() => {
|
:current-protocol="protocolVersions[item.instance.path]"
|
||||||
currentProfile = item.instance.path
|
:game-mode="
|
||||||
currentWorld = getWorldIdentifier(item.world)
|
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
||||||
joinWorld(item.world)
|
"
|
||||||
}
|
:instance-path="item.instance.path"
|
||||||
"
|
:instance-name="item.instance.name"
|
||||||
@play-instance="
|
:instance-icon="item.instance.icon_path"
|
||||||
() => {
|
@refresh="
|
||||||
currentProfile = item.instance.path
|
() =>
|
||||||
playInstance(item.instance)
|
item.world.type === 'server'
|
||||||
}
|
? refreshServer(item.world.address, item.instance.path)
|
||||||
"
|
: {}
|
||||||
@stop="() => stopInstance(item.instance.path)"
|
"
|
||||||
/>
|
@update="() => populateJumpBackIn()"
|
||||||
<InstanceItem v-else :instance="item.instance" :last_played="item.last_played" />
|
@play="
|
||||||
</template>
|
() => {
|
||||||
</div>
|
currentProfile = item.instance.path
|
||||||
</div>
|
currentWorld = getWorldIdentifier(item.world)
|
||||||
|
joinWorld(item.world)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@play-instance="
|
||||||
|
() => {
|
||||||
|
currentProfile = item.instance.path
|
||||||
|
playInstance(item.instance)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@stop="() => stopInstance(item.instance.path)"
|
||||||
|
/>
|
||||||
|
<InstanceItem v-else :instance="item.instance" :last_played="item.last_played" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.grid-when-huge {
|
.grid-when-huge {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(670px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(670px, 1fr));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,42 +1,49 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
|
||||||
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
|
||||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
|
||||||
import {
|
import {
|
||||||
useRelativeTime,
|
ClipboardCopyIcon,
|
||||||
Avatar,
|
EditIcon,
|
||||||
ButtonStyled,
|
EyeIcon,
|
||||||
commonMessages,
|
FolderOpenIcon,
|
||||||
OverflowMenu,
|
IssuesIcon,
|
||||||
SmartClickable,
|
MoreVerticalIcon,
|
||||||
} from '@modrinth/ui'
|
NoSignalIcon,
|
||||||
import {
|
PlayIcon,
|
||||||
IssuesIcon,
|
SignalIcon,
|
||||||
EyeIcon,
|
SkullIcon,
|
||||||
ClipboardCopyIcon,
|
SpinnerIcon,
|
||||||
EditIcon,
|
StopCircleIcon,
|
||||||
FolderOpenIcon,
|
TrashIcon,
|
||||||
MoreVerticalIcon,
|
UpdatedIcon,
|
||||||
NoSignalIcon,
|
UserIcon,
|
||||||
PlayIcon,
|
XIcon,
|
||||||
SignalIcon,
|
|
||||||
SkullIcon,
|
|
||||||
SpinnerIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
TrashIcon,
|
|
||||||
UpdatedIcon,
|
|
||||||
UserIcon,
|
|
||||||
XIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
commonMessages,
|
||||||
|
OverflowMenu,
|
||||||
|
SmartClickable,
|
||||||
|
useRelativeTime,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import type { MessageDescriptor } from '@vintl/vintl'
|
import type { MessageDescriptor } from '@vintl/vintl'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { Tooltip } from 'floating-vue'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { copyToClipboard } from '@/helpers/utils'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Tooltip } from 'floating-vue'
|
|
||||||
|
import { copyToClipboard } from '@/helpers/utils'
|
||||||
|
import type {
|
||||||
|
ProtocolVersion,
|
||||||
|
ServerStatus,
|
||||||
|
ServerWorld,
|
||||||
|
SingleplayerWorld,
|
||||||
|
World,
|
||||||
|
} from '@/helpers/worlds.ts'
|
||||||
|
import { getWorldIdentifier, set_world_display_status } from '@/helpers/worlds.ts'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
@@ -44,463 +51,477 @@ const formatRelativeTime = useRelativeTime()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||||
(e: 'open-folder', world: SingleplayerWorld): void
|
(e: 'open-folder', world: SingleplayerWorld): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
world: World
|
world: World
|
||||||
playingInstance?: boolean
|
playingInstance?: boolean
|
||||||
playingWorld?: boolean
|
playingWorld?: boolean
|
||||||
startingInstance?: boolean
|
startingInstance?: boolean
|
||||||
supportsQuickPlay?: boolean
|
supportsServerQuickPlay?: boolean
|
||||||
currentProtocol?: number | null
|
supportsWorldQuickPlay?: boolean
|
||||||
highlighted?: boolean
|
currentProtocol?: ProtocolVersion | null
|
||||||
|
highlighted?: boolean
|
||||||
|
|
||||||
// Server only
|
// Server only
|
||||||
refreshing?: boolean
|
refreshing?: boolean
|
||||||
serverStatus?: ServerStatus
|
serverStatus?: ServerStatus
|
||||||
renderedMotd?: string
|
renderedMotd?: string
|
||||||
|
|
||||||
// Singleplayer only
|
// Singleplayer only
|
||||||
gameMode?: {
|
gameMode?: {
|
||||||
icon: Component
|
icon: Component
|
||||||
message: MessageDescriptor
|
message: MessageDescriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance
|
// Instance
|
||||||
instancePath?: string
|
instancePath?: string
|
||||||
instanceName?: string
|
instanceName?: string
|
||||||
instanceIcon?: string
|
instanceIcon?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
playingInstance: false,
|
playingInstance: false,
|
||||||
playingWorld: false,
|
playingWorld: false,
|
||||||
startingInstance: false,
|
startingInstance: false,
|
||||||
supportsQuickPlay: false,
|
supportsServerQuickPlay: true,
|
||||||
currentProtocol: null,
|
supportsWorldQuickPlay: false,
|
||||||
|
currentProtocol: null,
|
||||||
|
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
serverStatus: undefined,
|
serverStatus: undefined,
|
||||||
renderedMotd: undefined,
|
renderedMotd: undefined,
|
||||||
|
|
||||||
gameMode: undefined,
|
gameMode: undefined,
|
||||||
|
|
||||||
instancePath: undefined,
|
instancePath: undefined,
|
||||||
instanceName: undefined,
|
instanceName: undefined,
|
||||||
instanceIcon: undefined,
|
instanceIcon: undefined,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const playingOtherWorld = computed(() => props.playingInstance && !props.playingWorld)
|
const playingOtherWorld = computed(() => props.playingInstance && !props.playingWorld)
|
||||||
const hasPlayersTooltip = computed(
|
const hasPlayersTooltip = computed(
|
||||||
() => !!props.serverStatus?.players?.sample && props.serverStatus.players?.sample?.length > 0,
|
() => !!props.serverStatus?.players?.sample && props.serverStatus.players?.sample?.length > 0,
|
||||||
)
|
)
|
||||||
const serverIncompatible = computed(
|
const serverIncompatible = computed(
|
||||||
() =>
|
() =>
|
||||||
!!props.serverStatus &&
|
!!props.serverStatus &&
|
||||||
!!props.serverStatus.version?.protocol &&
|
!!props.serverStatus.version?.protocol &&
|
||||||
!!props.currentProtocol &&
|
!!props.currentProtocol &&
|
||||||
props.serverStatus.version.protocol !== props.currentProtocol,
|
(props.serverStatus.version.protocol !== props.currentProtocol.version ||
|
||||||
|
props.serverStatus.version.legacy !== props.currentProtocol.legacy),
|
||||||
)
|
)
|
||||||
|
|
||||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
hardcore: {
|
hardcore: {
|
||||||
id: 'instance.worlds.hardcore',
|
id: 'instance.worlds.hardcore',
|
||||||
defaultMessage: 'Hardcore mode',
|
defaultMessage: 'Hardcore mode',
|
||||||
},
|
},
|
||||||
cantConnect: {
|
cantConnect: {
|
||||||
id: 'instance.worlds.cant_connect',
|
id: 'instance.worlds.cant_connect',
|
||||||
defaultMessage: "Can't connect to server",
|
defaultMessage: "Can't connect to server",
|
||||||
},
|
},
|
||||||
aMinecraftServer: {
|
aMinecraftServer: {
|
||||||
id: 'instance.worlds.a_minecraft_server',
|
id: 'instance.worlds.a_minecraft_server',
|
||||||
defaultMessage: 'A Minecraft Server',
|
defaultMessage: 'A Minecraft Server',
|
||||||
},
|
},
|
||||||
noQuickPlay: {
|
noServerQuickPlay: {
|
||||||
id: 'instance.worlds.no_quick_play',
|
id: 'instance.worlds.no_server_quick_play',
|
||||||
defaultMessage: 'You can only jump straight into worlds on Minecraft 1.20+',
|
defaultMessage: 'You can only jump straight into servers on Minecraft Alpha 1.0.5+',
|
||||||
},
|
},
|
||||||
gameAlreadyOpen: {
|
noSingleplayerQuickPlay: {
|
||||||
id: 'instance.worlds.game_already_open',
|
id: 'instance.worlds.no_singleplayer_quick_play',
|
||||||
defaultMessage: 'Instance is already open',
|
defaultMessage: 'You can only jump straight into singleplayer worlds on Minecraft 1.20+',
|
||||||
},
|
},
|
||||||
copyAddress: {
|
gameAlreadyOpen: {
|
||||||
id: 'instance.worlds.copy_address',
|
id: 'instance.worlds.game_already_open',
|
||||||
defaultMessage: 'Copy address',
|
defaultMessage: 'Instance is already open',
|
||||||
},
|
},
|
||||||
viewInstance: {
|
noContact: {
|
||||||
id: 'instance.worlds.view_instance',
|
id: 'instance.worlds.no_contact',
|
||||||
defaultMessage: 'View instance',
|
defaultMessage: "Server couldn't be contacted",
|
||||||
},
|
},
|
||||||
playAnyway: {
|
incompatibleServer: {
|
||||||
id: 'instance.worlds.play_anyway',
|
id: 'instance.worlds.incompatible_server',
|
||||||
defaultMessage: 'Play anyway',
|
defaultMessage: 'Server is incompatible',
|
||||||
},
|
},
|
||||||
playInstance: {
|
copyAddress: {
|
||||||
id: 'instance.worlds.play_instance',
|
id: 'instance.worlds.copy_address',
|
||||||
defaultMessage: 'Play instance',
|
defaultMessage: 'Copy address',
|
||||||
},
|
},
|
||||||
worldInUse: {
|
viewInstance: {
|
||||||
id: 'instance.worlds.world_in_use',
|
id: 'instance.worlds.view_instance',
|
||||||
defaultMessage: 'World is in use',
|
defaultMessage: 'View instance',
|
||||||
},
|
},
|
||||||
dontShowOnHome: {
|
playInstance: {
|
||||||
id: 'instance.worlds.dont_show_on_home',
|
id: 'instance.worlds.play_instance',
|
||||||
defaultMessage: `Don't show on Home`,
|
defaultMessage: 'Play instance',
|
||||||
},
|
},
|
||||||
|
worldInUse: {
|
||||||
|
id: 'instance.worlds.world_in_use',
|
||||||
|
defaultMessage: 'World is in use',
|
||||||
|
},
|
||||||
|
dontShowOnHome: {
|
||||||
|
id: 'instance.worlds.dont_show_on_home',
|
||||||
|
defaultMessage: `Don't show on Home`,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<SmartClickable>
|
<SmartClickable>
|
||||||
<template v-if="instancePath" #clickable>
|
<template v-if="instancePath" #clickable>
|
||||||
<router-link
|
<router-link
|
||||||
class="no-click-animation"
|
class="no-click-animation"
|
||||||
:to="`/instance/${encodeURIComponent(instancePath)}/worlds?highlight=${encodeURIComponent(getWorldIdentifier(world))}`"
|
:to="`/instance/${encodeURIComponent(instancePath)}/worlds?highlight=${encodeURIComponent(getWorldIdentifier(world))}`"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised smart-clickable:highlight-on-hover rounded-xl"
|
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised card-shadow smart-clickable:highlight-on-hover rounded-xl"
|
||||||
:class="{
|
:class="{
|
||||||
'world-item-highlighted': highlighted,
|
'world-item-highlighted': highlighted,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="
|
:src="
|
||||||
world.type === 'server' && serverStatus ? serverStatus.favicon ?? world.icon : world.icon
|
world.type === 'server' && serverStatus ? serverStatus.favicon ?? world.icon : world.icon
|
||||||
"
|
"
|
||||||
size="48px"
|
size="48px"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col justify-between h-full">
|
<div class="flex flex-col justify-between h-full">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-lg text-contrast font-bold truncate smart-clickable:underline-on-hover">
|
<div class="text-lg text-contrast font-bold truncate smart-clickable:underline-on-hover">
|
||||||
{{ world.name }}
|
{{ world.name }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="world.type === 'singleplayer'"
|
v-if="world.type === 'singleplayer'"
|
||||||
class="text-sm text-secondary flex items-center gap-1 font-semibold"
|
class="text-sm text-secondary flex items-center gap-1 font-semibold"
|
||||||
>
|
>
|
||||||
<UserIcon
|
<UserIcon
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="h-4 w-4 text-secondary shrink-0"
|
class="h-4 w-4 text-secondary shrink-0"
|
||||||
stroke-width="3px"
|
stroke-width="3px"
|
||||||
/>
|
/>
|
||||||
{{ formatMessage(commonMessages.singleplayerLabel) }}
|
{{ formatMessage(commonMessages.singleplayerLabel) }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="world.type === 'server'"
|
v-else-if="world.type === 'server'"
|
||||||
class="text-sm text-secondary flex items-center gap-1 font-semibold flex-nowrap whitespace-nowrap"
|
class="text-sm text-secondary flex items-center gap-1 font-semibold flex-nowrap whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<template v-if="refreshing">
|
<template v-if="refreshing">
|
||||||
<SpinnerIcon aria-hidden="true" class="animate-spin shrink-0" />
|
<SpinnerIcon aria-hidden="true" class="animate-spin shrink-0" />
|
||||||
Loading...
|
Loading...
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="serverStatus">
|
<template v-else-if="serverStatus">
|
||||||
<template v-if="serverIncompatible">
|
<template v-if="serverIncompatible">
|
||||||
<IssuesIcon class="shrink-0 text-orange" aria-hidden="true" />
|
<IssuesIcon class="shrink-0 text-orange" aria-hidden="true" />
|
||||||
<span class="text-orange">
|
<span class="text-orange">
|
||||||
Incompatible version {{ serverStatus.version?.name }}
|
Incompatible version {{ serverStatus.version?.name }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<SignalIcon
|
<SignalIcon
|
||||||
v-tooltip="serverStatus ? `${serverStatus.ping}ms` : null"
|
v-tooltip="serverStatus ? `${serverStatus.ping}ms` : null"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
:style="`--_signal-${getPingLevel(serverStatus.ping || 0)}: var(--color-green)`"
|
:style="`--_signal-${getPingLevel(serverStatus.ping || 0)}: var(--color-green)`"
|
||||||
stroke-width="3px"
|
stroke-width="3px"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
:class="{
|
:class="{
|
||||||
'smart-clickable:allow-pointer-events': serverStatus,
|
'smart-clickable:allow-pointer-events': serverStatus,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<Tooltip :disabled="!hasPlayersTooltip">
|
<Tooltip :disabled="!hasPlayersTooltip">
|
||||||
<span :class="{ 'cursor-help': hasPlayersTooltip }">
|
<span :class="{ 'cursor-help': hasPlayersTooltip }">
|
||||||
{{ formatNumber(serverStatus.players?.online, false) }} online
|
{{ formatNumber(serverStatus.players?.online, false) }}
|
||||||
</span>
|
online
|
||||||
<template #popper>
|
</span>
|
||||||
<div class="flex flex-col gap-1">
|
<template #popper>
|
||||||
<span v-for="player in serverStatus.players?.sample" :key="player.name">
|
<div class="flex flex-col gap-1">
|
||||||
{{ player.name }}
|
<span v-for="player in serverStatus.players?.sample" :key="player.name">
|
||||||
</span>
|
{{ player.name }}
|
||||||
</div>
|
</span>
|
||||||
</template>
|
</div>
|
||||||
</Tooltip>
|
</template>
|
||||||
</template>
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
</template>
|
||||||
<NoSignalIcon aria-hidden="true" stroke-width="3px" class="shrink-0" /> Offline
|
<template v-else>
|
||||||
</template>
|
<NoSignalIcon aria-hidden="true" stroke-width="3px" class="shrink-0" />
|
||||||
</div>
|
Offline
|
||||||
</div>
|
</template>
|
||||||
<div class="flex items-center gap-2 text-sm text-secondary">
|
</div>
|
||||||
<div
|
</div>
|
||||||
v-tooltip="
|
<div class="flex items-center gap-2 text-sm text-secondary">
|
||||||
world.last_played ? dayjs(world.last_played).format('MMMM D, YYYY [at] h:mm A') : null
|
<div
|
||||||
"
|
v-tooltip="
|
||||||
class="w-fit shrink-0"
|
world.last_played ? dayjs(world.last_played).format('MMMM D, YYYY [at] h:mm A') : null
|
||||||
:class="{ 'cursor-help smart-clickable:allow-pointer-events': world.last_played }"
|
"
|
||||||
>
|
class="w-fit shrink-0"
|
||||||
<template v-if="world.last_played">
|
:class="{
|
||||||
{{
|
'cursor-help smart-clickable:allow-pointer-events': world.last_played,
|
||||||
formatMessage(commonMessages.playedLabel, {
|
}"
|
||||||
time: formatRelativeTime(dayjs(world.last_played).toISOString()),
|
>
|
||||||
})
|
<template v-if="world.last_played">
|
||||||
}}
|
{{
|
||||||
</template>
|
formatMessage(commonMessages.playedLabel, {
|
||||||
<template v-else> Not played yet </template>
|
time: formatRelativeTime(dayjs(world.last_played).toISOString()),
|
||||||
</div>
|
})
|
||||||
<template v-if="instancePath">
|
}}
|
||||||
•
|
</template>
|
||||||
<router-link
|
<template v-else> Not played yet </template>
|
||||||
class="flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
</div>
|
||||||
:to="`/instance/${instancePath}`"
|
<template v-if="instancePath">
|
||||||
>
|
•
|
||||||
<Avatar
|
<router-link
|
||||||
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
class="flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
||||||
size="16px"
|
:to="`/instance/${instancePath}`"
|
||||||
:tint-by="instancePath"
|
>
|
||||||
class="shrink-0"
|
<Avatar
|
||||||
/>
|
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
||||||
<span class="truncate">{{ instanceName }}</span>
|
size="16px"
|
||||||
</router-link>
|
:tint-by="instancePath"
|
||||||
</template>
|
class="shrink-0"
|
||||||
</div>
|
/>
|
||||||
</div>
|
<span class="truncate">{{ instanceName }}</span>
|
||||||
<div
|
</router-link>
|
||||||
class="font-semibold flex items-center gap-1 justify-center text-center"
|
</template>
|
||||||
:class="world.type === 'singleplayer' && world.hardcore ? `text-red` : 'text-secondary'"
|
</div>
|
||||||
>
|
</div>
|
||||||
<template v-if="world.type === 'server'">
|
<div
|
||||||
<template v-if="refreshing">
|
class="font-semibold flex items-center gap-1 justify-center text-center"
|
||||||
<SpinnerIcon aria-hidden="true" class="animate-spin" />
|
:class="world.type === 'singleplayer' && world.hardcore ? `text-red` : 'text-secondary'"
|
||||||
{{ formatMessage(commonMessages.loadingLabel) }}
|
>
|
||||||
</template>
|
<template v-if="world.type === 'server'">
|
||||||
<div
|
<template v-if="refreshing">
|
||||||
v-else-if="renderedMotd"
|
<SpinnerIcon aria-hidden="true" class="animate-spin" />
|
||||||
class="motd-renderer font-normal font-minecraft line-clamp-2 text-secondary leading-5"
|
{{ formatMessage(commonMessages.loadingLabel) }}
|
||||||
v-html="renderedMotd"
|
</template>
|
||||||
/>
|
<div
|
||||||
<div v-else-if="!serverStatus" class="font-normal font-minecraft text-red leading-5">
|
v-else-if="renderedMotd"
|
||||||
{{ formatMessage(messages.cantConnect) }}
|
class="motd-renderer font-normal font-minecraft line-clamp-2 text-secondary leading-5"
|
||||||
</div>
|
v-html="renderedMotd"
|
||||||
<div v-else class="font-normal font-minecraft text-secondary leading-5">
|
/>
|
||||||
{{ formatMessage(messages.aMinecraftServer) }}
|
<div v-else-if="!serverStatus" class="font-normal font-minecraft text-red leading-5">
|
||||||
</div>
|
{{ formatMessage(messages.cantConnect) }}
|
||||||
</template>
|
</div>
|
||||||
<template v-else-if="world.type === 'singleplayer' && gameMode">
|
<div v-else class="font-normal font-minecraft text-secondary leading-5">
|
||||||
<template v-if="world.hardcore">
|
{{ formatMessage(messages.aMinecraftServer) }}
|
||||||
<SkullIcon aria-hidden="true" class="h-4 w-4 shrink-0" />
|
</div>
|
||||||
{{ formatMessage(messages.hardcore) }}
|
</template>
|
||||||
</template>
|
<template v-else-if="world.type === 'singleplayer' && gameMode">
|
||||||
<template v-else>
|
<template v-if="world.hardcore">
|
||||||
<component :is="gameMode.icon" aria-hidden="true" class="h-4 w-4 shrink-0" />
|
<SkullIcon aria-hidden="true" class="h-4 w-4 shrink-0" />
|
||||||
{{ formatMessage(gameMode.message) }}
|
{{ formatMessage(messages.hardcore) }}
|
||||||
</template>
|
</template>
|
||||||
</template>
|
<template v-else>
|
||||||
</div>
|
<component :is="gameMode.icon" aria-hidden="true" class="h-4 w-4 shrink-0" />
|
||||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
{{ formatMessage(gameMode.message) }}
|
||||||
<template v-if="world.type === 'singleplayer' || serverStatus">
|
</template>
|
||||||
<ButtonStyled
|
</template>
|
||||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
</div>
|
||||||
color="red"
|
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||||
>
|
<ButtonStyled
|
||||||
<button @click="emit('stop')">
|
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||||
<StopCircleIcon aria-hidden="true" />
|
color="red"
|
||||||
{{ formatMessage(commonMessages.stopButton) }}
|
>
|
||||||
</button>
|
<button @click="emit('stop')">
|
||||||
</ButtonStyled>
|
<StopCircleIcon aria-hidden="true" />
|
||||||
<ButtonStyled v-else>
|
{{ formatMessage(commonMessages.stopButton) }}
|
||||||
<button
|
</button>
|
||||||
v-tooltip="
|
</ButtonStyled>
|
||||||
serverIncompatible
|
<ButtonStyled v-else>
|
||||||
? 'Server is incompatible'
|
<button
|
||||||
: !supportsQuickPlay
|
v-tooltip="
|
||||||
? formatMessage(messages.noQuickPlay)
|
world.type === 'server'
|
||||||
: playingOtherWorld || locked
|
? !supportsServerQuickPlay
|
||||||
? formatMessage(messages.gameAlreadyOpen)
|
? formatMessage(messages.noServerQuickPlay)
|
||||||
: null
|
: playingOtherWorld
|
||||||
"
|
? formatMessage(messages.gameAlreadyOpen)
|
||||||
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
: !serverStatus
|
||||||
@click="emit('play')"
|
? formatMessage(messages.noContact)
|
||||||
>
|
: serverIncompatible
|
||||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
? formatMessage(messages.incompatibleServer)
|
||||||
<PlayIcon v-else aria-hidden="true" />
|
: null
|
||||||
{{ formatMessage(commonMessages.playButton) }}
|
: !supportsWorldQuickPlay
|
||||||
</button>
|
? formatMessage(messages.noSingleplayerQuickPlay)
|
||||||
</ButtonStyled>
|
: playingOtherWorld || locked
|
||||||
</template>
|
? formatMessage(messages.gameAlreadyOpen)
|
||||||
<ButtonStyled v-else>
|
: null
|
||||||
<button class="invisible">
|
"
|
||||||
<PlayIcon aria-hidden="true" />
|
:disabled="
|
||||||
{{ formatMessage(commonMessages.playButton) }}
|
playingOtherWorld ||
|
||||||
</button>
|
startingInstance ||
|
||||||
</ButtonStyled>
|
(world.type == 'server' && !supportsServerQuickPlay) ||
|
||||||
<ButtonStyled circular type="transparent">
|
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
||||||
<OverflowMenu
|
"
|
||||||
:options="[
|
@click="emit('play')"
|
||||||
{
|
>
|
||||||
id: 'play-instance',
|
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||||
shown: !!instancePath,
|
<PlayIcon v-else aria-hidden="true" />
|
||||||
disabled: playingInstance,
|
{{ formatMessage(commonMessages.playButton) }}
|
||||||
action: () => emit('play-instance'),
|
</button>
|
||||||
},
|
</ButtonStyled>
|
||||||
{
|
<ButtonStyled circular type="transparent">
|
||||||
id: 'play-anyway',
|
<OverflowMenu
|
||||||
shown: serverIncompatible && !playingInstance && supportsQuickPlay,
|
:options="[
|
||||||
action: () => emit('play'),
|
{
|
||||||
},
|
id: 'play-instance',
|
||||||
{
|
shown: !!instancePath,
|
||||||
id: 'open-instance',
|
disabled: playingInstance,
|
||||||
shown: !!instancePath,
|
action: () => emit('play-instance'),
|
||||||
action: () => router.push(encodeURI(`/instance/${instancePath}`)),
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'open-instance',
|
||||||
id: 'refresh',
|
shown: !!instancePath,
|
||||||
shown: world.type === 'server',
|
action: () => router.push(encodeURI(`/instance/${instancePath}`)),
|
||||||
action: () => emit('refresh'),
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'refresh',
|
||||||
id: 'copy-address',
|
shown: world.type === 'server',
|
||||||
shown: world.type === 'server',
|
action: () => emit('refresh'),
|
||||||
action: () => copyToClipboard((world as ServerWorld).address),
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'copy-address',
|
||||||
id: 'edit',
|
shown: world.type === 'server',
|
||||||
action: () => emit('edit'),
|
action: () => copyToClipboard((world as ServerWorld).address),
|
||||||
shown: !instancePath,
|
},
|
||||||
disabled: locked,
|
{
|
||||||
tooltip: locked ? formatMessage(messages.worldInUse) : undefined,
|
id: 'edit',
|
||||||
},
|
action: () => emit('edit'),
|
||||||
{
|
shown: !instancePath,
|
||||||
id: 'open-folder',
|
disabled: locked,
|
||||||
shown: world.type === 'singleplayer',
|
tooltip: locked ? formatMessage(messages.worldInUse) : undefined,
|
||||||
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'open-folder',
|
||||||
divider: true,
|
shown: world.type === 'singleplayer',
|
||||||
shown: !!instancePath,
|
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dont-show-on-home',
|
divider: true,
|
||||||
shown: !!instancePath,
|
shown: !!instancePath,
|
||||||
action: () => {
|
},
|
||||||
set_world_display_status(
|
{
|
||||||
instancePath,
|
id: 'dont-show-on-home',
|
||||||
world.type,
|
shown: !!instancePath,
|
||||||
getWorldIdentifier(world),
|
action: () => {
|
||||||
'hidden',
|
set_world_display_status(
|
||||||
).then(() => {
|
instancePath,
|
||||||
emit('update')
|
world.type,
|
||||||
})
|
getWorldIdentifier(world),
|
||||||
},
|
'hidden',
|
||||||
},
|
).then(() => {
|
||||||
{
|
emit('update')
|
||||||
divider: true,
|
})
|
||||||
shown: !instancePath,
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'delete',
|
divider: true,
|
||||||
color: 'red',
|
shown: !instancePath,
|
||||||
hoverFilled: true,
|
},
|
||||||
action: () => emit('delete'),
|
{
|
||||||
shown: !instancePath,
|
id: 'delete',
|
||||||
disabled: locked,
|
color: 'red',
|
||||||
tooltip: locked ? formatMessage(messages.worldInUse) : undefined,
|
hoverFilled: true,
|
||||||
},
|
action: () => emit('delete'),
|
||||||
]"
|
shown: !instancePath,
|
||||||
>
|
disabled: locked,
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
tooltip: locked ? formatMessage(messages.worldInUse) : undefined,
|
||||||
<template #play-instance>
|
},
|
||||||
<PlayIcon aria-hidden="true" />
|
]"
|
||||||
{{ formatMessage(messages.playInstance) }}
|
>
|
||||||
</template>
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
<template #play-anyway>
|
<template #play-instance>
|
||||||
<PlayIcon aria-hidden="true" />
|
<PlayIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.playAnyway) }}
|
{{ formatMessage(messages.playInstance) }}
|
||||||
</template>
|
</template>
|
||||||
<template #open-instance>
|
<template #open-instance>
|
||||||
<EyeIcon aria-hidden="true" />
|
<EyeIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.viewInstance) }}
|
{{ formatMessage(messages.viewInstance) }}
|
||||||
</template>
|
</template>
|
||||||
<template #edit>
|
<template #edit>
|
||||||
<EditIcon aria-hidden="true" /> {{ formatMessage(commonMessages.editButton) }}
|
<EditIcon aria-hidden="true" />
|
||||||
</template>
|
{{ formatMessage(commonMessages.editButton) }}
|
||||||
<template #open-folder>
|
</template>
|
||||||
<FolderOpenIcon aria-hidden="true" />
|
<template #open-folder>
|
||||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
<FolderOpenIcon aria-hidden="true" />
|
||||||
</template>
|
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||||
<template #copy-address>
|
</template>
|
||||||
<ClipboardCopyIcon aria-hidden="true" /> {{ formatMessage(messages.copyAddress) }}
|
<template #copy-address>
|
||||||
</template>
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
<template #refresh>
|
{{ formatMessage(messages.copyAddress) }}
|
||||||
<UpdatedIcon aria-hidden="true" /> {{ formatMessage(commonMessages.refreshButton) }}
|
</template>
|
||||||
</template>
|
<template #refresh>
|
||||||
<template #dont-show-on-home>
|
<UpdatedIcon aria-hidden="true" />
|
||||||
<XIcon aria-hidden="true" />
|
{{ formatMessage(commonMessages.refreshButton) }}
|
||||||
{{ formatMessage(messages.dontShowOnHome) }}
|
</template>
|
||||||
</template>
|
<template #dont-show-on-home>
|
||||||
<template #delete>
|
<XIcon aria-hidden="true" />
|
||||||
<TrashIcon aria-hidden="true" />
|
{{ formatMessage(messages.dontShowOnHome) }}
|
||||||
{{
|
</template>
|
||||||
formatMessage(
|
<template #delete>
|
||||||
world.type === 'server'
|
<TrashIcon aria-hidden="true" />
|
||||||
? commonMessages.removeButton
|
{{
|
||||||
: commonMessages.deleteLabel,
|
formatMessage(
|
||||||
)
|
world.type === 'server'
|
||||||
}}
|
? commonMessages.removeButton
|
||||||
</template>
|
: commonMessages.deleteLabel,
|
||||||
</OverflowMenu>
|
)
|
||||||
</ButtonStyled>
|
}}
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</OverflowMenu>
|
||||||
</SmartClickable>
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SmartClickable>
|
||||||
</template>
|
</template>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.world-item-highlighted {
|
.world-item-highlighted {
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: fade-highlight 4s ease-out;
|
animation: fade-highlight 4s ease-out;
|
||||||
filter: brightness(1);
|
filter: brightness(1);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@apply rounded-xl inset-0 absolute;
|
@apply rounded-xl inset-0 absolute;
|
||||||
|
|
||||||
animation: fade-opacity 4s ease-out;
|
animation: fade-opacity 4s ease-out;
|
||||||
|
|
||||||
content: '';
|
content: '';
|
||||||
box-shadow: 0 0 8px 2px var(--color-brand);
|
box-shadow: 0 0 8px 2px var(--color-brand);
|
||||||
border: 1.5px solid var(--color-brand);
|
border: 1.5px solid var(--color-brand);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-highlight {
|
@keyframes fade-highlight {
|
||||||
0% {
|
0% {
|
||||||
filter: brightness(1.25);
|
filter: brightness(1.25);
|
||||||
}
|
}
|
||||||
75% {
|
75% {
|
||||||
filter: brightness(1.25);
|
filter: brightness(1.25);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
filter: brightness(1);
|
filter: brightness(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-opacity {
|
@keyframes fade-opacity {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
75% {
|
75% {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.light-mode .motd-renderer {
|
.light-mode .motd-renderer {
|
||||||
filter: brightness(0.75);
|
filter: brightness(0.75);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, commonMessages } from '@modrinth/ui'
|
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
|
|
||||||
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { handleError } from '@/store/notifications'
|
import { ref } from 'vue'
|
||||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
|
||||||
|
|
||||||
|
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [server: ServerWorld, play: boolean]
|
submit: [server: ServerWorld, play: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
@@ -27,89 +28,89 @@ const address = ref()
|
|||||||
const resourcePack = ref<ServerPackStatus>('enabled')
|
const resourcePack = ref<ServerPackStatus>('enabled')
|
||||||
|
|
||||||
async function addServer(play: boolean) {
|
async function addServer(play: boolean) {
|
||||||
const serverName = name.value ? name.value : address.value
|
const serverName = name.value ? name.value : address.value
|
||||||
const resourcePackStatus = resourcePack.value
|
const resourcePackStatus = resourcePack.value
|
||||||
const index =
|
const index =
|
||||||
(await add_server_to_profile(
|
(await add_server_to_profile(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
serverName,
|
serverName,
|
||||||
address.value,
|
address.value,
|
||||||
resourcePackStatus,
|
resourcePackStatus,
|
||||||
).catch(handleError)) ?? 0
|
).catch(handleError)) ?? 0
|
||||||
emit(
|
emit(
|
||||||
'submit',
|
'submit',
|
||||||
{
|
{
|
||||||
name: serverName,
|
name: serverName,
|
||||||
type: 'server',
|
type: 'server',
|
||||||
index,
|
index,
|
||||||
address: address.value,
|
address: address.value,
|
||||||
pack_status: resourcePackStatus,
|
pack_status: resourcePackStatus,
|
||||||
},
|
},
|
||||||
play,
|
play,
|
||||||
)
|
)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
name.value = ''
|
name.value = ''
|
||||||
address.value = ''
|
address.value = ''
|
||||||
resourcePack.value = 'enabled'
|
resourcePack.value = 'enabled'
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: {
|
title: {
|
||||||
id: 'instance.add-server.title',
|
id: 'instance.add-server.title',
|
||||||
defaultMessage: 'Add a server',
|
defaultMessage: 'Add a server',
|
||||||
},
|
},
|
||||||
addServer: {
|
addServer: {
|
||||||
id: 'instance.add-server.add-server',
|
id: 'instance.add-server.add-server',
|
||||||
defaultMessage: 'Add server',
|
defaultMessage: 'Add server',
|
||||||
},
|
},
|
||||||
addAndPlay: {
|
addAndPlay: {
|
||||||
id: 'instance.add-server.add-and-play',
|
id: 'instance.add-server.add-and-play',
|
||||||
defaultMessage: 'Add and play',
|
defaultMessage: 'Add and play',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({ show, hide })
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||||
<InstanceModalTitlePrefix :instance="instance" />
|
<InstanceModalTitlePrefix :instance="instance" />
|
||||||
<span class="font-extrabold text-contrast">{{ formatMessage(messages.title) }}</span>
|
<span class="font-extrabold text-contrast">{{ formatMessage(messages.title) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<ServerModalBody
|
<ServerModalBody
|
||||||
v-model:name="name"
|
v-model:name="name"
|
||||||
v-model:address="address"
|
v-model:address="address"
|
||||||
v-model:resource-pack="resourcePack"
|
v-model:resource-pack="resourcePack"
|
||||||
/>
|
/>
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2 mt-4">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button :disabled="!address" @click="addServer(true)">
|
<button :disabled="!address" @click="addServer(true)">
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
{{ formatMessage(messages.addAndPlay) }}
|
{{ formatMessage(messages.addAndPlay) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button :disabled="!address" @click="addServer(false)">
|
<button :disabled="!address" @click="addServer(false)">
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
{{ formatMessage(messages.addServer) }}
|
{{ formatMessage(messages.addServer) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="hide()">
|
<button @click="hide()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
{{ formatMessage(commonMessages.cancelButton) }}
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, commonMessages } from '@modrinth/ui'
|
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||||
|
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import {
|
import {
|
||||||
type ServerPackStatus,
|
type DisplayStatus,
|
||||||
edit_server_in_profile,
|
edit_server_in_profile,
|
||||||
type ServerWorld,
|
type ServerPackStatus,
|
||||||
set_world_display_status,
|
type ServerWorld,
|
||||||
type DisplayStatus,
|
set_world_display_status,
|
||||||
} from '@/helpers/worlds.ts'
|
} from '@/helpers/worlds.ts'
|
||||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
|
||||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [server: ServerWorld]
|
submit: [server: ServerWorld]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
@@ -38,81 +39,81 @@ const hideFromHome = ref(false)
|
|||||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||||
|
|
||||||
async function saveServer() {
|
async function saveServer() {
|
||||||
const serverName = name.value ? name.value : address.value
|
const serverName = name.value ? name.value : address.value
|
||||||
const resourcePackStatus = resourcePack.value
|
const resourcePackStatus = resourcePack.value
|
||||||
await edit_server_in_profile(
|
await edit_server_in_profile(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
index.value,
|
index.value,
|
||||||
serverName,
|
serverName,
|
||||||
address.value,
|
address.value,
|
||||||
resourcePackStatus,
|
resourcePackStatus,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
|
|
||||||
if (newDisplayStatus.value !== displayStatus.value) {
|
if (newDisplayStatus.value !== displayStatus.value) {
|
||||||
await set_world_display_status(
|
await set_world_display_status(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
'server',
|
'server',
|
||||||
address.value,
|
address.value,
|
||||||
newDisplayStatus.value,
|
newDisplayStatus.value,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
name: serverName,
|
name: serverName,
|
||||||
type: 'server',
|
type: 'server',
|
||||||
index: index.value,
|
index: index.value,
|
||||||
address: address.value,
|
address: address.value,
|
||||||
pack_status: resourcePackStatus,
|
pack_status: resourcePackStatus,
|
||||||
display_status: newDisplayStatus.value,
|
display_status: newDisplayStatus.value,
|
||||||
})
|
})
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(server: ServerWorld) {
|
function show(server: ServerWorld) {
|
||||||
name.value = server.name
|
name.value = server.name
|
||||||
address.value = server.address
|
address.value = server.address
|
||||||
resourcePack.value = server.pack_status
|
resourcePack.value = server.pack_status
|
||||||
index.value = server.index
|
index.value = server.index
|
||||||
displayStatus.value = server.display_status
|
displayStatus.value = server.display_status
|
||||||
hideFromHome.value = server.display_status === 'hidden'
|
hideFromHome.value = server.display_status === 'hidden'
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ show })
|
defineExpose({ show })
|
||||||
|
|
||||||
const titleMessage = defineMessage({
|
const titleMessage = defineMessage({
|
||||||
id: 'instance.edit-server.title',
|
id: 'instance.edit-server.title',
|
||||||
defaultMessage: 'Edit server',
|
defaultMessage: 'Edit server',
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="font-extrabold text-lg text-contrast">{{ formatMessage(titleMessage) }}</span>
|
<span class="font-extrabold text-lg text-contrast">{{ formatMessage(titleMessage) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<ServerModalBody
|
<ServerModalBody
|
||||||
v-model:name="name"
|
v-model:name="name"
|
||||||
v-model:address="address"
|
v-model:address="address"
|
||||||
v-model:resource-pack="resourcePack"
|
v-model:resource-pack="resourcePack"
|
||||||
/>
|
/>
|
||||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2 mt-4">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button :disabled="!address" @click="saveServer">
|
<button :disabled="!address" @click="saveServer">
|
||||||
<SaveIcon />
|
<SaveIcon />
|
||||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="hide()">
|
<button @click="hide()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
{{ formatMessage(commonMessages.cancelButton) }}
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets'
|
import { ChevronRightIcon, SaveIcon, UndoIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled, commonMessages } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||||
import { set_world_display_status, rename_world, reset_world_icon } from '@/helpers/worlds.ts'
|
import { rename_world, reset_world_icon, set_world_display_status } from '@/helpers/worlds.ts'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [path: string, name: string, removeIcon: boolean, displayStatus: DisplayStatus]
|
submit: [path: string, name: string, removeIcon: boolean, displayStatus: DisplayStatus]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
@@ -32,98 +33,98 @@ const hideFromHome = ref(false)
|
|||||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||||
|
|
||||||
async function saveWorld() {
|
async function saveWorld() {
|
||||||
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
|
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
|
||||||
|
|
||||||
if (removeIcon.value) {
|
if (removeIcon.value) {
|
||||||
await reset_world_icon(props.instance.path, path.value).catch(handleError)
|
await reset_world_icon(props.instance.path, path.value).catch(handleError)
|
||||||
}
|
}
|
||||||
if (newDisplayStatus.value !== displayStatus.value) {
|
if (newDisplayStatus.value !== displayStatus.value) {
|
||||||
await set_world_display_status(
|
await set_world_display_status(
|
||||||
props.instance.path,
|
props.instance.path,
|
||||||
'singleplayer',
|
'singleplayer',
|
||||||
path.value,
|
path.value,
|
||||||
newDisplayStatus.value,
|
newDisplayStatus.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('submit', path.value, name.value, removeIcon.value, newDisplayStatus.value)
|
emit('submit', path.value, name.value, removeIcon.value, newDisplayStatus.value)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(world: SingleplayerWorld) {
|
function show(world: SingleplayerWorld) {
|
||||||
name.value = world.name
|
name.value = world.name
|
||||||
path.value = world.path
|
path.value = world.path
|
||||||
icon.value = world.icon
|
icon.value = world.icon
|
||||||
displayStatus.value = world.display_status
|
displayStatus.value = world.display_status
|
||||||
hideFromHome.value = world.display_status === 'hidden'
|
hideFromHome.value = world.display_status === 'hidden'
|
||||||
removeIcon.value = false
|
removeIcon.value = false
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ show })
|
defineExpose({ show })
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: {
|
title: {
|
||||||
id: 'instance.edit-world.title',
|
id: 'instance.edit-world.title',
|
||||||
defaultMessage: 'Edit world',
|
defaultMessage: 'Edit world',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
id: 'instance.edit-world.name',
|
id: 'instance.edit-world.name',
|
||||||
defaultMessage: 'Name',
|
defaultMessage: 'Name',
|
||||||
},
|
},
|
||||||
placeholderName: {
|
placeholderName: {
|
||||||
id: 'instance.edit-world.placeholder-name',
|
id: 'instance.edit-world.placeholder-name',
|
||||||
defaultMessage: 'Minecraft World',
|
defaultMessage: 'Minecraft World',
|
||||||
},
|
},
|
||||||
resetIcon: {
|
resetIcon: {
|
||||||
id: 'instance.edit-world.reset-icon',
|
id: 'instance.edit-world.reset-icon',
|
||||||
defaultMessage: 'Reset icon',
|
defaultMessage: 'Reset icon',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<Avatar :src="removeIcon || !icon ? undefined : icon" size="24px" />
|
<Avatar :src="removeIcon || !icon ? undefined : icon" size="24px" />
|
||||||
{{ instance.name }} <ChevronRightIcon />
|
{{ instance.name }} <ChevronRightIcon />
|
||||||
<span class="font-extrabold text-lg text-contrast">{{ formatMessage(messages.title) }}</span>
|
<span class="font-extrabold text-lg text-contrast">{{ formatMessage(messages.title) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<div class="w-[450px]">
|
<div class="w-[450px]">
|
||||||
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
|
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
|
||||||
{{ formatMessage(messages.name) }}
|
{{ formatMessage(messages.name) }}
|
||||||
</h2>
|
</h2>
|
||||||
<input
|
<input
|
||||||
v-model="name"
|
v-model="name"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.placeholderName)"
|
:placeholder="formatMessage(messages.placeholderName)"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2 mt-4">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button @click="saveWorld">
|
<button @click="saveWorld">
|
||||||
<SaveIcon />
|
<SaveIcon />
|
||||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button :disabled="removeIcon || !icon" @click="removeIcon = true">
|
<button :disabled="removeIcon || !icon" @click="removeIcon = true">
|
||||||
<UndoIcon />
|
<UndoIcon />
|
||||||
{{ formatMessage(messages.resetIcon) }}
|
{{ formatMessage(messages.resetIcon) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="hide()">
|
<button @click="hide()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
{{ formatMessage(commonMessages.cancelButton) }}
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Checkbox } from '@modrinth/ui'
|
||||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { Checkbox } from '@modrinth/ui'
|
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const value = defineModel<boolean>({ required: true })
|
const value = defineModel<boolean>({ required: true })
|
||||||
|
|
||||||
const labelMessage = defineMessage({
|
const labelMessage = defineMessage({
|
||||||
id: 'instance.edit-world.hide-from-home',
|
id: 'instance.edit-world.hide-from-home',
|
||||||
defaultMessage: `Hide from the Home page`,
|
defaultMessage: `Hide from the Home page`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const label = computed(() => formatMessage(labelMessage))
|
const label = computed(() => formatMessage(labelMessage))
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Checkbox v-model="value" :label="label" />
|
<Checkbox v-model="value" :label="label" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TeleportDropdownMenu } from '@modrinth/ui'
|
import { Combobox } from '@modrinth/ui'
|
||||||
|
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
||||||
|
|
||||||
import type { ServerPackStatus } from '@/helpers/worlds.ts'
|
import type { ServerPackStatus } from '@/helpers/worlds.ts'
|
||||||
import { type MessageDescriptor, defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
@@ -12,75 +13,82 @@ const resourcePack = defineModel<ServerPackStatus>('resourcePack')
|
|||||||
const resourcePackOptions: ServerPackStatus[] = ['enabled', 'prompt', 'disabled']
|
const resourcePackOptions: ServerPackStatus[] = ['enabled', 'prompt', 'disabled']
|
||||||
|
|
||||||
const resourcePackOptionMessages: Record<ServerPackStatus, MessageDescriptor> = defineMessages({
|
const resourcePackOptionMessages: Record<ServerPackStatus, MessageDescriptor> = defineMessages({
|
||||||
enabled: {
|
enabled: {
|
||||||
id: 'instance.add-server.resource-pack.enabled',
|
id: 'instance.add-server.resource-pack.enabled',
|
||||||
defaultMessage: 'Enabled',
|
defaultMessage: 'Enabled',
|
||||||
},
|
},
|
||||||
prompt: {
|
prompt: {
|
||||||
id: 'instance.add-server.resource-pack.prompt',
|
id: 'instance.add-server.resource-pack.prompt',
|
||||||
defaultMessage: 'Prompt',
|
defaultMessage: 'Prompt',
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
id: 'instance.add-server.resource-pack.disabled',
|
id: 'instance.add-server.resource-pack.disabled',
|
||||||
defaultMessage: 'Disabled',
|
defaultMessage: 'Disabled',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
name: {
|
name: {
|
||||||
id: 'instance.server-modal.name',
|
id: 'instance.server-modal.name',
|
||||||
defaultMessage: 'Name',
|
defaultMessage: 'Name',
|
||||||
},
|
},
|
||||||
address: {
|
address: {
|
||||||
id: 'instance.server-modal.address',
|
id: 'instance.server-modal.address',
|
||||||
defaultMessage: 'Address',
|
defaultMessage: 'Address',
|
||||||
},
|
},
|
||||||
resourcePack: {
|
resourcePack: {
|
||||||
id: 'instance.server-modal.resource-pack',
|
id: 'instance.server-modal.resource-pack',
|
||||||
defaultMessage: 'Resource pack',
|
defaultMessage: 'Resource pack',
|
||||||
},
|
},
|
||||||
placeholderName: {
|
placeholderName: {
|
||||||
id: 'instance.server-modal.placeholder-name',
|
id: 'instance.server-modal.placeholder-name',
|
||||||
defaultMessage: 'Minecraft Server',
|
defaultMessage: 'Minecraft Server',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({ resourcePackOptions })
|
defineExpose({ resourcePackOptions })
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="w-[450px]">
|
<div class="w-[450px]">
|
||||||
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
|
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
|
||||||
{{ formatMessage(messages.name) }}
|
{{ formatMessage(messages.name) }}
|
||||||
</h2>
|
</h2>
|
||||||
<input
|
<input
|
||||||
v-model="name"
|
v-model="name"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="formatMessage(messages.placeholderName)"
|
:placeholder="formatMessage(messages.placeholderName)"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<h2 class="text-lg font-extrabold text-contrast mt-3 mb-1">
|
<h2 class="text-lg font-extrabold text-contrast mt-3 mb-1">
|
||||||
{{ formatMessage(messages.address) }}
|
{{ formatMessage(messages.address) }}
|
||||||
</h2>
|
</h2>
|
||||||
<input
|
<input
|
||||||
v-model="address"
|
v-model="address"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="example.modrinth.gg"
|
placeholder="example.modrinth.gg"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<h2 class="text-lg font-extrabold text-contrast mt-3 mb-1">
|
<h2 class="text-lg font-extrabold text-contrast mt-3 mb-1">
|
||||||
{{ formatMessage(messages.resourcePack) }}
|
{{ formatMessage(messages.resourcePack) }}
|
||||||
</h2>
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<TeleportDropdownMenu
|
<Combobox
|
||||||
v-model="resourcePack"
|
v-model="resourcePack"
|
||||||
:options="resourcePackOptions"
|
:options="
|
||||||
name="Server resource pack"
|
resourcePackOptions.map((o) => ({
|
||||||
:display-name="
|
value: o,
|
||||||
(option: ServerPackStatus) => formatMessage(resourcePackOptionMessages[option])
|
label: formatMessage(resourcePackOptionMessages[o]),
|
||||||
"
|
}))
|
||||||
/>
|
"
|
||||||
</div>
|
name="Server resource pack"
|
||||||
</div>
|
:display-value="
|
||||||
|
resourcePack
|
||||||
|
? formatMessage(resourcePackOptionMessages[resourcePack])
|
||||||
|
: 'Select an option'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import cssContent from '@/assets/stylesheets/macFix.css?inline'
|
import cssContent from '@/assets/stylesheets/macFix.css?inline'
|
||||||
|
|
||||||
export async function useCheckDisableMouseover() {
|
export async function useCheckDisableMouseover() {
|
||||||
try {
|
try {
|
||||||
// Fetch the CSS content from the Rust backend
|
// Fetch the CSS content from the Rust backend
|
||||||
let should_disable_mouseover = await invoke('plugin:utils|should_disable_mouseover')
|
let should_disable_mouseover = await invoke('plugin:utils|should_disable_mouseover')
|
||||||
|
|
||||||
if (should_disable_mouseover) {
|
if (should_disable_mouseover) {
|
||||||
// Create a style element and set its content
|
// Create a style element and set its content
|
||||||
const styleElement = document.createElement('style')
|
const styleElement = document.createElement('style')
|
||||||
styleElement.innerHTML = cssContent
|
styleElement.innerHTML = cssContent
|
||||||
|
|
||||||
// Append the style element to the document's head
|
// Append the style element to the document's head
|
||||||
document.head.appendChild(styleElement)
|
document.head.appendChild(styleElement)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking OS version from Rust backend', error)
|
console.error('Error checking OS version from Rust backend', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
apps/app-frontend/src/composables/useMemorySlider.js
Normal file
21
apps/app-frontend/src/composables/useMemorySlider.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { get_max_memory } from '@/helpers/jre.js'
|
||||||
|
|
||||||
|
export default async function () {
|
||||||
|
const maxMemory = ref(Math.floor((await get_max_memory()) / 1024))
|
||||||
|
|
||||||
|
const snapPoints = computed(() => {
|
||||||
|
let points = []
|
||||||
|
let memory = 2048
|
||||||
|
|
||||||
|
while (memory <= maxMemory.value) {
|
||||||
|
points.push(memory)
|
||||||
|
memory *= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
})
|
||||||
|
|
||||||
|
return { maxMemory, snapPoints }
|
||||||
|
}
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
import { posthog } from 'posthog-js'
|
// import { posthog } from 'posthog-js'
|
||||||
|
|
||||||
export const initAnalytics = () => {
|
export const initAnalytics = () => {
|
||||||
posthog.init('phc_9Iqi6lFs9sr5BSqh9RRNRSJ0mATS9PSgirDiX3iOYJ', {
|
// posthog.init('phc_9Iqi6lFs9sr5BSqh9RRNRSJ0mATS9PSgirDiX3iOYJ', {
|
||||||
persistence: 'localStorage',
|
// persistence: 'localStorage',
|
||||||
api_host: 'https://posthog.modrinth.com',
|
// api_host: 'https://posthog.modrinth.com',
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const debugAnalytics = () => {
|
export const debugAnalytics = () => {
|
||||||
posthog.debug()
|
// posthog.debug()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const optOutAnalytics = () => {
|
export const optOutAnalytics = () => {
|
||||||
posthog.opt_out_capturing()
|
// posthog.opt_out_capturing()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const optInAnalytics = () => {
|
export const optInAnalytics = () => {
|
||||||
posthog.opt_in_capturing()
|
// posthog.opt_in_capturing()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const trackEvent = (eventName, properties) => {
|
export const trackEvent = (eventName, properties) => {
|
||||||
posthog.capture(eventName, properties)
|
// posthog.capture(eventName, properties)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,32 @@ export async function offline_login(name) {
|
|||||||
return await invoke('plugin:auth|offline_login', { name: name })
|
return await invoke('plugin:auth|offline_login', { name: name })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
export async function elyby_login(uuid, login, accessToken) {
|
||||||
|
return await invoke('plugin:auth|elyby_login', {
|
||||||
|
uuid,
|
||||||
|
login,
|
||||||
|
accessToken
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
export async function elyby_auth_authenticate(login, password, clientToken) {
|
||||||
|
return await invoke('plugin:auth|elyby_auth_authenticate', {
|
||||||
|
login,
|
||||||
|
password,
|
||||||
|
clientToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the authentication servers are reachable, throwing an exception if
|
||||||
|
* not reachable.
|
||||||
|
*/
|
||||||
|
export async function check_reachable() {
|
||||||
|
await invoke('plugin:auth|check_reachable')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate a user with Hydra - part 1.
|
* Authenticate a user with Hydra - part 1.
|
||||||
* This begins the authentication flow quasi-synchronously.
|
* This begins the authentication flow quasi-synchronously.
|
||||||
@@ -26,7 +52,7 @@ export async function offline_login(name) {
|
|||||||
* @property {string} user_code - The code to enter on the verification_uri page.
|
* @property {string} user_code - The code to enter on the verification_uri page.
|
||||||
*/
|
*/
|
||||||
export async function login() {
|
export async function login() {
|
||||||
return await invoke('plugin:auth|login')
|
return await invoke('plugin:auth|login')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,7 +60,7 @@ export async function login() {
|
|||||||
* @return {Promise<UUID | undefined>}
|
* @return {Promise<UUID | undefined>}
|
||||||
*/
|
*/
|
||||||
export async function get_default_user() {
|
export async function get_default_user() {
|
||||||
return await invoke('plugin:auth|get_default_user')
|
return await invoke('plugin:auth|get_default_user')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,7 +68,7 @@ export async function get_default_user() {
|
|||||||
* @param {UUID} user
|
* @param {UUID} user
|
||||||
*/
|
*/
|
||||||
export async function set_default_user(user) {
|
export async function set_default_user(user) {
|
||||||
return await invoke('plugin:auth|set_default_user', { user })
|
return await invoke('plugin:auth|set_default_user', { user })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,7 +76,7 @@ export async function set_default_user(user) {
|
|||||||
* @param {UUID} user
|
* @param {UUID} user
|
||||||
*/
|
*/
|
||||||
export async function remove_user(user) {
|
export async function remove_user(user) {
|
||||||
return await invoke('plugin:auth|remove_user', { user })
|
return await invoke('plugin:auth|remove_user', { user })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,5 +84,5 @@ export async function remove_user(user) {
|
|||||||
* @returns {Promise<Credential[]>}
|
* @returns {Promise<Credential[]>}
|
||||||
*/
|
*/
|
||||||
export async function users() {
|
export async function users() {
|
||||||
return await invoke('plugin:auth|get_users')
|
return await invoke('plugin:auth|get_users')
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user