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/
|
|
||||||
2
.idea/code.iml
generated
2
.idea/code.iml
generated
@@ -10,8 +10,8 @@
|
|||||||
<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" />
|
||||||
|
|||||||
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>
|
|
||||||
9
.idea/modules.xml
generated
9
.idea/modules.xml
generated
@@ -2,7 +2,14 @@
|
|||||||
<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"
|
||||||
|
filepath="$PROJECT_DIR$/.idea/code.iml"/>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/theseus.iml"
|
||||||
|
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>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -2,8 +2,10 @@
|
|||||||
<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"/>
|
||||||
|
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING"
|
||||||
|
enabled_by_default="true"/>
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
|
|||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Cargo.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
.github/**/*.png
|
||||||
28
.vscode/settings.json
vendored
28
.vscode/settings.json
vendored
@@ -2,8 +2,32 @@
|
|||||||
"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.insertSpaces": false,
|
||||||
|
"files.eol": "\n",
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"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
|
||||||
|
|||||||
143
README.md
143
README.md
@@ -1,32 +1,50 @@
|
|||||||
# 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)_
|
# Install Instructions
|
||||||
- `.deb` format for Linux OS systems _(Since there are quite a few distributions, we do not guarantee
|
|
||||||
|
To install the launcher:
|
||||||
|
|
||||||
|
1. Visit the [releases page](https://git.astralium.su/didirus/AstralRinth/releases) to download the correct version for your system.
|
||||||
|
2. Run the downloaded file or extract and launch it, depending on the format.
|
||||||
|
|
||||||
|
### Downloadable File Extensions
|
||||||
|
|
||||||
|
| 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:
|
||||||
|
|
||||||
### Installation subjects
|
|
||||||
- Builds in releases that are signed with the following prefixes are not recommended for installation and may contain errors:
|
|
||||||
- `dev`
|
- `dev`
|
||||||
- `nightly`
|
- `nightly`
|
||||||
- `dirty`
|
- `dirty`
|
||||||
@@ -34,43 +52,68 @@ Dive into the extraordinary world of AstralRinth, a fork of the original project
|
|||||||
- `dirty-nightly`
|
- `dirty-nightly`
|
||||||
- `dirty_dev`
|
- `dirty_dev`
|
||||||
- `dirty_nightly`
|
- `dirty_nightly`
|
||||||
- Auto-updating takes place through parsing special versions from releases, so we also distribute clean types of `.msi, .dmg and .deb`
|
|
||||||
|
---
|
||||||
|
|
||||||
# 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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script src="https://tally.so/widgets/embed.js" async></script>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,15 +9,17 @@
|
|||||||
"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",
|
||||||
|
"@sfirew/minecraft-motd-parser": "^1.1.6",
|
||||||
|
"@tanstack/vue-query": "^5.90.7",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||||
"@tauri-apps/plugin-http": "^2.5.0",
|
"@tauri-apps/plugin-http": "^2.5.0",
|
||||||
@@ -43,20 +45,20 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.1.1",
|
"@eslint/compat": "^1.1.1",
|
||||||
"@formatjs/cli": "^6.2.12",
|
"@formatjs/cli": "^6.2.12",
|
||||||
|
"@modrinth/tooling-config": "workspace:*",
|
||||||
"@nuxt/eslint-config": "^0.5.6",
|
"@nuxt/eslint-config": "^0.5.6",
|
||||||
"@taijased/vue-render-tracker": "^1.0.7",
|
"@taijased/vue-render-tracker": "^1.0.7",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.9.1",
|
||||||
"eslint-config-custom": "workspace:*",
|
|
||||||
"eslint-plugin-turbo": "^2.5.4",
|
"eslint-plugin-turbo": "^2.5.4",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.74.1",
|
"sass": "^1.74.1",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"tsconfig": "workspace:*",
|
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.6",
|
||||||
|
"vue-component-type-helpers": "^3.1.8",
|
||||||
"vue-tsc": "^2.1.6"
|
"vue-tsc": "^2.1.6"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.4.0",
|
"packageManager": "pnpm@9.4.0",
|
||||||
|
|||||||
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,6 +2,8 @@
|
|||||||
@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;
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
TrashIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
EyeIcon,
|
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
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: {
|
||||||
@@ -119,40 +122,50 @@ const handleOptionsClick = async (args) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 { group = 'Group', sortBy = 'Name' } = state.value
|
||||||
|
|
||||||
const instances = props.instances.filter((instance) => {
|
const instances = props.instances.filter((instance) => {
|
||||||
return instance.name.toLowerCase().includes(search.value.toLowerCase())
|
return instance.name.toLowerCase().includes(search.value.toLowerCase())
|
||||||
})
|
})
|
||||||
|
|
||||||
if (sortBy.value === 'Name') {
|
if (sortBy === 'Name') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Game version') {
|
if (sortBy === 'Game version') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return a.game_version.localeCompare(b.game_version)
|
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Last played') {
|
if (sortBy === 'Last played') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
|
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Date created') {
|
if (sortBy === 'Date created') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.date_created).diff(dayjs(a.date_created))
|
return dayjs(b.date_created).diff(dayjs(a.date_created))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy.value === 'Date modified') {
|
if (sortBy === 'Date modified') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
|
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
|
||||||
})
|
})
|
||||||
@@ -160,7 +173,7 @@ const filteredResults = computed(() => {
|
|||||||
|
|
||||||
const instanceMap = new Map()
|
const instanceMap = new Map()
|
||||||
|
|
||||||
if (group.value === 'Loader') {
|
if (group === 'Loader') {
|
||||||
instances.forEach((instance) => {
|
instances.forEach((instance) => {
|
||||||
const loader = formatCategoryHeader(instance.loader)
|
const loader = formatCategoryHeader(instance.loader)
|
||||||
if (!instanceMap.has(loader)) {
|
if (!instanceMap.has(loader)) {
|
||||||
@@ -169,7 +182,7 @@ const filteredResults = computed(() => {
|
|||||||
|
|
||||||
instanceMap.get(loader).push(instance)
|
instanceMap.get(loader).push(instance)
|
||||||
})
|
})
|
||||||
} else if (group.value === 'Game version') {
|
} else if (group === 'Game version') {
|
||||||
instances.forEach((instance) => {
|
instances.forEach((instance) => {
|
||||||
if (!instanceMap.has(instance.game_version)) {
|
if (!instanceMap.has(instance.game_version)) {
|
||||||
instanceMap.set(instance.game_version, [])
|
instanceMap.set(instance.game_version, [])
|
||||||
@@ -177,7 +190,7 @@ const filteredResults = computed(() => {
|
|||||||
|
|
||||||
instanceMap.get(instance.game_version).push(instance)
|
instanceMap.get(instance.game_version).push(instance)
|
||||||
})
|
})
|
||||||
} else if (group.value === 'Group') {
|
} else if (group === 'Group') {
|
||||||
instances.forEach((instance) => {
|
instances.forEach((instance) => {
|
||||||
if (instance.groups.length === 0) {
|
if (instance.groups.length === 0) {
|
||||||
instance.groups.push('None')
|
instance.groups.push('None')
|
||||||
@@ -197,7 +210,7 @@ const filteredResults = computed(() => {
|
|||||||
|
|
||||||
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
|
// 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
|
// 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') {
|
if (sortBy === 'Name') {
|
||||||
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||||
// None should always be first
|
// None should always be first
|
||||||
if (a[0] === 'None' && b[0] !== 'None') {
|
if (a[0] === 'None' && b[0] !== 'None') {
|
||||||
@@ -213,6 +226,17 @@ const filteredResults = computed(() => {
|
|||||||
instanceMap.set(entry[0], entry[1])
|
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
|
return instanceMap
|
||||||
})
|
})
|
||||||
@@ -228,7 +252,7 @@ const filteredResults = computed(() => {
|
|||||||
</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']"
|
||||||
@@ -239,7 +263,7 @@ const filteredResults = computed(() => {
|
|||||||
</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']"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<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({
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
FolderOpenIcon,
|
|
||||||
PlayIcon,
|
|
||||||
PlusIcon,
|
|
||||||
TrashIcon,
|
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
GlobeIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
PlayIcon,
|
||||||
|
PlusIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
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()
|
||||||
|
|
||||||
@@ -163,7 +165,14 @@ const handleOptionsClick = async (args) => {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div v-if="mode !== 'isolated'" ref="button"
|
||||||
v-if="mode !== 'isolated'"
|
|
||||||
ref="button"
|
|
||||||
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
||||||
:class="{ expanded: mode === 'expanded' }"
|
:class="{ expanded: mode === 'expanded' }" @click="toggleMenu">
|
||||||
@click="toggleMenu"
|
<Avatar size="36px" :src="selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||||
>
|
" />
|
||||||
<Avatar
|
|
||||||
size="36px"
|
|
||||||
:src="
|
|
||||||
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<span>
|
<span>
|
||||||
<component :is="getAccountType(selectedAccount)" v-if="selectedAccount" class="vector-icon" />
|
<component :is="getAccountType(selectedAccount)" v-if="selectedAccount" class="vector-icon" />
|
||||||
@@ -28,35 +20,28 @@
|
|||||||
<Avatar size="xs" :src="avatarUrl" />
|
<Avatar size="xs" :src="avatarUrl" />
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<component :is="getAccountType(selectedAccount)" class="vector-icon" /> {{ selectedAccount.profile.name }}
|
<component :is="getAccountType(selectedAccount)" class="vector-icon" /> {{
|
||||||
|
selectedAccount.profile.name }}
|
||||||
</h4>
|
</h4>
|
||||||
<p>Selected</p>
|
<p>Selected</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.profile.id)">
|
||||||
v-tooltip="'Log out'"
|
|
||||||
icon-only
|
|
||||||
color="raised"
|
|
||||||
@click="logout(selectedAccount.profile.id)"
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="login-section account">
|
<div v-else class="login-section account">
|
||||||
<h4>Not signed in</h4>
|
<h4>Not signed in</h4>
|
||||||
<Button
|
<Button v-tooltip="'Log via Microsoft'" :disabled="microsoftLoginDisabled" icon-only @click="login()">
|
||||||
v-tooltip="'Log in'"
|
<MicrosoftIcon v-if="!microsoftLoginDisabled" />
|
||||||
:disabled="loginDisabled"
|
|
||||||
icon-only
|
|
||||||
color="primary"
|
|
||||||
@click="login()"
|
|
||||||
>
|
|
||||||
<LogInIcon v-if="!loginDisabled" />
|
|
||||||
<SpinnerIcon v-else class="animate-spin" />
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
<MicrosoftIcon/>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'Add offline'" icon-only @click="tryOfflineLogin()">
|
<Button v-tooltip="'Add offline account'" icon-only @click="showOfflineLoginModal()">
|
||||||
<PirateIcon />
|
<PirateIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElybyLoginModal()">
|
||||||
|
<ElyByIcon v-if="!elybyLoginDisabled" />
|
||||||
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayAccounts.length > 0" class="account-group">
|
<div v-if="displayAccounts.length > 0" class="account-group">
|
||||||
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||||
@@ -73,35 +58,112 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="accounts.length > 0" class="login-section account centered">
|
<div v-if="accounts.length > 0" class="login-section account centered">
|
||||||
<Button v-tooltip="'Log in'" icon-only @click="login()">
|
<Button v-tooltip="'Log via Microsoft'" icon-only @click="login()">
|
||||||
<MicrosoftIcon />
|
<MicrosoftIcon v-if="!microsoftLoginDisabled" />
|
||||||
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'Add offline'" icon-only @click="tryOfflineLogin()">
|
<Button v-tooltip="'Add offline account'" icon-only @click="showOfflineLoginModal()">
|
||||||
<PirateIcon />
|
<PirateIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElybyLoginModal()">
|
||||||
|
<ElyByIcon v-if="!elybyLoginDisabled" />
|
||||||
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</transition>
|
</transition>
|
||||||
<ModalWrapper ref="loginOfflineModal" class="modal" header="Add new offline account">
|
<ModalWrapper ref="addElybyModal" class="modal" header="Authenticate with Ely.by">
|
||||||
<div class="modal-body">
|
<ModalWrapper ref="requestElybyTwoFactorCodeModal" class="modal"
|
||||||
<div class="label">Enter offline username</div>
|
header="Ely.by requested 2FA code for authentication">
|
||||||
<input type="text" v-model="playerName" placeholder="Provide offline player name" />
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
<Button icon-only color="secondary" @click="offlineLoginFinally()">
|
<label class="label">Enter your 2FA code</label>
|
||||||
|
<input v-model="elybyTwoFactorCode" type="text" placeholder="Your 2FA code here..." class="input" />
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button icon-only color="primary" class="continue-button" @click="addElybyProfile()">
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<ModalWrapper ref="loginErrorModal" class="modal" header="Error while proceed">
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
<div class="modal-body">
|
<label class="label">Enter your player name or email (preferred)</label>
|
||||||
<div class="label">Error occurred while adding offline account</div>
|
<input v-model="elybyLogin" type="text" placeholder="Your player name or email here..." class="input" />
|
||||||
<Button color="primary" @click="retryOfflineLogin()">
|
<label class="label">Enter your password</label>
|
||||||
|
<input v-model="elybyPassword" type="password" placeholder="Your password here..." class="input" />
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button icon-only color="primary" class="continue-button" @click="addElybyProfile()">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="addOfflineModal" class="modal" header="Add new offline account">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="label">Enter your player name</label>
|
||||||
|
<input v-model="offlinePlayerName" type="text" placeholder="Your player name here..." class="input" />
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button icon-only color="primary" class="continue-button" @click="addOfflineProfile()">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="authenticationElybyErrorModal" class="modal"
|
||||||
|
header="Error while proceeding authentication event with Ely.by">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
An error occurred while logging in.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" class="retry-button" @click="retryAddElybyProfile">
|
||||||
Try again
|
Try again
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<ModalWrapper ref="unexpectedErrorModal" class="modal" header="Ошибка">
|
<ModalWrapper ref="inputElybyErrorModal" class="modal" header="Error while proceeding input event with Ely.by">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
An error occurred while adding the Ely.by account. Please follow the instructions below.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>Check that you have entered the correct player name or email.</li>
|
||||||
|
<li>Check that you have entered the correct password.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" class="retry-button" @click="retryAddElybyProfile">
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="inputErrorModal" class="modal" header="Error while proceeding input event with offline account">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
An error occurred while adding the offline account. Please follow the instructions below.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>Check that you have entered the correct player name.</li>
|
||||||
|
<li>
|
||||||
|
Player name must be at least {{ minOfflinePlayerNameLength }} characters long and no more than
|
||||||
|
{{ maxOfflinePlayerNameLength }} characters.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" class="retry-button" @click="retryAddOfflineProfile">
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="exceptionErrorModal" class="modal" header="Unexpected error occurred">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="label">Unexcepted error</div>
|
<label class="label">An unexpected error has occurred. Please try again later.</label>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
@@ -109,17 +171,20 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
PlusIcon,
|
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
LogInIcon,
|
|
||||||
PirateIcon as Offline,
|
PirateIcon as Offline,
|
||||||
MicrosoftIcon as License,
|
MicrosoftIcon as License,
|
||||||
|
ElyByIcon as Elyby,
|
||||||
MicrosoftIcon,
|
MicrosoftIcon,
|
||||||
PirateIcon,
|
PirateIcon,
|
||||||
SpinnerIcon } from '@modrinth/assets'
|
ElyByIcon,
|
||||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
SpinnerIcon
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
|
elyby_auth_authenticate,
|
||||||
|
elyby_login,
|
||||||
offline_login,
|
offline_login,
|
||||||
users,
|
users,
|
||||||
remove_user,
|
remove_user,
|
||||||
@@ -127,13 +192,14 @@ import {
|
|||||||
login as login_flow,
|
login as login_flow,
|
||||||
get_default_user,
|
get_default_user,
|
||||||
} from '@/helpers/auth'
|
} from '@/helpers/auth'
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { get_available_skins } from '@/helpers/skins'
|
|
||||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||||
|
import { get_available_skins } from '@/helpers/skins'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
mode: {
|
mode: {
|
||||||
@@ -146,48 +212,180 @@ defineProps({
|
|||||||
const emit = defineEmits(['change'])
|
const emit = defineEmits(['change'])
|
||||||
|
|
||||||
const accounts = ref({})
|
const accounts = ref({})
|
||||||
const loginDisabled = ref(false)
|
const microsoftLoginDisabled = ref(false)
|
||||||
|
const elybyLoginDisabled = ref(false)
|
||||||
const defaultUser = ref()
|
const defaultUser = ref()
|
||||||
const loginOfflineModal = ref(null)
|
|
||||||
const loginErrorModal = ref(null)
|
|
||||||
const unexpectedErrorModal = ref(null)
|
|
||||||
const playerName = ref('')
|
|
||||||
|
|
||||||
async function tryOfflineLogin() { // Patched by AstralRinth
|
// [AR] • Feature
|
||||||
loginOfflineModal.value.show()
|
const clientToken = "astralrinth"
|
||||||
|
const addOfflineModal = ref(null)
|
||||||
|
const addElybyModal = ref(null)
|
||||||
|
const requestElybyTwoFactorCodeModal = ref(null)
|
||||||
|
const authenticationElybyErrorModal = ref(null)
|
||||||
|
const inputElybyErrorModal = ref(null)
|
||||||
|
const inputErrorModal = ref(null)
|
||||||
|
const exceptionErrorModal = ref(null)
|
||||||
|
const offlinePlayerName = ref('')
|
||||||
|
const elybyLogin = ref('')
|
||||||
|
const elybyPassword = ref('')
|
||||||
|
const elybyTwoFactorCode = ref('')
|
||||||
|
const minOfflinePlayerNameLength = 2
|
||||||
|
const maxOfflinePlayerNameLength = 20
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function getAccountType(account) {
|
||||||
|
switch (account.account_type) {
|
||||||
|
case 'microsoft':
|
||||||
|
return License
|
||||||
|
case 'pirate':
|
||||||
|
return Offline
|
||||||
|
case 'elyby':
|
||||||
|
return Elyby
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function offlineLoginFinally() { // Patched by AstralRinth
|
// [AR] • Feature
|
||||||
const name = playerName.value
|
function showOfflineLoginModal() {
|
||||||
if (name.length > 1 && name.length < 20 && name !== '') {
|
addOfflineModal.value?.show()
|
||||||
const loggedIn = await offline_login(name).catch(handleError)
|
}
|
||||||
loginOfflineModal.value.hide()
|
|
||||||
if (loggedIn) {
|
// [AR] • Feature
|
||||||
await setAccount(loggedIn)
|
function showElybyLoginModal() {
|
||||||
|
addElybyModal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function retryAddOfflineProfile() {
|
||||||
|
inputErrorModal.value?.hide()
|
||||||
|
clearOfflineFields()
|
||||||
|
showOfflineLoginModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function retryAddElybyProfile() {
|
||||||
|
authenticationElybyErrorModal.value?.hide()
|
||||||
|
inputElybyErrorModal.value?.hide()
|
||||||
|
clearElybyFields()
|
||||||
|
showElybyLoginModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function clearElybyFields() {
|
||||||
|
elybyLogin.value = ''
|
||||||
|
elybyPassword.value = ''
|
||||||
|
elybyTwoFactorCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function clearOfflineFields() {
|
||||||
|
offlinePlayerName.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
async function addOfflineProfile() {
|
||||||
|
const name = offlinePlayerName.value.trim()
|
||||||
|
const isValidName = name.length >= minOfflinePlayerNameLength && name.length <= maxOfflinePlayerNameLength
|
||||||
|
|
||||||
|
if (!isValidName) {
|
||||||
|
addOfflineModal.value?.hide()
|
||||||
|
inputErrorModal.value?.show()
|
||||||
|
clearOfflineFields()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await offline_login(name)
|
||||||
|
|
||||||
|
addOfflineModal.value?.hide()
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
await setAccount(result)
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
} else {
|
} else {
|
||||||
unexpectedErrorModal.value.show()
|
exceptionErrorModal.value?.show()
|
||||||
}
|
}
|
||||||
playerName.value = ''
|
} catch (error) {
|
||||||
} else {
|
handleError(error)
|
||||||
playerName.value = ''
|
exceptionErrorModal.value?.show()
|
||||||
loginOfflineModal.value.hide()
|
} finally {
|
||||||
loginErrorModal.value.show()
|
clearOfflineFields()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function retryOfflineLogin() { // Patched by AstralRinth
|
// [AR] • Feature
|
||||||
loginErrorModal.value.hide()
|
async function addElybyProfile() {
|
||||||
tryOfflineLogin()
|
if (!elybyLogin.value || !elybyPassword.value) {
|
||||||
|
addElybyModal.value?.hide()
|
||||||
|
inputElybyErrorModal.value?.show()
|
||||||
|
clearElybyFields()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elybyLoginDisabled.value = true
|
||||||
|
|
||||||
|
const login = elybyLogin.value.trim()
|
||||||
|
let password = elybyPassword.value.trim()
|
||||||
|
const twoFactorCode = elybyTwoFactorCode.value.trim()
|
||||||
|
if (password && twoFactorCode) {
|
||||||
|
password = `${password}:${twoFactorCode}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAccountType(account) { // Patched by AstralRinth
|
try {
|
||||||
if (account.access_token != "null" && account.access_token != null && account.access_token != "") {
|
const raw_result = await elyby_auth_authenticate(
|
||||||
return License
|
login,
|
||||||
} else {
|
password,
|
||||||
return Offline
|
clientToken
|
||||||
|
)
|
||||||
|
|
||||||
|
const json_data = JSON.parse(raw_result)
|
||||||
|
|
||||||
|
console.log(json_data?.error)
|
||||||
|
console.log(json_data?.errorMessage)
|
||||||
|
|
||||||
|
if (!json_data.accessToken) {
|
||||||
|
if (
|
||||||
|
json_data.error === 'ForbiddenOperationException' &&
|
||||||
|
json_data.errorMessage?.includes('two factor')
|
||||||
|
) {
|
||||||
|
requestElybyTwoFactorCodeModal.value?.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addElybyModal.value?.hide()
|
||||||
|
requestElybyTwoFactorCodeModal.value?.hide()
|
||||||
|
authenticationElybyErrorModal.value?.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = json_data.accessToken
|
||||||
|
const selectedProfileId = convertRawStringToUUIDv4(json_data.selectedProfile.id)
|
||||||
|
const selectedProfileName = json_data.selectedProfile.name
|
||||||
|
|
||||||
|
const result = await elyby_login(selectedProfileId, selectedProfileName, accessToken)
|
||||||
|
|
||||||
|
addElybyModal.value?.hide()
|
||||||
|
requestElybyTwoFactorCodeModal.value?.hide()
|
||||||
|
|
||||||
|
clearElybyFields()
|
||||||
|
|
||||||
|
await setAccount(result)
|
||||||
|
await refreshValues()
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err)
|
||||||
|
exceptionErrorModal.value?.show()
|
||||||
|
} finally {
|
||||||
|
elybyLoginDisabled.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function convertRawStringToUUIDv4(rawId) {
|
||||||
|
if (rawId.length !== 32) {
|
||||||
|
console.warn('Invalid UUID string:', rawId)
|
||||||
|
return rawId
|
||||||
|
}
|
||||||
|
return `${rawId.slice(0, 8)}-${rawId.slice(8, 12)}-${rawId.slice(12, 16)}-${rawId.slice(16, 20)}-${rawId.slice(20)}`
|
||||||
|
}
|
||||||
|
|
||||||
const equippedSkin = ref(null)
|
const equippedSkin = ref(null)
|
||||||
const headUrlCache = ref(new Map())
|
const headUrlCache = ref(new Map())
|
||||||
|
|
||||||
@@ -213,13 +411,13 @@ async function refreshValues() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setLoginDisabled(value) {
|
function setLoginDisabled(value) {
|
||||||
loginDisabled.value = value
|
microsoftLoginDisabled.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
refreshValues,
|
refreshValues,
|
||||||
setLoginDisabled,
|
setLoginDisabled,
|
||||||
loginDisabled,
|
microsoftLoginDisabled,
|
||||||
})
|
})
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
|
|
||||||
@@ -265,7 +463,7 @@ async function setAccount(account) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
loginDisabled.value = true
|
microsoftLoginDisabled.value = true
|
||||||
const loggedIn = await login_flow().catch(handleSevereError)
|
const loggedIn = await login_flow().catch(handleSevereError)
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
@@ -274,7 +472,7 @@ async function login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trackEvent('AccountLogIn')
|
trackEvent('AccountLogIn')
|
||||||
loginDisabled.value = false
|
microsoftLoginDisabled.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const logout = async (id) => {
|
const logout = async (id) => {
|
||||||
@@ -373,12 +571,12 @@ onUnmounted(() => {
|
|||||||
z-index: 11;
|
z-index: 11;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-divider);
|
||||||
width: max-content;
|
width: max-content;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
max-height: 98vh;
|
max-height: calc(100vh - 300px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
@@ -475,7 +673,7 @@ onUnmounted(() => {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-divider);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<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,
|
||||||
|
|||||||
@@ -42,11 +42,12 @@
|
|||||||
</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()
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-show="shown" ref="contextMenu" class="context-menu" :style="{
|
<div
|
||||||
|
v-show="shown"
|
||||||
|
ref="contextMenu"
|
||||||
|
class="context-menu"
|
||||||
|
:style="{
|
||||||
left: left,
|
left: left,
|
||||||
top: top,
|
top: top,
|
||||||
}">
|
}"
|
||||||
|
>
|
||||||
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
||||||
<hr v-if="option.type === 'divider'" class="divider" />
|
<hr v-if="option.type === 'divider'" class="divider" />
|
||||||
<div v-else-if="!(isLinkedData(item) && option.name === `add_content`)" class="item clickable"
|
<div
|
||||||
:class="[option.color ?? 'base']">
|
v-else-if="!(isLinkedData(item) && option.name === `add_content`)"
|
||||||
|
class="item clickable"
|
||||||
|
:class="[option.color ?? 'base']"
|
||||||
|
>
|
||||||
<slot :name="option.name" />
|
<slot :name="option.name" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,7 +24,7 @@
|
|||||||
</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'])
|
||||||
|
|
||||||
@@ -32,22 +40,27 @@ defineExpose({
|
|||||||
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
|
|
||||||
|
|
||||||
if (menuWidth + event.pageX >= window.innerWidth) {
|
|
||||||
left.value = event.pageX - menuWidth + 2 + 'px'
|
|
||||||
} else {
|
|
||||||
left.value = event.pageX - 2 + 'px'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (menuHeight + event.pageY >= window.innerHeight) {
|
|
||||||
top.value = event.pageY - menuHeight + 2 + 'px'
|
|
||||||
} else {
|
|
||||||
top.value = event.pageY - 2 + 'px'
|
|
||||||
}
|
|
||||||
|
|
||||||
shown.value = true
|
shown.value = true
|
||||||
|
|
||||||
|
// then, adjust position if overflowing
|
||||||
|
nextTick(() => {
|
||||||
|
const menuWidth = contextMenu.value?.clientWidth || 200
|
||||||
|
const menuHeight = contextMenu.value?.clientHeight || 100
|
||||||
|
const minFromEdge = 10
|
||||||
|
|
||||||
|
if (event.pageX + menuWidth + minFromEdge >= window.innerWidth) {
|
||||||
|
left.value = Math.max(minFromEdge, event.pageX - menuWidth - minFromEdge) + 'px'
|
||||||
|
} else {
|
||||||
|
left.value = event.pageX + minFromEdge + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.pageY + menuHeight + minFromEdge >= window.innerHeight) {
|
||||||
|
top.value = Math.max(minFromEdge, event.pageY - menuHeight - minFromEdge) + 'px'
|
||||||
|
} else {
|
||||||
|
top.value = event.pageY + minFromEdge + 'px'
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -106,7 +119,7 @@ onBeforeUnmount(() => {
|
|||||||
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;
|
||||||
@@ -150,7 +163,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
CopyIcon,
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
XIcon,
|
|
||||||
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')
|
||||||
@@ -70,7 +78,7 @@ defineExpose({
|
|||||||
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 {
|
||||||
@@ -148,6 +156,26 @@ async function copyToClipboard(text) {
|
|||||||
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>
|
||||||
@@ -158,7 +186,7 @@ async function copyToClipboard(text) {
|
|||||||
<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
|
||||||
@@ -172,7 +200,7 @@ async function copyToClipboard(text) {
|
|||||||
<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
|
||||||
@@ -211,7 +239,7 @@ async function copyToClipboard(text) {
|
|||||||
<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>
|
||||||
@@ -225,7 +253,7 @@ async function copyToClipboard(text) {
|
|||||||
</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>
|
||||||
@@ -255,7 +283,7 @@ async function copyToClipboard(text) {
|
|||||||
</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>
|
||||||
@@ -265,7 +293,7 @@ async function copyToClipboard(text) {
|
|||||||
</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">
|
||||||
@@ -313,10 +341,78 @@ async function copyToClipboard(text) {
|
|||||||
/>
|
/>
|
||||||
</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"
|
||||||
|
>{{ debugInfo }}</pre>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="errorType === 'state_init'">
|
||||||
|
<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>
|
||||||
|
</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>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
@@ -333,6 +429,16 @@ async function copyToClipboard(text) {
|
|||||||
</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;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<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: {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
GameIcon,
|
GameIcon,
|
||||||
@@ -9,17 +7,20 @@ import {
|
|||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TimerIcon,
|
TimerIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled, useRelativeTime } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { finish_install, kill, run } from '@/helpers/profile'
|
import dayjs from 'dayjs'
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
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({
|
||||||
@@ -93,7 +94,7 @@ const stop = async (e, 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 () => {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<p class="input-label">Game version</p>
|
<p class="input-label">Game version</p>
|
||||||
<div class="versions">
|
<div class="flex gap-4 items-center">
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="game_version"
|
v-model="game_version"
|
||||||
class="selector"
|
class="selector"
|
||||||
@@ -45,19 +45,14 @@
|
|||||||
open-direction="top"
|
open-direction="top"
|
||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox v-model="showSnapshots" class="shrink-0" label="Show all versions" />
|
||||||
v-if="showAdvanced"
|
|
||||||
v-model="showSnapshots"
|
|
||||||
class="filter-checkbox"
|
|
||||||
label="Include snapshots"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showAdvanced && loader !== 'vanilla'" class="input-row">
|
<div v-if="loader !== 'vanilla'" class="input-row">
|
||||||
<p class="input-label">Loader version</p>
|
<p class="input-label">Loader version</p>
|
||||||
<Chips v-model="loader_version" :items="['stable', 'latest', 'other']" />
|
<Chips v-model="loader_version" :items="['stable', 'latest', 'other']" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showAdvanced && loader_version === 'other' && loader !== 'vanilla'">
|
<div v-if="loader_version === 'other' && loader !== 'vanilla'">
|
||||||
<div v-if="game_version" class="input-row">
|
<div v-if="game_version" class="input-row">
|
||||||
<p class="input-label">Select version</p>
|
<p class="input-label">Select version</p>
|
||||||
<multiselect
|
<multiselect
|
||||||
@@ -75,10 +70,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group push-right">
|
<div class="input-group push-right">
|
||||||
<Button @click="toggle_advanced">
|
|
||||||
<CodeIcon />
|
|
||||||
{{ showAdvanced ? 'Hide advanced' : 'Show advanced' }}
|
|
||||||
</Button>
|
|
||||||
<Button @click="hide()">
|
<Button @click="hide()">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Cancel
|
Cancel
|
||||||
@@ -110,7 +101,7 @@
|
|||||||
placeholder="Path to launcher"
|
placeholder="Path to launcher"
|
||||||
@change="setPath"
|
@change="setPath"
|
||||||
/>
|
/>
|
||||||
<Button class="r-btn" @click="() => (selectedLauncherPath = '')">
|
<Button class="r-btn" @click="() => (selectedProfileType.path = '')">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,6 +154,14 @@
|
|||||||
<div v-else class="table-content empty">No profiles found</div>
|
<div v-else class="table-content empty">No profiles found</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
|
<Button
|
||||||
|
v-if="selectedProfileType.name === 'Curseforge'"
|
||||||
|
@click="showCurseForgeProfileModal"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<CodeIcon />
|
||||||
|
Import from Profile Code
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
:disabled="
|
:disabled="
|
||||||
loading ||
|
loading ||
|
||||||
@@ -194,12 +193,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
|
<CurseForgeProfileImportModal ref="curseforgeProfileModal" :close-parent="hide" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import {
|
import {
|
||||||
CodeIcon,
|
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
FolderSearchIcon,
|
FolderSearchIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
@@ -208,24 +206,29 @@ import {
|
|||||||
UploadIcon,
|
UploadIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, Checkbox, Chips } from '@modrinth/ui'
|
import { Avatar, Button, Checkbox, Chips, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
|
||||||
import { get_loaders } from '@/helpers/tags'
|
|
||||||
import { create } from '@/helpers/profile'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { create_profile_and_install_from_file } from '@/helpers/pack.js'
|
|
||||||
import {
|
import {
|
||||||
get_default_launcher_path,
|
get_default_launcher_path,
|
||||||
get_importable_instances,
|
get_importable_instances,
|
||||||
import_instance,
|
import_instance,
|
||||||
} from '@/helpers/import.js'
|
} from '@/helpers/import.js'
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
import { create_profile_and_install_from_file } from '@/helpers/pack.js'
|
||||||
|
import { create } from '@/helpers/profile'
|
||||||
|
import { get_loaders } from '@/helpers/tags'
|
||||||
|
|
||||||
|
import CurseForgeProfileImportModal from '@/components/ui/CurseForgeProfileImportModal.vue'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const profile_name = ref('')
|
const profile_name = ref('')
|
||||||
const game_version = ref('')
|
const game_version = ref('')
|
||||||
@@ -234,7 +237,6 @@ const loader_version = ref('stable')
|
|||||||
const specified_loader_version = ref('')
|
const specified_loader_version = ref('')
|
||||||
const icon = ref(null)
|
const icon = ref(null)
|
||||||
const display_icon = ref(null)
|
const display_icon = ref(null)
|
||||||
const showAdvanced = ref(false)
|
|
||||||
const creating = ref(false)
|
const creating = ref(false)
|
||||||
const showSnapshots = ref(false)
|
const showSnapshots = ref(false)
|
||||||
const creationType = ref('custom')
|
const creationType = ref('custom')
|
||||||
@@ -246,7 +248,6 @@ defineExpose({
|
|||||||
specified_loader_version.value = ''
|
specified_loader_version.value = ''
|
||||||
profile_name.value = ''
|
profile_name.value = ''
|
||||||
creating.value = false
|
creating.value = false
|
||||||
showAdvanced.value = false
|
|
||||||
showSnapshots.value = false
|
showSnapshots.value = false
|
||||||
loader.value = 'vanilla'
|
loader.value = 'vanilla'
|
||||||
loader_version.value = 'stable'
|
loader_version.value = 'stable'
|
||||||
@@ -283,6 +284,11 @@ const hide = () => {
|
|||||||
unlistener.value = null
|
unlistener.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showCurseForgeProfileModal = () => {
|
||||||
|
curseforgeProfileModal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (unlistener.value) {
|
if (unlistener.value) {
|
||||||
unlistener.value()
|
unlistener.value()
|
||||||
@@ -305,12 +311,16 @@ const [
|
|||||||
get_game_versions().then(shallowRef).catch(handleError),
|
get_game_versions().then(shallowRef).catch(handleError),
|
||||||
get_loaders()
|
get_loaders()
|
||||||
.then((value) =>
|
.then((value) =>
|
||||||
|
ref(
|
||||||
value
|
value
|
||||||
.filter((item) => item.supported_project_types.includes('modpack'))
|
.filter((item) => item.supported_project_types.includes('modpack'))
|
||||||
.map((item) => item.name.toLowerCase()),
|
.map((item) => item.name.toLowerCase()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.then(ref)
|
.catch((err) => {
|
||||||
.catch(handleError),
|
handleError(err)
|
||||||
|
return ref([])
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
loaders.value.unshift('vanilla')
|
loaders.value.unshift('vanilla')
|
||||||
|
|
||||||
@@ -334,6 +344,7 @@ const game_versions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const modal = ref(null)
|
const modal = ref(null)
|
||||||
|
const curseforgeProfileModal = ref(null)
|
||||||
|
|
||||||
const check_valid = computed(() => {
|
const check_valid = computed(() => {
|
||||||
return (
|
return (
|
||||||
@@ -411,10 +422,6 @@ const selectable_versions = computed(() => {
|
|||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggle_advanced = () => {
|
|
||||||
showAdvanced.value = !showAdvanced.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const openFile = async () => {
|
const openFile = async () => {
|
||||||
const newProject = await open({ multiple: false })
|
const newProject = await open({ multiple: false })
|
||||||
if (!newProject) return
|
if (!newProject) return
|
||||||
@@ -556,12 +563,6 @@ const next = async () => {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.versions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(button.checkbox) {
|
:deep(button.checkbox) {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<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
|
||||||
|
|||||||
@@ -35,13 +35,15 @@
|
|||||||
</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)
|
||||||
|
|||||||
@@ -53,20 +53,22 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
SearchIcon,
|
|
||||||
PlayIcon,
|
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
XIcon,
|
|
||||||
FolderSearchIcon,
|
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
FolderSearchIcon,
|
||||||
|
PlayIcon,
|
||||||
|
SearchIcon,
|
||||||
|
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: {
|
||||||
@@ -108,7 +110,6 @@ 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
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<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: {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
: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"
|
||||||
>
|
>
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
v-else
|
v-else
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
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"
|
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"
|
||||||
@click="to"
|
@click="to"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
@@ -29,12 +31,18 @@ const route = useRoute()
|
|||||||
|
|
||||||
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
|
type RouteFunction = (route: RouteLocationNormalizedLoaded) => boolean
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
to: (() => void) | string
|
to: (() => void) | string
|
||||||
isPrimary?: RouteFunction
|
isPrimary?: RouteFunction
|
||||||
isSubpage?: RouteFunction
|
isSubpage?: RouteFunction
|
||||||
highlightOverride?: boolean
|
highlightOverride?: boolean
|
||||||
}>()
|
disabled?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<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()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-bar__fill" :style="{ width: `${progress}%` }"></div>
|
<div
|
||||||
|
class="progress-bar__fill"
|
||||||
|
:style="{
|
||||||
|
width: `${progress}%`,
|
||||||
|
'background-color': error ? 'var(--color-red)' : 'var(--color-brand)',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -13,6 +19,10 @@ defineProps({
|
|||||||
return value >= 0 && value <= 100
|
return value >= 0 && value <= 100
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
error: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -27,7 +37,6 @@ defineProps({
|
|||||||
|
|
||||||
.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)
|
||||||
@@ -21,14 +21,11 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
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(() => {
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<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 () => {
|
||||||
@@ -67,7 +69,7 @@ onUnmounted(() => {
|
|||||||
<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>
|
||||||
|
|||||||
@@ -20,12 +20,21 @@
|
|||||||
>
|
>
|
||||||
{{ 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"
|
||||||
|
class="arrow button-base"
|
||||||
|
:class="{ rotate: showProfiles }"
|
||||||
|
@click="toggleProfiles()"
|
||||||
|
>
|
||||||
<DropdownIcon />
|
<DropdownIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click="stop(selectedProcess)">
|
<Button
|
||||||
|
v-tooltip="'Stop instance'"
|
||||||
|
icon-only
|
||||||
|
class="icon-button stop"
|
||||||
|
@click="stop(selectedProcess)"
|
||||||
|
>
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
||||||
@@ -36,44 +45,6 @@
|
|||||||
<span class="circle stopped" />
|
<span class="circle stopped" />
|
||||||
<span class="running-text"> No instances running </span>
|
<span class="running-text"> No instances running </span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="updateState">
|
|
||||||
<a>
|
|
||||||
<Button class="download" :disabled="installState" @click="confirmUpdating(), getRemote(false, false)">
|
|
||||||
<DownloadIcon />
|
|
||||||
{{
|
|
||||||
installState
|
|
||||||
? "Downloading new update..."
|
|
||||||
: "Download new update"
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ModalWrapper ref="confirmUpdate" :has-to-type="false" header="Request to update the AstralRinth launcher">
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="markdown-body">
|
|
||||||
<p>The new version of the AstralRinth launcher is available.</p>
|
|
||||||
<p>Your version is outdated. We recommend that you update to the latest version.</p>
|
|
||||||
<p><strong>⚠️ Warning ⚠️</strong></p>
|
|
||||||
<p>
|
|
||||||
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
|
|
||||||
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
|
|
||||||
your files, so you should always make copies of them and keep them in a safe place.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span>Source • Git Astralium</span>
|
|
||||||
<span>Version on remote server • <p id="releaseData" class="cosmic inline-fix"></p></span>
|
|
||||||
<span>Version on local device •
|
|
||||||
<p class="cosmic inline-fix">v{{ version }}</p>
|
|
||||||
</span>
|
|
||||||
<div class="button-group push-right">
|
|
||||||
<Button class="download-modal" @click="confirmUpdate.hide()">
|
|
||||||
Decline</Button>
|
|
||||||
<Button class="download-modal" @click="approveUpdate()">
|
|
||||||
Accept
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
</div>
|
</div>
|
||||||
<transition name="download">
|
<transition name="download">
|
||||||
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
||||||
@@ -83,20 +54,39 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}% {{ loadingBar.message }}
|
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}%
|
||||||
|
{{ loadingBar.message }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="download">
|
<transition name="download">
|
||||||
<Card v-if="showProfiles === true && currentProcesses.length > 0" ref="profiles" class="profile-card">
|
<Card
|
||||||
<Button v-for="process in currentProcesses" :key="process.uuid" class="profile-button"
|
v-if="showProfiles === true && currentProcesses.length > 0"
|
||||||
@click="selectProcess(process)">
|
ref="profiles"
|
||||||
|
class="profile-card"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
v-for="process in currentProcesses"
|
||||||
|
:key="process.uuid"
|
||||||
|
class="profile-button"
|
||||||
|
@click="selectProcess(process)"
|
||||||
|
>
|
||||||
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
|
<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)">
|
<Button
|
||||||
|
v-tooltip="'Stop instance'"
|
||||||
|
icon-only
|
||||||
|
class="icon-button stop"
|
||||||
|
@click.stop="stop(process)"
|
||||||
|
>
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click.stop="goToTerminal(process.profile.path)">
|
<Button
|
||||||
|
v-tooltip="'View logs'"
|
||||||
|
icon-only
|
||||||
|
class="icon-button"
|
||||||
|
@click.stop="goToTerminal(process.profile.path)"
|
||||||
|
>
|
||||||
<TerminalSquareIcon />
|
<TerminalSquareIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -107,40 +97,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
DropdownIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TerminalSquareIcon,
|
TerminalSquareIcon,
|
||||||
DropdownIcon,
|
|
||||||
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)
|
||||||
@@ -204,8 +177,8 @@ 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
|
||||||
}
|
}
|
||||||
@@ -217,8 +190,8 @@ const refreshInfo = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -298,101 +271,6 @@ onBeforeUnmount(() => {
|
|||||||
</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;
|
||||||
@@ -404,7 +282,6 @@ onBeforeUnmount(() => {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -416,7 +293,7 @@ onBeforeUnmount(() => {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,10 +303,8 @@ onBeforeUnmount(() => {
|
|||||||
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;
|
|
||||||
/* IE 10 and IE 11 */
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&.clickable:hover {
|
&.clickable:hover {
|
||||||
@@ -481,7 +356,7 @@ onBeforeUnmount(() => {
|
|||||||
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%);
|
||||||
@@ -579,7 +454,7 @@ onBeforeUnmount(() => {
|
|||||||
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 {
|
||||||
|
|||||||
@@ -117,16 +117,19 @@
|
|||||||
</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({
|
||||||
@@ -174,7 +177,7 @@ async function install() {
|
|||||||
(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')
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!hidden" class="splash-screen dark" :class="{ 'fade-out': doneLoading }">
|
<div v-if="!hidden" class="splash-screen dark" :class="{ 'fade-out': doneLoading }">
|
||||||
<div v-if="os !== 'MacOS'" class="app-buttons">
|
<div v-if="os !== 'MacOS'" class="app-buttons">
|
||||||
<button class="btn icon-only transparent" icon-only @click="() => getCurrent().minimize()">
|
<button
|
||||||
|
class="btn icon-only transparent"
|
||||||
|
icon-only
|
||||||
|
@click="() => getCurrentWindow().minimize()"
|
||||||
|
>
|
||||||
<MinimizeIcon />
|
<MinimizeIcon />
|
||||||
</button>
|
</button>
|
||||||
<button class="btn icon-only transparent" @click="() => getCurrent().toggleMaximize()">
|
<button class="btn icon-only transparent" @click="() => getCurrentWindow().toggleMaximize()">
|
||||||
<MaximizeIcon />
|
<MaximizeIcon />
|
||||||
</button>
|
</button>
|
||||||
<button class="btn icon-only transparent" @click="handleClose">
|
<button class="btn icon-only transparent" @click="handleClose">
|
||||||
@@ -82,11 +86,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { MaximizeIcon, MinimizeIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||||
import { loading_listener } from '@/helpers/events.js'
|
import { loading_listener } from '@/helpers/events.js'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
|
||||||
import { XIcon, MaximizeIcon, MinimizeIcon } from '@modrinth/assets'
|
|
||||||
import { getOS } from '@/helpers/utils.js'
|
import { getOS } from '@/helpers/utils.js'
|
||||||
import { useLoading } from '@/store/loading.js'
|
import { useLoading } from '@/store/loading.js'
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ watch(loading, (newValue) => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
hidden.value = true
|
hidden.value = true
|
||||||
loading.setEnabled(true)
|
loading.setEnabled(true)
|
||||||
}, 250)
|
}, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -130,9 +135,6 @@ loading_listener(async (e) => {
|
|||||||
if (e.event.type === 'directory_move') {
|
if (e.event.type === 'directory_move') {
|
||||||
loadingProgress.value = 100 * (e.fraction ?? 1)
|
loadingProgress.value = 100 * (e.fraction ?? 1)
|
||||||
message.value = 'Updating app directory...'
|
message.value = 'Updating app directory...'
|
||||||
} else if (e.event.type === 'launcher_update') {
|
|
||||||
loadingProgress.value = 100 * (e.fraction ?? 1)
|
|
||||||
message.value = 'Updating Modrinth App...'
|
|
||||||
} else if (e.event.type === 'checking_for_updates') {
|
} else if (e.event.type === 'checking_for_updates') {
|
||||||
loadingProgress.value = 100 * (e.fraction ?? 1)
|
loadingProgress.value = 100 * (e.fraction ?? 1)
|
||||||
message.value = 'Checking for updates...'
|
message.value = 'Checking for updates...'
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -37,7 +39,14 @@ defineExpose({
|
|||||||
|
|
||||||
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>
|
||||||
|
|
||||||
|
|||||||
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,34 +1,41 @@
|
|||||||
<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('')
|
||||||
@@ -40,46 +47,25 @@ async function addFriendFromModal() {
|
|||||||
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)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addFriend(friend: Friend) {
|
|
||||||
await add_friend(
|
|
||||||
friend.id === userCredentials.value.user_id ? friend.friend_id : friend.id,
|
|
||||||
).catch(handleError)
|
|
||||||
await loadFriends()
|
await loadFriends()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function removeFriend(friend: Friend) {
|
async function removeFriend(friend: FriendWithUserData) {
|
||||||
await remove_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()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Friend = {
|
|
||||||
id: string
|
|
||||||
friend_id: string | null
|
|
||||||
status: string | null
|
|
||||||
last_updated: Dayjs | null
|
|
||||||
created: Dayjs
|
|
||||||
username: string
|
|
||||||
accepted: boolean
|
|
||||||
online: boolean
|
|
||||||
avatar: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userFriends = ref<Friend[]>([])
|
const userFriends = ref<FriendWithUserData[]>([])
|
||||||
const acceptedFriends = computed(() =>
|
const sortedFriends = computed<FriendWithUserData[]>(() =>
|
||||||
userFriends.value
|
userFriends.value.slice().sort((a, b) => {
|
||||||
.filter((x) => x.accepted)
|
|
||||||
.toSorted((a, b) => {
|
|
||||||
if (a.last_updated === null && b.last_updated === null) {
|
if (a.last_updated === null && b.last_updated === null) {
|
||||||
return 0 // Both are null, equal in sorting
|
return 0 // Both are null, equal in sorting
|
||||||
}
|
}
|
||||||
@@ -93,8 +79,32 @@ const acceptedFriends = computed(() =>
|
|||||||
return b.last_updated.diff(a.last_updated)
|
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()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeFriends = computed<FriendWithUserData[]>(() =>
|
||||||
|
filteredFriends.value.filter((x) => !!x.status && x.online && x.accepted),
|
||||||
|
)
|
||||||
|
const onlineFriends = computed<FriendWithUserData[]>(() =>
|
||||||
|
filteredFriends.value.filter((x) => x.online && !x.status && x.accepted),
|
||||||
|
)
|
||||||
|
const offlineFriends = computed<FriendWithUserData[]>(() =>
|
||||||
|
filteredFriends.value.filter((x) => !x.online && x.accepted),
|
||||||
|
)
|
||||||
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)
|
||||||
@@ -103,34 +113,7 @@ async function loadFriends(timeout = false) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const friendsList = await friends()
|
const friendsList = await friends()
|
||||||
|
userFriends.value = await transformFriends(friendsList, userCredentials.value)
|
||||||
if (friendsList.length === 0) {
|
|
||||||
userFriends.value = []
|
|
||||||
} else {
|
|
||||||
const friendStatuses = await friend_statuses()
|
|
||||||
const users = await get_user_many(
|
|
||||||
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
|
loading.value = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error loading friends', e)
|
console.error('Error loading friends', e)
|
||||||
@@ -145,6 +128,7 @@ watch(
|
|||||||
() => {
|
() => {
|
||||||
if (userCredentials.value === undefined) {
|
if (userCredentials.value === undefined) {
|
||||||
userFriends.value = []
|
userFriends.value = []
|
||||||
|
loading.value = false
|
||||||
} else if (userCredentials.value === null) {
|
} else if (userCredentials.value === null) {
|
||||||
userFriends.value = []
|
userFriends.value = []
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -159,49 +143,87 @@ 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">
|
|
||||||
<p v-if="acceptedFriends.length === 0">Add friends to share what you're playing!</p>
|
|
||||||
<div v-else class="flex flex-col gap-4 min-w-[20rem]">
|
|
||||||
<input v-model="search" type="text" placeholder="Search friends..." class="w-full" />
|
|
||||||
<div
|
|
||||||
v-for="friend in acceptedFriends.filter(
|
|
||||||
(x) => !search || x.username.toLowerCase().includes(search),
|
|
||||||
)"
|
|
||||||
:key="friend.username"
|
|
||||||
class="flex gap-2 items-center"
|
|
||||||
>
|
|
||||||
<div class="relative">
|
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
|
||||||
<span
|
|
||||||
v-if="friend.online"
|
|
||||||
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>{{ friend.username }}</div>
|
|
||||||
<div class="ml-auto">
|
|
||||||
<ButtonStyled>
|
|
||||||
<button @click="removeFriend(friend)">
|
|
||||||
<XIcon />
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
<ModalWrapper ref="friendInvitesModal" header="View friend requests">
|
<ModalWrapper ref="friendInvitesModal" header="View friend requests">
|
||||||
<p v-if="pendingFriends.length === 0">You have no pending friend requests :C</p>
|
<p v-if="incomingRequests.length === 0">You have no pending friend requests :C</p>
|
||||||
<div v-else class="flex flex-col gap-4">
|
<div v-else class="flex flex-col gap-4 min-w-[40rem]">
|
||||||
<div v-for="friend in pendingFriends" :key="friend.username" class="flex gap-2">
|
<div v-for="friend in incomingRequests" :key="friend.username" class="flex gap-2">
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
||||||
<div class="flex flex-col gap-2">
|
<div class="grid grid-cols-[1fr_auto] w-full gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
<template v-if="friend.id === userCredentials.user_id">
|
<template v-if="friend.id === userCredentials?.user_id">
|
||||||
<span class="font-bold">{{ friend.username }}</span> sent you a friend request
|
<span class="text-contrast">{{ friend.username }}</span> sent you a friend request
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
||||||
@@ -212,7 +234,7 @@ onUnmounted(() => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<template v-if="friend.id === userCredentials.user_id">
|
<template v-if="friend.id === userCredentials?.user_id">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button @click="addFriend(friend)">
|
<button @click="addFriend(friend)">
|
||||||
<UserPlusIcon />
|
<UserPlusIcon />
|
||||||
@@ -239,72 +261,89 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<ModalWrapper ref="addFriendModal" header="Add a friend">
|
<ModalWrapper ref="addFriendModal" :header="formatMessage(messages.addingAFriend)">
|
||||||
<div class="mb-4">
|
<div class="min-w-[30rem]">
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Username</h2>
|
<h2 class="m-0 text-base font-medium text-primary">
|
||||||
<p class="m-0 mt-1 leading-tight">You can add friends with their Modrinth username.</p>
|
{{ formatMessage(messages.usernameTitle) }}
|
||||||
<input v-model="username" class="mt-2 w-full" type="text" placeholder="Enter username..." />
|
</h2>
|
||||||
|
<p class="m-0 mt-1 text-sm text-secondary leading-tight">
|
||||||
|
{{ formatMessage(messages.usernameDescription) }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-2 mt-4">
|
||||||
|
<div class="iconified-input flex-1">
|
||||||
|
<UserIcon aria-hidden="true" />
|
||||||
|
<input
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
:placeholder="formatMessage(messages.usernamePlaceholder)"
|
||||||
|
@keyup.enter="addFriendFromModal"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
||||||
<UserPlusIcon />
|
<SendIcon />
|
||||||
Add friend
|
{{ formatMessage(messages.sendFriendRequest) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<div class="flex justify-between items-center">
|
<div v-if="userCredentials && !loading" class="flex gap-1 items-center mb-3 ml-2 mr-1">
|
||||||
<h3 class="text-lg m-0">Friends</h3>
|
<template v-if="sortedFriends.length > 0">
|
||||||
<ButtonStyled v-if="userCredentials" type="transparent" circular>
|
<ButtonStyled circular type="transparent">
|
||||||
<OverflowMenu
|
<button
|
||||||
:options="[
|
v-tooltip="formatMessage(messages.addFriend)"
|
||||||
{
|
:aria-label="formatMessage(messages.addFriend)"
|
||||||
id: 'add-friend',
|
@click="addFriendModal.show"
|
||||||
action: () => addFriendModal.show(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'manage-friends',
|
|
||||||
action: () => manageFriendsModal.show(),
|
|
||||||
shown: acceptedFriends.length > 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'view-requests',
|
|
||||||
action: () => friendInvitesModal.show(),
|
|
||||||
shown: pendingFriends.length > 0,
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
aria-label="More options"
|
|
||||||
>
|
>
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
<UserPlusIcon />
|
||||||
<template #add-friend>
|
</button>
|
||||||
<UserPlusIcon aria-hidden="true" />
|
</ButtonStyled>
|
||||||
Add friend
|
<div class="iconified-input flex-1">
|
||||||
</template>
|
<input
|
||||||
<template #manage-friends>
|
v-model="search"
|
||||||
<SettingsIcon aria-hidden="true" />
|
type="text"
|
||||||
Manage friends
|
class="friends-search-bar flex w-full"
|
||||||
<div
|
:placeholder="formatMessage(messages.searchFriends)"
|
||||||
v-if="acceptedFriends.length > 0"
|
@keyup.esc="search = ''"
|
||||||
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
/>
|
||||||
|
<button
|
||||||
|
v-if="search"
|
||||||
|
v-tooltip="formatMessage(commonMessages.clearButton)"
|
||||||
|
class="r-btn flex items-center justify-center bg-transparent button-animation p-2 cursor-pointer appearance-none border-none"
|
||||||
|
@click="search = ''"
|
||||||
>
|
>
|
||||||
{{ acceptedFriends.length }}
|
<XIcon />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #view-requests>
|
<h3 v-else class="ml-2 w-full text-base text-primary font-medium m-0">
|
||||||
<MailIcon aria-hidden="true" />
|
{{ formatMessage(messages.friends) }}
|
||||||
View friend requests
|
</h3>
|
||||||
<div
|
<ButtonStyled v-if="incomingRequests.length > 0" circular type="transparent">
|
||||||
v-if="pendingFriends.length > 0"
|
<button
|
||||||
class="bg-button-bg w-6 h-6 rounded-full flex items-center justify-center"
|
v-tooltip="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||||
|
class="relative"
|
||||||
|
:aria-label="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||||
|
@click="friendInvitesModal.show"
|
||||||
>
|
>
|
||||||
{{ pendingFriends.length }}
|
<MailIcon />
|
||||||
</div>
|
<span
|
||||||
</template>
|
v-if="incomingRequests.length > 0"
|
||||||
</OverflowMenu>
|
aria-hidden="true"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{{ incomingRequests.length }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 mt-2">
|
<div class="flex flex-col gap-3">
|
||||||
|
<h3 v-if="loading" class="ml-4 mr-1 text-base text-primary font-medium m-0">
|
||||||
|
{{ formatMessage(messages.friends) }}
|
||||||
|
</h3>
|
||||||
<template v-if="loading">
|
<template v-if="loading">
|
||||||
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
|
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse ml-4 mr-1">
|
||||||
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
||||||
@@ -312,50 +351,77 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="acceptedFriends.length === 0">
|
<template v-else-if="sortedFriends.length === 0">
|
||||||
<div class="text-sm">
|
<div class="text-sm ml-4 mr-1">
|
||||||
<div v-if="!userCredentials">
|
<div v-if="!userCredentials">
|
||||||
<span class="text-link cursor-pointer" @click="signIn">Sign in</span> to add friends!
|
<IntlFormatted :message-id="messages.signInToAddFriends">
|
||||||
|
<template #link="{ children }">
|
||||||
|
<span class="font-semibold text-brand cursor-pointer" @click="signIn">
|
||||||
|
<component :is="() => children" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</IntlFormatted>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<span class="text-link cursor-pointer" @click="addFriendModal.show()">Add friends</span>
|
<IntlFormatted :message-id="messages.addFriendsToShare">
|
||||||
to share what you're playing!
|
<template #link="{ children }">
|
||||||
|
<span class="font-semibold text-brand cursor-pointer" @click="addFriendModal.show">
|
||||||
|
<component :is="() => children" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</IntlFormatted>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ContextMenu ref="friendOptions" @option-clicked="handleFriendOptions">
|
<FriendsSection
|
||||||
<template #remove-friend> <TrashIcon /> Remove friend </template>
|
v-if="activeFriends.length > 0"
|
||||||
</ContextMenu>
|
:is-searching="!!search"
|
||||||
<div
|
open-by-default
|
||||||
v-for="friend in acceptedFriends.slice(0, 5)"
|
:friends="activeFriends"
|
||||||
:key="friend.username"
|
:heading="formatMessage(messages.active)"
|
||||||
class="flex gap-2 items-center"
|
:remove-friend="removeFriend"
|
||||||
:class="{ grayscale: !friend.online }"
|
|
||||||
@contextmenu.prevent.stop="
|
|
||||||
(event) =>
|
|
||||||
friendOptions.showMenu(event, friend, [
|
|
||||||
{
|
|
||||||
name: 'remove-friend',
|
|
||||||
color: 'danger',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div class="relative">
|
|
||||||
<Avatar :src="friend.avatar" class="w-12 h-12 rounded-full" size="2.25rem" circle />
|
|
||||||
<span
|
|
||||||
v-if="friend.online"
|
|
||||||
class="bottom-1 right-0 absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<FriendsSection
|
||||||
<div class="flex flex-col">
|
v-if="onlineFriends.length > 0"
|
||||||
<span class="text-md m-0 font-medium" :class="{ 'text-secondary': !friend.online }">
|
:is-searching="!!search"
|
||||||
{{ friend.username }}
|
open-by-default
|
||||||
</span>
|
:friends="onlineFriends"
|
||||||
<span v-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
:heading="formatMessage(messages.online)"
|
||||||
</div>
|
:remove-friend="removeFriend"
|
||||||
</div>
|
/>
|
||||||
|
<FriendsSection
|
||||||
|
v-if="offlineFriends.length > 0"
|
||||||
|
:is-searching="!!search"
|
||||||
|
:open-by-default="activeFriends.length + onlineFriends.length < 3"
|
||||||
|
:friends="offlineFriends"
|
||||||
|
:heading="formatMessage(messages.offline)"
|
||||||
|
:remove-friend="removeFriend"
|
||||||
|
/>
|
||||||
|
<FriendsSection
|
||||||
|
v-if="pendingFriends.length > 0"
|
||||||
|
:is-searching="!!search"
|
||||||
|
open-by-default
|
||||||
|
:friends="pendingFriends"
|
||||||
|
:heading="formatMessage(messages.pending)"
|
||||||
|
:remove-friend="removeFriend"
|
||||||
|
/>
|
||||||
|
<p v-if="filteredFriends.length === 0 && search" class="text-sm text-secondary my-1 mx-4">
|
||||||
|
{{ formatMessage(messages.noFriendsMatch, { query: search }) }}
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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>
|
||||||
@@ -56,16 +56,18 @@
|
|||||||
</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,10 +78,10 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -1,29 +1,34 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
|
CheckIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
RightArrowIcon,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
RightArrowIcon,
|
|
||||||
CheckIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { computed, ref } from '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 {
|
import {
|
||||||
add_project_from_version as installMod,
|
add_project_from_version as installMod,
|
||||||
check_installed,
|
check_installed,
|
||||||
|
create,
|
||||||
get,
|
get,
|
||||||
list,
|
list,
|
||||||
create,
|
|
||||||
} from '@/helpers/profile'
|
} from '@/helpers/profile'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import {
|
||||||
import { installVersionDependencies } from '@/store/install.js'
|
findPreferredVersion,
|
||||||
import { handleError } from '@/store/notifications.js'
|
installVersionDependencies,
|
||||||
import { useRouter } from 'vue-router'
|
isVersionCompatible,
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
} from '@/store/install.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const versions = ref()
|
const versions = ref()
|
||||||
@@ -48,14 +53,11 @@ const shownProfiles = computed(() =>
|
|||||||
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)
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,14 +95,7 @@ defineExpose({
|
|||||||
|
|
||||||
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
|
||||||
@@ -109,7 +104,7 @@ async function install(instance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -184,7 +179,7 @@ const createInstance = async () => {
|
|||||||
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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
TransferIcon,
|
|
||||||
IssuesIcon,
|
|
||||||
HammerIcon,
|
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
WrenchIcon,
|
HammerIcon,
|
||||||
UndoIcon,
|
IssuesIcon,
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
UnplugIcon,
|
TransferIcon,
|
||||||
|
UndoIcon,
|
||||||
UnlinkIcon,
|
UnlinkIcon,
|
||||||
|
UnplugIcon,
|
||||||
|
WrenchIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Checkbox, Chips, ButtonStyled, TeleportDropdownMenu } from '@modrinth/ui'
|
import {
|
||||||
import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
|
Avatar,
|
||||||
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
|
ButtonStyled,
|
||||||
import { handleError } from '@/store/notifications'
|
Checkbox,
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
Chips,
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
Combobox,
|
||||||
import { get_loader_versions } from '@/helpers/metadata'
|
injectNotificationManager,
|
||||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
} from '@modrinth/ui'
|
||||||
import {
|
import {
|
||||||
formatCategory,
|
formatCategory,
|
||||||
type GameVersionTag,
|
type GameVersionTag,
|
||||||
@@ -25,16 +25,31 @@ import {
|
|||||||
type Project,
|
type Project,
|
||||||
type Version,
|
type Version,
|
||||||
} from '@modrinth/utils'
|
} from '@modrinth/utils'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { get_project, get_version_many } from '@/helpers/cache'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { get_project, get_version_many } from '@/helpers/cache'
|
||||||
|
import { get_loader_versions } from '@/helpers/metadata'
|
||||||
|
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
|
||||||
|
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
InstanceSettingsTabProps,
|
InstanceSettingsTabProps,
|
||||||
ManifestLoaderVersion,
|
|
||||||
Manifest,
|
Manifest,
|
||||||
|
ManifestLoaderVersion,
|
||||||
} from '../../../helpers/types'
|
} from '../../../helpers/types'
|
||||||
|
|
||||||
|
import { initAuthlibPatching } from '@/helpers/utils.js'
|
||||||
|
const authLibPatchingModal = ref(null)
|
||||||
|
const isAuthLibPatchedSuccess = ref(false)
|
||||||
|
const isAuthLibPatching = ref(false)
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const repairConfirmModal = ref()
|
const repairConfirmModal = ref()
|
||||||
@@ -149,6 +164,21 @@ const selectableGameVersionNumbers = computed(() => {
|
|||||||
.map((x) => x.version)
|
.map((x) => x.version)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const gameVersionOptions = computed(() =>
|
||||||
|
(selectableGameVersionNumbers.value ?? []).map((v) => ({ value: v, label: v })),
|
||||||
|
)
|
||||||
|
|
||||||
|
const loaderVersionOptions = computed(() =>
|
||||||
|
(selectableLoaderVersions.value ?? []).map((opt, index) => ({ value: index, label: opt.id })),
|
||||||
|
)
|
||||||
|
|
||||||
|
const loaderVersionLabel = computed(() => {
|
||||||
|
const idx = loaderVersionIndex.value
|
||||||
|
return idx >= 0 && selectableLoaderVersions.value
|
||||||
|
? selectableLoaderVersions.value[idx]?.id
|
||||||
|
: 'Select version'
|
||||||
|
})
|
||||||
|
|
||||||
const selectableLoaderVersions: ComputedRef<ManifestLoaderVersion[] | undefined> = computed(() => {
|
const selectableLoaderVersions: ComputedRef<ManifestLoaderVersion[] | undefined> = computed(() => {
|
||||||
if (gameVersion.value) {
|
if (gameVersion.value) {
|
||||||
if (loader.value === 'fabric') {
|
if (loader.value === 'fabric') {
|
||||||
@@ -447,9 +477,43 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'reinstall',
|
defaultMessage: 'reinstall',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function handleInitAuthLibPatching(ismojang: boolean) {
|
||||||
|
isAuthLibPatching.value = true
|
||||||
|
let state = false
|
||||||
|
let instance_path = props.instance.loader_version != null ? props.instance.game_version + "-" + props.instance.loader_version : props.instance.game_version
|
||||||
|
try {
|
||||||
|
state = await initAuthlibPatching(instance_path, ismojang)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
isAuthLibPatching.value = false
|
||||||
|
isAuthLibPatchedSuccess.value = state
|
||||||
|
authLibPatchingModal.value.show()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="authLibPatchingModal"
|
||||||
|
:header="'AuthLib installation report'"
|
||||||
|
:closable="true"
|
||||||
|
@close="authLibPatchingModal.hide()"
|
||||||
|
>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h2 class="text-lg font-bold text-contrast space-y-2">
|
||||||
|
<p class="flex items-center gap-2 neon-text">
|
||||||
|
<span v-if="isAuthLibPatchedSuccess" class="neon-text">
|
||||||
|
AuthLib installation completed successfully! Now you can log in and play!
|
||||||
|
</span>
|
||||||
|
<span v-else class="neon-text">
|
||||||
|
Failed to install AuthLib. It's possible that no compatible AuthLib version was found for the selected game and/or mod loader version.
|
||||||
|
There may also be a problem with accessing resources behind CloudFlare.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="repairConfirmModal"
|
ref="repairConfirmModal"
|
||||||
:title="formatMessage(messages.repairConfirmTitle)"
|
:title="formatMessage(messages.repairConfirmTitle)"
|
||||||
@@ -505,7 +569,8 @@ const messages = defineMessages({
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="!modpackProject && instance.linked_data && !fetching" class="mb-2">
|
<div v-else-if="!modpackProject && instance.linked_data && !fetching" class="mb-2">
|
||||||
<p class="text-brand-red font-medium mt-0">
|
<p class="text-brand-red font-medium mt-0">
|
||||||
<IssuesIcon class="top-[3px] relative" /> {{ formatMessage(messages.noModpackFound) }}
|
<IssuesIcon class="top-[3px] relative" />
|
||||||
|
{{ formatMessage(messages.noModpackFound) }}
|
||||||
</p>
|
</p>
|
||||||
<p>{{ formatMessage(messages.debugInformation) }}</p>
|
<p>{{ formatMessage(messages.debugInformation) }}</p>
|
||||||
<div class="bg-bg p-6 rounded-2xl mt-2 text-sm text-secondary">
|
<div class="bg-bg p-6 rounded-2xl mt-2 text-sm text-secondary">
|
||||||
@@ -532,7 +597,9 @@ const messages = defineMessages({
|
|||||||
{{
|
{{
|
||||||
modpackProject
|
modpackProject
|
||||||
? modpackProject.title
|
? modpackProject.title
|
||||||
: formatMessage(messages.minecraftVersion, { version: instance.game_version })
|
: formatMessage(messages.minecraftVersion, {
|
||||||
|
version: instance.game_version,
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm text-secondary leading-none">
|
<span class="text-sm text-secondary leading-none">
|
||||||
@@ -635,11 +702,11 @@ const messages = defineMessages({
|
|||||||
{{ formatMessage(messages.gameVersion) }}
|
{{ formatMessage(messages.gameVersion) }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-wrap mt-2 gap-2">
|
<div class="flex flex-wrap mt-2 gap-2">
|
||||||
<TeleportDropdownMenu
|
<Combobox
|
||||||
v-if="selectableGameVersionNumbers !== undefined"
|
v-if="selectableGameVersionNumbers !== undefined"
|
||||||
v-model="gameVersion"
|
v-model="gameVersion"
|
||||||
:options="selectableGameVersionNumbers"
|
:options="gameVersionOptions"
|
||||||
name="Game Version Dropdown"
|
:display-value="gameVersion || formatMessage(messages.unknownVersion)"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="hasSnapshots"
|
v-if="hasSnapshots"
|
||||||
@@ -651,18 +718,22 @@ const messages = defineMessages({
|
|||||||
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||||
{{ formatMessage(messages.loaderVersion, { loader: formatCategory(loader) }) }}
|
{{ formatMessage(messages.loaderVersion, { loader: formatCategory(loader) }) }}
|
||||||
</h2>
|
</h2>
|
||||||
<TeleportDropdownMenu
|
<Combobox
|
||||||
v-if="selectableLoaderVersions"
|
v-if="selectableLoaderVersions"
|
||||||
:model-value="selectableLoaderVersions[loaderVersionIndex]"
|
v-model="loaderVersionIndex"
|
||||||
:options="selectableLoaderVersions"
|
:options="loaderVersionOptions"
|
||||||
:display-name="(option: ManifestLoaderVersion) => option?.id"
|
:display-value="loaderVersionLabel"
|
||||||
name="Version selector"
|
name="Version selector"
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
@change="(value) => (loaderVersionIndex = value.index)"
|
|
||||||
/>
|
/>
|
||||||
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
|
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
|
||||||
<IssuesIcon />
|
<IssuesIcon />
|
||||||
{{ formatMessage(messages.noLoaderVersions, { loader: loader, version: gameVersion }) }}
|
{{
|
||||||
|
formatMessage(messages.noLoaderVersions, {
|
||||||
|
loader: loader,
|
||||||
|
version: gameVersion,
|
||||||
|
})
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
@@ -720,6 +791,24 @@ const messages = defineMessages({
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||||
|
<div v-if="isAuthLibPatching" class="w-6 h-6 cursor-pointer hover:brightness-75 neon-icon pulse">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
Auth system (Skins) <span class="text-sm font-bold px-2 bg-brand-highlight text-brand rounded-full">Beta</span>
|
||||||
|
</h2>
|
||||||
|
<div class="mt-4 flex gap-2">
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button :disabled="isAuthLibPatching" @click="handleInitAuthLibPatching(true)">
|
||||||
|
Install Microsoft
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button :disabled="isAuthLibPatching" @click="handleInitAuthLibPatching(false) ">
|
||||||
|
Install Ely.By
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-if="instance.linked_data && instance.linked_data.locked">
|
<template v-if="instance.linked_data && instance.linked_data.locked">
|
||||||
@@ -787,3 +876,9 @@ const messages = defineMessages({
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-button.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-text.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-icon.scss';
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
<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('='))
|
||||||
@@ -34,39 +36,29 @@ const envVars = ref(
|
|||||||
|
|
||||||
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
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overrideJavaArgs.value) {
|
|
||||||
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overrideEnvVars.value) {
|
|
||||||
editProfile.custom_env_vars = envVars.value
|
|
||||||
.trim()
|
.trim()
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((x) => x.split('=').filter(Boolean))
|
.map((x) => x.split('=').filter(Boolean))
|
||||||
|
: null,
|
||||||
|
memory: overrideMemorySettings.value ? memory.value : null,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overrideMemorySettings.value) {
|
|
||||||
editProfile.memory = memory.value
|
|
||||||
}
|
|
||||||
|
|
||||||
return editProfile
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -156,6 +148,8 @@ const messages = defineMessages({
|
|||||||
:min="512"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
|
:snap-points="snapPoints"
|
||||||
|
:snap-range="512"
|
||||||
unit="MB"
|
unit="MB"
|
||||||
/>
|
/>
|
||||||
<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">
|
||||||
|
|||||||
@@ -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>()
|
||||||
@@ -24,20 +26,16 @@ const fullscreenSetting: Ref<boolean> = ref(
|
|||||||
)
|
)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if (!fullscreenSetting.value) {
|
|
||||||
editProfile.game_resolution = resolution.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
return editProfile
|
force_fullscreen: fullscreenSetting.value,
|
||||||
|
game_resolution: fullscreenSetting.value ? null : resolution.value,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -93,14 +91,6 @@ const messages = defineMessages({
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
v-model="overrideWindowSettings"
|
v-model="overrideWindowSettings"
|
||||||
:label="formatMessage(messages.customWindowSettings)"
|
:label="formatMessage(messages.customWindowSettings)"
|
||||||
@update:model-value="
|
|
||||||
(value) => {
|
|
||||||
if (!value) {
|
|
||||||
resolution = globalSettings.game_resolution
|
|
||||||
fullscreenSetting = globalSettings.force_fullscreen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<div class="mt-2 flex items-center gap-4 justify-between">
|
<div class="mt-2 flex items-center gap-4 justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,29 +1,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ReportIcon,
|
|
||||||
AstralRinthLogo,
|
|
||||||
ShieldIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
GaugeIcon,
|
|
||||||
PaintbrushIcon,
|
|
||||||
GameIcon,
|
|
||||||
CoffeeIcon,
|
CoffeeIcon,
|
||||||
|
GameIcon,
|
||||||
|
GaugeIcon,
|
||||||
|
AstralRinthLogo,
|
||||||
|
DownloadIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
PaintbrushIcon,
|
||||||
|
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()
|
||||||
@@ -97,6 +119,8 @@ 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()
|
||||||
@@ -122,6 +146,13 @@ function devModeCount() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
downloading: {
|
||||||
|
id: 'app.settings.downloading',
|
||||||
|
defaultMessage: 'Downloading v{version}',
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal">
|
<ModalWrapper ref="modal">
|
||||||
@@ -134,13 +165,24 @@ function devModeCount() {
|
|||||||
<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">
|
||||||
|
<div class="mb-3">
|
||||||
|
<template v-if="progress > 0 && progress < 1">
|
||||||
|
<p class="m-0 mb-2">
|
||||||
|
{{ formatMessage(messages.downloading, { version: downloadingVersion }) }}
|
||||||
|
</p>
|
||||||
|
<ProgressBar :progress="progress" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
<p v-if="themeStore.devMode" class="text-brand font-semibold m-0 mb-2">
|
||||||
{{ formatMessage(developerModeEnabled) }}
|
{{ formatMessage(developerModeEnabled) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||||
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
:class="{
|
||||||
|
'text-brand': themeStore.devMode,
|
||||||
|
'text-secondary': !themeStore.devMode,
|
||||||
|
}"
|
||||||
@click="devModeCount"
|
@click="devModeCount"
|
||||||
>
|
>
|
||||||
<AstralRinthLogo class="w-6 h-6" />
|
<AstralRinthLogo class="w-6 h-6" />
|
||||||
@@ -148,14 +190,98 @@ function devModeCount() {
|
|||||||
<div>
|
<div>
|
||||||
<p class="m-0">AstralRinth App {{ version }}</p>
|
<p class="m-0">AstralRinth App {{ version }}</p>
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
<span v-if="osPlatform === 'macos'">MacOS</span>
|
<span v-if="osPlatform === 'macos'">macOS</span>
|
||||||
<span v-else class="capitalize">{{ osPlatform }}</span>
|
<span v-else class="capitalize">{{ osPlatform }}</span>
|
||||||
{{ osVersion }}
|
{{ osVersion }}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</TabbedModal>
|
</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>
|
</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,8 +1,9 @@
|
|||||||
<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()
|
||||||
|
|
||||||
@@ -52,10 +53,11 @@ const modal = ref(null)
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
|
// hide_ads_window()
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
},
|
},
|
||||||
hide: () => {
|
hide: () => {
|
||||||
// onModalHide()
|
onModalHide()
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
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<{
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
|
CodeIcon,
|
||||||
CoffeeIcon,
|
CoffeeIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
WrenchIcon,
|
|
||||||
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()
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<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()
|
||||||
|
|
||||||
@@ -11,6 +12,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
hideHeader: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
closable: {
|
closable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
@@ -48,7 +53,14 @@ function onModalHide() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
|
<Modal
|
||||||
|
ref="modal"
|
||||||
|
:header="header"
|
||||||
|
:noblur="!themeStore.advancedRendering"
|
||||||
|
:closable="closable"
|
||||||
|
:hide-header="hideHeader"
|
||||||
|
@hide="onModalHide"
|
||||||
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<slot name="title" />
|
<slot name="title" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<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()
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ const modal = ref(null)
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (passedContent) => {
|
show: (passedContent) => {
|
||||||
|
// hide_ads_window()
|
||||||
modal.value.show(passedContent)
|
modal.value.show(passedContent)
|
||||||
},
|
},
|
||||||
hide: () => {
|
hide: () => {
|
||||||
@@ -40,9 +42,21 @@ defineExpose({
|
|||||||
modal.value.hide()
|
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()
|
||||||
@@ -49,7 +50,7 @@ watch(
|
|||||||
: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
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -85,12 +86,13 @@ watch(
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -121,7 +123,7 @@ watch(
|
|||||||
: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
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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,7 +13,10 @@ 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,
|
||||||
@@ -107,6 +112,8 @@ watch(
|
|||||||
:min="512"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
|
:snap-points="snapPoints"
|
||||||
|
:snap-range="512"
|
||||||
unit="MB"
|
unit="MB"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<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) {
|
||||||
@@ -18,7 +21,7 @@ async function updateJavaVersion(version) {
|
|||||||
}
|
}
|
||||||
</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>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<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())
|
||||||
|
|
||||||
@@ -26,11 +27,11 @@ watch(
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -38,12 +39,12 @@ watch(
|
|||||||
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<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(
|
||||||
|
|||||||
@@ -100,36 +100,41 @@
|
|||||||
</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,
|
||||||
|
ChevronRightIcon,
|
||||||
|
SaveIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
UploadIcon,
|
||||||
|
XIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
Button,
|
Button,
|
||||||
RadioButtons,
|
ButtonStyled,
|
||||||
CapeButton,
|
CapeButton,
|
||||||
CapeLikeTextButton,
|
CapeLikeTextButton,
|
||||||
ButtonStyled,
|
injectNotificationManager,
|
||||||
|
RadioButtons,
|
||||||
|
SkinPreviewRenderer,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
|
import { computed, ref, useTemplateRef, watch } from '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 {
|
import {
|
||||||
add_and_equip_custom_skin,
|
add_and_equip_custom_skin,
|
||||||
remove_custom_skin,
|
|
||||||
unequip_skin,
|
|
||||||
type Skin,
|
|
||||||
type Cape,
|
type Cape,
|
||||||
type SkinModel,
|
determineModelType,
|
||||||
get_normalized_skin_texture,
|
get_normalized_skin_texture,
|
||||||
|
remove_custom_skin,
|
||||||
|
type Skin,
|
||||||
|
type SkinModel,
|
||||||
|
type SkinTextureUrl,
|
||||||
|
unequip_skin,
|
||||||
} from '@/helpers/skins.ts'
|
} from '@/helpers/skins.ts'
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import {
|
const { handleError } = injectNotificationManager()
|
||||||
UploadIcon,
|
|
||||||
CheckIcon,
|
|
||||||
SaveIcon,
|
|
||||||
XIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
SpinnerIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
|
||||||
|
|
||||||
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')
|
||||||
@@ -184,7 +189,7 @@ function getSortedCapeExcluding(excludeId: string): Cape | undefined {
|
|||||||
|
|
||||||
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)
|
||||||
@@ -249,11 +254,11 @@ async function show(e: MouseEvent, skin?: Skin) {
|
|||||||
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()
|
||||||
@@ -263,7 +268,7 @@ async function showNew(e: MouseEvent, skinTextureUrl: string) {
|
|||||||
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()
|
||||||
|
|
||||||
@@ -357,7 +362,7 @@ async function save() {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTemplateRef, ref, computed } from 'vue'
|
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||||
import type { Cape, SkinModel } from '@/helpers/skins.ts'
|
|
||||||
import {
|
import {
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
ScrollablePanel,
|
|
||||||
CapeButton,
|
CapeButton,
|
||||||
CapeLikeTextButton,
|
CapeLikeTextButton,
|
||||||
|
ScrollablePanel,
|
||||||
SkinPreviewRenderer,
|
SkinPreviewRenderer,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
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')
|
||||||
|
|
||||||
|
|||||||
@@ -27,14 +27,15 @@
|
|||||||
</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>()
|
||||||
@@ -99,7 +100,7 @@ async function setupDragDropListener() {
|
|||||||
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',
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Dayjs } from 'dayjs'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import {
|
import {
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
@@ -13,25 +11,29 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
|
injectNotificationManager,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
SmartClickable,
|
SmartClickable,
|
||||||
useRelativeTime,
|
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()
|
||||||
|
|
||||||
@@ -128,7 +130,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
</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"
|
||||||
|
|||||||
@@ -1,29 +1,37 @@
|
|||||||
<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[]
|
||||||
@@ -32,8 +40,10 @@ const props = defineProps<{
|
|||||||
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
|
||||||
@@ -63,9 +73,13 @@ watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await populateJumpBackIn().catch(() => {
|
populateJumpBackIn()
|
||||||
|
.catch(() => {
|
||||||
console.error('Failed to populate jump back in')
|
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...')
|
||||||
@@ -121,11 +135,8 @@ async function populateJumpBackIn() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// fetch each server's data
|
servers.forEach(({ instancePath, address }) =>
|
||||||
Promise.all(
|
|
||||||
servers.map(({ instancePath, address }) =>
|
|
||||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,8 +161,8 @@ async function populateJumpBackIn() {
|
|||||||
.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) {
|
||||||
@@ -228,7 +239,15 @@ onUnmounted(() => {
|
|||||||
</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">
|
||||||
|
<span class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold">
|
||||||
|
Jump back in
|
||||||
|
</span>
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<LoaderCircleIcon class="mx-auto size-8 animate-spin text-contrast" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||||
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
||||||
Jump back in
|
Jump back in
|
||||||
</HeadingLink>
|
</HeadingLink>
|
||||||
@@ -255,7 +274,14 @@ onUnmounted(() => {
|
|||||||
? serverData[item.world.address].refreshing && !serverData[item.world.address].status
|
? serverData[item.world.address].refreshing && !serverData[item.world.address].status
|
||||||
: undefined
|
: undefined
|
||||||
"
|
"
|
||||||
supports-quick-play
|
:supports-server-quick-play="
|
||||||
|
item.world.type === 'server' &&
|
||||||
|
hasServerQuickPlaySupport(gameVersions, item.instance.game_version || '')
|
||||||
|
"
|
||||||
|
:supports-world-quick-play="
|
||||||
|
item.world.type === 'singleplayer' &&
|
||||||
|
hasWorldQuickPlaySupport(gameVersions, item.instance.game_version || '')
|
||||||
|
"
|
||||||
:server-status="
|
:server-status="
|
||||||
item.world.type === 'server' ? serverData[item.world.address].status : undefined
|
item.world.type === 'server' ? serverData[item.world.address].status : undefined
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
<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,
|
|
||||||
Avatar,
|
|
||||||
ButtonStyled,
|
|
||||||
commonMessages,
|
|
||||||
OverflowMenu,
|
|
||||||
SmartClickable,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import {
|
|
||||||
IssuesIcon,
|
|
||||||
EyeIcon,
|
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
|
IssuesIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
NoSignalIcon,
|
NoSignalIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
@@ -29,14 +17,33 @@ import {
|
|||||||
UserIcon,
|
UserIcon,
|
||||||
XIcon,
|
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()
|
||||||
@@ -54,8 +61,9 @@ const props = withDefaults(
|
|||||||
playingInstance?: boolean
|
playingInstance?: boolean
|
||||||
playingWorld?: boolean
|
playingWorld?: boolean
|
||||||
startingInstance?: boolean
|
startingInstance?: boolean
|
||||||
supportsQuickPlay?: boolean
|
supportsServerQuickPlay?: boolean
|
||||||
currentProtocol?: number | null
|
supportsWorldQuickPlay?: boolean
|
||||||
|
currentProtocol?: ProtocolVersion | null
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
|
|
||||||
// Server only
|
// Server only
|
||||||
@@ -78,7 +86,8 @@ const props = withDefaults(
|
|||||||
playingInstance: false,
|
playingInstance: false,
|
||||||
playingWorld: false,
|
playingWorld: false,
|
||||||
startingInstance: false,
|
startingInstance: false,
|
||||||
supportsQuickPlay: false,
|
supportsServerQuickPlay: true,
|
||||||
|
supportsWorldQuickPlay: false,
|
||||||
currentProtocol: null,
|
currentProtocol: null,
|
||||||
|
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
@@ -102,7 +111,8 @@ 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)
|
||||||
@@ -120,14 +130,26 @@ const messages = defineMessages({
|
|||||||
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+',
|
||||||
|
},
|
||||||
|
noSingleplayerQuickPlay: {
|
||||||
|
id: 'instance.worlds.no_singleplayer_quick_play',
|
||||||
|
defaultMessage: 'You can only jump straight into singleplayer worlds on Minecraft 1.20+',
|
||||||
},
|
},
|
||||||
gameAlreadyOpen: {
|
gameAlreadyOpen: {
|
||||||
id: 'instance.worlds.game_already_open',
|
id: 'instance.worlds.game_already_open',
|
||||||
defaultMessage: 'Instance is already open',
|
defaultMessage: 'Instance is already open',
|
||||||
},
|
},
|
||||||
|
noContact: {
|
||||||
|
id: 'instance.worlds.no_contact',
|
||||||
|
defaultMessage: "Server couldn't be contacted",
|
||||||
|
},
|
||||||
|
incompatibleServer: {
|
||||||
|
id: 'instance.worlds.incompatible_server',
|
||||||
|
defaultMessage: 'Server is incompatible',
|
||||||
|
},
|
||||||
copyAddress: {
|
copyAddress: {
|
||||||
id: 'instance.worlds.copy_address',
|
id: 'instance.worlds.copy_address',
|
||||||
defaultMessage: 'Copy address',
|
defaultMessage: 'Copy address',
|
||||||
@@ -136,10 +158,6 @@ const messages = defineMessages({
|
|||||||
id: 'instance.worlds.view_instance',
|
id: 'instance.worlds.view_instance',
|
||||||
defaultMessage: 'View instance',
|
defaultMessage: 'View instance',
|
||||||
},
|
},
|
||||||
playAnyway: {
|
|
||||||
id: 'instance.worlds.play_anyway',
|
|
||||||
defaultMessage: 'Play anyway',
|
|
||||||
},
|
|
||||||
playInstance: {
|
playInstance: {
|
||||||
id: 'instance.worlds.play_instance',
|
id: 'instance.worlds.play_instance',
|
||||||
defaultMessage: 'Play instance',
|
defaultMessage: 'Play instance',
|
||||||
@@ -163,7 +181,7 @@ const messages = defineMessages({
|
|||||||
/>
|
/>
|
||||||
</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,
|
||||||
}"
|
}"
|
||||||
@@ -218,7 +236,8 @@ const messages = defineMessages({
|
|||||||
/>
|
/>
|
||||||
<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) }}
|
||||||
|
online
|
||||||
</span>
|
</span>
|
||||||
<template #popper>
|
<template #popper>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
@@ -231,7 +250,8 @@ const messages = defineMessages({
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<NoSignalIcon aria-hidden="true" stroke-width="3px" class="shrink-0" /> Offline
|
<NoSignalIcon aria-hidden="true" stroke-width="3px" class="shrink-0" />
|
||||||
|
Offline
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,7 +261,9 @@ const messages = defineMessages({
|
|||||||
world.last_played ? dayjs(world.last_played).format('MMMM D, YYYY [at] h:mm A') : null
|
world.last_played ? dayjs(world.last_played).format('MMMM D, YYYY [at] h:mm A') : null
|
||||||
"
|
"
|
||||||
class="w-fit shrink-0"
|
class="w-fit shrink-0"
|
||||||
:class="{ 'cursor-help smart-clickable:allow-pointer-events': world.last_played }"
|
:class="{
|
||||||
|
'cursor-help smart-clickable:allow-pointer-events': world.last_played,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<template v-if="world.last_played">
|
<template v-if="world.last_played">
|
||||||
{{
|
{{
|
||||||
@@ -302,7 +324,6 @@ const messages = defineMessages({
|
|||||||
</template>
|
</template>
|
||||||
</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">
|
||||||
<template v-if="world.type === 'singleplayer' || serverStatus">
|
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -315,15 +336,28 @@ const messages = defineMessages({
|
|||||||
<ButtonStyled v-else>
|
<ButtonStyled v-else>
|
||||||
<button
|
<button
|
||||||
v-tooltip="
|
v-tooltip="
|
||||||
serverIncompatible
|
world.type === 'server'
|
||||||
? 'Server is incompatible'
|
? !supportsServerQuickPlay
|
||||||
: !supportsQuickPlay
|
? formatMessage(messages.noServerQuickPlay)
|
||||||
? formatMessage(messages.noQuickPlay)
|
: playingOtherWorld
|
||||||
|
? formatMessage(messages.gameAlreadyOpen)
|
||||||
|
: !serverStatus
|
||||||
|
? formatMessage(messages.noContact)
|
||||||
|
: serverIncompatible
|
||||||
|
? formatMessage(messages.incompatibleServer)
|
||||||
|
: null
|
||||||
|
: !supportsWorldQuickPlay
|
||||||
|
? formatMessage(messages.noSingleplayerQuickPlay)
|
||||||
: playingOtherWorld || locked
|
: playingOtherWorld || locked
|
||||||
? formatMessage(messages.gameAlreadyOpen)
|
? formatMessage(messages.gameAlreadyOpen)
|
||||||
: null
|
: null
|
||||||
"
|
"
|
||||||
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
:disabled="
|
||||||
|
playingOtherWorld ||
|
||||||
|
startingInstance ||
|
||||||
|
(world.type == 'server' && !supportsServerQuickPlay) ||
|
||||||
|
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
||||||
|
"
|
||||||
@click="emit('play')"
|
@click="emit('play')"
|
||||||
>
|
>
|
||||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||||
@@ -331,13 +365,6 @@ const messages = defineMessages({
|
|||||||
{{ formatMessage(commonMessages.playButton) }}
|
{{ formatMessage(commonMessages.playButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
|
||||||
<ButtonStyled v-else>
|
|
||||||
<button class="invisible">
|
|
||||||
<PlayIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(commonMessages.playButton) }}
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled circular type="transparent">
|
<ButtonStyled circular type="transparent">
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
@@ -347,11 +374,6 @@ const messages = defineMessages({
|
|||||||
disabled: playingInstance,
|
disabled: playingInstance,
|
||||||
action: () => emit('play-instance'),
|
action: () => emit('play-instance'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'play-anyway',
|
|
||||||
shown: serverIncompatible && !playingInstance && supportsQuickPlay,
|
|
||||||
action: () => emit('play'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'open-instance',
|
id: 'open-instance',
|
||||||
shown: !!instancePath,
|
shown: !!instancePath,
|
||||||
@@ -417,26 +439,25 @@ const messages = defineMessages({
|
|||||||
<PlayIcon aria-hidden="true" />
|
<PlayIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.playInstance) }}
|
{{ formatMessage(messages.playInstance) }}
|
||||||
</template>
|
</template>
|
||||||
<template #play-anyway>
|
|
||||||
<PlayIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(messages.playAnyway) }}
|
|
||||||
</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" />
|
||||||
|
{{ formatMessage(commonMessages.editButton) }}
|
||||||
</template>
|
</template>
|
||||||
<template #open-folder>
|
<template #open-folder>
|
||||||
<FolderOpenIcon aria-hidden="true" />
|
<FolderOpenIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||||
</template>
|
</template>
|
||||||
<template #copy-address>
|
<template #copy-address>
|
||||||
<ClipboardCopyIcon aria-hidden="true" /> {{ formatMessage(messages.copyAddress) }}
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.copyAddress) }}
|
||||||
</template>
|
</template>
|
||||||
<template #refresh>
|
<template #refresh>
|
||||||
<UpdatedIcon aria-hidden="true" /> {{ formatMessage(commonMessages.refreshButton) }}
|
<UpdatedIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(commonMessages.refreshButton) }}
|
||||||
</template>
|
</template>
|
||||||
<template #dont-show-on-home>
|
<template #dont-show-on-home>
|
||||||
<XIcon aria-hidden="true" />
|
<XIcon aria-hidden="true" />
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<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<{
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
<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 ServerPackStatus,
|
||||||
type ServerWorld,
|
type ServerWorld,
|
||||||
set_world_display_status,
|
set_world_display_status,
|
||||||
type DisplayStatus,
|
|
||||||
} 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<{
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<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<{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<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 })
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
@@ -73,12 +74,19 @@ defineExpose({ resourcePackOptions })
|
|||||||
{{ formatMessage(messages.resourcePack) }}
|
{{ formatMessage(messages.resourcePack) }}
|
||||||
</h2>
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<TeleportDropdownMenu
|
<Combobox
|
||||||
v-model="resourcePack"
|
v-model="resourcePack"
|
||||||
:options="resourcePackOptions"
|
:options="
|
||||||
|
resourcePackOptions.map((o) => ({
|
||||||
|
value: o,
|
||||||
|
label: formatMessage(resourcePackOptionMessages[o]),
|
||||||
|
}))
|
||||||
|
"
|
||||||
name="Server resource pack"
|
name="Server resource pack"
|
||||||
:display-name="
|
:display-value="
|
||||||
(option: ServerPackStatus) => formatMessage(resourcePackOptionMessages[option])
|
resourcePack
|
||||||
|
? formatMessage(resourcePackOptionMessages[resourcePack])
|
||||||
|
: 'Select an option'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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() {
|
||||||
|
|||||||
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.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { fetch } from '@tauri-apps/plugin-http'
|
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
|
|
||||||
export const useFetch = async (url, item, isSilent) => {
|
export const useFetch = async (url, item, isSilent) => {
|
||||||
try {
|
try {
|
||||||
@@ -11,8 +10,9 @@ export const useFetch = async (url, item, isSilent) => {
|
|||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!isSilent) {
|
if (!isSilent) {
|
||||||
handleError({ message: `Error fetching ${item}` })
|
throw err
|
||||||
}
|
} else {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
|
|
||||||
export async function friends() {
|
|
||||||
return await invoke('plugin:friends|friends')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function friend_statuses() {
|
|
||||||
return await invoke('plugin:friends|friend_statuses')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function add_friend(userId) {
|
|
||||||
return await invoke('plugin:friends|add_friend', { userId })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function remove_friend(userId) {
|
|
||||||
return await invoke('plugin:friends|remove_friend', { userId })
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user