Compare commits
840 Commits
issue-temp
...
shared-ins
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96782b52cd | ||
|
|
aae00e1312 | ||
|
|
24e90f0a54 | ||
|
|
f5208a85b0 | ||
|
|
8db4b3f83b | ||
|
|
33ad04d036 | ||
|
|
4bcdb3f495 | ||
|
|
70979172b0 | ||
|
|
493b9a3975 | ||
|
|
5a21a67d46 | ||
|
|
ff72c906ba | ||
|
|
907b1f67ed | ||
|
|
72cbe7f905 | ||
|
|
deb16aa7ab | ||
|
|
d0efa44c9e | ||
|
|
d321843c02 | ||
|
|
2b44b145cb | ||
|
|
9f5606889e | ||
|
|
417f2a8b91 | ||
|
|
d3a7bf967e | ||
|
|
db145cd8ad | ||
|
|
7d614f7ac5 | ||
|
|
c7a2a3e29b | ||
|
|
3b966c03ee | ||
|
|
66d943d391 | ||
|
|
f5f876e458 | ||
|
|
27c3439120 | ||
|
|
f212d04261 | ||
|
|
06f01aa85c | ||
|
|
5f48dc08a9 | ||
|
|
e81e056758 | ||
|
|
2d95ff0830 | ||
|
|
81d921d625 | ||
|
|
e988513ed7 | ||
|
|
783d4f82d9 | ||
|
|
b5011f458f | ||
|
|
5d0e4366d2 | ||
|
|
8643dc02dd | ||
|
|
7c4dcb2817 | ||
|
|
6b64fdafcb | ||
|
|
185dd47668 | ||
|
|
f165665a35 | ||
|
|
ad38749f98 | ||
|
|
7825dd64ca | ||
|
|
f6af620643 | ||
|
|
b5aeef7ebf | ||
|
|
f5a201dd94 | ||
|
|
72dab12033 | ||
|
|
ce4b4ba41d | ||
|
|
945206153d | ||
|
|
9f977d082b | ||
|
|
4a2ec0c40c | ||
|
|
70bf61a645 | ||
|
|
674f4b1095 | ||
|
|
3304070034 | ||
|
|
19ca5f08c6 | ||
|
|
5605910ac8 | ||
|
|
b1eda435a5 | ||
|
|
e88ca8430e | ||
|
|
d39db02a73 | ||
|
|
86a64ca929 | ||
|
|
5ea0f7d4c7 | ||
|
|
b3fa2fa6d2 | ||
|
|
999dc640bc | ||
|
|
9be9658ffb | ||
|
|
595d5362f6 | ||
|
|
f212fcf892 | ||
|
|
8c1c5572c0 | ||
|
|
31d151638a | ||
|
|
fb6b41630c | ||
|
|
6ab806cbde | ||
|
|
c0267f7746 | ||
|
|
a54b6dc7b9 | ||
|
|
9ec43ebe70 | ||
|
|
486cd68bf7 | ||
|
|
98c050e7e9 | ||
|
|
25fcee984b | ||
|
|
86922c4547 | ||
|
|
7bbdfd25cd | ||
|
|
b8ad22a6fb | ||
|
|
8dd955563e | ||
|
|
663ab83b08 | ||
|
|
c143929b69 | ||
|
|
1b73d248b3 | ||
|
|
cc22a92daf | ||
|
|
39f0408929 | ||
|
|
e66f46a464 | ||
|
|
9243296197 | ||
|
|
26ce83f8f1 | ||
|
|
907ef38189 | ||
|
|
a7d4001b00 | ||
|
|
e3a3379615 | ||
|
|
356a06e694 | ||
|
|
fce516a76f | ||
|
|
42ade0fbd1 | ||
|
|
ba07f5dad4 | ||
|
|
cc89e0f3f1 | ||
|
|
0e14d3f9c1 | ||
|
|
ff7975773e | ||
|
|
6716e2277d | ||
|
|
f986dc5d11 | ||
|
|
570a4096f9 | ||
|
|
c88bfbb5f0 | ||
|
|
d302795512 | ||
|
|
115acce80c | ||
|
|
a8731b0ca2 | ||
|
|
fd596bf418 | ||
|
|
ef7cfffeb6 | ||
|
|
ac9bcabd9c | ||
|
|
5bb961f16b | ||
|
|
a46677832b | ||
|
|
624abf0df4 | ||
|
|
e81a4ade97 | ||
|
|
9708685506 | ||
|
|
28b6bf8603 | ||
|
|
2713f0e610 | ||
|
|
f7d1cd2a4f | ||
|
|
3b8963fad0 | ||
|
|
060682a1ac | ||
|
|
95cd48571e | ||
|
|
3d619e6a98 | ||
|
|
edb7e5f323 | ||
|
|
0221034b60 | ||
|
|
9500384100 | ||
|
|
0b31f2eb41 | ||
|
|
b3a6393c91 | ||
|
|
5b5599128a | ||
|
|
cb0f03ca9c | ||
|
|
2e35f3608b | ||
|
|
16c5a5a3a6 | ||
|
|
2e7db502a9 | ||
|
|
d29b71ec45 | ||
|
|
9cd0af914a | ||
|
|
4a575393f0 | ||
|
|
76c93c767d | ||
|
|
e69337a1fc | ||
|
|
50734af6cd | ||
|
|
81b0922c93 | ||
|
|
d4f8fff7af | ||
|
|
bd61f5d591 | ||
|
|
016c3d779b | ||
|
|
acf26940d6 | ||
|
|
4bafae881f | ||
|
|
8311451420 | ||
|
|
4b75cb8357 | ||
|
|
679ffbcce7 | ||
|
|
637a923e84 | ||
|
|
1f8d569b79 | ||
|
|
93ae24e707 | ||
|
|
7dd340f0b6 | ||
|
|
1d0d8d7fbe | ||
|
|
6de8d2684a | ||
|
|
9763a43943 | ||
|
|
60edbcd5f0 | ||
|
|
0ae1e40d79 | ||
|
|
34d931573c | ||
|
|
721365578a | ||
|
|
7be02318e0 | ||
|
|
88db79188c | ||
|
|
8a0329b23d | ||
|
|
02ebe59d2f | ||
|
|
6e8e053b88 | ||
|
|
fc3056b0e0 | ||
|
|
4274a8ed68 | ||
|
|
8b16cd1b36 | ||
|
|
5148e27448 | ||
|
|
608e55c01f | ||
|
|
b8963d272a | ||
|
|
beaaed6613 | ||
|
|
6bbd8c9b16 | ||
|
|
872ffa02ce | ||
|
|
b933202694 | ||
|
|
49cf0c8a9a | ||
|
|
83ccf4928f | ||
|
|
28b0d34bff | ||
|
|
0a0837ea02 | ||
|
|
a0aa350a08 | ||
|
|
decfcb6c27 | ||
|
|
730913bec4 | ||
|
|
f8f037196e | ||
|
|
e2ffeab8fa | ||
|
|
04d834187b | ||
|
|
33b2a94d90 | ||
|
|
ce3b024fea | ||
|
|
a02aa7586b | ||
|
|
d5107f2ef6 | ||
|
|
5b63b0b398 | ||
|
|
fc577241bd | ||
|
|
bb8a0e596c | ||
|
|
2a63b703f9 | ||
|
|
bfeff78164 | ||
|
|
4826289020 | ||
|
|
0aebf37ef8 | ||
|
|
d1a09d0b95 | ||
|
|
7b00003958 | ||
|
|
4483bb147c | ||
|
|
ef31c0c0da | ||
|
|
76c885f080 | ||
|
|
f16e93bd3a | ||
|
|
05d2a96900 | ||
|
|
9c70c35669 | ||
|
|
9d54c41a2b | ||
|
|
3464fbb2e8 | ||
|
|
d51d6517be | ||
|
|
5f6cc1281e | ||
|
|
035fc69060 | ||
|
|
34baf44534 | ||
|
|
c3448033de | ||
|
|
aee9b6a951 | ||
|
|
75e5bec962 | ||
|
|
59c269c8d0 | ||
|
|
541022cdc3 | ||
|
|
527521328f | ||
|
|
917b89e44f | ||
|
|
87862f3e23 | ||
|
|
10eed05d87 | ||
|
|
f5802fee31 | ||
|
|
cf9c8cbb4f | ||
|
|
f199ecf8e9 | ||
|
|
3bdd551d40 | ||
|
|
4a7936a51d | ||
|
|
76e00c2432 | ||
|
|
b46f3bf2c4 | ||
|
|
f7b4b782bf | ||
|
|
60c535e861 | ||
|
|
d59c522f7f | ||
|
|
9f798559cf | ||
|
|
f939e59463 | ||
|
|
50e89ad98b | ||
|
|
c0c5978028 | ||
|
|
abbeed394e | ||
|
|
f5b8c15388 | ||
|
|
f53b6b550f | ||
|
|
00e55b1874 | ||
|
|
90954dac49 | ||
|
|
6217523cc8 | ||
|
|
27ccd3dfa8 | ||
|
|
235f4f10ef | ||
|
|
945e5a2dc3 | ||
|
|
4b6a2685d0 | ||
|
|
e76b6c3bde | ||
|
|
4630d175d7 | ||
|
|
27055b96e3 | ||
|
|
0ef96c0bca | ||
|
|
b2be4a7d67 | ||
|
|
a70df067bc | ||
|
|
2d92b08404 | ||
|
|
4bbc57b0dc | ||
|
|
756c14d988 | ||
|
|
b3b55210f7 | ||
|
|
58093a9438 | ||
|
|
ed33dd2127 | ||
|
|
d4f9c97cca | ||
|
|
f731c1080d | ||
|
|
fd18185ef0 | ||
|
|
0efbbed5e2 | ||
|
|
bad350e49b | ||
|
|
172b93d07f | ||
|
|
ade8c162cd | ||
|
|
79e634316d | ||
|
|
dfba6c7c91 | ||
|
|
e06a77af28 | ||
|
|
74973e73e6 | ||
|
|
ac07ac5234 | ||
|
|
f4880d0519 | ||
|
|
375f992a0c | ||
|
|
ae1c5342f2 | ||
|
|
97ccb7df94 | ||
|
|
a818199b5a | ||
|
|
6fe1fa3455 | ||
|
|
aab95444a8 | ||
|
|
40f28be3b4 | ||
|
|
911d442340 | ||
|
|
d5594b03e3 | ||
|
|
89f1ddf4d7 | ||
|
|
6cfd4637db | ||
|
|
8803e11945 | ||
|
|
b1ca2cc2b6 | ||
|
|
9a8f3d7bad | ||
|
|
9d0e762f36 | ||
|
|
abf4cd71ba | ||
|
|
d66270eef0 | ||
|
|
07ecd13554 | ||
|
|
f1ff88f452 | ||
|
|
d92272ffa0 | ||
|
|
dfa43f3c5a | ||
|
|
259c5ef3d0 | ||
|
|
a1b59d4545 | ||
|
|
58a61051b9 | ||
|
|
51777c3f33 | ||
|
|
3767e9fae9 | ||
|
|
4bbfc8ccc5 | ||
|
|
531b8214c0 | ||
|
|
5cab618bf7 | ||
|
|
3380f4d11c | ||
|
|
4bf030993a | ||
|
|
f65f949a36 | ||
|
|
2864abd8c2 | ||
|
|
9bd2cb3c7e | ||
|
|
35cd277fcf | ||
|
|
57b1932b5e | ||
|
|
e6818023a3 | ||
|
|
35a541f99b | ||
|
|
5fb00a947c | ||
|
|
6288f679b9 | ||
|
|
e766759b8c | ||
|
|
c1d28381e8 | ||
|
|
2fa8371bae | ||
|
|
a1cfdf1a5b | ||
|
|
e9c7f5d664 | ||
|
|
c85f12fe2c | ||
|
|
13e5644c89 | ||
|
|
eac029aef4 | ||
|
|
a5195920fa | ||
|
|
5676a13290 | ||
|
|
d11f0e864e | ||
|
|
df83fcc5b9 | ||
|
|
f21c756793 | ||
|
|
4b07ee2fa8 | ||
|
|
ae3a39ee65 | ||
|
|
e9f5bd4ac1 | ||
|
|
1f4ad732fd | ||
|
|
5637d37ee1 | ||
|
|
c370da2fef | ||
|
|
d168209721 | ||
|
|
ca0468b8d5 | ||
|
|
039d26feeb | ||
|
|
10e7b66f38 | ||
|
|
4bb47d7e01 | ||
|
|
ec80c2b9db | ||
|
|
a89418e33b | ||
|
|
0d88ff8dae | ||
|
|
4bdf9bff3a | ||
|
|
7fbb8838e7 | ||
|
|
366ea63209 | ||
|
|
6c0ad7fe1a | ||
|
|
ef9c90a43a | ||
|
|
239214ef92 | ||
|
|
b0057b130e | ||
|
|
d64c043838 | ||
|
|
dd3599f5b3 | ||
|
|
0d56127758 | ||
|
|
ea043517c5 | ||
|
|
b84d9c5d55 | ||
|
|
6c628afe5d | ||
|
|
abc99c7e69 | ||
|
|
fe25cd3bec | ||
|
|
2eb51edfb6 | ||
|
|
715d564028 | ||
|
|
989b704efc | ||
|
|
0bfaeb8521 | ||
|
|
3db00534c2 | ||
|
|
b713b324f9 | ||
|
|
6512dbae1c | ||
|
|
3afda71349 | ||
|
|
568e5a9bb8 | ||
|
|
89e56ae279 | ||
|
|
339ac05443 | ||
|
|
72cfa683cf | ||
|
|
3a6b9f04f9 | ||
|
|
59f24df294 | ||
|
|
5c559af936 | ||
|
|
bb80505b76 | ||
|
|
a560f6e9f6 | ||
|
|
95ae981698 | ||
|
|
969eb67217 | ||
|
|
0dfebbad9d | ||
|
|
f87f4bd8cc | ||
|
|
352caa85da | ||
|
|
8f61e9876f | ||
|
|
fd19bb7cd5 | ||
|
|
0c2e9137a2 | ||
|
|
bf5a25a96f | ||
|
|
aa84f21fde | ||
|
|
b9de2b4b58 | ||
|
|
9754f2d1c5 | ||
|
|
f66fc06b4f | ||
|
|
79ceb56c60 | ||
|
|
7605df1bd9 | ||
|
|
b91ec48178 | ||
|
|
3c2f144795 | ||
|
|
0271337f8e | ||
|
|
630a71c46c | ||
|
|
150329dd4a | ||
|
|
59d7bce518 | ||
|
|
3c1e3cd38e | ||
|
|
a2eb0bf9fe | ||
|
|
5d48ecf86a | ||
|
|
00d09aa01e | ||
|
|
9afdc55416 | ||
|
|
2c942c8809 | ||
|
|
c15acc4ce3 | ||
|
|
b056610eaa | ||
|
|
8eb9fb1834 | ||
|
|
7ec518b41c | ||
|
|
dc15914a85 | ||
|
|
3b22f59988 | ||
|
|
afdab0300e | ||
|
|
26533c47e7 | ||
|
|
df3aeed291 | ||
|
|
867ba7b68f | ||
|
|
1679a3f844 | ||
|
|
1611049623 | ||
|
|
7d195367a8 | ||
|
|
88a4f25689 | ||
|
|
161dee89ec | ||
|
|
34af33607b | ||
|
|
5bb188a822 | ||
|
|
60bb6f105d | ||
|
|
df4680ee09 | ||
|
|
6aaab09601 | ||
|
|
5da42575fd | ||
|
|
fe256d6a62 | ||
|
|
5f175141e1 | ||
|
|
983e2df065 | ||
|
|
16d5a70c08 | ||
|
|
b7e2d7fb8e | ||
|
|
fb5f7a336d | ||
|
|
78dc5f4bf4 | ||
|
|
45dbf5393f | ||
|
|
9fed1cde25 | ||
|
|
5c7b175e90 | ||
|
|
30b29de8ce | ||
|
|
a5f9331023 | ||
|
|
d8b9d8431e | ||
|
|
91a2ce2b3f | ||
|
|
4da1871567 | ||
|
|
e809f77461 | ||
|
|
e96d23cc3f | ||
|
|
c34e2ab3e1 | ||
|
|
820519b4f7 | ||
|
|
34688852a4 | ||
|
|
151f28081a | ||
|
|
213a64b1ff | ||
|
|
f259d81249 | ||
|
|
589761bfd9 | ||
|
|
18fde86a20 | ||
|
|
ba28bc94d3 | ||
|
|
da19a07943 | ||
|
|
ecc500fc91 | ||
|
|
c22ac1e60a | ||
|
|
55d9aa2a4c | ||
|
|
1d391e68e5 | ||
|
|
0429c44d18 | ||
|
|
2c1bcaafc1 | ||
|
|
35891c74cd | ||
|
|
2ca6e67b37 | ||
|
|
6e72be54cb | ||
|
|
07edb998e4 | ||
|
|
3e52f804a7 | ||
|
|
75b7583832 | ||
|
|
d754eb74f7 | ||
|
|
60252267d5 | ||
|
|
b25af641e2 | ||
|
|
e7c3f8bf47 | ||
|
|
4c1dca73c4 | ||
|
|
0bbb6b91fe | ||
|
|
ee93d9b495 | ||
|
|
bf8ac214a1 | ||
|
|
9c7b34d5e6 | ||
|
|
76c0fa2fe2 | ||
|
|
ac3a17b178 | ||
|
|
c76b527b93 | ||
|
|
ded4f95537 | ||
|
|
8272386733 | ||
|
|
33988ed3fb | ||
|
|
411b8e3cb6 | ||
|
|
916da16523 | ||
|
|
d165c081f7 | ||
|
|
992de7d66e | ||
|
|
46ab7bbcbe | ||
|
|
b04bced37f | ||
|
|
13335cadc6 | ||
|
|
b864791fa6 | ||
|
|
6614b56298 | ||
|
|
02c3894fc9 | ||
|
|
68f7dc9512 | ||
|
|
18d1bc56fd | ||
|
|
1e4d07a52c | ||
|
|
1fc579e907 | ||
|
|
827c4e31ee | ||
|
|
c2a1ed926e | ||
|
|
4f86c117c3 | ||
|
|
93817ba92f | ||
|
|
18153e0fcc | ||
|
|
3123f6444f | ||
|
|
4e97a3b3d5 | ||
|
|
134c43ad9e | ||
|
|
e74b4b35b9 | ||
|
|
16e7194dfe | ||
|
|
932b0ccf24 | ||
|
|
3e5c7f62d0 | ||
|
|
bf19f5b9c0 | ||
|
|
08a879bbb1 | ||
|
|
cd514285d9 | ||
|
|
782bb11894 | ||
|
|
355689ed19 | ||
|
|
75614fb13c | ||
|
|
5c4a864680 | ||
|
|
eaeff891d6 | ||
|
|
f0ab40d748 | ||
|
|
e497af4c26 | ||
|
|
f860f57363 | ||
|
|
02bf5ada89 | ||
|
|
d3b578fe8f | ||
|
|
d29d910ac6 | ||
|
|
e7b41f9a4c | ||
|
|
dd0aed4614 | ||
|
|
b9b4f2bb7f | ||
|
|
3533d2a2cc | ||
|
|
26d9ef5398 | ||
|
|
a0f840bcf8 | ||
|
|
33d2a77e37 | ||
|
|
80e00a80d5 | ||
|
|
a3d5479878 | ||
|
|
a49dc04f5d | ||
|
|
d1c0c9739d | ||
|
|
7415b07586 | ||
|
|
023663b268 | ||
|
|
3883c509b9 | ||
|
|
18f34b4f83 | ||
|
|
caed86d846 | ||
|
|
459e36c027 | ||
|
|
725f8571bb | ||
|
|
b7c7c0e862 | ||
|
|
3f671b918a | ||
|
|
9492363b22 | ||
|
|
3ee144459f | ||
|
|
c0c80c0fdf | ||
|
|
7c80b61666 | ||
|
|
d128f3e14e | ||
|
|
4498b89ac4 | ||
|
|
e576a58ead | ||
|
|
eb4375258e | ||
|
|
0cbc2001e2 | ||
|
|
6bf5dbabee | ||
|
|
6a89646e66 | ||
|
|
f3234a6b5e | ||
|
|
73a8c302e9 | ||
|
|
989f2d3001 | ||
|
|
2badcfa546 | ||
|
|
384e14b32d | ||
|
|
016e743653 | ||
|
|
00adf3631a | ||
|
|
09aef18999 | ||
|
|
1d86aac338 | ||
|
|
80173634a0 | ||
|
|
c9598b674c | ||
|
|
2a588d1e9a | ||
|
|
5a6c06c8a3 | ||
|
|
b2ef4e9619 | ||
|
|
9e9d6e45b4 | ||
|
|
6752457ad8 | ||
|
|
ddcb5cd4d3 | ||
|
|
a54b2db81b | ||
|
|
6740124364 | ||
|
|
157731e4f8 | ||
|
|
2dd1496ef4 | ||
|
|
d1e4e72693 | ||
|
|
77e8143290 | ||
|
|
7f791d4919 | ||
|
|
d7e0468776 | ||
|
|
f6c611bbba | ||
|
|
0990ac4fc1 | ||
|
|
e91f8f693b | ||
|
|
2a7dbda133 | ||
|
|
793e542312 | ||
|
|
240269eb25 | ||
|
|
c744dc8cc3 | ||
|
|
d596bdb454 | ||
|
|
bec54b4283 | ||
|
|
061b88f5b5 | ||
|
|
8704eff632 | ||
|
|
fb16f25b07 | ||
|
|
e8057a5c8a | ||
|
|
3c5edb6171 | ||
|
|
e36a191240 | ||
|
|
ecdfd65f50 | ||
|
|
4294081abb | ||
|
|
5218543c58 | ||
|
|
d8332a27e5 | ||
|
|
673658dfd2 | ||
|
|
6528d3d7da | ||
|
|
16af479b83 | ||
|
|
13187de97d | ||
|
|
32850f6770 | ||
|
|
4a7f4bde4a | ||
|
|
0010119440 | ||
|
|
91065a6168 | ||
|
|
efa8d5c575 | ||
|
|
04998d0215 | ||
|
|
d0efa5d3fe | ||
|
|
c87e72e08e | ||
|
|
efb82847cb | ||
|
|
f37e267a5e | ||
|
|
69928219a3 | ||
|
|
fdf8845a2f | ||
|
|
4073a7abc3 | ||
|
|
ffd9a34cf5 | ||
|
|
07226c6d21 | ||
|
|
b1bc7c1fc2 | ||
|
|
1b33f0cea9 | ||
|
|
8ece3b00f5 | ||
|
|
c9c58b65a6 | ||
|
|
66becbc4cc | ||
|
|
5b8612c919 | ||
|
|
430c22e06e | ||
|
|
76b62eda3a | ||
|
|
bc983162f3 | ||
|
|
4922598aee | ||
|
|
b2f8bb9990 | ||
|
|
9ee92fb9e9 | ||
|
|
981bf1d56f | ||
|
|
d2c2503cfa | ||
|
|
2a4caa856e | ||
|
|
157962e42a | ||
|
|
16db28060c | ||
|
|
712424c339 | ||
|
|
15c56dfcb8 | ||
|
|
b98ad47618 | ||
|
|
4778c5f5e8 | ||
|
|
d041671dc5 | ||
|
|
5cab65d197 | ||
|
|
b5bf627fb1 | ||
|
|
6104150b77 | ||
|
|
a13bae2f39 | ||
|
|
fd80e98207 | ||
|
|
f43b95f001 | ||
|
|
38802d3522 | ||
|
|
9f7813622d | ||
|
|
75d67207aa | ||
|
|
e596a8f731 | ||
|
|
5b0cc73792 | ||
|
|
2163d4465f | ||
|
|
8becf45714 | ||
|
|
853ead26ca | ||
|
|
0ccb6cb873 | ||
|
|
e46ff3de8b | ||
|
|
a02e08a879 | ||
|
|
109d7d87bd | ||
|
|
3df740702c | ||
|
|
06bb6f7bff | ||
|
|
e33738a876 | ||
|
|
f887f5dca3 | ||
|
|
257c16690a | ||
|
|
7114e88992 | ||
|
|
de7e869ca9 | ||
|
|
eaee9c9522 | ||
|
|
bee11a6d41 | ||
|
|
647e44147f | ||
|
|
4a5d46915d | ||
|
|
8ad19585cb | ||
|
|
951a33fae9 | ||
|
|
bcf174cd1e | ||
|
|
1e82909f58 | ||
|
|
33e83b8414 | ||
|
|
592c56cb20 | ||
|
|
710528c01a | ||
|
|
03cadc2604 | ||
|
|
dd73bb95a4 | ||
|
|
40e0a55378 | ||
|
|
269884d9f3 | ||
|
|
fcd548c313 | ||
|
|
140a8b6804 | ||
|
|
b5378c1296 | ||
|
|
1445d6ea8c | ||
|
|
5385431051 | ||
|
|
f14f4498fb | ||
|
|
fc2786f5e8 | ||
|
|
174dbb5e74 | ||
|
|
68517c15f2 | ||
|
|
11ee142e4b | ||
|
|
1e1d047e07 | ||
|
|
62f1e39e6e | ||
|
|
05756da495 | ||
|
|
fa35b2a66f | ||
|
|
d2094e2b68 | ||
|
|
075b2df738 | ||
|
|
ec3c31a106 | ||
|
|
e2183c2214 | ||
|
|
4994064e6e | ||
|
|
a40b9f4054 | ||
|
|
ee8d65977d | ||
|
|
b4ea11e55e | ||
|
|
b8d2ef1eb5 | ||
|
|
0efeffeaa3 | ||
|
|
7a86d272bb | ||
|
|
4d780904d1 | ||
|
|
0a6fa65075 | ||
|
|
6440220640 | ||
|
|
4350c9a72b | ||
|
|
77feae0ab9 | ||
|
|
1261696ba2 | ||
|
|
6b35a0a527 | ||
|
|
fd33ff81c9 | ||
|
|
178b1e8bb0 | ||
|
|
9d50f03cb1 | ||
|
|
8c1688657a | ||
|
|
833cb99f41 | ||
|
|
bd5d84abcd | ||
|
|
d451ba9b6e | ||
|
|
14bd02a569 | ||
|
|
4beace1bb0 | ||
|
|
6996dfcd3b | ||
|
|
42c46d7d5c | ||
|
|
9b31ce83c5 | ||
|
|
cb5250527b | ||
|
|
f0b73fd696 | ||
|
|
df5684a9f8 | ||
|
|
b3f724c799 | ||
|
|
a7be6504a2 | ||
|
|
1da5357df6 | ||
|
|
92e1847c59 | ||
|
|
0500994def | ||
|
|
da911bfeb8 | ||
|
|
578d673a4e | ||
|
|
c8e58a1e5b | ||
|
|
d477874535 | ||
|
|
da79386cc3 | ||
|
|
a4ba6d1444 | ||
|
|
ef28459b61 | ||
|
|
1ff8c908b8 | ||
|
|
e966ef96e5 | ||
|
|
b05b38b269 | ||
|
|
8e1f1ff2e6 | ||
|
|
680d6c20ca | ||
|
|
c886e7949e | ||
|
|
e0b972f6d6 | ||
|
|
25daa9f2da | ||
|
|
d0fb5c3bd5 | ||
|
|
520b12e56b | ||
|
|
5c8ffe961e | ||
|
|
7983e82b60 | ||
|
|
77d35b61a9 | ||
|
|
285a97aaf8 | ||
|
|
ad29f2477e | ||
|
|
1072d1306b | ||
|
|
b8eda40937 | ||
|
|
2719ae5df2 | ||
|
|
68ee2bdcdc | ||
|
|
da654fdff5 | ||
|
|
d7f9d5a66f | ||
|
|
c4fb7b7928 | ||
|
|
43a791db65 | ||
|
|
217311211a | ||
|
|
ca55890ad2 | ||
|
|
d6ecf5b8a9 | ||
|
|
e52edde11f | ||
|
|
2e514735ec | ||
|
|
3d32c30d2d | ||
|
|
05235f8385 | ||
|
|
cd28a75c86 | ||
|
|
34075738ea | ||
|
|
88c0b8a8f0 | ||
|
|
e8bbc117e1 | ||
|
|
b99f45874f | ||
|
|
0dfa378e38 | ||
|
|
3da0c07bcd | ||
|
|
2196b53075 | ||
|
|
a8340f37bb | ||
|
|
7b1710ee63 | ||
|
|
38b7d9724e | ||
|
|
2b1ed49e9a | ||
|
|
017cf9e464 | ||
|
|
781f0c843e | ||
|
|
e2bf474332 | ||
|
|
7e2f1c9a8b | ||
|
|
8e798dde48 | ||
|
|
c05ae6e94c | ||
|
|
ff28ea8fa8 | ||
|
|
7914e89212 | ||
|
|
558ff90e27 | ||
|
|
ee69653a83 | ||
|
|
95339a8338 | ||
|
|
39b1435725 | ||
|
|
b1d3e258bd | ||
|
|
6ff7fa74e2 | ||
|
|
91305262f1 | ||
|
|
6d16b68f11 | ||
|
|
f22e4f1cc7 | ||
|
|
2851d12357 | ||
|
|
73968e4277 | ||
|
|
7a6ecd86c6 | ||
|
|
1ff2a08d19 | ||
|
|
d1efd62e7b | ||
|
|
366c95cd3c | ||
|
|
7e03f3958e | ||
|
|
e069184721 | ||
|
|
5182441cb3 | ||
|
|
0de55a8ff5 | ||
|
|
0900d7c764 | ||
|
|
8540e09ba7 | ||
|
|
6e301601f9 | ||
|
|
1bf0eab2d9 | ||
|
|
d560f656f4 | ||
|
|
0bc256aa23 | ||
|
|
ebc073a52e | ||
|
|
23503dc439 | ||
|
|
1f4985c7dd | ||
|
|
ed88d9e10d | ||
|
|
906196bac3 | ||
|
|
cb9751be04 | ||
|
|
09e03d579b | ||
|
|
9fcca2698e | ||
|
|
bba8cd9b58 | ||
|
|
c8bb30289a | ||
|
|
69f56c1eb7 | ||
|
|
adc8e23356 | ||
|
|
88005c6603 | ||
|
|
33ee4c36b4 | ||
|
|
1a142b7fe6 | ||
|
|
90373806c0 | ||
|
|
a47634bf49 | ||
|
|
e03e58323b | ||
|
|
ab1e31c0e7 | ||
|
|
aa5505d693 | ||
|
|
74f8f687cf | ||
|
|
d54500bad5 | ||
|
|
4966b4d58d | ||
|
|
b75a4667c2 | ||
|
|
14579a9320 | ||
|
|
1d92eff974 | ||
|
|
91274267e5 | ||
|
|
c49f0ede16 | ||
|
|
42a0f452b1 | ||
|
|
c24ab9831a | ||
|
|
506a68ee6a | ||
|
|
1291f792ab | ||
|
|
51cfb1903d | ||
|
|
1b1c15694a | ||
|
|
251f1941a9 | ||
|
|
625617bb20 | ||
|
|
f80f161886 | ||
|
|
4b4889d5f2 | ||
|
|
fee34ba257 | ||
|
|
c29ab25dd2 | ||
|
|
e0308a11c9 | ||
|
|
a738998c2c | ||
|
|
6be22c474d | ||
|
|
da19743ba5 |
4
.github/ISSUE_TEMPLATE/2-web-bug.yml
vendored
4
.github/ISSUE_TEMPLATE/2-web-bug.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: 🌐 Website bug (modrinth.com)
|
||||
description: Report an issue on the Modrinth website.
|
||||
labels: [bug, web]
|
||||
labels: [bug, frontend]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
@@ -49,4 +49,4 @@ body:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
required: false
|
||||
4
.github/ISSUE_TEMPLATE/4-feature-request.yml
vendored
4
.github/ISSUE_TEMPLATE/4-feature-request.yml
vendored
@@ -9,8 +9,6 @@ body:
|
||||
options:
|
||||
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate feature requests
|
||||
required: true
|
||||
- label: I checked the [existing discussions](https://github.com/orgs/modrinth/discussions) for duplicate feature requests
|
||||
required: true
|
||||
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -45,4 +43,4 @@ body:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the suggested enhancement here.
|
||||
validations:
|
||||
required: false
|
||||
required: false
|
||||
43
.github/workflows/daedalus-docker.yml
vendored
Normal file
43
.github/workflows/daedalus-docker.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: daedalus-docker-build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- .github/workflows/daedalus-docker.yml
|
||||
- 'apps/daedalus_client/**'
|
||||
pull_request:
|
||||
types: [ opened, synchronize ]
|
||||
paths:
|
||||
- .github/workflows/daedalus-docker.yml
|
||||
- 'apps/daedalus_client/**'
|
||||
merge_group:
|
||||
types: [ checks_requested ]
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Fetch docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ghcr.io/modrinth/daedalus
|
||||
-
|
||||
name: Login to GitHub Images
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
file: ./apps/daedalus_client/Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
52
.github/workflows/daedalus-run.yml
vendored
Normal file
52
.github/workflows/daedalus-run.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Run Meta
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/5 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
run-docker:
|
||||
if: github.repository_owner == 'modrinth'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
- name: Pull Docker image from GHCR
|
||||
run: docker pull ghcr.io/modrinth/daedalus:main
|
||||
|
||||
- name: Run Docker container
|
||||
env:
|
||||
BASE_URL: ${{ secrets.BASE_URL }}
|
||||
S3_ACCESS_TOKEN: ${{ secrets.S3_ACCESS_TOKEN }}
|
||||
S3_SECRET: ${{ secrets.S3_SECRET }}
|
||||
S3_URL: ${{ secrets.S3_URL }}
|
||||
S3_REGION: ${{ secrets.S3_REGION }}
|
||||
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
|
||||
CLOUDFLARE_INTEGRATION: ${{ secrets.CLOUDFLARE_INTEGRATION }}
|
||||
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
|
||||
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||
run: |
|
||||
docker run \
|
||||
--name daedalus \
|
||||
-e RUST_LOG=warn,daedalus_client=trace \
|
||||
-e BASE_URL=$BASE_URL \
|
||||
-e S3_ACCESS_TOKEN=$S3_ACCESS_TOKEN \
|
||||
-e S3_SECRET=$S3_SECRET \
|
||||
-e S3_URL=$S3_URL \
|
||||
-e S3_REGION=$S3_REGION \
|
||||
-e S3_BUCKET_NAME=$S3_BUCKET_NAME \
|
||||
-e CLOUDFLARE_INTEGRATION=$CLOUDFLARE_INTEGRATION \
|
||||
-e CLOUDFLARE_TOKEN=$CLOUDFLARE_TOKEN \
|
||||
-e CLOUDFLARE_ZONE_ID=$CLOUDFLARE_ZONE_ID \
|
||||
ghcr.io/modrinth/daedalus:main
|
||||
8
.github/workflows/frontend-pages.yml
vendored
8
.github/workflows/frontend-pages.yml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Deploy frontend
|
||||
name: Clear pages cache
|
||||
|
||||
on: push
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- prod
|
||||
|
||||
jobs:
|
||||
wait:
|
||||
@@ -16,7 +19,6 @@ jobs:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: '9ddae624c98677d68d93df6e524a6061'
|
||||
project: 'frontend'
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
commitHash: ${{ steps.push-changes.outputs.commit-hash }}
|
||||
- name: Purge cache
|
||||
if: github.ref == 'refs/heads/prod'
|
||||
|
||||
46
.github/workflows/labrinth-docker.yml
vendored
Normal file
46
.github/workflows/labrinth-docker.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: docker-build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- .github/workflows/labrinth-docker.yml
|
||||
- 'apps/labrinth/**'
|
||||
pull_request:
|
||||
types: [ opened, synchronize ]
|
||||
paths:
|
||||
- .github/workflows/labrinth-docker.yml
|
||||
- 'apps/labrinth/**'
|
||||
merge_group:
|
||||
types: [ checks_requested ]
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./apps/labrinth
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Fetch docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ghcr.io/modrinth/labrinth
|
||||
-
|
||||
name: Login to GitHub Images
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./apps/labrinth
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
@@ -44,7 +44,28 @@ jobs:
|
||||
- name: Setup rust cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target/**
|
||||
path: |
|
||||
target/**
|
||||
!target/*/release/bundle/*/*.dmg
|
||||
!target/*/release/bundle/*/*.app.tar.gz
|
||||
!target/*/release/bundle/*/*.app.tar.gz.sig
|
||||
!target/release/bundle/*/*.dmg
|
||||
!target/release/bundle/*/*.app.tar.gz
|
||||
!target/release/bundle/*/*.app.tar.gz.sig
|
||||
|
||||
!target/release/bundle/*/*.AppImage
|
||||
!target/release/bundle/*/*.AppImage.tar.gz
|
||||
!target/release/bundle/*/*.AppImage.tar.gz.sig
|
||||
!target/release/bundle/*/*.deb
|
||||
!target/release/bundle/*/*.rpm
|
||||
|
||||
!target/release/bundle/msi/*.msi
|
||||
!target/release/bundle/msi/*.msi.zip
|
||||
!target/release/bundle/msi/*.msi.zip.sig
|
||||
|
||||
!target/release/bundle/nsis/*.exe
|
||||
!target/release/bundle/nsis/*.nsis.zip
|
||||
!target/release/bundle/nsis/*.nsis.zip.sig
|
||||
key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-rust-target-
|
||||
@@ -78,7 +99,7 @@ jobs:
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev sqlite3
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install
|
||||
@@ -124,8 +145,11 @@ jobs:
|
||||
target/release/bundle/*/*.AppImage.tar.gz.sig
|
||||
target/release/bundle/*/*.deb
|
||||
target/release/bundle/*/*.rpm
|
||||
|
||||
target/release/bundle/*/*.msi
|
||||
target/release/bundle/*/*.msi.zip
|
||||
target/release/bundle/*/*.msi.zip.sig
|
||||
|
||||
target/release/bundle/msi/*.msi
|
||||
target/release/bundle/msi/*.msi.zip
|
||||
target/release/bundle/msi/*.msi.zip.sig
|
||||
|
||||
target/release/bundle/nsis/*.exe
|
||||
target/release/bundle/nsis/*.nsis.zip
|
||||
target/release/bundle/nsis/*.nsis.zip.sig
|
||||
@@ -62,9 +62,19 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
|
||||
- name: Start docker compose
|
||||
run: docker compose up -d
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -55,3 +55,9 @@ generated
|
||||
|
||||
# app testing dir
|
||||
app-playground-data/*
|
||||
|
||||
# soley because i need the PORT to be 3002 due to WSL stuff
|
||||
.env
|
||||
apps/frontend/.env
|
||||
|
||||
.astro
|
||||
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
15
.idea/daedalus.iml
generated
Normal file
15
.idea/daedalus.iml
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/daedalus_client_new/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/apps/daedalus_client/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/daedalus/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
7
.idea/discord.xml
generated
Normal file
7
.idea/discord.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
26
.idea/libraries/KotlinJavaRuntime.xml
generated
Normal file
26
.idea/libraries/KotlinJavaRuntime.xml
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
<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>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/daedalus.iml" filepath="$PROJECT_DIR$/.idea/daedalus.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
5289
Cargo.lock
generated
5289
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,10 @@ resolver = '2'
|
||||
members = [
|
||||
'./packages/app-lib',
|
||||
'./apps/app-playground',
|
||||
'./apps/app'
|
||||
'./apps/app',
|
||||
'./apps/labrinth',
|
||||
'./apps/daedalus_client',
|
||||
'./packages/daedalus',
|
||||
]
|
||||
|
||||
# Optimize for speed and reduce size on release builds
|
||||
@@ -16,3 +19,6 @@ strip = true # Remove debug symbols
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
|
||||
[patch.crates-io]
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "27fb16b" }
|
||||
|
||||
@@ -22,7 +22,7 @@ This repository contains two primary packages. For detailed development informat
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Before submitting any contributions, please read our [contributing guidelines](https://support.modrinth.com/en/articles/8802215-contributing-to-modrinth).
|
||||
We welcome contributions! Before submitting any contributions, please read our [contributing guidelines](https://docs.modrinth.com/contributing/getting-started/).
|
||||
|
||||
If you plan to fork this repository for your own purposes, please review our [copying guidelines](COPYING.md).
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['custom/vue'],
|
||||
}
|
||||
22
apps/app-frontend/eslint.config.mjs
Normal file
22
apps/app-frontend/eslint.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
|
||||
import { fixupPluginRules } from '@eslint/compat'
|
||||
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',
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Modrinth App</title>
|
||||
|
||||
<link rel="stylesheet" href="/src/assets/stylesheets/global.scss" />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "0.8.3-1",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"tsc:check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write ."
|
||||
},
|
||||
@@ -13,36 +14,41 @@
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@sentry/vue": "^8.27.0",
|
||||
"@tauri-apps/api": "^2.0.0-rc.3",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.0.0-rc.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"floating-vue": "^5.2.2",
|
||||
"ofetch": "^1.3.4",
|
||||
"pinia": "^2.1.7",
|
||||
"posthog-js": "^1.158.2",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue": "^3.4.21",
|
||||
"vue-multiselect": "3.0.0",
|
||||
"vue-router": "4.3.0",
|
||||
"vue-virtual-scroller": "v2.0.0-beta.8",
|
||||
"posthog-js": "^1.158.2",
|
||||
"@sentry/vue": "^8.27.0"
|
||||
"vue-virtual-scroller": "v2.0.0-beta.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.0-rc",
|
||||
"@eslint/compat": "^1.1.1",
|
||||
"@nuxt/eslint-config": "^0.5.6",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"eslint-plugin-turbo": "^2.1.1",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.74.1",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tsconfig": "workspace:*",
|
||||
"vite": "^5.2.8"
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.2.8",
|
||||
"vue-tsc": "^2.1.6"
|
||||
},
|
||||
"packageManager": "pnpm@9.4.0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
|
||||
import { HomeIcon, SearchIcon, LibraryIcon, PlusIcon, SettingsIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
HomeIcon,
|
||||
SearchIcon,
|
||||
LibraryIcon,
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
XIcon,
|
||||
DownloadIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, Notifications } from '@modrinth/ui'
|
||||
import { useLoading, useTheming } from '@/store/state'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
@@ -16,7 +24,7 @@ import { handleError, useNotifications } from '@/store/notifications.js'
|
||||
import { command_listener, warning_listener } from '@/helpers/events.js'
|
||||
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { isDev, getOS } from '@/helpers/utils.js'
|
||||
import { isDev, getOS, restartApp } from '@/helpers/utils.js'
|
||||
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
@@ -31,6 +39,10 @@ import { useInstall } from '@/store/install.js'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-shell'
|
||||
import { get_opening_command, initialize_state } from '@/helpers/state'
|
||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { renderString } from '@modrinth/utils'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
import { check } from '@tauri-apps/plugin-updater'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -51,6 +63,8 @@ const os = ref('')
|
||||
|
||||
const stateInitialized = ref(false)
|
||||
|
||||
const criticalErrorMessage = ref()
|
||||
|
||||
onMounted(async () => {
|
||||
await useCheckDisableMouseover()
|
||||
})
|
||||
@@ -107,7 +121,18 @@ async function setupApp() {
|
||||
}),
|
||||
)
|
||||
|
||||
useFetch(
|
||||
`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
|
||||
'criticalAnnouncements',
|
||||
true,
|
||||
).then((res) => {
|
||||
if (res && res.header && res.body) {
|
||||
criticalErrorMessage.value = res
|
||||
}
|
||||
})
|
||||
|
||||
get_opening_command().then(handleCommand)
|
||||
checkUpdates()
|
||||
}
|
||||
|
||||
const stateFailed = ref(false)
|
||||
@@ -126,6 +151,7 @@ initialize_state()
|
||||
})
|
||||
|
||||
const handleClose = async () => {
|
||||
await saveWindowState(StateFlags.ALL)
|
||||
await getCurrentWindow().close()
|
||||
}
|
||||
|
||||
@@ -171,7 +197,8 @@ document.querySelector('body').addEventListener('click', function (e) {
|
||||
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
|
||||
!target.classList.contains('router-link-active') &&
|
||||
!target.href.startsWith('http://localhost') &&
|
||||
!target.href.startsWith('https://tauri.localhost')
|
||||
!target.href.startsWith('https://tauri.localhost') &&
|
||||
!target.href.startsWith('http://tauri.localhost')
|
||||
) {
|
||||
open(target.href)
|
||||
}
|
||||
@@ -215,6 +242,20 @@ async function handleCommand(e) {
|
||||
urlModal.value.show(e)
|
||||
}
|
||||
}
|
||||
|
||||
const updateAvailable = ref(false)
|
||||
async function checkUpdates() {
|
||||
const update = await check()
|
||||
console.log(update)
|
||||
updateAvailable.value = !!update
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
checkUpdates()
|
||||
},
|
||||
5 * 1000 * 60,
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -248,6 +289,14 @@ async function handleCommand(e) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings pages-list">
|
||||
<button
|
||||
v-if="updateAvailable"
|
||||
v-tooltip="'Install update'"
|
||||
class="btn btn-outline btn-primary icon-only collapsed-button"
|
||||
@click="restartApp()"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
<Button
|
||||
v-tooltip="'Create profile'"
|
||||
class="sleek-primary collapsed-button"
|
||||
@@ -263,6 +312,10 @@ async function handleCommand(e) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="view">
|
||||
<div v-if="criticalErrorMessage" class="critical-error-banner" data-tauri-drag-region>
|
||||
<h1>{{ criticalErrorMessage.header }}</h1>
|
||||
<div class="markdown-body" v-html="renderString(criticalErrorMessage.body ?? '')"></div>
|
||||
</div>
|
||||
<div class="appbar-row">
|
||||
<div data-tauri-drag-region class="appbar">
|
||||
<section class="navigation-controls">
|
||||
@@ -375,6 +428,16 @@ async function handleCommand(e) {
|
||||
width: calc(100% - var(--sidebar-width));
|
||||
background-color: var(--color-raised-bg);
|
||||
|
||||
.critical-error-banner {
|
||||
margin-top: -1.25rem;
|
||||
padding: 1rem;
|
||||
background-color: rgba(203, 34, 69, 0.1);
|
||||
border-left: 2px solid var(--color-red);
|
||||
border-bottom: 2px solid var(--color-red);
|
||||
border-right: 2px solid var(--color-red);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.appbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -12,13 +12,13 @@ import {
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ConfirmModal, Button, Card, DropdownSelect } from '@modrinth/ui'
|
||||
import { Button, Card, DropdownSelect } from '@modrinth/ui'
|
||||
import { formatCategoryHeader } from '@modrinth/utils'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { duplicate, remove } from '@/helpers/profile.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
|
||||
const props = defineProps({
|
||||
instances: {
|
||||
@@ -35,7 +35,6 @@ const props = defineProps({
|
||||
const instanceOptions = ref(null)
|
||||
const instanceComponents = ref(null)
|
||||
|
||||
const themeStore = useTheming()
|
||||
const currentDeleteInstance = ref(null)
|
||||
const confirmModal = ref(null)
|
||||
|
||||
@@ -230,13 +229,12 @@ const filteredResults = computed(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ConfirmModal
|
||||
<ConfirmModalWrapper
|
||||
ref="confirmModal"
|
||||
title="Are you sure you want to delete this instance?"
|
||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||
:has-to-type="false"
|
||||
proceed-label="Delete"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
@proceed="deleteProfile"
|
||||
/>
|
||||
<Card class="header">
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
EyeIcon,
|
||||
ChevronRightIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import Instance from '@/components/ui/Instance.vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
@@ -22,7 +22,6 @@ import { handleError } from '@/store/notifications.js'
|
||||
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { useTheming } from '@/store/state.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
@@ -53,7 +52,6 @@ const instanceComponents = ref(null)
|
||||
const rows = ref(null)
|
||||
const deleteConfirmModal = ref(null)
|
||||
|
||||
const themeStore = useTheming()
|
||||
const currentDeleteInstance = ref(null)
|
||||
|
||||
async function deleteProfile() {
|
||||
@@ -207,13 +205,12 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmModal
|
||||
<ConfirmModalWrapper
|
||||
ref="deleteConfirmModal"
|
||||
title="Are you sure you want to delete this instance?"
|
||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||
:has-to-type="false"
|
||||
proceed-label="Delete"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
@proceed="deleteProfile"
|
||||
/>
|
||||
<div class="content">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-tooltip.right="'Minecraft accounts'"
|
||||
class="button-base avatar-button"
|
||||
:class="{ expanded: mode === 'expanded' }"
|
||||
@click="showCard = !showCard"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<Avatar
|
||||
:size="mode === 'expanded' ? 'xs' : 'sm'"
|
||||
@@ -73,6 +73,7 @@ import { handleError } from '@/store/state.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
@@ -133,9 +134,9 @@ const logout = async (id) => {
|
||||
trackEvent('AccountLogOut')
|
||||
}
|
||||
|
||||
let showCard = ref(false)
|
||||
let card = ref(null)
|
||||
let button = ref(null)
|
||||
const showCard = ref(false)
|
||||
const card = ref(null)
|
||||
const button = ref(null)
|
||||
const handleClickOutside = (event) => {
|
||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||
if (
|
||||
@@ -144,7 +145,20 @@ const handleClickOutside = (event) => {
|
||||
!elements.includes(card.value.$el) &&
|
||||
!button.value.contains(event.target)
|
||||
) {
|
||||
toggleMenu(false)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMenu(override = true) {
|
||||
if (showCard.value || !override) {
|
||||
if (showCard.value) {
|
||||
show_ads_window()
|
||||
}
|
||||
|
||||
showCard.value = false
|
||||
} else {
|
||||
hide_ads_window()
|
||||
showCard.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const handleAddContentFromFile = async () => {
|
||||
if (!newProject) return
|
||||
|
||||
for (const project of newProject) {
|
||||
await add_project_from_path(props.instance.path, project).catch(handleError)
|
||||
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
|
||||
const emit = defineEmits(['menu-closed', 'option-clicked'])
|
||||
|
||||
@@ -37,6 +38,7 @@ const shown = ref(false)
|
||||
|
||||
defineExpose({
|
||||
showMenu: (event, passedItem, passedOptions) => {
|
||||
hide_ads_window()
|
||||
item.value = passedItem
|
||||
options.value = passedOptions
|
||||
|
||||
@@ -69,6 +71,9 @@ const isLinkedData = (item) => {
|
||||
}
|
||||
|
||||
const hideContextMenu = () => {
|
||||
if (shown.value) {
|
||||
show_ads_window()
|
||||
}
|
||||
shown.value = false
|
||||
emit('menu-closed')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { XIcon, HammerIcon, LogInIcon, UpdatedIcon } from '@modrinth/assets'
|
||||
import { Modal } from '@modrinth/ui'
|
||||
import { ChatIcon } from '@/assets/icons'
|
||||
import { ref } from 'vue'
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
@@ -9,6 +8,7 @@ import { handleSevereError } from '@/store/error.js'
|
||||
import { cancel_directory_change } from '@/helpers/settings.js'
|
||||
import { install } from '@/helpers/profile.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const errorModal = ref()
|
||||
const error = ref()
|
||||
@@ -121,7 +121,7 @@ async function repairInstance() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal ref="errorModal" :header="title" :closable="closable">
|
||||
<ModalWrapper ref="errorModal" :header="title" :closable="closable">
|
||||
<div class="modal-body">
|
||||
<div class="markdown-body">
|
||||
<template v-if="errorType === 'minecraft_auth'">
|
||||
@@ -230,7 +230,7 @@ async function repairInstance() {
|
||||
</p>
|
||||
<p>You may be able to fix it through one of the following ways:</p>
|
||||
<ul>
|
||||
<li>Ennsuring you are connected to the internet, then try restarting the app.</li>
|
||||
<li>Ensuring you are connected to the internet, then try restarting the app.</li>
|
||||
<li>Redownloading the app.</li>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -272,7 +272,7 @@ async function repairInstance() {
|
||||
<button v-if="closable" class="btn" @click="errorModal.hide()"><XIcon /> Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup>
|
||||
import { XIcon, PlusIcon } from '@modrinth/assets'
|
||||
import { Button, Checkbox, Modal } from '@modrinth/ui'
|
||||
import { Button, Checkbox } 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 { handleError } from '@/store/notifications.js'
|
||||
import { useTheming } from '@/store/theme'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
@@ -30,8 +30,6 @@ const files = ref([])
|
||||
const folders = ref([])
|
||||
const showingFiles = ref(false)
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const initFiles = async () => {
|
||||
const newFolders = new Map()
|
||||
const sep = '/'
|
||||
@@ -106,7 +104,7 @@ const exportPack = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal ref="exportModal" header="Export modpack" :noblur="!themeStore.advancedRendering">
|
||||
<ModalWrapper ref="exportModal" header="Export modpack">
|
||||
<div class="modal-body">
|
||||
<div class="labeled_input">
|
||||
<p>Modpack Name</p>
|
||||
@@ -208,7 +206,7 @@ const exportPack = async () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Modal ref="modal" header="Create instance" :noblur="!themeStore.advancedRendering">
|
||||
<ModalWrapper ref="modal" header="Create instance">
|
||||
<div class="modal-header">
|
||||
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
|
||||
</div>
|
||||
@@ -193,10 +193,11 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import {
|
||||
PlusIcon,
|
||||
UploadIcon,
|
||||
@@ -207,7 +208,7 @@ import {
|
||||
FolderSearchIcon,
|
||||
UpdatedIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Button, Chips, Modal, Checkbox } from '@modrinth/ui'
|
||||
import { Avatar, Button, Chips, Checkbox } from '@modrinth/ui'
|
||||
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import { get_loaders } from '@/helpers/tags'
|
||||
import { create } from '@/helpers/profile'
|
||||
@@ -217,8 +218,6 @@ import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { useTheming } from '@/store/state.js'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { install_from_file } from '@/helpers/pack.js'
|
||||
import {
|
||||
get_default_launcher_path,
|
||||
@@ -226,8 +225,7 @@ import {
|
||||
import_instance,
|
||||
} from '@/helpers/import.js'
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
|
||||
const themeStore = useTheming()
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
|
||||
const profile_name = ref('')
|
||||
const game_version = ref('')
|
||||
@@ -257,13 +255,15 @@ defineExpose({
|
||||
isShowing.value = true
|
||||
modal.value.show()
|
||||
|
||||
unlistener.value = await listen('tauri://file-drop', async (event) => {
|
||||
unlistener.value = await getCurrentWebview().onDragDropEvent(async (event) => {
|
||||
// Only if modal is showing
|
||||
if (!isShowing.value) return
|
||||
if (event.payload.type !== 'drop') return
|
||||
if (creationType.value !== 'from file') return
|
||||
hide()
|
||||
if (event.payload && event.payload.length > 0 && event.payload[0].endsWith('.mrpack')) {
|
||||
await install_from_file(event.payload[0]).catch(handleError)
|
||||
const { paths } = event.payload
|
||||
if (paths && paths.length > 0 && paths[0].endsWith('.mrpack')) {
|
||||
await install_from_file(paths[0]).catch(handleError)
|
||||
trackEvent('InstanceCreate', {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
@@ -371,7 +371,7 @@ const create_instance = async () => {
|
||||
}
|
||||
|
||||
const upload_icon = async () => {
|
||||
icon.value = await open({
|
||||
const res = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
@@ -381,6 +381,8 @@ const upload_icon = async () => {
|
||||
],
|
||||
})
|
||||
|
||||
icon.value = res.path ?? res
|
||||
|
||||
if (!icon.value) return
|
||||
display_icon.value = convertFileSrc(icon.value)
|
||||
}
|
||||
@@ -417,7 +419,7 @@ const openFile = async () => {
|
||||
const newProject = await open({ multiple: false })
|
||||
if (!newProject) return
|
||||
hide()
|
||||
await install_from_file(newProject).catch(handleError)
|
||||
await install_from_file(newProject.path ?? newProject).catch(handleError)
|
||||
|
||||
trackEvent('InstanceCreate', {
|
||||
source: 'CreationModalFileOpen',
|
||||
@@ -462,7 +464,7 @@ const promises = profileOptions.value.map(async (option) => {
|
||||
option.name,
|
||||
instances.map((name) => ({ name, selected: false })),
|
||||
)
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Allow failure silently
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Modal ref="detectJavaModal" header="Select java version" :noblur="!themeStore.advancedRendering">
|
||||
<ModalWrapper ref="detectJavaModal" header="Select java version">
|
||||
<div class="auto-detect-modal">
|
||||
<div class="table">
|
||||
<div class="table-row table-head">
|
||||
@@ -32,18 +32,16 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
<script setup>
|
||||
import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets'
|
||||
import { Modal, Button } from '@modrinth/ui'
|
||||
import { Button } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { find_filtered_jres } from '@/helpers/jre.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
|
||||
const themeStore = useTheming()
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const chosenInstallOptions = ref([])
|
||||
const detectJavaModal = ref(null)
|
||||
|
||||
@@ -124,20 +124,19 @@ async function testJava() {
|
||||
}
|
||||
|
||||
async function handleJavaFileInput() {
|
||||
let filePath = await open()
|
||||
const filePath = await open()
|
||||
|
||||
if (filePath) {
|
||||
let result = await get_jre(filePath)
|
||||
let result = await get_jre(filePath.path ?? filePath)
|
||||
if (!result) {
|
||||
result = {
|
||||
path: filePath,
|
||||
path: filePath.path ?? filePath,
|
||||
version: props.version.toString(),
|
||||
architecture: 'x86',
|
||||
}
|
||||
}
|
||||
|
||||
trackEvent('JavaManualSelect', {
|
||||
path: filePath,
|
||||
version: props.version,
|
||||
})
|
||||
|
||||
@@ -150,7 +149,7 @@ async function autoDetect() {
|
||||
if (!props.compact) {
|
||||
detectJavaModal.value.show(props.version, props.modelValue)
|
||||
} else {
|
||||
let versions = await find_filtered_jres(props.version).catch(handleError)
|
||||
const versions = await find_filtered_jres(props.version).catch(handleError)
|
||||
if (versions.length > 0) {
|
||||
emit('update:modelValue', versions[0])
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
import { CheckIcon } from '@modrinth/assets'
|
||||
import { Button, Modal, Badge } from '@modrinth/ui'
|
||||
import { Button, Badge } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useTheming } from '@/store/theme'
|
||||
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||
import { releaseColor } from '@/helpers/utils'
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const props = defineProps({
|
||||
versions: {
|
||||
@@ -33,8 +33,6 @@ const installedVersion = computed(() => props.instance?.linked_data?.version_id)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
const inProgress = ref(false)
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const switchVersion = async (versionId) => {
|
||||
inProgress.value = true
|
||||
await update_managed_modrinth_version(props.instance.path, versionId)
|
||||
@@ -43,11 +41,10 @@ const switchVersion = async (versionId) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
<ModalWrapper
|
||||
ref="modpackVersionModal"
|
||||
class="modpack-version-modal"
|
||||
header="Change modpack version"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<Card v-if="instance.linked_data" class="mod-card">
|
||||
@@ -111,7 +108,7 @@ const switchVersion = async (versionId) => {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Promotion } from '@modrinth/ui'
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { get as getCreds } from '@/helpers/mr_auth.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { get_user } from '@/helpers/cache.js'
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
import { init_ads_window, open_ads_link, record_ads_click } from '@/helpers/ads.js'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
const showAd = ref(true)
|
||||
|
||||
defineExpose({
|
||||
scroll() {
|
||||
updateAdPosition()
|
||||
},
|
||||
})
|
||||
|
||||
const creds = await getCreds().catch(handleError)
|
||||
if (creds && creds.user_id) {
|
||||
const user = await get_user(creds.user_id).catch(handleError)
|
||||
@@ -16,8 +24,103 @@ if (creds && creds.user_id) {
|
||||
showAd.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const adsWrapper = ref(null)
|
||||
let resizeObserver
|
||||
let scrollHandler
|
||||
let intersectionObserver
|
||||
let mutationObserver
|
||||
onMounted(() => {
|
||||
if (showAd.value) {
|
||||
updateAdPosition(true)
|
||||
|
||||
resizeObserver = new ResizeObserver(() => updateAdPosition())
|
||||
resizeObserver.observe(adsWrapper.value)
|
||||
|
||||
intersectionObserver = new IntersectionObserver(() => updateAdPosition())
|
||||
intersectionObserver.observe(adsWrapper.value)
|
||||
|
||||
mutationObserver = new MutationObserver(() => updateAdPosition())
|
||||
mutationObserver.observe(adsWrapper.value, { attributes: true, childList: true, subtree: true })
|
||||
|
||||
// Add scroll event listener
|
||||
scrollHandler = () => {
|
||||
requestAnimationFrame(() => updateAdPosition())
|
||||
}
|
||||
window.addEventListener('scroll', scrollHandler, { passive: true })
|
||||
}
|
||||
})
|
||||
|
||||
function updateAdPosition(overrideShown = false) {
|
||||
if (adsWrapper.value) {
|
||||
const rect = adsWrapper.value.getBoundingClientRect()
|
||||
|
||||
let y = rect.top + window.scrollY
|
||||
let height = rect.bottom - rect.top
|
||||
|
||||
// Prevent ad from overlaying the app bar
|
||||
if (y <= 52) {
|
||||
y = 52
|
||||
height = rect.bottom - 52
|
||||
|
||||
if (height < 0) {
|
||||
height = 0
|
||||
y = -1000
|
||||
}
|
||||
}
|
||||
|
||||
init_ads_window(rect.left + window.scrollX, y, rect.right - rect.left, height, overrideShown)
|
||||
}
|
||||
}
|
||||
|
||||
async function openPlusLink() {
|
||||
await record_ads_click()
|
||||
await open_ads_link('https://modrinth.com/plus', 'https://modrinth.com')
|
||||
}
|
||||
|
||||
const unlisten = await listen('ads-scroll', (event) => {
|
||||
if (adsWrapper.value) {
|
||||
adsWrapper.value.parentNode.scrollTop += event.payload.scroll
|
||||
updateAdPosition()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
if (intersectionObserver) {
|
||||
intersectionObserver.disconnect()
|
||||
}
|
||||
if (mutationObserver) {
|
||||
mutationObserver.disconnect()
|
||||
}
|
||||
if (scrollHandler) {
|
||||
window.removeEventListener('scroll', scrollHandler)
|
||||
}
|
||||
|
||||
unlisten()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Promotion v-if="showAd" :external="false" query-param="?r=launcher" />
|
||||
<div
|
||||
v-if="showAd"
|
||||
ref="adsWrapper"
|
||||
class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised cursor-pointer"
|
||||
>
|
||||
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
|
||||
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
|
||||
<button
|
||||
class="mt-auto items-center gap-1 text-purple hover:underline bg-transparent border-none text-left cursor-pointer outline-none"
|
||||
@click="openPlusLink"
|
||||
>
|
||||
<span>
|
||||
Support creators and Modrinth ad-free with
|
||||
<span class="font-bold">Modrinth+</span>
|
||||
</span>
|
||||
<ChevronRightIcon class="relative top-[3px] h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -88,8 +88,6 @@ import { loading_listener } from '@/helpers/events.js'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import { MaximizeIcon, MinimizeIcon } from '@/assets/icons/index.js'
|
||||
import { TauriEvent } from '@tauri-apps/api/event'
|
||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { getOS } from '@/helpers/utils.js'
|
||||
import { useLoading } from '@/store/loading.js'
|
||||
|
||||
@@ -130,9 +128,16 @@ const os = ref('')
|
||||
getOS().then((x) => (os.value = x))
|
||||
|
||||
loading_listener(async (e) => {
|
||||
console.log(e)
|
||||
if (e.event.type === 'directory_move') {
|
||||
loadingProgress.value = 100 * (e.fraction ?? 1)
|
||||
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') {
|
||||
loadingProgress.value = 100 * (e.fraction ?? 1)
|
||||
message.value = 'Checking for updates...'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup>
|
||||
import { Modal, Button } from '@modrinth/ui'
|
||||
import { Button } from '@modrinth/ui'
|
||||
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'
|
||||
|
||||
const confirmModal = ref(null)
|
||||
const project = ref(null)
|
||||
@@ -41,7 +42,7 @@ async function install() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal ref="confirmModal" :header="`Install ${project?.title}`">
|
||||
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`">
|
||||
<div class="modal-body">
|
||||
<SearchCard
|
||||
:project="project"
|
||||
@@ -60,7 +61,7 @@ async function install() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<Modal
|
||||
ref="incompatibleModal"
|
||||
header="Incompatibility warning"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
:on-hide="onInstall"
|
||||
>
|
||||
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
|
||||
@@ -51,20 +46,19 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { XIcon, DownloadIcon } from '@modrinth/assets'
|
||||
import { Button, Modal, DropdownSelect } from '@modrinth/ui'
|
||||
import { Button, DropdownSelect } from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
||||
import { ref } from 'vue'
|
||||
import { handleError, useTheming } from '@/store/state.js'
|
||||
import { handleError } from '@/store/state.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const instance = ref(null)
|
||||
const project = ref(null)
|
||||
const versions = ref(null)
|
||||
@@ -72,7 +66,7 @@ const selectedVersion = ref(null)
|
||||
const incompatibleModal = ref(null)
|
||||
const installing = ref(false)
|
||||
|
||||
let onInstall = ref(() => {})
|
||||
const onInstall = ref(() => {})
|
||||
|
||||
defineExpose({
|
||||
show: (instanceVal, projectVal, projectVersions, callback) => {
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<script setup>
|
||||
import { XIcon, DownloadIcon } from '@modrinth/assets'
|
||||
import { Button, Modal } from '@modrinth/ui'
|
||||
import { Button } from '@modrinth/ui'
|
||||
import { install as pack_install } from '@/helpers/pack'
|
||||
import { ref } from 'vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { handleError } from '@/store/state.js'
|
||||
|
||||
const themeStore = useTheming()
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const versionId = ref()
|
||||
const project = ref()
|
||||
const confirmModal = ref(null)
|
||||
const installing = ref(false)
|
||||
|
||||
let onInstall = ref(() => {})
|
||||
const onInstall = ref(() => {})
|
||||
|
||||
defineExpose({
|
||||
show: (projectVal, versionIdVal, callback) => {
|
||||
@@ -52,12 +50,7 @@ async function install() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
ref="confirmModal"
|
||||
header="Are you sure?"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
:on-hide="onInstall"
|
||||
>
|
||||
<ModalWrapper ref="confirmModal" header="Are you sure?" :on-hide="onInstall">
|
||||
<div class="modal-body">
|
||||
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
|
||||
<div class="input-group push-right">
|
||||
@@ -67,7 +60,7 @@ async function install() {
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
RightArrowIcon,
|
||||
CheckIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Modal, Button, Card } from '@modrinth/ui'
|
||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
add_project_from_version as installMod,
|
||||
@@ -19,12 +19,11 @@ import {
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { installVersionDependencies } from '@/store/install.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const router = useRouter()
|
||||
|
||||
const versions = ref()
|
||||
@@ -49,7 +48,7 @@ const shownProfiles = computed(() =>
|
||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
})
|
||||
.filter((profile) => {
|
||||
let loaders = versions.value.flatMap((v) => v.loaders)
|
||||
const loaders = versions.value.flatMap((v) => v.loaders)
|
||||
|
||||
return (
|
||||
versions.value.flatMap((v) => v.game_versions).includes(profile.game_version) &&
|
||||
@@ -60,7 +59,7 @@ const shownProfiles = computed(() =>
|
||||
}),
|
||||
)
|
||||
|
||||
let onInstall = ref(() => {})
|
||||
const onInstall = ref(() => {})
|
||||
|
||||
defineExpose({
|
||||
show: async (projectVal, versionsVal, callback) => {
|
||||
@@ -78,7 +77,7 @@ defineExpose({
|
||||
onInstall.value = callback
|
||||
|
||||
const profilesVal = await list().catch(handleError)
|
||||
for (let profile of profilesVal) {
|
||||
for (const profile of profilesVal) {
|
||||
profile.installing = false
|
||||
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
|
||||
handleError,
|
||||
@@ -142,7 +141,7 @@ const toggleCreation = () => {
|
||||
}
|
||||
|
||||
const upload_icon = async () => {
|
||||
icon.value = await open({
|
||||
const res = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
@@ -151,6 +150,7 @@ const upload_icon = async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
icon.value = res.path ?? res
|
||||
|
||||
if (!icon.value) return
|
||||
display_icon.value = convertFileSrc(icon.value)
|
||||
@@ -213,12 +213,7 @@ const createInstance = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
ref="installModal"
|
||||
header="Install project to instance"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
:on-hide="onInstall"
|
||||
>
|
||||
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall">
|
||||
<div class="modal-body">
|
||||
<input
|
||||
v-model="searchFilter"
|
||||
@@ -235,7 +230,7 @@ const createInstance = async () => {
|
||||
@click="installModal.hide()"
|
||||
>
|
||||
<Avatar
|
||||
:src="profile.icon_path ? tauri.convertFileSrc(profile.icon_path) : null"
|
||||
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
|
||||
class="profile-image"
|
||||
/>
|
||||
{{ profile.name }}
|
||||
@@ -304,7 +299,7 @@ const createInstance = async () => {
|
||||
<Button @click="installModal.hide()">Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
defineProps({
|
||||
confirmationText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasToType: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'No title defined',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'No description defined',
|
||||
required: true,
|
||||
},
|
||||
proceedLabel: {
|
||||
type: String,
|
||||
default: 'Proceed',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['proceed'])
|
||||
const modal = ref(null)
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
hide_ads_window()
|
||||
modal.value.show()
|
||||
},
|
||||
hide: () => {
|
||||
onModalHide()
|
||||
modal.value.hide()
|
||||
},
|
||||
})
|
||||
|
||||
function onModalHide() {
|
||||
show_ads_window()
|
||||
}
|
||||
|
||||
function proceed() {
|
||||
emit('proceed')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmModal
|
||||
ref="modal"
|
||||
:confirmation-text="confirmationText"
|
||||
:has-to-type="hasToType"
|
||||
:title="title"
|
||||
:description="description"
|
||||
:proceed-label="proceedLabel"
|
||||
:on-hide="onModalHide"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
@proceed="proceed"
|
||||
/>
|
||||
</template>
|
||||
49
apps/app-frontend/src/components/ui/modal/ModalWrapper.vue
Normal file
49
apps/app-frontend/src/components/ui/modal/ModalWrapper.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Modal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const props = defineProps({
|
||||
header: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
onHide: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const modal = ref(null)
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
hide_ads_window()
|
||||
modal.value.show()
|
||||
},
|
||||
hide: () => {
|
||||
onModalHide()
|
||||
modal.value.hide()
|
||||
},
|
||||
})
|
||||
|
||||
function onModalHide() {
|
||||
show_ads_window()
|
||||
props.onHide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal ref="modal" :header="header" :noblur="!themeStore.advancedRendering" @hide="onModalHide">
|
||||
<slot />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ShareModal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
defineProps({
|
||||
header: {
|
||||
type: String,
|
||||
default: 'Share',
|
||||
},
|
||||
shareTitle: {
|
||||
type: String,
|
||||
default: 'Modrinth',
|
||||
},
|
||||
shareText: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
link: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
openInNewTab: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const modal = ref(null)
|
||||
|
||||
defineExpose({
|
||||
show: (passedContent) => {
|
||||
hide_ads_window()
|
||||
modal.value.show(passedContent)
|
||||
},
|
||||
hide: () => {
|
||||
onModalHide()
|
||||
modal.value.hide()
|
||||
},
|
||||
})
|
||||
|
||||
function onModalHide() {
|
||||
show_ads_window()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ShareModal
|
||||
ref="modal"
|
||||
:header="header"
|
||||
:share-title="shareTitle"
|
||||
:share-text="shareText"
|
||||
:link="link"
|
||||
:open-in-new-tab="openInNewTab"
|
||||
:on-hide="onModalHide"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { UserIcon, LockIcon, MailIcon } from '@modrinth/assets'
|
||||
import { Button, Card, Checkbox, Modal } from '@modrinth/ui'
|
||||
import { Button, Card, Checkbox } from '@modrinth/ui'
|
||||
import {
|
||||
DiscordIcon,
|
||||
GithubIcon,
|
||||
@@ -13,6 +13,7 @@ import { login, login_2fa, create_account, login_pass } from '@/helpers/mr_auth.
|
||||
import { handleError, useNotifications } from '@/store/state.js'
|
||||
import { ref } from 'vue'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const props = defineProps({
|
||||
callback: {
|
||||
@@ -132,7 +133,7 @@ async function createAccount() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal ref="modal" :on-hide="removeWidget">
|
||||
<ModalWrapper ref="modal" :on-hide="removeWidget">
|
||||
<Card>
|
||||
<template v-if="twoFactorFlow">
|
||||
<h1>Enter two-factor code</h1>
|
||||
@@ -217,17 +218,17 @@ async function createAccount() {
|
||||
v-else-if="loggingIn"
|
||||
color="primary"
|
||||
large
|
||||
@click="signIn"
|
||||
:disabled="!turnstileToken"
|
||||
@click="signIn"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
<Button v-else color="primary" large @click="createAccount" :disabled="!turnstileToken">
|
||||
<Button v-else color="primary" large :disabled="!turnstileToken" @click="createAccount">
|
||||
Create account
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
21
apps/app-frontend/src/helpers/ads.js
Normal file
21
apps/app-frontend/src/helpers/ads.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export async function init_ads_window(x, y, width, height, overrideShown = false) {
|
||||
return await invoke('plugin:ads|init_ads_window', { x, y, width, height, overrideShown })
|
||||
}
|
||||
|
||||
export async function show_ads_window() {
|
||||
return await invoke('plugin:ads|show_ads_window')
|
||||
}
|
||||
|
||||
export async function hide_ads_window(reset) {
|
||||
return await invoke('plugin:ads|hide_ads_window', { reset })
|
||||
}
|
||||
|
||||
export async function record_ads_click() {
|
||||
return await invoke('plugin:ads|record_ads_click')
|
||||
}
|
||||
|
||||
export async function open_ads_link(path, origin) {
|
||||
return await invoke('plugin:ads|open_link', { path, origin })
|
||||
}
|
||||
@@ -33,6 +33,10 @@ export async function highlightModInProfile(profilePath, projectPath) {
|
||||
return await highlightInFolder(fullPath)
|
||||
}
|
||||
|
||||
export async function restartApp() {
|
||||
return await invoke('restart_app')
|
||||
}
|
||||
|
||||
export const releaseColor = (releaseType) => {
|
||||
switch (releaseType) {
|
||||
case 'release':
|
||||
@@ -52,7 +56,7 @@ export function debounce(fn, wait) {
|
||||
if (timer) {
|
||||
clearTimeout(timer) // clear any pre-existing timer
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
|
||||
const context = this // get the current context
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(context, args) // call the function if time expires
|
||||
|
||||
@@ -381,20 +381,20 @@ const sortedCategories = computed(() => {
|
||||
// identifier[0], then if it ties, identifier[1], etc
|
||||
async function sortByNameOrNumber(sortable, identifiers) {
|
||||
sortable.sort((a, b) => {
|
||||
for (let identifier of identifiers) {
|
||||
let aNum = parseFloat(a[identifier])
|
||||
let bNum = parseFloat(b[identifier])
|
||||
for (const identifier of identifiers) {
|
||||
const aNum = parseFloat(a[identifier])
|
||||
const bNum = parseFloat(b[identifier])
|
||||
if (isNaN(aNum) && isNaN(bNum)) {
|
||||
// Both are strings, sort alphabetically
|
||||
let stringComp = a[identifier].localeCompare(b[identifier])
|
||||
const stringComp = a[identifier].localeCompare(b[identifier])
|
||||
if (stringComp != 0) return stringComp
|
||||
} else if (!isNaN(aNum) && !isNaN(bNum)) {
|
||||
// Both are numbers, sort numerically
|
||||
let numComp = aNum - bNum
|
||||
const numComp = aNum - bNum
|
||||
if (numComp != 0) return numComp
|
||||
} else {
|
||||
// One is a number and one is a string, numbers go first
|
||||
let numStringComp = isNaN(aNum) ? 1 : -1
|
||||
const numStringComp = isNaN(aNum) ? 1 : -1
|
||||
if (numStringComp != 0) return numStringComp
|
||||
}
|
||||
}
|
||||
@@ -528,7 +528,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
|
||||
|
||||
<template>
|
||||
<div ref="searchWrapper" class="search-container">
|
||||
<aside class="filter-panel">
|
||||
<aside class="filter-panel" @scroll="$refs.promo.scroll()">
|
||||
<PromotionWrapper ref="promo" />
|
||||
<Card v-if="instanceContext" class="small-instance">
|
||||
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
|
||||
<Avatar
|
||||
@@ -675,8 +676,7 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
|
||||
</Card>
|
||||
</aside>
|
||||
<div class="search">
|
||||
<PromotionWrapper class="mt-4" />
|
||||
<Card class="project-type-container">
|
||||
<Card class="project-type-container mt-4">
|
||||
<NavRow :links="selectableProjectTypes" />
|
||||
</Card>
|
||||
<Card class="search-panel-container">
|
||||
@@ -878,13 +878,13 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
|
||||
|
||||
.filter-panel {
|
||||
position: fixed;
|
||||
width: 20rem;
|
||||
padding: 1rem 0.5rem 1rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: fit-content;
|
||||
min-height: calc(100vh - 3.25rem);
|
||||
max-height: calc(100vh - 3.25rem);
|
||||
width: 20rem;
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
@@ -903,8 +903,8 @@ const isModProject = computed(() => ['modpack', 'mod'].includes(projectType.valu
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 0 1rem 0.5rem 20.5rem;
|
||||
width: calc(100% - 20.5rem);
|
||||
margin: 0 1rem 0.5rem calc(20rem + 1rem);
|
||||
width: calc(100% - calc(20rem + 1rem));
|
||||
|
||||
.offline {
|
||||
margin: 1rem;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onUnmounted, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import RowDisplay from '@/components/RowDisplay.vue'
|
||||
import { list } from '@/helpers/profile.js'
|
||||
@@ -8,6 +8,11 @@ import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import dayjs from 'dayjs'
|
||||
import { get_search_results } from '@/helpers/cache.js'
|
||||
import { hide_ads_window } from '@/helpers/ads.js'
|
||||
|
||||
onMounted(() => {
|
||||
hide_ads_window(true)
|
||||
})
|
||||
|
||||
const featuredModpacks = ref({})
|
||||
const featuredMods = ref({})
|
||||
@@ -42,7 +47,7 @@ const getInstances = async () => {
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
let filters = []
|
||||
const filters = []
|
||||
for (const instance of recentInstances.value) {
|
||||
if (instance.linked_data && instance.linked_data.project_id) {
|
||||
filters.push(`NOT"project_id"="${instance.linked_data.project_id}"`)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { onUnmounted, ref, shallowRef } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
import { list } from '@/helpers/profile.js'
|
||||
import { useRoute } from 'vue-router'
|
||||
@@ -10,6 +10,11 @@ import { Button } from '@modrinth/ui'
|
||||
import { PlusIcon } from '@modrinth/assets'
|
||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||
import { NewInstanceImage } from '@/assets/icons'
|
||||
import { hide_ads_window } from '@/helpers/ads.js'
|
||||
|
||||
onMounted(() => {
|
||||
hide_ads_window(true)
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { LogOutIcon, LogInIcon, BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { Card, Slider, DropdownSelect, Toggle, ConfirmModal, Button } from '@modrinth/ui'
|
||||
import { Card, Slider, DropdownSelect, Toggle, Button } from '@modrinth/ui'
|
||||
import { handleError, useTheming } from '@/store/state'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get_java_versions, get_max_memory, set_java_version } from '@/helpers/jre'
|
||||
@@ -13,6 +13,12 @@ import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { getOS } from '@/helpers/utils.js'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { get_user, purge_cache_types } from '@/helpers/cache.js'
|
||||
import { hide_ads_window } from '@/helpers/ads.js'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
|
||||
onMounted(() => {
|
||||
hide_ads_window(true)
|
||||
})
|
||||
|
||||
const pageOptions = ['Home', 'Library']
|
||||
|
||||
@@ -169,13 +175,12 @@ async function purgeCache() {
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
<ConfirmModalWrapper
|
||||
ref="purgeCacheConfirmModal"
|
||||
title="Are you sure you want to purge the cache?"
|
||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||
:has-to-type="false"
|
||||
proceed-label="Purge cache"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
@proceed="purgeCache"
|
||||
/>
|
||||
<div class="adjacent-input">
|
||||
@@ -358,6 +363,25 @@ async function purgeCache() {
|
||||
<span class="label__title size-card-header">Privacy</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="opt-out-analytics">
|
||||
<span class="label__title">Personalized ads</span>
|
||||
<span class="label__description">
|
||||
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.
|
||||
</span>
|
||||
</label>
|
||||
<Toggle
|
||||
id="opt-out-analytics"
|
||||
:model-value="settings.personalized_ads"
|
||||
:checked="settings.personalized_ads"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.personalized_ads = e
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label for="opt-out-analytics">
|
||||
<span class="label__title">Telemetry</span>
|
||||
@@ -401,14 +425,14 @@ async function purgeCache() {
|
||||
<span class="label__title size-card-header">Java settings</span>
|
||||
</h3>
|
||||
</div>
|
||||
<template v-for="version in [21, 17, 8]">
|
||||
<label :for="'java-' + version">
|
||||
<span class="label__title">Java {{ version }} location</span>
|
||||
<template v-for="javaVersion in [21, 17, 8]" :key="`java-${javaVersion}`">
|
||||
<label :for="'java-' + javaVersion">
|
||||
<span class="label__title">Java {{ javaVersion }} location</span>
|
||||
</label>
|
||||
<JavaSelector
|
||||
:id="'java-selector-' + version"
|
||||
v-model="javaVersions[version]"
|
||||
:version="version"
|
||||
:id="'java-selector-' + javaVersion"
|
||||
v-model="javaVersions[javaVersion]"
|
||||
:version="javaVersion"
|
||||
@update:model-value="updateJavaVersion"
|
||||
/>
|
||||
</template>
|
||||
@@ -449,7 +473,7 @@ async function purgeCache() {
|
||||
:min="8"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
unit="mb"
|
||||
unit="MB"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="instance-container">
|
||||
<div class="side-cards">
|
||||
<div class="side-cards pb-4" @scroll="$refs.promo.scroll()">
|
||||
<Card class="instance-card" @contextmenu.prevent.stop="handleRightClick">
|
||||
<Avatar size="lg" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
|
||||
<Avatar size="md" :src="instance.icon_path ? convertFileSrc(instance.icon_path) : null" />
|
||||
<div class="instance-info">
|
||||
<h2 class="name">{{ instance.name }}</h2>
|
||||
<span class="metadata"> {{ instance.loader }} {{ instance.game_version }} </span>
|
||||
@@ -61,9 +61,9 @@
|
||||
</RouterLink>
|
||||
</div>
|
||||
</Card>
|
||||
<PromotionWrapper ref="promo" class="mt-4" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<PromotionWrapper />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
|
||||
@@ -311,7 +311,6 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 17rem;
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -325,12 +324,13 @@ Button {
|
||||
}
|
||||
|
||||
.side-cards {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
min-height: calc(100% - 3.25rem);
|
||||
max-height: calc(100% - 3.25rem);
|
||||
|
||||
min-height: calc(100vh - 3.25rem);
|
||||
max-height: calc(100vh - 3.25rem);
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
@@ -374,10 +374,7 @@ Button {
|
||||
overflow: auto;
|
||||
gap: 1rem;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: 19rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.instance-info {
|
||||
@@ -451,10 +448,10 @@ Button {
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
margin: 0 1rem 0.5rem 20rem;
|
||||
width: calc(100% - 20rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem 1rem 0 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
</div>
|
||||
</RecycleScroller>
|
||||
</div>
|
||||
<ShareModal
|
||||
<ShareModalWrapper
|
||||
ref="shareModal"
|
||||
header="Share Log"
|
||||
share-title="Instance Log"
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
<script setup>
|
||||
import { CheckIcon, ClipboardCopyIcon, ShareIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { Button, Card, ShareModal, Checkbox, DropdownSelect } from '@modrinth/ui'
|
||||
import { Button, Card, Checkbox, DropdownSelect } from '@modrinth/ui'
|
||||
import {
|
||||
delete_logs_by_filename,
|
||||
get_logs,
|
||||
@@ -107,6 +107,7 @@ import { handleError } from '@/store/notifications.js'
|
||||
import { ofetch } from 'ofetch'
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
|
||||
dayjs.extend(isToday)
|
||||
dayjs.extend(isYesterday)
|
||||
@@ -295,7 +296,7 @@ if (logs.value.length > 1 && !props.playing) {
|
||||
|
||||
const deleteLog = async () => {
|
||||
if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) {
|
||||
let deleteIndex = selectedLogIndex.value
|
||||
const deleteIndex = selectedLogIndex.value
|
||||
selectedLogIndex.value = deleteIndex - 1
|
||||
await delete_logs_by_filename(
|
||||
props.instance.path,
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
:link-function="(page) => `?page=${page}`"
|
||||
@switch-page="switchPage"
|
||||
/>
|
||||
<Modal ref="deleteWarning" header="Are you sure?">
|
||||
<ModalWrapper ref="deleteWarning" header="Are you sure?">
|
||||
<div class="modal-body">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
@@ -302,8 +302,8 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal ref="deleteDisabledWarning" header="Are you sure?">
|
||||
</ModalWrapper>
|
||||
<ModalWrapper ref="deleteDisabledWarning" header="Are you sure?">
|
||||
<div class="modal-body">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
@@ -325,8 +325,8 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ShareModal
|
||||
</ModalWrapper>
|
||||
<ShareModalWrapper
|
||||
ref="shareModal"
|
||||
share-title="Sharing modpack content"
|
||||
share-text="Check out the projects I'm using in my modpack!"
|
||||
@@ -360,8 +360,6 @@ import {
|
||||
import {
|
||||
Pagination,
|
||||
DropdownSelect,
|
||||
ShareModal,
|
||||
Modal,
|
||||
Checkbox,
|
||||
AnimatedLogo,
|
||||
Avatar,
|
||||
@@ -380,7 +378,6 @@ import {
|
||||
} from '@/helpers/profile.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { highlightModInProfile } from '@/helpers/utils.js'
|
||||
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage, PackageIcon } from '@/assets/icons'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
@@ -393,6 +390,9 @@ import {
|
||||
get_version_many,
|
||||
} from '@/helpers/cache.js'
|
||||
import { profile_listener } from '@/helpers/events.js'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
@@ -717,7 +717,7 @@ const updateProject = async (mod) => {
|
||||
})
|
||||
}
|
||||
|
||||
let locks = {}
|
||||
const locks = {}
|
||||
|
||||
const toggleDisableMod = async (mod) => {
|
||||
// Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property.
|
||||
@@ -725,7 +725,7 @@ const toggleDisableMod = async (mod) => {
|
||||
locks[mod.id] = ref(null)
|
||||
}
|
||||
|
||||
let lock = locks[mod.id]
|
||||
const lock = locks[mod.id]
|
||||
|
||||
while (lock.value) {
|
||||
await lock.value
|
||||
@@ -784,6 +784,7 @@ const deleteDisabled = async () => {
|
||||
}
|
||||
|
||||
const shareNames = async () => {
|
||||
console.log(functionValues.value)
|
||||
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
|
||||
}
|
||||
|
||||
@@ -878,8 +879,10 @@ async function refreshProjects() {
|
||||
refreshingProjects.value = false
|
||||
}
|
||||
|
||||
const unlisten = await listen('tauri://file-drop', async (event) => {
|
||||
for (const file of event.payload) {
|
||||
const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
|
||||
if (event.payload.type !== 'drop') return
|
||||
|
||||
for (const file of event.payload.paths) {
|
||||
if (file.endsWith('.mrpack')) continue
|
||||
await add_project_from_path(props.instance.path, file).catch(handleError)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
<ConfirmModalWrapper
|
||||
ref="modal_confirm"
|
||||
title="Are you sure you want to delete this instance?"
|
||||
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
|
||||
:has-to-type="false"
|
||||
proceed-label="Delete"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
@proceed="removeProfile"
|
||||
/>
|
||||
<Modal
|
||||
ref="modalConfirmUnlock"
|
||||
header="Are you sure you want to unlock this instance?"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
>
|
||||
<ModalWrapper ref="modalConfirmUnlock" header="Are you sure you want to unlock this instance?">
|
||||
<div class="modal-delete">
|
||||
<div
|
||||
class="markdown-body"
|
||||
@@ -31,13 +26,9 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
|
||||
<Modal
|
||||
ref="modalConfirmUnpair"
|
||||
header="Are you sure you want to unpair this instance?"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
>
|
||||
<ModalWrapper ref="modalConfirmUnpair" header="Are you sure you want to unpair this instance?">
|
||||
<div class="modal-delete">
|
||||
<div
|
||||
class="markdown-body"
|
||||
@@ -56,13 +47,9 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
|
||||
<Modal
|
||||
ref="changeVersionsModal"
|
||||
header="Change instance versions"
|
||||
:noblur="!themeStore.advancedRendering"
|
||||
>
|
||||
<ModalWrapper ref="changeVersionsModal" header="Change instance versions">
|
||||
<div class="change-versions-modal universal-body">
|
||||
<div class="input-row">
|
||||
<p class="input-label">Loader</p>
|
||||
@@ -106,7 +93,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalWrapper>
|
||||
<section class="card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
@@ -511,18 +498,7 @@ import {
|
||||
DownloadIcon,
|
||||
ClipboardCopyIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
Toggle,
|
||||
ConfirmModal,
|
||||
Card,
|
||||
Slider,
|
||||
Checkbox,
|
||||
Avatar,
|
||||
Modal,
|
||||
Chips,
|
||||
DropdownSelect,
|
||||
} from '@modrinth/ui'
|
||||
import { Button, Toggle, Card, Slider, Checkbox, Avatar, Chips, DropdownSelect } from '@modrinth/ui'
|
||||
import { SwapIcon } from '@/assets/icons'
|
||||
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
@@ -546,10 +522,11 @@ import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { get_loader_versions } from '@/helpers/metadata.js'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
@@ -570,8 +547,6 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const title = ref(props.instance.name)
|
||||
const icon = ref(props.instance.icon_path)
|
||||
const groups = ref(props.instance.groups)
|
||||
@@ -606,7 +581,7 @@ async function setIcon() {
|
||||
|
||||
if (!value) return
|
||||
|
||||
icon.value = value
|
||||
icon.value = value.path ?? value
|
||||
await edit_icon(props.instance.path, icon.value).catch(handleError)
|
||||
|
||||
trackEvent('InstanceSetIcon')
|
||||
@@ -621,12 +596,12 @@ const overrideJavaInstall = ref(!!props.instance.java_path)
|
||||
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
||||
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
|
||||
|
||||
const overrideJavaArgs = ref(!!props.instance.extra_launch_args)
|
||||
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
|
||||
const javaArgs = ref(
|
||||
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
||||
)
|
||||
|
||||
const overrideEnvVars = ref(!!props.instance.custom_env_vars)
|
||||
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
|
||||
const envVars = ref(
|
||||
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
||||
.map((x) => x.join('='))
|
||||
@@ -710,19 +685,15 @@ const editProfileObject = computed(() => {
|
||||
}
|
||||
|
||||
if (overrideJavaArgs.value) {
|
||||
if (javaArgs.value !== '') {
|
||||
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
|
||||
if (overrideEnvVars.value) {
|
||||
if (envVars.value !== '') {
|
||||
editProfile.custom_env_vars = envVars.value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
}
|
||||
editProfile.custom_env_vars = envVars.value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
}
|
||||
|
||||
if (overrideMemorySettings.value) {
|
||||
@@ -905,7 +876,7 @@ const editing = ref(false)
|
||||
async function saveGvLoaderEdits() {
|
||||
editing.value = true
|
||||
|
||||
let editProfile = editProfileObject.value
|
||||
const editProfile = editProfileObject.value
|
||||
editProfile.loader = loader.value
|
||||
editProfile.game_version = gameVersion.value
|
||||
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
</span>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="expandedGalleryItem = null">
|
||||
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="hideImage">
|
||||
<div class="content">
|
||||
<img
|
||||
class="image"
|
||||
:class="{ 'zoomed-in': zoomedIn }"
|
||||
:src="
|
||||
expandedGalleryItem.url
|
||||
? expandedGalleryItem.url
|
||||
expandedGalleryItem.raw_url
|
||||
? expandedGalleryItem.raw_url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
|
||||
@@ -45,15 +45,15 @@
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<Button class="close" icon-only @click="expandedGalleryItem = null">
|
||||
<Button class="close" icon-only @click="hideImage">
|
||||
<XIcon aria-hidden="true" />
|
||||
</Button>
|
||||
<a
|
||||
class="open btn icon-only"
|
||||
target="_blank"
|
||||
:href="
|
||||
expandedGalleryItem.url
|
||||
? expandedGalleryItem.url
|
||||
expandedGalleryItem.raw_url
|
||||
? expandedGalleryItem.raw_url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
>
|
||||
@@ -94,6 +94,7 @@ import {
|
||||
import { Button, Card } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
@@ -102,9 +103,14 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
let expandedGalleryItem = ref(null)
|
||||
let expandedGalleryIndex = ref(0)
|
||||
let zoomedIn = ref(false)
|
||||
const expandedGalleryItem = ref(null)
|
||||
const expandedGalleryIndex = ref(0)
|
||||
const zoomedIn = ref(false)
|
||||
|
||||
const hideImage = () => {
|
||||
expandedGalleryItem.value = null
|
||||
show_ads_window()
|
||||
}
|
||||
|
||||
const nextImage = () => {
|
||||
expandedGalleryIndex.value++
|
||||
@@ -131,6 +137,7 @@ const previousImage = () => {
|
||||
}
|
||||
|
||||
const expandImage = (item, index) => {
|
||||
hide_ads_window()
|
||||
expandedGalleryItem.value = item
|
||||
expandedGalleryIndex.value = index
|
||||
zoomedIn.value = false
|
||||
@@ -140,6 +147,20 @@ const expandImage = (item, index) => {
|
||||
url: item.url,
|
||||
})
|
||||
}
|
||||
|
||||
function keyListener(e) {
|
||||
if (expandedGalleryItem.value) {
|
||||
e.preventDefault()
|
||||
if (e.key === 'Escape') {
|
||||
hideImage()
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
previousImage()
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
nextImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('keypress', keyListener)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="root-container">
|
||||
<div v-if="data" class="project-sidebar">
|
||||
<div v-if="data" class="project-sidebar" @scroll="$refs.promo.scroll()">
|
||||
<Card v-if="instance" class="small-instance">
|
||||
<router-link class="instance" :to="`/instance/${encodeURIComponent(instance.path)}`">
|
||||
<Avatar
|
||||
@@ -20,7 +20,7 @@
|
||||
</router-link>
|
||||
</Card>
|
||||
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
|
||||
<Avatar size="lg" :src="data.icon_url" />
|
||||
<Avatar size="md" :src="data.icon_url" />
|
||||
<div class="instance-info">
|
||||
<h2 class="name">{{ data.title }}</h2>
|
||||
{{ data.description }}
|
||||
@@ -61,7 +61,9 @@
|
||||
Site
|
||||
</a>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
</Card>
|
||||
<PromotionWrapper ref="promo" />
|
||||
<Card class="sidebar-card">
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
@@ -163,7 +165,6 @@
|
||||
</Card>
|
||||
</div>
|
||||
<div v-if="data" class="content-container">
|
||||
<PromotionWrapper />
|
||||
<Card class="tabs">
|
||||
<NavRow
|
||||
v-if="data.gallery.length > 0"
|
||||
@@ -231,15 +232,7 @@ import {
|
||||
GlobeIcon,
|
||||
ClipboardCopyIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Categories,
|
||||
EnvironmentIndicator,
|
||||
Card,
|
||||
Avatar,
|
||||
Button,
|
||||
Promotion,
|
||||
NavRow,
|
||||
} from '@modrinth/ui'
|
||||
import { Categories, EnvironmentIndicator, Card, Avatar, Button, NavRow } from '@modrinth/ui'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import {
|
||||
BuyMeACoffeeIcon,
|
||||
@@ -260,7 +253,7 @@ import { handleError } from '@/store/notifications.js'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
import { get_project, get_project_many, get_team, get_version_many } from '@/helpers/cache.js'
|
||||
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
|
||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
@@ -309,11 +302,13 @@ async function fetchProjectData() {
|
||||
|
||||
await fetchProjectData()
|
||||
|
||||
const promo = ref(null)
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async () => {
|
||||
if (route.params.id && route.path.startsWith('/project')) {
|
||||
await fetchProjectData()
|
||||
promo.value.scroll()
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -377,7 +372,7 @@ const handleOptionsClick = (args) => {
|
||||
|
||||
.project-sidebar {
|
||||
position: fixed;
|
||||
width: 20rem;
|
||||
width: calc(300px + 1.5rem);
|
||||
min-height: calc(100vh - 3.25rem);
|
||||
height: fit-content;
|
||||
max-height: calc(100vh - 3.25rem);
|
||||
@@ -403,7 +398,7 @@ const handleOptionsClick = (args) => {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin-left: 19.5rem;
|
||||
margin-left: calc(300px + 1rem);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
|
||||
24
apps/app-frontend/tsconfig.app.json
Normal file
24
apps/app-frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
"strict": true,
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"target": "ESNext"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
17
apps/app-frontend/tsconfig.node.json
Normal file
17
apps/app-frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -42,8 +42,8 @@ export default defineConfig({
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
},
|
||||
// to make use of `TAURI_DEBUG` and other env variables
|
||||
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||
// to make use of `TAURI_ENV_DEBUG` and other env variables
|
||||
// https://v2.tauri.app/reference/environment-variables/#tauri-cli-hook-commands
|
||||
envPrefix: ['VITE_', 'TAURI_'],
|
||||
build: {
|
||||
// Tauri supports es2021
|
||||
|
||||
@@ -10,7 +10,6 @@ theseus = { path = "../../packages/app-lib", features = ["cli"] }
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = "2.0.0-rc.4"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
thiserror = "1.0"
|
||||
url = "2.2"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@modrinth/app-playground",
|
||||
"scripts": {
|
||||
"build": "cargo build --release",
|
||||
"lint": "cargo fmt --check && cargo clippy -- -D warnings",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix",
|
||||
"dev": "cargo run",
|
||||
"test": "cargo test"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
version = "0.8.3-1"
|
||||
version = "0.8.9"
|
||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/modrinth/code/apps/app/"
|
||||
@@ -16,18 +16,19 @@ theseus = { path = "../../packages/app-lib", features = ["tauri"] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
tauri = { version = "2.0.0-rc.6", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||
tauri = { version = "2.0.0-rc", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||
tauri-plugin-window-state = "2.0.0-rc"
|
||||
tauri-plugin-deep-link = "2.0.0-rc"
|
||||
tauri-plugin-os = "2.0.0-rc"
|
||||
tauri-plugin-shell = "2.0.0-rc"
|
||||
tauri-plugin-dialog = "2.0.0-rc"
|
||||
tauri-plugin-updater = { version = "2.0.0-rc.1", optional = true }
|
||||
tauri-plugin-updater = { version = "2.0.0-rc" }
|
||||
tauri-plugin-single-instance = { version = "2.0.0-rc" }
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
thiserror = "1.0"
|
||||
futures = "0.3"
|
||||
daedalus = "0.2.3"
|
||||
daedalus = { path = "../../packages/daedalus" }
|
||||
chrono = "0.4.26"
|
||||
|
||||
dirs = "5.0.1"
|
||||
@@ -57,6 +58,9 @@ cocoa = "0.25.0"
|
||||
objc = "0.2.7"
|
||||
rand = "0.8.5"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tauri-plugin-updater = { version = "2.0.0-rc", optional = true, features = ["native-tls-vendored", "zip"], default-features = false }
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
@@ -64,4 +68,4 @@ default = ["custom-protocol"]
|
||||
# this feature is used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
updater = ["dep:tauri-plugin-updater"]
|
||||
updater = []
|
||||
|
||||
@@ -15,7 +15,7 @@ Before you begin, ensure you have the following installed on your machine:
|
||||
- [Node.js](https://nodejs.org/en/)
|
||||
- [pnpm](https://pnpm.io/)
|
||||
- [Rust](https://www.rust-lang.org/tools/install)
|
||||
- [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites/#installing)
|
||||
- [Tauri](https://v2.tauri.app/start/prerequisites/)
|
||||
|
||||
### Setup
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ fn main() {
|
||||
InlinedPlugin::new()
|
||||
.commands(&[
|
||||
"get_java_versions",
|
||||
"set_java_versions",
|
||||
"set_java_version",
|
||||
"jre_find_filtered_jres",
|
||||
"jre_get_jre",
|
||||
"jre_test_jre",
|
||||
@@ -217,6 +217,22 @@ fn main() {
|
||||
.default_permission(
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
)
|
||||
.plugin(
|
||||
"ads",
|
||||
InlinedPlugin::new()
|
||||
.commands(&[
|
||||
"init_ads_window",
|
||||
"hide_ads_window",
|
||||
"scroll_ads_window",
|
||||
"show_ads_window",
|
||||
"record_ads_click",
|
||||
"open_link",
|
||||
"get_ads_personalization",
|
||||
])
|
||||
.default_permission(
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
),
|
||||
)
|
||||
.expect("Failed to run tauri-build");
|
||||
|
||||
14
apps/app/capabilities/ads.json
Normal file
14
apps/app/capabilities/ads.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"identifier": "ads",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"remote": {
|
||||
"urls": ["https://modrinth.com/*", "http://localhost:3000/*"]
|
||||
},
|
||||
"webviews": [
|
||||
"ads-window"
|
||||
],
|
||||
"permissions": [
|
||||
"ads:default"
|
||||
]
|
||||
}
|
||||
@@ -2,9 +2,7 @@
|
||||
"identifier": "plugins",
|
||||
"description": "",
|
||||
"local": true,
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-confirm",
|
||||
@@ -35,6 +33,7 @@
|
||||
"cache:default",
|
||||
"settings:default",
|
||||
"tags:default",
|
||||
"utils:default"
|
||||
"utils:default",
|
||||
"ads:default"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
11
apps/app/capabilities/updater.json
Normal file
11
apps/app/capabilities/updater.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"identifier": "updater",
|
||||
"description": "",
|
||||
"local": true,
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"updater:default"
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"core":{"identifier":"core","description":"","local":true,"windows":["main"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-maximize","core:window:allow-toggle-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-start-dragging","core:webview:allow-set-webview-zoom"]},"plugins":{"identifier":"plugins","description":"","local":true,"windows":["main"],"permissions":["dialog:allow-open","dialog:allow-confirm","shell:allow-open","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","deep-link:default","window-state:default","window-state:allow-restore-state","window-state:allow-save-window-state","auth:default","import:default","jre:default","logs:default","metadata:default","mr-auth:default","profile-create:default","pack:default","process:default","profile:default","cache:default","settings:default","tags:default","utils:default"]}}
|
||||
{"ads":{"identifier":"ads","description":"","remote":{"urls":["https://modrinth.com/*","http://localhost:3000/*"]},"local":false,"webviews":["ads-window"],"permissions":["ads:default"]},"core":{"identifier":"core","description":"","local":true,"windows":["main"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","core:window:allow-create","core:window:allow-maximize","core:window:allow-toggle-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-start-dragging","core:webview:allow-set-webview-zoom"]},"plugins":{"identifier":"plugins","description":"","local":true,"windows":["main"],"permissions":["dialog:allow-open","dialog:allow-confirm","shell:allow-open","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","deep-link:default","window-state:default","window-state:allow-restore-state","window-state:allow-save-window-state","auth:default","import:default","jre:default","logs:default","metadata:default","mr-auth:default","profile-create:default","pack:default","process:default","profile:default","cache:default","settings:default","tags:default","utils:default","ads:default"]},"updater":{"identifier":"updater","description":"","local":true,"windows":["main"],"permissions":["updater:default"]}}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -299,6 +299,69 @@
|
||||
},
|
||||
"Identifier": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "ads:default -> Default plugin permissions.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ads:default"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ads:allow-hide-ads-window -> Enables the hide_ads_window command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ads:allow-hide-ads-window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ads:allow-init-ads-window -> Enables the init_ads_window command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ads:allow-init-ads-window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ads:allow-scroll-ads-window -> Enables the scroll_ads_window command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ads:allow-scroll-ads-window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ads:allow-show-ads-window -> Enables the show_ads_window command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ads:allow-show-ads-window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ads:deny-hide-ads-window -> Denies the hide_ads_window command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ads:deny-hide-ads-window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ads:deny-init-ads-window -> Denies the init_ads_window command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ads:deny-init-ads-window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ads:deny-scroll-ads-window -> Denies the scroll_ads_window command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ads:deny-scroll-ads-window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "ads:deny-show-ads-window -> Denies the show_ads_window command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ads:deny-show-ads-window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "auth:default -> Default plugin permissions.",
|
||||
"type": "string",
|
||||
@@ -2785,10 +2848,10 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "jre:allow-set-java-versions -> Enables the set_java_versions command without any pre-configured scope.",
|
||||
"description": "jre:allow-set-java-version -> Enables the set_java_version command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"jre:allow-set-java-versions"
|
||||
"jre:allow-set-java-version"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2834,10 +2897,10 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "jre:deny-set-java-versions -> Denies the set_java_versions command without any pre-configured scope.",
|
||||
"description": "jre:deny-set-java-version -> Denies the set_java_version command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"jre:deny-set-java-versions"
|
||||
"jre:deny-set-java-version"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
3314
apps/app/gen/schemas/windows-schema.json
Normal file
3314
apps/app/gen/schemas/windows-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,346 +0,0 @@
|
||||
<?if $(sys.BUILDARCH)="x86"?>
|
||||
<?define Win64 = "no" ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||
<?elseif $(sys.BUILDARCH)="x64"?>
|
||||
<?define Win64 = "yes" ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||
<?else?>
|
||||
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
|
||||
<?endif?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product
|
||||
Id="*"
|
||||
Name="{{product_name}}"
|
||||
UpgradeCode="{{upgrade_code}}"
|
||||
Language="!(loc.TauriLanguage)"
|
||||
Manufacturer="{{manufacturer}}"
|
||||
Version="{{version}}">
|
||||
|
||||
<Package Id="*"
|
||||
Keywords="Installer"
|
||||
InstallerVersion="450"
|
||||
Languages="0"
|
||||
Compressed="yes"
|
||||
InstallScope="perMachine"
|
||||
SummaryCodepage="!(loc.TauriCodepage)"/>
|
||||
|
||||
<!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->
|
||||
<!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
|
||||
<Property Id="REINSTALLMODE" Value="amus" />
|
||||
|
||||
{{#if allow_downgrades}}
|
||||
<MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
|
||||
{{else}}
|
||||
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="!(loc.DowngradeErrorMessage)" AllowSameVersionUpgrades="yes" />
|
||||
{{/if}}
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
|
||||
|
||||
{{#if banner_path}}
|
||||
<WixVariable Id="WixUIBannerBmp" Value="{{banner_path}}" />
|
||||
{{/if}}
|
||||
{{#if dialog_image_path}}
|
||||
<WixVariable Id="WixUIDialogBmp" Value="{{dialog_image_path}}" />
|
||||
{{/if}}
|
||||
{{#if license}}
|
||||
<WixVariable Id="WixUILicenseRtf" Value="{{license}}" />
|
||||
{{/if}}
|
||||
|
||||
<Icon Id="ProductIcon" SourceFile="{{icon_path}}"/>
|
||||
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
|
||||
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" /> <!-- Remove repair -->
|
||||
<SetProperty Id="ARPNOMODIFY" Value="1" After="InstallValidate" Sequence="execute"/>
|
||||
|
||||
<!-- initialize with previous InstallDir -->
|
||||
<Property Id="INSTALLDIR">
|
||||
<RegistrySearch Id="PrevInstallDirReg" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw"/>
|
||||
</Property>
|
||||
|
||||
<!-- launch app checkbox -->
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
|
||||
<Property Id="WixShellExecTarget" Value="[!Path]" />
|
||||
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
|
||||
|
||||
<UI>
|
||||
<!-- launch app checkbox -->
|
||||
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
|
||||
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
|
||||
|
||||
{{#unless license}}
|
||||
<!-- Skip license dialog -->
|
||||
<Publish Dialog="WelcomeDlg"
|
||||
Control="Next"
|
||||
Event="NewDialog"
|
||||
Value="InstallDirDlg"
|
||||
Order="2">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg"
|
||||
Control="Back"
|
||||
Event="NewDialog"
|
||||
Value="WelcomeDlg"
|
||||
Order="2">1</Publish>
|
||||
{{/unless}}
|
||||
</UI>
|
||||
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="DesktopFolder" Name="Desktop">
|
||||
<Component Id="ApplicationShortcutDesktop" Guid="*">
|
||||
<Shortcut Id="ApplicationDesktopShortcut" Name="{{product_name}}" Description="Runs {{product_name}}" Target="[!Path]" WorkingDirectory="INSTALLDIR" />
|
||||
<RemoveFolder Id="DesktopFolder" On="uninstall" />
|
||||
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Desktop Shortcut" Type="integer" Value="1" KeyPath="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
|
||||
<Directory Id="INSTALLDIR" Name="{{product_name}}"/>
|
||||
</Directory>
|
||||
<Directory Id="ProgramMenuFolder">
|
||||
<Directory Id="ApplicationProgramsFolder" Name="{{product_name}}"/>
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<DirectoryRef Id="INSTALLDIR">
|
||||
<Component Id="RegistryEntries" Guid="*">
|
||||
<RegistryKey Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}">
|
||||
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLDIR]" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
|
||||
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
|
||||
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
|
||||
<!-- THESEUS -->
|
||||
<ProgId Id="theseus.mrpack.Document" Description="Modrinth File">
|
||||
<Extension Id="mrpack" ContentType="application/mrpack">
|
||||
<!-- no flags on argument, so we can hijack deep link library-->
|
||||
<Verb Id="open" Command="Open" TargetFile="Path" Argument=""%1"" />
|
||||
</Extension>
|
||||
</ProgId>
|
||||
<!-- /THESEUS -->
|
||||
</Component>
|
||||
{{#each binaries as |bin| ~}}
|
||||
<Component Id="{{ bin.id }}" Guid="{{bin.guid}}" Win64="$(var.Win64)">
|
||||
<File Id="Bin_{{ bin.id }}" Source="{{bin.path}}" KeyPath="yes"/>
|
||||
</Component>
|
||||
{{/each~}}
|
||||
{{#if enable_elevated_update_task}}
|
||||
<Component Id="UpdateTask" Guid="C492327D-9720-4CD5-8DB8-F09082AF44BE" Win64="$(var.Win64)">
|
||||
<File Id="UpdateTask" Source="update.xml" KeyPath="yes" Checksum="yes"/>
|
||||
</Component>
|
||||
<Component Id="UpdateTaskInstaller" Guid="011F25ED-9BE3-50A7-9E9B-3519ED2B9932" Win64="$(var.Win64)">
|
||||
<File Id="UpdateTaskInstaller" Source="install-task.ps1" KeyPath="yes" Checksum="yes"/>
|
||||
</Component>
|
||||
<Component Id="UpdateTaskUninstaller" Guid="D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1" Win64="$(var.Win64)">
|
||||
<File Id="UpdateTaskUninstaller" Source="uninstall-task.ps1" KeyPath="yes" Checksum="yes"/>
|
||||
</Component>
|
||||
{{/if}}
|
||||
{{resources}}
|
||||
<Component Id="CMP_UninstallShortcut" Guid="*">
|
||||
|
||||
<Shortcut Id="UninstallShortcut"
|
||||
Name="Uninstall {{product_name}}"
|
||||
Description="Uninstalls {{product_name}}"
|
||||
Target="[System64Folder]msiexec.exe"
|
||||
Arguments="/x [ProductCode]" />
|
||||
|
||||
<RemoveFolder Id="INSTALLDIR"
|
||||
On="uninstall" />
|
||||
|
||||
<RegistryValue Root="HKCU"
|
||||
Key="Software\\{{manufacturer}}\\{{product_name}}"
|
||||
Name="Uninstaller Shortcut"
|
||||
Type="integer"
|
||||
Value="1"
|
||||
KeyPath="yes" />
|
||||
</Component>
|
||||
|
||||
<!-- THESEUS -->
|
||||
<Component Id="FileTypeAssociationsReg" Guid="*">
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationDescription" Value="theseus" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationIcon" Value="[INSTALLDIR]theseus,0" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities" Name="ApplicationName" Value="theseus" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\DefaultIcon" Value="[INSTALLDIR]theseus,1" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\FileAssociations" Name=".mrpack" Value="theseus.mrpack.Document" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\MIMEAssociations" Name="application/mrpack" Value="theseus.mrpack.Document" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\modrinth\theseus\Capabilities\shell\Open\command" Value=""[INSTALLDIR]theseus" -e "%1"" Type="string" />
|
||||
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\RegisteredApplications" Name="theseus" Value="SOFTWARE\modrinth\theseus\Capabilities" Type="string" KeyPath="yes" />
|
||||
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\theseus.mrpack.Document" Name="MRPACK File" Value="Modrinth Modpack Installer" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack" Name="Content Type" Value="application/mrpack" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack\OpenWithList\theseus" Value="" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\.mrpack\OpenWithProgids" Name="theseus.mrpack.Document" Value="" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\Applications\mrpack\SupportedTypes" Name=".mrpack" Value="" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="SOFTWARE\Classes\Applications\mrpack\shell\open" Name="FriendlyAppName" Value="theseus" Type="string" />
|
||||
</Component>
|
||||
<!-- /THESEUS -->
|
||||
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="ApplicationProgramsFolder">
|
||||
<Component Id="ApplicationShortcut" Guid="*">
|
||||
<Shortcut Id="ApplicationStartMenuShortcut"
|
||||
Name="{{product_name}}"
|
||||
Description="Runs {{product_name}}"
|
||||
Target="[!Path]"
|
||||
Icon="ProductIcon"
|
||||
WorkingDirectory="INSTALLDIR">
|
||||
<ShortcutProperty Key="System.AppUserModel.ID" Value="{{bundle_id}}"/>
|
||||
</Shortcut>
|
||||
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
|
||||
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Start Menu Shortcut" Type="integer" Value="1" KeyPath="yes"/>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
{{#each merge_modules as |msm| ~}}
|
||||
<DirectoryRef Id="TARGETDIR">
|
||||
<Merge Id="{{ msm.name }}" SourceFile="{{ msm.path }}" DiskId="1" Language="!(loc.TauriLanguage)" />
|
||||
</DirectoryRef>
|
||||
|
||||
<Feature Id="{{ msm.name }}" Title="{{ msm.name }}" AllowAdvertise="no" Display="hidden" Level="1">
|
||||
<MergeRef Id="{{ msm.name }}"/>
|
||||
</Feature>
|
||||
{{/each~}}
|
||||
|
||||
<Feature
|
||||
Id="MainProgram"
|
||||
Title="Application"
|
||||
Description="!(loc.InstallAppFeature)"
|
||||
Level="1"
|
||||
ConfigurableDirectory="INSTALLDIR"
|
||||
AllowAdvertise="no"
|
||||
Display="expand"
|
||||
Absent="disallow">
|
||||
|
||||
<ComponentRef Id="RegistryEntries"/>
|
||||
|
||||
<!-- THESEUS -->
|
||||
<ComponentRef Id="FileTypeAssociationsReg" />
|
||||
<!-- /THESEUS -->
|
||||
|
||||
{{#each resource_file_ids as |resource_file_id| ~}}
|
||||
<ComponentRef Id="{{ resource_file_id }}"/>
|
||||
{{/each~}}
|
||||
|
||||
{{#if enable_elevated_update_task}}
|
||||
<ComponentRef Id="UpdateTask" />
|
||||
<ComponentRef Id="UpdateTaskInstaller" />
|
||||
<ComponentRef Id="UpdateTaskUninstaller" />
|
||||
{{/if}}
|
||||
|
||||
<Feature Id="ShortcutsFeature"
|
||||
Title="Shortcuts"
|
||||
Level="1">
|
||||
<ComponentRef Id="Path"/>
|
||||
<ComponentRef Id="CMP_UninstallShortcut" />
|
||||
<ComponentRef Id="ApplicationShortcut" />
|
||||
<ComponentRef Id="ApplicationShortcutDesktop" />
|
||||
</Feature>
|
||||
|
||||
<Feature
|
||||
Id="Environment"
|
||||
Title="PATH Environment Variable"
|
||||
Description="!(loc.PathEnvVarFeature)"
|
||||
Level="1"
|
||||
Absent="allow">
|
||||
<ComponentRef Id="Path"/>
|
||||
{{#each binaries as |bin| ~}}
|
||||
<ComponentRef Id="{{ bin.id }}"/>
|
||||
{{/each~}}
|
||||
</Feature>
|
||||
</Feature>
|
||||
|
||||
<Feature Id="External" AllowAdvertise="no" Absent="disallow">
|
||||
{{#each component_group_refs as |id| ~}}
|
||||
<ComponentGroupRef Id="{{ id }}"/>
|
||||
{{/each~}}
|
||||
{{#each component_refs as |id| ~}}
|
||||
<ComponentRef Id="{{ id }}"/>
|
||||
{{/each~}}
|
||||
{{#each feature_group_refs as |id| ~}}
|
||||
<FeatureGroupRef Id="{{ id }}"/>
|
||||
{{/each~}}
|
||||
{{#each feature_refs as |id| ~}}
|
||||
<FeatureRef Id="{{ id }}"/>
|
||||
{{/each~}}
|
||||
{{#each merge_refs as |id| ~}}
|
||||
<MergeRef Id="{{ id }}"/>
|
||||
{{/each~}}
|
||||
</Feature>
|
||||
|
||||
{{#if install_webview}}
|
||||
<!-- WebView2 -->
|
||||
<Property Id="WVRTINSTALLED">
|
||||
<RegistrySearch Id="WVRTInstalledSystem" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" Win64="no" />
|
||||
<RegistrySearch Id="WVRTInstalledUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
|
||||
</Property>
|
||||
|
||||
{{#if download_bootstrapper}}
|
||||
<CustomAction Id='DownloadAndInvokeBootstrapper' Directory="INSTALLDIR" Execute="deferred" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\{] [\[]Net.ServicePointManager[\]]::SecurityProtocol = [\[]Net.SecurityProtocolType[\]]::Tls12 [\}] catch [\{][\}]; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ({{webview_installer_args}} '/install') -Wait' Return='check'/>
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
|
||||
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
<!-- Embedded webview bootstrapper mode -->
|
||||
{{#if webview2_bootstrapper_path}}
|
||||
<Binary Id="MicrosoftEdgeWebview2Setup.exe" SourceFile="{{webview2_bootstrapper_path}}"/>
|
||||
<CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action='InvokeBootstrapper' Before='InstallFinalize'>
|
||||
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
<!-- Embedded offline installer -->
|
||||
{{#if webview2_installer_path}}
|
||||
<Binary Id="MicrosoftEdgeWebView2RuntimeInstaller.exe" SourceFile="{{webview2_installer_path}}"/>
|
||||
<CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action='InvokeStandalone' Before='InstallFinalize'>
|
||||
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
{{#if enable_elevated_update_task}}
|
||||
<!-- Install an elevated update task within Windows Task Scheduler -->
|
||||
<CustomAction
|
||||
Id="CreateUpdateTask"
|
||||
Return="check"
|
||||
Directory="INSTALLDIR"
|
||||
Execute="commit"
|
||||
Impersonate="yes"
|
||||
ExeCommand="powershell.exe -WindowStyle hidden .\install-task.ps1" />
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action='CreateUpdateTask' Before='InstallFinalize'>
|
||||
NOT(REMOVE)
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
<!-- Remove elevated update task during uninstall -->
|
||||
<CustomAction
|
||||
Id="DeleteUpdateTask"
|
||||
Return="check"
|
||||
Directory="INSTALLDIR"
|
||||
ExeCommand="powershell.exe -WindowStyle hidden .\uninstall-task.ps1" />
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="DeleteUpdateTask" Before='InstallFinalize'>
|
||||
(REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
|
||||
</Product>
|
||||
</Wix>
|
||||
41
apps/app/nsis/hooks.nsi
Normal file
41
apps/app/nsis/hooks.nsi
Normal file
@@ -0,0 +1,41 @@
|
||||
!macro NSIS_HOOK_POSTINSTALL
|
||||
SetShellVarContext current
|
||||
|
||||
IfFileExists "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe" file_found file_not_found
|
||||
file_found:
|
||||
Delete "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
|
||||
|
||||
Delete "$LOCALAPPDATA${PRODUCTNAME}\uninstall.exe"
|
||||
RMDir "$LOCALAPPDATA${PRODUCTNAME}"
|
||||
|
||||
!insertmacro DeleteAppUserModelId
|
||||
|
||||
; Remove start menu shortcut
|
||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
||||
!insertmacro IsShortcutTarget "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk"
|
||||
Delete "$SMPROGRAMS$AppStartMenuFolder${PRODUCTNAME}.lnk"
|
||||
RMDir "$SMPROGRAMS$AppStartMenuFolder"
|
||||
${EndIf}
|
||||
!insertmacro IsShortcutTarget "$SMPROGRAMS${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS${PRODUCTNAME}.lnk"
|
||||
Delete "$SMPROGRAMS${PRODUCTNAME}.lnk"
|
||||
${EndIf}
|
||||
|
||||
!insertmacro IsShortcutTarget "$DESKTOP${PRODUCTNAME}.lnk" "$LOCALAPPDATA${PRODUCTNAME}\theseus_gui.exe"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro UnpinShortcut "$DESKTOP${PRODUCTNAME}.lnk"
|
||||
Delete "$DESKTOP${PRODUCTNAME}.lnk"
|
||||
${EndIf}
|
||||
|
||||
DeleteRegKey HKCU "${UNINSTKEY}"
|
||||
|
||||
goto end_of_test ;<== important for not continuing on the else branch
|
||||
file_not_found:
|
||||
end_of_test:
|
||||
!macroend
|
||||
@@ -5,14 +5,15 @@
|
||||
"tauri": "tauri",
|
||||
"dev": "tauri dev",
|
||||
"test": "cargo test",
|
||||
"lint": "cargo fmt --check && cargo clippy -- -D warnings",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.0.0-rc.5"
|
||||
"@tauri-apps/cli": "2.0.0-rc.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/app-frontend": "workspace:*",
|
||||
"@modrinth/app-lib": "workspace:*"
|
||||
"@modrinth/app-lib": "workspace:*",
|
||||
"@modrinth/daedalus": "workspace:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
apps/app/src/api/ads-init.js
Normal file
23
apps/app/src/api/ads-init.js
Normal file
@@ -0,0 +1,23 @@
|
||||
document.addEventListener(
|
||||
'click',
|
||||
function (e) {
|
||||
window.top.postMessage({ modrinthAdClick: true }, 'https://modrinth.com')
|
||||
|
||||
let target = e.target
|
||||
while (target != null) {
|
||||
if (target.matches('a')) {
|
||||
e.preventDefault()
|
||||
if (target.href) {
|
||||
window.top.postMessage({ modrinthOpenUrl: target.href }, 'https://modrinth.com')
|
||||
}
|
||||
break
|
||||
}
|
||||
target = target.parentElement
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
window.open = (url, target, features) => {
|
||||
window.top.postMessage({ modrinthOpenUrl: url }, 'https://modrinth.com')
|
||||
}
|
||||
216
apps/app/src/api/ads.rs
Normal file
216
apps/app/src/api/ads.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
use serde::Serialize;
|
||||
use std::collections::HashSet;
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::{Emitter, LogicalPosition, LogicalSize, Manager, Runtime};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use theseus::settings;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub struct AdsState {
|
||||
pub shown: bool,
|
||||
pub size: Option<LogicalSize<f32>>,
|
||||
pub position: Option<LogicalPosition<f32>>,
|
||||
pub last_click: Option<Instant>,
|
||||
pub malicious_origins: HashSet<String>,
|
||||
}
|
||||
|
||||
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
tauri::plugin::Builder::<R>::new("ads")
|
||||
.setup(|app, _api| {
|
||||
app.manage(RwLock::new(AdsState {
|
||||
shown: true,
|
||||
size: None,
|
||||
position: None,
|
||||
last_click: None,
|
||||
malicious_origins: HashSet::new(),
|
||||
}));
|
||||
|
||||
// We refresh the ads window every 5 minutes for performance
|
||||
let app = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
if let Some(webview) = app.webviews().get_mut("ads-window")
|
||||
{
|
||||
let _ = webview.navigate(AD_LINK.parse().unwrap());
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60 * 5))
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
init_ads_window,
|
||||
hide_ads_window,
|
||||
scroll_ads_window,
|
||||
show_ads_window,
|
||||
record_ads_click,
|
||||
open_link,
|
||||
get_ads_personalization,
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub async fn init_ads_window<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
override_shown: bool,
|
||||
) -> crate::api::Result<()> {
|
||||
use tauri::WebviewUrl;
|
||||
const LINK_SCRIPT: &str = include_str!("ads-init.js");
|
||||
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
state.size = Some(LogicalSize::new(width, height));
|
||||
state.position = Some(LogicalPosition::new(x, y));
|
||||
|
||||
if override_shown {
|
||||
state.shown = true;
|
||||
}
|
||||
|
||||
if let Some(webview) = app.webviews().get("ads-window") {
|
||||
if state.shown {
|
||||
let _ = webview.set_position(LogicalPosition::new(x, y));
|
||||
let _ = webview.set_size(LogicalSize::new(width, height));
|
||||
}
|
||||
} else if let Some(window) = app.get_window("main") {
|
||||
let _ = window.add_child(
|
||||
tauri::webview::WebviewBuilder::new(
|
||||
"ads-window",
|
||||
WebviewUrl::External(
|
||||
AD_LINK.parse().unwrap(),
|
||||
),
|
||||
)
|
||||
.initialization_script(LINK_SCRIPT)
|
||||
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")
|
||||
.zoom_hotkeys_enabled(false)
|
||||
.transparent(true),
|
||||
if state.shown {
|
||||
LogicalPosition::new(x, y)
|
||||
} else {
|
||||
LogicalPosition::new(-1000.0, -1000.0)
|
||||
},
|
||||
LogicalSize::new(width, height),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: make ads work on linux
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn init_ads_window() {}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn show_ads_window<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> crate::api::Result<()> {
|
||||
if let Some(webview) = app.webviews().get("ads-window") {
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
|
||||
state.shown = true;
|
||||
if let Some(size) = state.size {
|
||||
let _ = webview.set_size(size);
|
||||
}
|
||||
|
||||
if let Some(position) = state.position {
|
||||
let _ = webview.set_position(position);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn hide_ads_window<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
reset: Option<bool>,
|
||||
) -> crate::api::Result<()> {
|
||||
if let Some(webview) = app.webviews().get("ads-window") {
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
state.shown = false;
|
||||
|
||||
if reset.unwrap_or(false) {
|
||||
state.size = None;
|
||||
state.position = None;
|
||||
}
|
||||
|
||||
let _ = webview.set_position(LogicalPosition::new(-1000, -1000));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
struct ScrollEvent {
|
||||
scroll: f32,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn scroll_ads_window<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
scroll: f32,
|
||||
) -> crate::api::Result<()> {
|
||||
let _ = app.emit("ads-scroll", ScrollEvent { scroll });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn record_ads_click<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> crate::api::Result<()> {
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
|
||||
let mut state = state.write().await;
|
||||
state.last_click = Some(Instant::now());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_link<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
path: String,
|
||||
origin: String,
|
||||
) -> crate::api::Result<()> {
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
|
||||
if url::Url::parse(&path).is_ok()
|
||||
&& !state.malicious_origins.contains(&origin)
|
||||
{
|
||||
if let Some(last_click) = state.last_click {
|
||||
if last_click.elapsed() < Duration::from_millis(100) {
|
||||
let _ = app.shell().open(&path, None);
|
||||
state.last_click = None;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Malicious click: {path} origin {origin}");
|
||||
state.malicious_origins.insert(origin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_ads_personalization() -> crate::api::Result<bool> {
|
||||
let res = settings::get().await?;
|
||||
Ok(res.personalized_ads)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ pub mod settings;
|
||||
pub mod tags;
|
||||
pub mod utils;
|
||||
|
||||
pub mod ads;
|
||||
pub mod cache;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
|
||||
@@ -39,6 +40,10 @@ pub enum TheseusSerializableError {
|
||||
|
||||
#[error("Tauri error: {0}")]
|
||||
Tauri(#[from] tauri::Error),
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
#[error("Tauri updater error: {0}")]
|
||||
TauriUpdater(#[from] tauri_plugin_updater::Error),
|
||||
}
|
||||
|
||||
// Generic implementation of From<T> for ErrorTypeA
|
||||
@@ -86,7 +91,15 @@ macro_rules! impl_serialize {
|
||||
}
|
||||
|
||||
// Use the macro to implement Serialize for TheseusSerializableError
|
||||
#[cfg(not(feature = "updater"))]
|
||||
impl_serialize! {
|
||||
IO,
|
||||
Tauri,
|
||||
}
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
impl_serialize! {
|
||||
IO,
|
||||
Tauri,
|
||||
TauriUpdater,
|
||||
}
|
||||
|
||||
@@ -26,11 +26,73 @@ extern crate objc;
|
||||
#[tauri::command]
|
||||
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
||||
theseus::EventState::init(app.clone()).await?;
|
||||
State::init().await?;
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
{
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
let updater = app.updater_builder().build()?;
|
||||
|
||||
let update_fut = updater.check();
|
||||
|
||||
State::init().await?;
|
||||
|
||||
let check_bar = theseus::init_loading(
|
||||
theseus::LoadingBarType::CheckingForUpdates,
|
||||
1.0,
|
||||
"Checking for updates...",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let update = update_fut.await;
|
||||
|
||||
drop(check_bar);
|
||||
|
||||
if let Some(update) = update.ok().flatten() {
|
||||
tracing::info!("Update found: {:?}", update.download_url);
|
||||
let loader_bar_id = theseus::init_loading(
|
||||
theseus::LoadingBarType::LauncherUpdate {
|
||||
version: update.version.clone(),
|
||||
current_version: update.current_version.clone(),
|
||||
},
|
||||
1.0,
|
||||
"Updating Modrinth App...",
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 100 MiB
|
||||
const DEFAULT_CONTENT_LENGTH: u64 = 1024 * 1024 * 100;
|
||||
|
||||
update
|
||||
.download_and_install(
|
||||
|chunk_length, content_length| {
|
||||
let _ = theseus::emit_loading(
|
||||
&loader_bar_id,
|
||||
(chunk_length as f64)
|
||||
/ (content_length
|
||||
.unwrap_or(DEFAULT_CONTENT_LENGTH)
|
||||
as f64),
|
||||
None,
|
||||
);
|
||||
},
|
||||
|| {},
|
||||
)
|
||||
.await?;
|
||||
|
||||
app.restart();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "updater"))]
|
||||
{
|
||||
State::init().await?;
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
app.asset_protocol_scope()
|
||||
.allow_directory(state.directories.caches_dir(), true)?;
|
||||
app.asset_protocol_scope()
|
||||
.allow_directory(state.directories.caches_dir().join("icons"), true)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -39,7 +101,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[tauri::command]
|
||||
fn show_window(app: tauri::AppHandle) {
|
||||
let win = app.get_webview_window("main").unwrap();
|
||||
let win = app.get_window("main").unwrap();
|
||||
if let Err(e) = win.show() {
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
@@ -73,6 +135,11 @@ async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn restart_app(app: tauri::AppHandle) {
|
||||
app.restart();
|
||||
}
|
||||
|
||||
// if Tauri app is called with arguments, then those arguments will be treated as commands
|
||||
// ie: deep links or filepaths for .mrpacks
|
||||
fn main() {
|
||||
@@ -102,6 +169,19 @@ fn main() {
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||
if let Some(payload) = args.get(1) {
|
||||
tracing::info!("Handling deep link from arg {payload}");
|
||||
let payload = payload.clone();
|
||||
tauri::async_runtime::spawn(api::utils::handle_command(
|
||||
payload,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(win) = app.get_window("main") {
|
||||
let _ = win.set_focus();
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
@@ -179,12 +259,14 @@ fn main() {
|
||||
.plugin(api::tags::init())
|
||||
.plugin(api::utils::init())
|
||||
.plugin(api::cache::init())
|
||||
.plugin(api::ads::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
initialize_state,
|
||||
is_dev,
|
||||
toggle_decorations,
|
||||
api::mr_auth::modrinth_auth_login,
|
||||
show_window,
|
||||
restart_app,
|
||||
]);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -235,7 +317,7 @@ fn main() {
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
.set_title("Initialization error")
|
||||
.set_text("Your Microsoft Edge WebView2 installation is corrupt.\n\nMicrosoft Edge WebView2 is required to run Modrinth App.\n\nLearn how to repair it at https://docs.modrinth.com/faq/app/webview2")
|
||||
.set_text("Your Microsoft Edge WebView2 installation is corrupt.\n\nMicrosoft Edge WebView2 is required to run Modrinth App.\n\nLearn how to repair it at https://support.modrinth.com/en/articles/8797765-corrupted-microsoft-edge-webview2-installation")
|
||||
.show_alert()
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -5,10 +5,15 @@
|
||||
"build": {
|
||||
"features": ["updater"]
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"capabilities": ["ads", "core", "plugins", "updater"]
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwMzM5QkE0M0FCOERBMzkKUldRNTJyZzZwSnN6SUdPRGdZREtUUGxMblZqeG9OVHYxRUlRTzJBc2U3MUNJaDMvZDQ1UytZZmYK",
|
||||
"endpoints": ["https://launcher-files.modrinth.com/updates.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "http://timestamp.digicert.com",
|
||||
"wix": {
|
||||
"template": "./msi/main.wxs"
|
||||
"nsis": {
|
||||
"installMode": "perMachine",
|
||||
"installerHooks": "./nsis/hooks.nsi"
|
||||
}
|
||||
},
|
||||
"longDescription": "",
|
||||
@@ -48,7 +49,8 @@
|
||||
]
|
||||
},
|
||||
"productName": "Modrinth App",
|
||||
"version": "0.8.3-1",
|
||||
"version": "0.8.9",
|
||||
"mainBinaryName": "Modrinth App",
|
||||
"identifier": "ModrinthApp",
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
@@ -70,10 +72,10 @@
|
||||
"resizable": true,
|
||||
"title": "Modrinth App",
|
||||
"width": 1280,
|
||||
"minHeight": 700,
|
||||
"minHeight": 750,
|
||||
"minWidth": 1100,
|
||||
"visible": false,
|
||||
"zoomHotkeysEnabled": true,
|
||||
"zoomHotkeysEnabled": false,
|
||||
"decorations": false
|
||||
}
|
||||
],
|
||||
@@ -86,7 +88,18 @@
|
||||
],
|
||||
"enable": true
|
||||
},
|
||||
"csp": "default-src 'self'; connect-src ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://*.cloudflare.com https://api.mclo.gs; font-src https://cdn-raw.modrinth.com/fonts/inter/; img-src tauri: https: data: blob: 'unsafe-inline' asset: https://asset.localhost; script-src https://*.cloudflare.com 'self'; frame-src https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'; style-src unsafe-inline 'self'"
|
||||
"capabilities": ["ads", "core", "plugins"],
|
||||
"csp": {
|
||||
"default-src": "'self' customprotocol: asset:",
|
||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://*.cloudflare.com https://api.mclo.gs https://cmp.inmobi.com",
|
||||
"font-src": [
|
||||
"https://cdn-raw.modrinth.com/fonts/inter/"
|
||||
],
|
||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
||||
"style-src": "'unsafe-inline' 'self'",
|
||||
"script-src": "https://cmp.inmobi.com https://*.cloudflare.com https://*.posthog.com 'self'",
|
||||
"frame-src": "https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/app/tauri.linux.conf.json
Normal file
3
apps/app/tauri.linux.conf.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"mainBinaryName": "ModrinthApp"
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"minHeight": 700,
|
||||
"minWidth": 1100,
|
||||
"visible": false,
|
||||
"zoomHotkeysEnabled": true,
|
||||
"zoomHotkeysEnabled": false,
|
||||
"decorations": true
|
||||
}
|
||||
]
|
||||
|
||||
15
apps/daedalus_client/.env
Normal file
15
apps/daedalus_client/.env
Normal file
@@ -0,0 +1,15 @@
|
||||
RUST_LOG=warn,daedalus_client=trace
|
||||
|
||||
BASE_URL=http://localhost:9000/meta
|
||||
|
||||
CONCURRENCY_LIMIT=10
|
||||
|
||||
S3_ACCESS_TOKEN=none
|
||||
S3_SECRET=none
|
||||
S3_URL=http://localhost:9000
|
||||
S3_REGION=path-style
|
||||
S3_BUCKET_NAME=meta
|
||||
|
||||
CLOUDFLARE_INTEGRATION=false
|
||||
CLOUDFLARE_TOKEN=none
|
||||
CLOUDFLARE_ZONE_ID=none
|
||||
42
apps/daedalus_client/Cargo.toml
Normal file
42
apps/daedalus_client/Cargo.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "daedalus_client"
|
||||
version = "0.2.2"
|
||||
authors = ["Jai A <jai@modrinth.com>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
daedalus = { path = "../../packages/daedalus" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
futures = "0.3.25"
|
||||
dotenvy = "0.15.6"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde-xml-rs = "0.6.0"
|
||||
lazy_static = "1.4.0"
|
||||
thiserror = "1.0"
|
||||
reqwest = { version = "0.12.5", default-features = false, features = [
|
||||
"stream",
|
||||
"json",
|
||||
"rustls-tls-native-roots",
|
||||
] }
|
||||
async_zip = { version = "0.0.17", features = ["full"] }
|
||||
semver = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
bytes = "1.6.0"
|
||||
rust-s3 = { version = "0.33.0", default-features = false, features = [
|
||||
"fail-on-err",
|
||||
"tags",
|
||||
"tokio-rustls-tls",
|
||||
"reqwest",
|
||||
] }
|
||||
dashmap = "5.5.3"
|
||||
sha1_smol = { version = "1.0.0", features = ["std"] }
|
||||
indexmap = { version = "2.2.6", features = ["serde"] }
|
||||
itertools = "0.13.0"
|
||||
tracing-error = "0.2.0"
|
||||
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-futures = { version = "0.2.5", features = ["futures", "tokio"] }
|
||||
21
apps/daedalus_client/Dockerfile
Normal file
21
apps/daedalus_client/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM rust:1.82.0 as build
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR /usr/src/daedalus
|
||||
COPY . .
|
||||
RUN cargo build --release --package daedalus_client
|
||||
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN update-ca-certificates
|
||||
|
||||
COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
||||
WORKDIR /daedalus_client
|
||||
|
||||
CMD /daedalus/daedalus_client
|
||||
7
apps/daedalus_client/LICENSE
Normal file
7
apps/daedalus_client/LICENSE
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright © 2024 Rinth, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
9
apps/daedalus_client/README.md
Normal file
9
apps/daedalus_client/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Daedalus
|
||||
|
||||
Daedalus is a powerful tool which queries and generates metadata for the Minecraft (and other games in the future!) game
|
||||
and mod loaders for:
|
||||
- Performance (Serving static files can be easily cached and is extremely quick)
|
||||
- Ease for Launcher Devs (Metadata is served in an easy to query and use format)
|
||||
- Reliability (Provides a versioning system which ensures no breakage with updates)
|
||||
|
||||
Daedalus supports the original Minecraft data and reposting for the Forge, Fabric, Quilt, and NeoForge loaders.
|
||||
17
apps/daedalus_client/docker-compose.yml
Normal file
17
apps/daedalus_client/docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
minio:
|
||||
image: quay.io/minio/minio
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: miniosecret
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
volumes:
|
||||
minio-data:
|
||||
2880
apps/daedalus_client/library-patches.json
Normal file
2880
apps/daedalus_client/library-patches.json
Normal file
File diff suppressed because it is too large
Load Diff
13
apps/daedalus_client/package.json
Normal file
13
apps/daedalus_client/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@modrinth/daedalus_client",
|
||||
"scripts": {
|
||||
"build": "cargo build --release",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix",
|
||||
"dev": "cargo run",
|
||||
"test": "cargo test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/daedalus": "workspace:*"
|
||||
}
|
||||
}
|
||||
63
apps/daedalus_client/src/error.rs
Normal file
63
apps/daedalus_client/src/error.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use tracing_error::InstrumentError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ErrorKind {
|
||||
#[error("Daedalus Error: {0}")]
|
||||
Daedalus(#[from] daedalus::Error),
|
||||
#[error("Invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
#[error("Error while managing asynchronous tasks")]
|
||||
TaskError(#[from] tokio::task::JoinError),
|
||||
#[error("Error while deserializing JSON: {0}")]
|
||||
SerdeJSON(#[from] serde_json::Error),
|
||||
#[error("Error while deserializing XML: {0}")]
|
||||
SerdeXML(#[from] serde_xml_rs::Error),
|
||||
#[error("Failed to validate file checksum at url {url} with hash {hash} after {tries} tries")]
|
||||
ChecksumFailure {
|
||||
hash: String,
|
||||
url: String,
|
||||
tries: u32,
|
||||
},
|
||||
#[error("Unable to fetch {item}")]
|
||||
Fetch { inner: reqwest::Error, item: String },
|
||||
#[error("Error while uploading file to S3: {file}")]
|
||||
S3 {
|
||||
inner: s3::error::S3Error,
|
||||
file: String,
|
||||
},
|
||||
#[error("Error acquiring semaphore: {0}")]
|
||||
Acquire(#[from] tokio::sync::AcquireError),
|
||||
#[error("Tracing error: {0}")]
|
||||
Tracing(#[from] tracing::subscriber::SetGlobalDefaultError),
|
||||
#[error("Zip error: {0}")]
|
||||
Zip(#[from] async_zip::error::ZipError),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
pub source: tracing_error::TracedError<ErrorKind>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.source)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Into<ErrorKind>> From<E> for Error {
|
||||
fn from(source: E) -> Self {
|
||||
let error = Into::<ErrorKind>::into(source);
|
||||
|
||||
Self {
|
||||
source: error.in_current_span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
pub fn as_error(self) -> Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
301
apps/daedalus_client/src/fabric.rs
Normal file
301
apps/daedalus_client/src/fabric.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
use crate::util::{download_file, fetch_json, format_url};
|
||||
use crate::{insert_mirrored_artifact, Error, MirrorArtifact, UploadFile};
|
||||
use daedalus::modded::{Manifest, PartialVersionInfo, DUMMY_REPLACE_STRING};
|
||||
use dashmap::DashMap;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
#[tracing::instrument(skip(semaphore, upload_files, mirror_artifacts))]
|
||||
pub async fn fetch_fabric(
|
||||
semaphore: Arc<Semaphore>,
|
||||
upload_files: &DashMap<String, UploadFile>,
|
||||
mirror_artifacts: &DashMap<String, MirrorArtifact>,
|
||||
) -> Result<(), Error> {
|
||||
fetch(
|
||||
daedalus::modded::CURRENT_FABRIC_FORMAT_VERSION,
|
||||
"fabric",
|
||||
"https://meta.fabricmc.net/v2",
|
||||
"https://maven.fabricmc.net/",
|
||||
&[],
|
||||
semaphore,
|
||||
upload_files,
|
||||
mirror_artifacts,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(semaphore, upload_files, mirror_artifacts))]
|
||||
pub async fn fetch_quilt(
|
||||
semaphore: Arc<Semaphore>,
|
||||
upload_files: &DashMap<String, UploadFile>,
|
||||
mirror_artifacts: &DashMap<String, MirrorArtifact>,
|
||||
) -> Result<(), Error> {
|
||||
fetch(
|
||||
daedalus::modded::CURRENT_QUILT_FORMAT_VERSION,
|
||||
"quilt",
|
||||
"https://meta.quiltmc.org/v3",
|
||||
"https://maven.quiltmc.org/repository/release/",
|
||||
&[
|
||||
// This version is broken as it contains invalid library coordinates
|
||||
"0.17.5-beta.4",
|
||||
],
|
||||
semaphore,
|
||||
upload_files,
|
||||
mirror_artifacts,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tracing::instrument(skip(semaphore, upload_files, mirror_artifacts))]
|
||||
async fn fetch(
|
||||
format_version: usize,
|
||||
mod_loader: &str,
|
||||
meta_url: &str,
|
||||
maven_url: &str,
|
||||
skip_versions: &[&str],
|
||||
semaphore: Arc<Semaphore>,
|
||||
upload_files: &DashMap<String, UploadFile>,
|
||||
mirror_artifacts: &DashMap<String, MirrorArtifact>,
|
||||
) -> Result<(), Error> {
|
||||
let modrinth_manifest = fetch_json::<Manifest>(
|
||||
&format_url(&format!("{mod_loader}/v{format_version}/manifest.json",)),
|
||||
&semaphore,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
let fabric_manifest = fetch_json::<FabricVersions>(
|
||||
&format!("{meta_url}/versions"),
|
||||
&semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// We check Modrinth's fabric version manifest and compare if the fabric version exists in Modrinth's database
|
||||
// We also check intermediary versions that are newly added to query
|
||||
let (fetch_fabric_versions, fetch_intermediary_versions) =
|
||||
if let Some(modrinth_manifest) = modrinth_manifest {
|
||||
let (mut fetch_versions, mut fetch_intermediary_versions) =
|
||||
(Vec::new(), Vec::new());
|
||||
|
||||
for version in &fabric_manifest.loader {
|
||||
if !modrinth_manifest
|
||||
.game_versions
|
||||
.iter()
|
||||
.any(|x| x.loaders.iter().any(|x| x.id == version.version))
|
||||
&& !skip_versions.contains(&&*version.version)
|
||||
{
|
||||
fetch_versions.push(version);
|
||||
}
|
||||
}
|
||||
|
||||
for version in &fabric_manifest.intermediary {
|
||||
if !modrinth_manifest
|
||||
.game_versions
|
||||
.iter()
|
||||
.any(|x| x.id == version.version)
|
||||
&& fabric_manifest
|
||||
.game
|
||||
.iter()
|
||||
.any(|x| x.version == version.version)
|
||||
{
|
||||
fetch_intermediary_versions.push(version);
|
||||
}
|
||||
}
|
||||
|
||||
(fetch_versions, fetch_intermediary_versions)
|
||||
} else {
|
||||
(
|
||||
fabric_manifest
|
||||
.loader
|
||||
.iter()
|
||||
.filter(|x| !skip_versions.contains(&&*x.version))
|
||||
.collect(),
|
||||
fabric_manifest.intermediary.iter().collect(),
|
||||
)
|
||||
};
|
||||
|
||||
const DUMMY_GAME_VERSION: &str = "1.21";
|
||||
|
||||
if !fetch_intermediary_versions.is_empty() {
|
||||
for x in &fetch_intermediary_versions {
|
||||
insert_mirrored_artifact(
|
||||
&x.maven,
|
||||
None,
|
||||
vec![maven_url.to_string()],
|
||||
false,
|
||||
mirror_artifacts,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !fetch_fabric_versions.is_empty() {
|
||||
let fabric_version_manifest_urls = fetch_fabric_versions
|
||||
.iter()
|
||||
.map(|x| {
|
||||
format!(
|
||||
"{}/versions/loader/{}/{}/profile/json",
|
||||
meta_url, DUMMY_GAME_VERSION, x.version
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let fabric_version_manifests = futures::future::try_join_all(
|
||||
fabric_version_manifest_urls
|
||||
.iter()
|
||||
.map(|x| download_file(x, None, &semaphore)),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| serde_json::from_slice(&x))
|
||||
.collect::<Result<Vec<PartialVersionInfo>, serde_json::Error>>()?;
|
||||
|
||||
let patched_version_manifests = fabric_version_manifests
|
||||
.into_iter()
|
||||
.map(|mut version_info| {
|
||||
for lib in &mut version_info.libraries {
|
||||
let new_name = lib
|
||||
.name
|
||||
.replace(DUMMY_GAME_VERSION, DUMMY_REPLACE_STRING);
|
||||
|
||||
// Hard-code: This library is not present on fabric's maven, so we fetch it from MC libraries
|
||||
if &*lib.name == "net.minecraft:launchwrapper:1.12" {
|
||||
lib.url = Some(
|
||||
"https://libraries.minecraft.net/".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// If a library is not intermediary, we add it to mirror artifacts to be mirrored
|
||||
if lib.name == new_name {
|
||||
insert_mirrored_artifact(
|
||||
&new_name,
|
||||
None,
|
||||
vec![lib
|
||||
.url
|
||||
.clone()
|
||||
.unwrap_or_else(|| maven_url.to_string())],
|
||||
false,
|
||||
mirror_artifacts,
|
||||
)?;
|
||||
} else {
|
||||
lib.name = new_name;
|
||||
}
|
||||
|
||||
lib.url = Some(format_url("maven/"));
|
||||
}
|
||||
|
||||
version_info.id = version_info
|
||||
.id
|
||||
.replace(DUMMY_GAME_VERSION, DUMMY_REPLACE_STRING);
|
||||
version_info.inherits_from = version_info
|
||||
.inherits_from
|
||||
.replace(DUMMY_GAME_VERSION, DUMMY_REPLACE_STRING);
|
||||
|
||||
Ok(version_info)
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
let serialized_version_manifests = patched_version_manifests
|
||||
.iter()
|
||||
.map(|x| serde_json::to_vec(x).map(bytes::Bytes::from))
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?;
|
||||
|
||||
serialized_version_manifests
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.for_each(|(index, bytes)| {
|
||||
let loader = fetch_fabric_versions[index];
|
||||
|
||||
let version_path = format!(
|
||||
"{mod_loader}/v{format_version}/versions/{}.json",
|
||||
loader.version
|
||||
);
|
||||
|
||||
upload_files.insert(
|
||||
version_path,
|
||||
UploadFile {
|
||||
file: bytes,
|
||||
content_type: Some("application/json".to_string()),
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if !fetch_fabric_versions.is_empty()
|
||||
|| !fetch_intermediary_versions.is_empty()
|
||||
{
|
||||
let fabric_manifest_path =
|
||||
format!("{mod_loader}/v{format_version}/manifest.json",);
|
||||
|
||||
let loader_versions = daedalus::modded::Version {
|
||||
id: DUMMY_REPLACE_STRING.to_string(),
|
||||
stable: true,
|
||||
loaders: fabric_manifest
|
||||
.loader
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let version_path = format!(
|
||||
"{mod_loader}/v{format_version}/versions/{}.json",
|
||||
x.version,
|
||||
);
|
||||
|
||||
daedalus::modded::LoaderVersion {
|
||||
id: x.version,
|
||||
url: format_url(&version_path),
|
||||
stable: x.stable,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let manifest = daedalus::modded::Manifest {
|
||||
game_versions: std::iter::once(loader_versions)
|
||||
.chain(fabric_manifest.game.into_iter().map(|x| {
|
||||
daedalus::modded::Version {
|
||||
id: x.version,
|
||||
stable: x.stable,
|
||||
loaders: vec![],
|
||||
}
|
||||
}))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
upload_files.insert(
|
||||
fabric_manifest_path,
|
||||
UploadFile {
|
||||
file: bytes::Bytes::from(serde_json::to_vec(&manifest)?),
|
||||
content_type: Some("application/json".to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
struct FabricVersions {
|
||||
pub loader: Vec<FabricLoaderVersion>,
|
||||
pub game: Vec<FabricGameVersion>,
|
||||
#[serde(alias = "hashed")]
|
||||
pub intermediary: Vec<FabricIntermediaryVersion>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
struct FabricLoaderVersion {
|
||||
// pub separator: String,
|
||||
// pub build: u32,
|
||||
// pub maven: String,
|
||||
pub version: String,
|
||||
#[serde(default)]
|
||||
pub stable: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
struct FabricIntermediaryVersion {
|
||||
pub maven: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
struct FabricGameVersion {
|
||||
pub version: String,
|
||||
pub stable: bool,
|
||||
}
|
||||
792
apps/daedalus_client/src/forge.rs
Normal file
792
apps/daedalus_client/src/forge.rs
Normal file
@@ -0,0 +1,792 @@
|
||||
use crate::util::{download_file, fetch_json, fetch_xml, format_url};
|
||||
use crate::{insert_mirrored_artifact, Error, MirrorArtifact, UploadFile};
|
||||
use chrono::{DateTime, Utc};
|
||||
use daedalus::get_path_from_artifact;
|
||||
use daedalus::modded::PartialVersionInfo;
|
||||
use dashmap::DashMap;
|
||||
use futures::io::Cursor;
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
#[tracing::instrument(skip(semaphore, upload_files, mirror_artifacts))]
|
||||
pub async fn fetch_forge(
|
||||
semaphore: Arc<Semaphore>,
|
||||
upload_files: &DashMap<String, UploadFile>,
|
||||
mirror_artifacts: &DashMap<String, MirrorArtifact>,
|
||||
) -> Result<(), Error> {
|
||||
let forge_manifest = fetch_json::<IndexMap<String, Vec<String>>>(
|
||||
"https://files.minecraftforge.net/net/minecraftforge/forge/maven-metadata.json",
|
||||
&semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut format_version = 0;
|
||||
|
||||
let forge_versions = forge_manifest.into_iter().flat_map(|(game_version, versions)| versions.into_iter().map(|loader_version| {
|
||||
// Forge versions can be in these specific formats:
|
||||
// 1.10.2-12.18.1.2016-failtests
|
||||
// 1.9-12.16.0.1886
|
||||
// 1.9-12.16.0.1880-1.9
|
||||
// 1.14.4-28.1.30
|
||||
// This parses them to get the actual Forge version. Ex: 1.15.2-31.1.87 -> 31.1.87
|
||||
let version_split = loader_version.split('-').nth(1).unwrap_or(&loader_version).to_string();
|
||||
|
||||
// Forge has 3 installer formats:
|
||||
// - Format 0 (Unsupported ATM): Forge Legacy (pre-1.5.2). Uses Binary Patch method to install
|
||||
// To install: Download patch, download minecraft client JAR. Combine patch and client JAR and delete META-INF/.
|
||||
// (pre-1.3-2) Client URL: https://maven.minecraftforge.net/net/minecraftforge/forge/{version}/forge-{version}-client.zip
|
||||
// (pre-1.3-2) Server URL: https://maven.minecraftforge.net/net/minecraftforge/forge/{version}/forge-{version}-server.zip
|
||||
// (1.3-2-onwards) Universal URL: https://maven.minecraftforge.net/net/minecraftforge/forge/{version}/forge-{version}-universal.zip
|
||||
// - Format 1: Forge Installer Legacy (1.5.2-1.12.2ish)
|
||||
// To install: Extract install_profile.json from archive. "versionInfo" is the profile's version info. Convert it to the modern format
|
||||
// Extract forge library from archive. Path is at "install"."path".
|
||||
// - Format 2: Forge Installer Modern
|
||||
// To install: Extract install_profile.json from archive. Extract version.json from archive. Combine the two and extract all libraries
|
||||
// which are embedded into the installer JAR.
|
||||
// Then upload. The launcher will need to run processors!
|
||||
if format_version != 1 && &*version_split == "7.8.0.684" {
|
||||
format_version = 1;
|
||||
} else if format_version != 2 && &*version_split == "14.23.5.2851" {
|
||||
format_version = 2;
|
||||
}
|
||||
|
||||
ForgeVersion {
|
||||
format_version,
|
||||
installer_url: format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{0}/forge-{0}-installer.jar", loader_version),
|
||||
raw: loader_version,
|
||||
loader_version: version_split,
|
||||
game_version: game_version.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
// TODO: support format version 0 (see above)
|
||||
.filter(|x| x.format_version != 0)
|
||||
.filter(|x| {
|
||||
// These following Forge versions are broken and cannot be installed
|
||||
const BLACKLIST : &[&str] = &[
|
||||
// Not supported due to `data` field being `[]` even though the type is a map
|
||||
"1.12.2-14.23.5.2851",
|
||||
// Malformed Archives
|
||||
"1.6.1-8.9.0.749",
|
||||
"1.6.1-8.9.0.751",
|
||||
"1.6.4-9.11.1.960",
|
||||
"1.6.4-9.11.1.961",
|
||||
"1.6.4-9.11.1.963",
|
||||
"1.6.4-9.11.1.964",
|
||||
];
|
||||
|
||||
!BLACKLIST.contains(&&*x.raw)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
fetch(
|
||||
daedalus::modded::CURRENT_FORGE_FORMAT_VERSION,
|
||||
"forge",
|
||||
"https://maven.minecraftforge.net/",
|
||||
forge_versions,
|
||||
semaphore,
|
||||
upload_files,
|
||||
mirror_artifacts,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(semaphore, upload_files, mirror_artifacts))]
|
||||
pub async fn fetch_neo(
|
||||
semaphore: Arc<Semaphore>,
|
||||
upload_files: &DashMap<String, UploadFile>,
|
||||
mirror_artifacts: &DashMap<String, MirrorArtifact>,
|
||||
) -> Result<(), Error> {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Metadata {
|
||||
versioning: Versioning,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Versioning {
|
||||
versions: Versions,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Versions {
|
||||
version: Vec<String>,
|
||||
}
|
||||
|
||||
let forge_versions = fetch_xml::<Metadata>(
|
||||
"https://maven.neoforged.net/net/neoforged/forge/maven-metadata.xml",
|
||||
&semaphore,
|
||||
)
|
||||
.await?;
|
||||
let neo_versions = fetch_xml::<Metadata>(
|
||||
"https://maven.neoforged.net/net/neoforged/neoforge/maven-metadata.xml",
|
||||
&semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let parsed_versions = forge_versions.versioning.versions.version.into_iter().map(|loader_version| {
|
||||
// NeoForge Forge versions can be in these specific formats:
|
||||
// 1.20.1-47.1.74
|
||||
// 47.1.82
|
||||
// This parses them to get the actual Forge version. Ex: 1.20.1-47.1.74 -> 47.1.74
|
||||
let version_split = loader_version.split('-').nth(1).unwrap_or(&loader_version).to_string();
|
||||
|
||||
Ok(ForgeVersion {
|
||||
format_version: 2,
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/forge/{0}/forge-{0}-installer.jar", loader_version),
|
||||
raw: loader_version,
|
||||
loader_version: version_split,
|
||||
game_version: "1.20.1".to_string(), // All NeoForge Forge versions are for 1.20.1
|
||||
})
|
||||
}).chain(neo_versions.versioning.versions.version.into_iter().map(|loader_version| {
|
||||
let mut parts = loader_version.split('.');
|
||||
|
||||
// NeoForge Forge versions are in this format: 20.2.29-beta, 20.6.119
|
||||
// Where the first number is the major MC version, the second is the minor MC version, and the third is the NeoForge version
|
||||
let major = parts.next().ok_or_else(
|
||||
|| crate::ErrorKind::InvalidInput(format!("Unable to find major game version for NeoForge {loader_version}"))
|
||||
)?;
|
||||
|
||||
let minor = parts.next().ok_or_else(
|
||||
|| crate::ErrorKind::InvalidInput(format!("Unable to find minor game version for NeoForge {loader_version}"))
|
||||
)?;
|
||||
|
||||
let game_version = if minor == "0" {
|
||||
format!("1.{major}")
|
||||
} else {
|
||||
format!("1.{major}.{minor}")
|
||||
};
|
||||
|
||||
Ok(ForgeVersion {
|
||||
format_version: 2,
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/neoforge/{0}/neoforge-{0}-installer.jar", loader_version),
|
||||
loader_version: loader_version.clone(),
|
||||
raw: loader_version,
|
||||
game_version,
|
||||
})
|
||||
}))
|
||||
.collect::<Result<Vec<_>, Error>>()?
|
||||
.into_iter()
|
||||
.filter(|x| {
|
||||
// These following Forge versions are broken and cannot be installed
|
||||
const BLACKLIST : &[&str] = &[
|
||||
// Unreachable / 404
|
||||
"1.20.1-47.1.7",
|
||||
"47.1.82",
|
||||
];
|
||||
|
||||
!BLACKLIST.contains(&&*x.raw)
|
||||
}).collect();
|
||||
|
||||
fetch(
|
||||
daedalus::modded::CURRENT_NEOFORGE_FORMAT_VERSION,
|
||||
"neo",
|
||||
"https://maven.neoforged.net/",
|
||||
parsed_versions,
|
||||
semaphore,
|
||||
upload_files,
|
||||
mirror_artifacts,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(
|
||||
forge_versions,
|
||||
semaphore,
|
||||
upload_files,
|
||||
mirror_artifacts
|
||||
))]
|
||||
async fn fetch(
|
||||
format_version: usize,
|
||||
mod_loader: &str,
|
||||
maven_url: &str,
|
||||
forge_versions: Vec<ForgeVersion>,
|
||||
semaphore: Arc<Semaphore>,
|
||||
upload_files: &DashMap<String, UploadFile>,
|
||||
mirror_artifacts: &DashMap<String, MirrorArtifact>,
|
||||
) -> Result<(), Error> {
|
||||
let modrinth_manifest = fetch_json::<daedalus::modded::Manifest>(
|
||||
&format_url(&format!("{mod_loader}/v{format_version}/manifest.json",)),
|
||||
&semaphore,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let fetch_versions = if let Some(modrinth_manifest) = modrinth_manifest {
|
||||
let mut fetch_versions = Vec::new();
|
||||
|
||||
for version in &forge_versions {
|
||||
if !modrinth_manifest.game_versions.iter().any(|x| {
|
||||
x.id == version.game_version
|
||||
&& x.loaders.iter().any(|x| x.id == version.loader_version)
|
||||
}) {
|
||||
fetch_versions.push(version);
|
||||
}
|
||||
}
|
||||
|
||||
fetch_versions
|
||||
} else {
|
||||
forge_versions.iter().collect()
|
||||
};
|
||||
|
||||
if !fetch_versions.is_empty() {
|
||||
let forge_installers = futures::future::try_join_all(
|
||||
fetch_versions
|
||||
.iter()
|
||||
.map(|x| download_file(&x.installer_url, None, &semaphore)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[tracing::instrument(skip(raw, upload_files, mirror_artifacts))]
|
||||
async fn read_forge_installer(
|
||||
raw: bytes::Bytes,
|
||||
loader: &ForgeVersion,
|
||||
maven_url: &str,
|
||||
mod_loader: &str,
|
||||
upload_files: &DashMap<String, UploadFile>,
|
||||
mirror_artifacts: &DashMap<String, MirrorArtifact>,
|
||||
) -> Result<PartialVersionInfo, Error> {
|
||||
tracing::trace!(
|
||||
"Reading forge installer for {}",
|
||||
loader.loader_version
|
||||
);
|
||||
type ZipFileReader = async_zip::base::read::seek::ZipFileReader<
|
||||
Cursor<bytes::Bytes>,
|
||||
>;
|
||||
|
||||
let cursor = Cursor::new(raw);
|
||||
let mut zip = ZipFileReader::new(cursor).await?;
|
||||
|
||||
#[tracing::instrument(skip(zip))]
|
||||
async fn read_file(
|
||||
zip: &mut ZipFileReader,
|
||||
file_name: &str,
|
||||
) -> Result<Option<Vec<u8>>, Error> {
|
||||
let zip_index_option =
|
||||
zip.file().entries().iter().position(|f| {
|
||||
f.filename().as_str().unwrap_or_default() == file_name
|
||||
});
|
||||
|
||||
if let Some(zip_index) = zip_index_option {
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = zip.reader_with_entry(zip_index).await?;
|
||||
reader.read_to_end_checked(&mut buffer).await?;
|
||||
|
||||
Ok(Some(buffer))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(zip))]
|
||||
async fn read_json<T: DeserializeOwned>(
|
||||
zip: &mut ZipFileReader,
|
||||
file_name: &str,
|
||||
) -> Result<Option<T>, Error> {
|
||||
if let Some(file) = read_file(zip, file_name).await? {
|
||||
Ok(Some(serde_json::from_slice(&file)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
if loader.format_version == 1 {
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ForgeInstallerProfileInstallDataV1 {
|
||||
// pub mirror_list: String,
|
||||
// pub target: String,
|
||||
/// Path to the Forge universal library
|
||||
pub file_path: String,
|
||||
// pub logo: String,
|
||||
// pub welcome: String,
|
||||
// pub version: String,
|
||||
/// Maven coordinates of the Forge universal library
|
||||
pub path: String,
|
||||
// pub profile_name: String,
|
||||
pub minecraft: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ForgeInstallerProfileManifestV1 {
|
||||
pub id: String,
|
||||
pub libraries: Vec<daedalus::minecraft::Library>,
|
||||
pub main_class: Option<String>,
|
||||
pub minecraft_arguments: Option<String>,
|
||||
pub release_time: DateTime<Utc>,
|
||||
pub time: DateTime<Utc>,
|
||||
pub type_: daedalus::minecraft::VersionType,
|
||||
// pub assets: Option<String>,
|
||||
// pub inherits_from: Option<String>,
|
||||
// pub jar: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ForgeInstallerProfileV1 {
|
||||
pub install: ForgeInstallerProfileInstallDataV1,
|
||||
pub version_info: ForgeInstallerProfileManifestV1,
|
||||
}
|
||||
|
||||
let install_profile = read_json::<ForgeInstallerProfileV1>(
|
||||
&mut zip,
|
||||
"install_profile.json",
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InvalidInput(format!(
|
||||
"No install_profile.json present for loader {}",
|
||||
loader.installer_url
|
||||
))
|
||||
})?;
|
||||
|
||||
let forge_library =
|
||||
read_file(&mut zip, &install_profile.install.file_path)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InvalidInput(format!(
|
||||
"No forge library present for loader {}",
|
||||
loader.installer_url
|
||||
))
|
||||
})?;
|
||||
|
||||
upload_files.insert(
|
||||
format!(
|
||||
"maven/{}",
|
||||
get_path_from_artifact(&install_profile.install.path)?
|
||||
),
|
||||
UploadFile {
|
||||
file: bytes::Bytes::from(forge_library),
|
||||
content_type: None,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(PartialVersionInfo {
|
||||
id: install_profile.version_info.id,
|
||||
inherits_from: install_profile.install.minecraft,
|
||||
release_time: install_profile.version_info.release_time,
|
||||
time: install_profile.version_info.time,
|
||||
main_class: install_profile.version_info.main_class,
|
||||
minecraft_arguments: install_profile
|
||||
.version_info
|
||||
.minecraft_arguments
|
||||
.clone(),
|
||||
arguments: install_profile
|
||||
.version_info
|
||||
.minecraft_arguments
|
||||
.map(|x| {
|
||||
[(
|
||||
daedalus::minecraft::ArgumentType::Game,
|
||||
x.split(' ')
|
||||
.map(|x| {
|
||||
daedalus::minecraft::Argument::Normal(
|
||||
x.to_string(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect()
|
||||
}),
|
||||
libraries: install_profile
|
||||
.version_info
|
||||
.libraries
|
||||
.into_iter()
|
||||
.map(|mut lib| {
|
||||
// For all libraries besides the forge lib extracted, we mirror them from maven servers
|
||||
// unless the URL is empty/null or available on Minecraft's servers
|
||||
if let Some(ref url) = lib.url {
|
||||
if lib.name == install_profile.install.path {
|
||||
lib.url = Some(format_url("maven/"));
|
||||
} else if !url.is_empty()
|
||||
&& !url.contains(
|
||||
"https://libraries.minecraft.net/",
|
||||
)
|
||||
{
|
||||
insert_mirrored_artifact(
|
||||
&lib.name,
|
||||
None,
|
||||
vec![
|
||||
url.clone(),
|
||||
"https://maven.creeperhost.net/"
|
||||
.to_string(),
|
||||
maven_url.to_string(),
|
||||
],
|
||||
false,
|
||||
mirror_artifacts,
|
||||
)?;
|
||||
|
||||
lib.url = Some(format_url("maven/"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(lib)
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error>>()?,
|
||||
type_: install_profile.version_info.type_,
|
||||
data: None,
|
||||
processors: None,
|
||||
})
|
||||
} else if loader.format_version == 2 {
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ForgeInstallerProfileV2 {
|
||||
// pub spec: i32,
|
||||
// pub profile: String,
|
||||
// pub version: String,
|
||||
// pub json: String,
|
||||
// pub path: Option<String>,
|
||||
// pub minecraft: String,
|
||||
pub data: HashMap<String, daedalus::modded::SidedDataEntry>,
|
||||
pub libraries: Vec<daedalus::minecraft::Library>,
|
||||
pub processors: Vec<daedalus::modded::Processor>,
|
||||
}
|
||||
|
||||
let install_profile = read_json::<ForgeInstallerProfileV2>(
|
||||
&mut zip,
|
||||
"install_profile.json",
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InvalidInput(format!(
|
||||
"No install_profile.json present for loader {}",
|
||||
loader.installer_url
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut version_info =
|
||||
read_json::<PartialVersionInfo>(&mut zip, "version.json")
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InvalidInput(format!(
|
||||
"No version.json present for loader {}",
|
||||
loader.installer_url
|
||||
))
|
||||
})?;
|
||||
|
||||
version_info.processors = Some(install_profile.processors);
|
||||
version_info.libraries.extend(
|
||||
install_profile.libraries.into_iter().map(|mut x| {
|
||||
x.include_in_classpath = false;
|
||||
|
||||
x
|
||||
}),
|
||||
);
|
||||
|
||||
async fn mirror_forge_library(
|
||||
mut zip: ZipFileReader,
|
||||
mut lib: daedalus::minecraft::Library,
|
||||
maven_url: &str,
|
||||
upload_files: &DashMap<String, UploadFile>,
|
||||
mirror_artifacts: &DashMap<String, MirrorArtifact>,
|
||||
) -> Result<daedalus::minecraft::Library, Error>
|
||||
{
|
||||
let artifact_path = get_path_from_artifact(&lib.name)?;
|
||||
|
||||
if let Some(ref mut artifact) =
|
||||
lib.downloads.as_mut().and_then(|x| x.artifact.as_mut())
|
||||
{
|
||||
if !artifact.url.is_empty() {
|
||||
insert_mirrored_artifact(
|
||||
&lib.name,
|
||||
Some(artifact.sha1.clone()),
|
||||
vec![artifact.url.clone()],
|
||||
true,
|
||||
mirror_artifacts,
|
||||
)?;
|
||||
|
||||
artifact.url =
|
||||
format_url(&format!("maven/{}", artifact_path));
|
||||
|
||||
return Ok(lib);
|
||||
}
|
||||
} else if let Some(url) = &lib.url {
|
||||
if !url.is_empty() {
|
||||
insert_mirrored_artifact(
|
||||
&lib.name,
|
||||
None,
|
||||
vec![
|
||||
url.clone(),
|
||||
"https://libraries.minecraft.net/"
|
||||
.to_string(),
|
||||
"https://maven.creeperhost.net/"
|
||||
.to_string(),
|
||||
maven_url.to_string(),
|
||||
],
|
||||
false,
|
||||
mirror_artifacts,
|
||||
)?;
|
||||
|
||||
lib.url = Some(format_url("maven/"));
|
||||
|
||||
return Ok(lib);
|
||||
}
|
||||
}
|
||||
|
||||
// Other libraries are generally available in the "maven" directory of the installer. If they are
|
||||
// not present here, they will be generated by Forge processors.
|
||||
let extract_path = format!("maven/{artifact_path}");
|
||||
if let Some(file) =
|
||||
read_file(&mut zip, &extract_path).await?
|
||||
{
|
||||
upload_files.insert(
|
||||
extract_path,
|
||||
UploadFile {
|
||||
file: bytes::Bytes::from(file),
|
||||
content_type: None,
|
||||
},
|
||||
);
|
||||
|
||||
lib.url = Some(format_url("maven/"));
|
||||
} else {
|
||||
lib.downloadable = false;
|
||||
}
|
||||
|
||||
Ok(lib)
|
||||
}
|
||||
|
||||
version_info.libraries = futures::future::try_join_all(
|
||||
version_info.libraries.into_iter().map(|lib| {
|
||||
mirror_forge_library(
|
||||
zip.clone(),
|
||||
lib,
|
||||
maven_url,
|
||||
upload_files,
|
||||
mirror_artifacts,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// In Minecraft Forge modern installers, processors are run during the install process. Some processors
|
||||
// are extracted from the installer JAR. This function finds these files, extracts them, and uploads them
|
||||
// and registers them as libraries instead.
|
||||
// Ex:
|
||||
// "BINPATCH": {
|
||||
// "client": "/data/client.lzma",
|
||||
// "server": "/data/server.lzma"
|
||||
// },
|
||||
// Becomes:
|
||||
// "BINPATCH": {
|
||||
// "client": "[net.minecraftforge:forge:1.20.3-49.0.1:shim:client@lzma]",
|
||||
// "server": "[net.minecraftforge:forge:1.20.3-49.0.1:shim:server@lzma]"
|
||||
// },
|
||||
// And the resulting library is added to the profile's libraries
|
||||
let mut new_data = HashMap::new();
|
||||
for (key, entry) in install_profile.data {
|
||||
async fn extract_data(
|
||||
zip: &mut ZipFileReader,
|
||||
key: &str,
|
||||
value: &str,
|
||||
upload_files: &DashMap<String, UploadFile>,
|
||||
libs: &mut Vec<daedalus::minecraft::Library>,
|
||||
mod_loader: &str,
|
||||
version: &ForgeVersion,
|
||||
) -> Result<String, Error> {
|
||||
let extract_file =
|
||||
read_file(zip, &value[1..value.len()])
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InvalidInput(format!(
|
||||
"Unable reading data key {key} at path {value}",
|
||||
))
|
||||
})?;
|
||||
|
||||
let file_name = value.split('/').last()
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InvalidInput(format!(
|
||||
"Unable reading filename for data key {key} at path {value}",
|
||||
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut file = file_name.split('.');
|
||||
let file_name = file.next()
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InvalidInput(format!(
|
||||
"Unable reading filename only for data key {key} at path {value}",
|
||||
))
|
||||
})?;
|
||||
let ext = file.next()
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InvalidInput(format!(
|
||||
"Unable reading extension only for data key {key} at path {value}",
|
||||
))
|
||||
})?;
|
||||
|
||||
let path = format!(
|
||||
"com.modrinth.daedalus:{}-installer-extracts:{}:{}@{}",
|
||||
mod_loader,
|
||||
version.raw,
|
||||
file_name,
|
||||
ext
|
||||
);
|
||||
|
||||
upload_files.insert(
|
||||
format!("maven/{}", get_path_from_artifact(&path)?),
|
||||
UploadFile {
|
||||
file: bytes::Bytes::from(extract_file),
|
||||
content_type: None,
|
||||
},
|
||||
);
|
||||
|
||||
libs.push(daedalus::minecraft::Library {
|
||||
downloads: None,
|
||||
extract: None,
|
||||
name: path.clone(),
|
||||
url: Some(format_url("maven/")),
|
||||
natives: None,
|
||||
rules: None,
|
||||
checksums: None,
|
||||
include_in_classpath: false,
|
||||
downloadable: true,
|
||||
});
|
||||
|
||||
Ok(format!("[{path}]"))
|
||||
}
|
||||
|
||||
let client = if entry.client.starts_with('/') {
|
||||
extract_data(
|
||||
&mut zip,
|
||||
&key,
|
||||
&entry.client,
|
||||
upload_files,
|
||||
&mut version_info.libraries,
|
||||
mod_loader,
|
||||
loader,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
entry.client.clone()
|
||||
};
|
||||
|
||||
let server = if entry.server.starts_with('/') {
|
||||
extract_data(
|
||||
&mut zip,
|
||||
&key,
|
||||
&entry.server,
|
||||
upload_files,
|
||||
&mut version_info.libraries,
|
||||
mod_loader,
|
||||
loader,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
entry.server.clone()
|
||||
};
|
||||
|
||||
new_data.insert(
|
||||
key.clone(),
|
||||
daedalus::modded::SidedDataEntry { client, server },
|
||||
);
|
||||
}
|
||||
|
||||
version_info.data = Some(new_data);
|
||||
|
||||
Ok(version_info)
|
||||
} else {
|
||||
Err(crate::ErrorKind::InvalidInput(format!(
|
||||
"Unknown format version {} for loader {}",
|
||||
loader.format_version, loader.installer_url
|
||||
))
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
let forge_version_infos = futures::future::try_join_all(
|
||||
forge_installers
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, raw)| {
|
||||
let loader = fetch_versions[index];
|
||||
|
||||
read_forge_installer(
|
||||
raw,
|
||||
loader,
|
||||
maven_url,
|
||||
mod_loader,
|
||||
upload_files,
|
||||
mirror_artifacts,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let serialized_version_manifests = forge_version_infos
|
||||
.iter()
|
||||
.map(|x| serde_json::to_vec(x).map(bytes::Bytes::from))
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?;
|
||||
|
||||
serialized_version_manifests
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.for_each(|(index, bytes)| {
|
||||
let loader = fetch_versions[index];
|
||||
|
||||
let version_path = format!(
|
||||
"{mod_loader}/v{format_version}/versions/{}.json",
|
||||
loader.loader_version
|
||||
);
|
||||
|
||||
upload_files.insert(
|
||||
version_path,
|
||||
UploadFile {
|
||||
file: bytes,
|
||||
content_type: Some("application/json".to_string()),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
let forge_manifest_path =
|
||||
format!("{mod_loader}/v{format_version}/manifest.json",);
|
||||
|
||||
let manifest = daedalus::modded::Manifest {
|
||||
game_versions: forge_versions
|
||||
.into_iter()
|
||||
.sorted_by(|a, b| b.game_version.cmp(&a.game_version))
|
||||
.rev()
|
||||
.chunk_by(|x| x.game_version.clone())
|
||||
.into_iter()
|
||||
.map(|(game_version, loaders)| daedalus::modded::Version {
|
||||
id: game_version,
|
||||
stable: true,
|
||||
loaders: loaders
|
||||
.map(|x| daedalus::modded::LoaderVersion {
|
||||
url: format_url(&format!(
|
||||
"{mod_loader}/v{format_version}/versions/{}.json",
|
||||
x.loader_version
|
||||
)),
|
||||
id: x.loader_version,
|
||||
stable: false,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
upload_files.insert(
|
||||
forge_manifest_path,
|
||||
UploadFile {
|
||||
file: bytes::Bytes::from(serde_json::to_vec(&manifest)?),
|
||||
content_type: Some("application/json".to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ForgeVersion {
|
||||
pub format_version: usize,
|
||||
pub raw: String,
|
||||
pub loader_version: String,
|
||||
pub game_version: String,
|
||||
pub installer_url: String,
|
||||
}
|
||||
218
apps/daedalus_client/src/main.rs
Normal file
218
apps/daedalus_client/src/main.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use crate::util::{
|
||||
format_url, upload_file_to_bucket, upload_url_to_bucket_mirrors,
|
||||
REQWEST_CLIENT,
|
||||
};
|
||||
use daedalus::get_path_from_artifact;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
mod error;
|
||||
mod fabric;
|
||||
mod forge;
|
||||
mod minecraft;
|
||||
pub mod util;
|
||||
|
||||
pub use error::{Error, ErrorKind, Result};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
.with(fmt::layer())
|
||||
.with(EnvFilter::from_default_env())
|
||||
.with(ErrorLayer::default());
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
|
||||
tracing::info!("Initialized tracing. Starting Daedalus!");
|
||||
|
||||
if check_env_vars() {
|
||||
tracing::error!("Some environment variables are missing!");
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(
|
||||
dotenvy::var("CONCURRENCY_LIMIT")
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok())
|
||||
.unwrap_or(10),
|
||||
));
|
||||
|
||||
// path, upload file
|
||||
let upload_files: DashMap<String, UploadFile> = DashMap::new();
|
||||
// path, mirror artifact
|
||||
let mirror_artifacts: DashMap<String, MirrorArtifact> = DashMap::new();
|
||||
|
||||
minecraft::fetch(semaphore.clone(), &upload_files, &mirror_artifacts)
|
||||
.await?;
|
||||
fabric::fetch_fabric(semaphore.clone(), &upload_files, &mirror_artifacts)
|
||||
.await?;
|
||||
fabric::fetch_quilt(semaphore.clone(), &upload_files, &mirror_artifacts)
|
||||
.await?;
|
||||
forge::fetch_neo(semaphore.clone(), &upload_files, &mirror_artifacts)
|
||||
.await?;
|
||||
forge::fetch_forge(semaphore.clone(), &upload_files, &mirror_artifacts)
|
||||
.await?;
|
||||
|
||||
futures::future::try_join_all(upload_files.iter().map(|x| {
|
||||
upload_file_to_bucket(
|
||||
x.key().clone(),
|
||||
x.value().file.clone(),
|
||||
x.value().content_type.clone(),
|
||||
&semaphore,
|
||||
)
|
||||
}))
|
||||
.await?;
|
||||
|
||||
futures::future::try_join_all(mirror_artifacts.iter().map(|x| {
|
||||
upload_url_to_bucket_mirrors(
|
||||
format!("maven/{}", x.key()),
|
||||
x.value()
|
||||
.mirrors
|
||||
.iter()
|
||||
.map(|mirror| {
|
||||
if mirror.entire_url {
|
||||
mirror.path.clone()
|
||||
} else {
|
||||
format!("{}{}", mirror.path, x.key())
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
x.sha1.clone(),
|
||||
&semaphore,
|
||||
)
|
||||
}))
|
||||
.await?;
|
||||
|
||||
if dotenvy::var("CLOUDFLARE_INTEGRATION")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Ok(token) = dotenvy::var("CLOUDFLARE_TOKEN") {
|
||||
if let Ok(zone_id) = dotenvy::var("CLOUDFLARE_ZONE_ID") {
|
||||
let cache_clears = upload_files
|
||||
.into_iter()
|
||||
.map(|x| format_url(&x.0))
|
||||
.chain(
|
||||
mirror_artifacts
|
||||
.into_iter()
|
||||
.map(|x| format_url(&format!("maven/{}", x.0))),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Cloudflare ratelimits cache clears to 500 files per request
|
||||
for chunk in cache_clears.chunks(500) {
|
||||
REQWEST_CLIENT.post(format!("https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache"))
|
||||
.bearer_auth(&token)
|
||||
.json(&serde_json::json!({
|
||||
"files": chunk
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
ErrorKind::Fetch {
|
||||
inner: err,
|
||||
item: "cloudflare clear cache".to_string(),
|
||||
}
|
||||
})?
|
||||
.error_for_status()
|
||||
.map_err(|err| {
|
||||
ErrorKind::Fetch {
|
||||
inner: err,
|
||||
item: "cloudflare clear cache".to_string(),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct UploadFile {
|
||||
file: bytes::Bytes,
|
||||
content_type: Option<String>,
|
||||
}
|
||||
|
||||
pub struct MirrorArtifact {
|
||||
pub sha1: Option<String>,
|
||||
pub mirrors: DashSet<Mirror>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
pub struct Mirror {
|
||||
path: String,
|
||||
entire_url: bool,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(mirror_artifacts))]
|
||||
pub fn insert_mirrored_artifact(
|
||||
artifact: &str,
|
||||
sha1: Option<String>,
|
||||
mirrors: Vec<String>,
|
||||
entire_url: bool,
|
||||
mirror_artifacts: &DashMap<String, MirrorArtifact>,
|
||||
) -> Result<()> {
|
||||
let val = mirror_artifacts
|
||||
.entry(get_path_from_artifact(artifact)?)
|
||||
.or_insert(MirrorArtifact {
|
||||
sha1,
|
||||
mirrors: DashSet::new(),
|
||||
});
|
||||
|
||||
for mirror in mirrors {
|
||||
val.mirrors.insert(Mirror {
|
||||
path: mirror,
|
||||
entire_url,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_env_vars() -> bool {
|
||||
let mut failed = false;
|
||||
|
||||
fn check_var<T: std::str::FromStr>(var: &str) -> bool {
|
||||
if dotenvy::var(var)
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<T>().ok())
|
||||
.is_none()
|
||||
{
|
||||
tracing::warn!(
|
||||
"Variable `{}` missing in dotenvy or not of type `{}`",
|
||||
var,
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
failed |= check_var::<String>("BASE_URL");
|
||||
|
||||
failed |= check_var::<String>("S3_ACCESS_TOKEN");
|
||||
failed |= check_var::<String>("S3_SECRET");
|
||||
failed |= check_var::<String>("S3_URL");
|
||||
failed |= check_var::<String>("S3_REGION");
|
||||
failed |= check_var::<String>("S3_BUCKET_NAME");
|
||||
|
||||
if dotenvy::var("CLOUDFLARE_INTEGRATION")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
failed |= check_var::<String>("CLOUDFLARE_TOKEN");
|
||||
failed |= check_var::<String>("CLOUDFLARE_ZONE_ID");
|
||||
}
|
||||
|
||||
failed
|
||||
}
|
||||
230
apps/daedalus_client/src/minecraft.rs
Normal file
230
apps/daedalus_client/src/minecraft.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use crate::util::fetch_json;
|
||||
use crate::{
|
||||
util::download_file, util::format_url, util::sha1_async, Error,
|
||||
MirrorArtifact, UploadFile,
|
||||
};
|
||||
use daedalus::minecraft::{
|
||||
merge_partial_library, Library, PartialLibrary, VersionInfo,
|
||||
VersionManifest, VERSION_MANIFEST_URL,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
#[tracing::instrument(skip(semaphore, upload_files, _mirror_artifacts))]
|
||||
pub async fn fetch(
|
||||
semaphore: Arc<Semaphore>,
|
||||
upload_files: &DashMap<String, UploadFile>,
|
||||
_mirror_artifacts: &DashMap<String, MirrorArtifact>,
|
||||
) -> Result<(), Error> {
|
||||
let modrinth_manifest = fetch_json::<VersionManifest>(
|
||||
&format_url(&format!(
|
||||
"minecraft/v{}/manifest.json",
|
||||
daedalus::minecraft::CURRENT_FORMAT_VERSION
|
||||
)),
|
||||
&semaphore,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
let mojang_manifest =
|
||||
fetch_json::<VersionManifest>(VERSION_MANIFEST_URL, &semaphore).await?;
|
||||
|
||||
// TODO: experimental snapshots: https://github.com/PrismLauncher/meta/blob/main/meta/common/mojang-minecraft-experiments.json
|
||||
// TODO: old snapshots: https://github.com/PrismLauncher/meta/blob/main/meta/common/mojang-minecraft-old-snapshots.json
|
||||
|
||||
// We check Modrinth's version manifest and compare if the version 1) exists in Modrinth's database and 2) is unchanged
|
||||
// If they are not, we will fetch them
|
||||
let (fetch_versions, existing_versions) =
|
||||
if let Some(mut modrinth_manifest) = modrinth_manifest {
|
||||
let (mut fetch_versions, mut existing_versions) =
|
||||
(Vec::new(), Vec::new());
|
||||
|
||||
for version in mojang_manifest.versions {
|
||||
if let Some(index) = modrinth_manifest
|
||||
.versions
|
||||
.iter()
|
||||
.position(|x| x.id == version.id)
|
||||
{
|
||||
let modrinth_version =
|
||||
modrinth_manifest.versions.remove(index);
|
||||
|
||||
if modrinth_version
|
||||
.original_sha1
|
||||
.as_ref()
|
||||
.map(|x| x == &version.sha1)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
existing_versions.push(modrinth_version);
|
||||
} else {
|
||||
fetch_versions.push(version);
|
||||
}
|
||||
} else {
|
||||
fetch_versions.push(version);
|
||||
}
|
||||
}
|
||||
|
||||
(fetch_versions, existing_versions)
|
||||
} else {
|
||||
(mojang_manifest.versions, Vec::new())
|
||||
};
|
||||
|
||||
if !fetch_versions.is_empty() {
|
||||
let version_manifests = futures::future::try_join_all(
|
||||
fetch_versions
|
||||
.iter()
|
||||
.map(|x| download_file(&x.url, Some(&x.sha1), &semaphore)),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| serde_json::from_slice(&x))
|
||||
.collect::<Result<Vec<VersionInfo>, serde_json::Error>>()?;
|
||||
|
||||
// Patch libraries of Minecraft versions for M-series Mac Support, Better Linux Compatibility, etc
|
||||
let library_patches = fetch_library_patches()?;
|
||||
let patched_version_manifests = version_manifests
|
||||
.into_iter()
|
||||
.map(|mut x| {
|
||||
if !library_patches.is_empty() {
|
||||
let mut new_libraries = Vec::new();
|
||||
for library in x.libraries {
|
||||
let mut libs = patch_library(&library_patches, library);
|
||||
new_libraries.append(&mut libs)
|
||||
}
|
||||
x.libraries = new_libraries
|
||||
}
|
||||
|
||||
x
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// serialize + compute hashes
|
||||
let serialized_version_manifests = patched_version_manifests
|
||||
.iter()
|
||||
.map(|x| serde_json::to_vec(x).map(bytes::Bytes::from))
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?;
|
||||
let hashes_version_manifests = futures::future::try_join_all(
|
||||
serialized_version_manifests
|
||||
.iter()
|
||||
.map(|x| sha1_async(x.clone())),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// We upload the new version manifests and add them to the versions list
|
||||
let mut new_versions = patched_version_manifests
|
||||
.into_iter()
|
||||
.zip(serialized_version_manifests.into_iter())
|
||||
.zip(hashes_version_manifests.into_iter())
|
||||
.map(|((version, bytes), hash)| {
|
||||
let version_path = format!(
|
||||
"minecraft/v{}/versions/{}.json",
|
||||
daedalus::minecraft::CURRENT_FORMAT_VERSION,
|
||||
version.id
|
||||
);
|
||||
|
||||
let url = format_url(&version_path);
|
||||
upload_files.insert(
|
||||
version_path,
|
||||
UploadFile {
|
||||
file: bytes,
|
||||
content_type: Some("application/json".to_string()),
|
||||
},
|
||||
);
|
||||
|
||||
daedalus::minecraft::Version {
|
||||
original_sha1: fetch_versions
|
||||
.iter()
|
||||
.find(|x| x.id == version.id)
|
||||
.map(|x| x.sha1.clone()),
|
||||
id: version.id,
|
||||
type_: version.type_,
|
||||
url,
|
||||
time: version.time,
|
||||
release_time: version.release_time,
|
||||
sha1: hash,
|
||||
compliance_level: 1,
|
||||
}
|
||||
})
|
||||
.chain(existing_versions.into_iter())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
new_versions.sort_by(|a, b| b.release_time.cmp(&a.release_time));
|
||||
|
||||
// create and upload the new manifest
|
||||
let version_manifest_path = format!(
|
||||
"minecraft/v{}/manifest.json",
|
||||
daedalus::minecraft::CURRENT_FORMAT_VERSION
|
||||
);
|
||||
|
||||
let new_manifest = VersionManifest {
|
||||
latest: mojang_manifest.latest,
|
||||
versions: new_versions,
|
||||
};
|
||||
|
||||
upload_files.insert(
|
||||
version_manifest_path,
|
||||
UploadFile {
|
||||
file: bytes::Bytes::from(serde_json::to_vec(&new_manifest)?),
|
||||
content_type: Some("application/json".to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LibraryPatch {
|
||||
#[serde(rename = "_comment")]
|
||||
pub _comment: String,
|
||||
#[serde(rename = "match")]
|
||||
pub match_: Vec<String>,
|
||||
pub additional_libraries: Option<Vec<Library>>,
|
||||
#[serde(rename = "override")]
|
||||
pub override_: Option<PartialLibrary>,
|
||||
pub patch_additional_libraries: Option<bool>,
|
||||
}
|
||||
|
||||
fn fetch_library_patches() -> Result<Vec<LibraryPatch>, Error> {
|
||||
let patches = include_bytes!("../library-patches.json");
|
||||
Ok(serde_json::from_slice(patches)?)
|
||||
}
|
||||
|
||||
pub fn patch_library(
|
||||
patches: &Vec<LibraryPatch>,
|
||||
mut library: Library,
|
||||
) -> Vec<Library> {
|
||||
let mut val = Vec::new();
|
||||
|
||||
let actual_patches = patches
|
||||
.iter()
|
||||
.filter(|x| x.match_.contains(&library.name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !actual_patches.is_empty() {
|
||||
for patch in actual_patches {
|
||||
if let Some(override_) = &patch.override_ {
|
||||
library = merge_partial_library(override_.clone(), library);
|
||||
}
|
||||
|
||||
if let Some(additional_libraries) = &patch.additional_libraries {
|
||||
for additional_library in additional_libraries {
|
||||
if patch.patch_additional_libraries.unwrap_or(false) {
|
||||
let mut libs =
|
||||
patch_library(patches, additional_library.clone());
|
||||
val.append(&mut libs)
|
||||
} else {
|
||||
val.push(additional_library.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val.push(library);
|
||||
} else {
|
||||
val.push(library);
|
||||
}
|
||||
|
||||
val
|
||||
}
|
||||
234
apps/daedalus_client/src/util.rs
Normal file
234
apps/daedalus_client/src/util.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use crate::{Error, ErrorKind};
|
||||
use bytes::Bytes;
|
||||
use s3::creds::Credentials;
|
||||
use s3::{Bucket, Region};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref BUCKET : Bucket = {
|
||||
let region = dotenvy::var("S3_REGION").unwrap();
|
||||
let b = Bucket::new(
|
||||
&dotenvy::var("S3_BUCKET_NAME").unwrap(),
|
||||
if &*region == "r2" {
|
||||
Region::R2 {
|
||||
account_id: dotenvy::var("S3_URL").unwrap(),
|
||||
}
|
||||
} else {
|
||||
Region::Custom {
|
||||
region: region.clone(),
|
||||
endpoint: dotenvy::var("S3_URL").unwrap(),
|
||||
}
|
||||
},
|
||||
Credentials::new(
|
||||
Some(&*dotenvy::var("S3_ACCESS_TOKEN").unwrap()),
|
||||
Some(&*dotenvy::var("S3_SECRET").unwrap()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
|
||||
if region == "path-style" {
|
||||
b.with_path_style()
|
||||
} else {
|
||||
b
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref REQWEST_CLIENT: reqwest::Client = {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
if let Ok(header) = reqwest::header::HeaderValue::from_str(&format!(
|
||||
"modrinth/daedalus/{} (support@modrinth.com)",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)) {
|
||||
headers.insert(reqwest::header::USER_AGENT, header);
|
||||
}
|
||||
|
||||
reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(std::time::Duration::from_secs(10)))
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(bytes, semaphore))]
|
||||
pub async fn upload_file_to_bucket(
|
||||
path: String,
|
||||
bytes: Bytes,
|
||||
content_type: Option<String>,
|
||||
semaphore: &Arc<Semaphore>,
|
||||
) -> Result<(), Error> {
|
||||
let _permit = semaphore.acquire().await?;
|
||||
let key = path.clone();
|
||||
|
||||
const RETRIES: i32 = 3;
|
||||
for attempt in 1..=(RETRIES + 1) {
|
||||
tracing::trace!("Attempting file upload, attempt {attempt}");
|
||||
let result = if let Some(ref content_type) = content_type {
|
||||
BUCKET
|
||||
.put_object_with_content_type(key.clone(), &bytes, content_type)
|
||||
.await
|
||||
} else {
|
||||
BUCKET.put_object(key.clone(), &bytes).await
|
||||
}
|
||||
.map_err(|err| ErrorKind::S3 {
|
||||
inner: err,
|
||||
file: path.clone(),
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(_) if attempt <= RETRIES => continue,
|
||||
Err(_) => {
|
||||
result?;
|
||||
}
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub async fn upload_url_to_bucket_mirrors(
|
||||
upload_path: String,
|
||||
mirrors: Vec<String>,
|
||||
sha1: Option<String>,
|
||||
semaphore: &Arc<Semaphore>,
|
||||
) -> Result<(), Error> {
|
||||
if mirrors.is_empty() {
|
||||
return Err(ErrorKind::InvalidInput(
|
||||
"No mirrors provided!".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
for (index, mirror) in mirrors.iter().enumerate() {
|
||||
let result = upload_url_to_bucket(
|
||||
upload_path.clone(),
|
||||
mirror.clone(),
|
||||
sha1.clone(),
|
||||
semaphore,
|
||||
)
|
||||
.await;
|
||||
|
||||
if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(semaphore))]
|
||||
pub async fn upload_url_to_bucket(
|
||||
path: String,
|
||||
url: String,
|
||||
sha1: Option<String>,
|
||||
semaphore: &Arc<Semaphore>,
|
||||
) -> Result<(), Error> {
|
||||
let data = download_file(&url, sha1.as_deref(), semaphore).await?;
|
||||
|
||||
upload_file_to_bucket(path, data, None, semaphore).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(bytes))]
|
||||
pub async fn sha1_async(bytes: Bytes) -> Result<String, Error> {
|
||||
let hash = tokio::task::spawn_blocking(move || {
|
||||
sha1_smol::Sha1::from(bytes).hexdigest()
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(semaphore))]
|
||||
pub async fn download_file(
|
||||
url: &str,
|
||||
sha1: Option<&str>,
|
||||
semaphore: &Arc<Semaphore>,
|
||||
) -> Result<bytes::Bytes, crate::Error> {
|
||||
let _permit = semaphore.acquire().await?;
|
||||
tracing::trace!("Starting file download");
|
||||
|
||||
const RETRIES: u32 = 10;
|
||||
for attempt in 1..=(RETRIES + 1) {
|
||||
let result = REQWEST_CLIENT
|
||||
.get(url.replace("http://", "https://"))
|
||||
.send()
|
||||
.await
|
||||
.and_then(|x| x.error_for_status());
|
||||
|
||||
match result {
|
||||
Ok(x) => {
|
||||
let bytes = x.bytes().await;
|
||||
|
||||
if let Ok(bytes) = bytes {
|
||||
if let Some(sha1) = sha1 {
|
||||
if &*sha1_async(bytes.clone()).await? != sha1 {
|
||||
if attempt <= 3 {
|
||||
continue;
|
||||
} else {
|
||||
return Err(
|
||||
crate::ErrorKind::ChecksumFailure {
|
||||
hash: sha1.to_string(),
|
||||
url: url.to_string(),
|
||||
tries: attempt,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(bytes);
|
||||
} else if attempt <= RETRIES {
|
||||
continue;
|
||||
} else if let Err(err) = bytes {
|
||||
return Err(crate::ErrorKind::Fetch {
|
||||
inner: err,
|
||||
item: url.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
Err(_) if attempt <= RETRIES => continue,
|
||||
Err(err) => {
|
||||
return Err(crate::ErrorKind::Fetch {
|
||||
inner: err,
|
||||
item: url.to_string(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub async fn fetch_json<T: DeserializeOwned>(
|
||||
url: &str,
|
||||
semaphore: &Arc<Semaphore>,
|
||||
) -> Result<T, Error> {
|
||||
Ok(serde_json::from_slice(
|
||||
&download_file(url, None, semaphore).await?,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub async fn fetch_xml<T: DeserializeOwned>(
|
||||
url: &str,
|
||||
semaphore: &Arc<Semaphore>,
|
||||
) -> Result<T, Error> {
|
||||
Ok(serde_xml_rs::from_reader(
|
||||
&*download_file(url, None, semaphore).await?,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn format_url(path: &str) -> String {
|
||||
format!("{}/{}", &*dotenvy::var("BASE_URL").unwrap(), path)
|
||||
}
|
||||
21
apps/docs/.gitignore
vendored
Normal file
21
apps/docs/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user