Compare commits
820 Commits
v0.8.7
...
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 | ||
|
|
5b5599128a | ||
|
|
cb0f03ca9c | ||
|
|
2e35f3608b | ||
|
|
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 |
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
@@ -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
@@ -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
@@ -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 }}
|
||||
@@ -41,9 +41,6 @@ jobs:
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Install Tauri CLI
|
||||
run: cargo install tauri-cli --git https://github.com/modrinth/tauri.git --rev 5e2942876c2266594ed1db516c1d9975c873c36a
|
||||
|
||||
- name: Setup rust cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -65,7 +62,7 @@ jobs:
|
||||
!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
|
||||
@@ -121,7 +118,6 @@ jobs:
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
V1_COMPATIBLE_BIN_NAME: true
|
||||
|
||||
- name: build app
|
||||
run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json"
|
||||
@@ -131,7 +127,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
V1_COMPATIBLE_BIN_NAME: true
|
||||
|
||||
- name: upload ${{ matrix.platform }}
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -154,7 +149,7 @@ jobs:
|
||||
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
|
||||
@@ -32,9 +32,6 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install Tauri CLI
|
||||
run: cargo install tauri-cli --git https://github.com/modrinth/tauri.git --rev 5e2942876c2266594ed1db516c1d9975c873c36a
|
||||
|
||||
- name: Setup Node.JS environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -65,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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
4863
Cargo.lock
generated
@@ -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
|
||||
@@ -18,5 +21,4 @@ strip = true # Remove debug symbols
|
||||
opt-level = 3
|
||||
|
||||
[patch.crates-io]
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "5840108" }
|
||||
tauri = { git = "https://github.com/modrinth/tauri", rev = "5e29428" }
|
||||
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,7 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -218,7 +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 { listen } from '@tauri-apps/api/event'
|
||||
import { install_from_file } from '@/helpers/pack.js'
|
||||
import {
|
||||
get_default_launcher_path,
|
||||
@@ -226,6 +225,7 @@ import {
|
||||
import_instance,
|
||||
} from '@/helpers/import.js'
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
|
||||
const profile_name = ref('')
|
||||
const game_version = ref('')
|
||||
@@ -255,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',
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 } from '@/helpers/ads.js'
|
||||
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)
|
||||
@@ -73,6 +73,11 @@ function updateAdPosition(overrideShown = false) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -105,17 +110,17 @@ onUnmounted(() => {
|
||||
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">90% of ad revenue goes to creators</p>
|
||||
<a
|
||||
href="https://modrinth.com/plus"
|
||||
class="mt-auto items-center gap-1 text-purple hover:underline"
|
||||
<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" />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -230,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 }}
|
||||
|
||||
@@ -11,3 +11,11 @@ export async function 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 })
|
||||
}
|
||||
|
||||
@@ -363,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>
|
||||
@@ -454,7 +473,7 @@ async function purgeCache() {
|
||||
:min="8"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
unit="mb"
|
||||
unit="MB"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -378,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 +392,7 @@ import {
|
||||
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: {
|
||||
@@ -879,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)
|
||||
}
|
||||
|
||||
@@ -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.7"
|
||||
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/"
|
||||
@@ -8,7 +8,7 @@ edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { git = "https://github.com/modrinth/tauri", features = ["codegen"], rev = "5e29428" }
|
||||
tauri-build = { version = "2.0.0-rc", features = ["codegen"] }
|
||||
|
||||
[dependencies]
|
||||
theseus = { path = "../../packages/app-lib", features = ["tauri"] }
|
||||
@@ -16,7 +16,7 @@ theseus = { path = "../../packages/app-lib", features = ["tauri"] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
tauri = { git = "https://github.com/modrinth/tauri", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"], rev = "5e29428" }
|
||||
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"
|
||||
@@ -28,7 +28,7 @@ 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"
|
||||
|
||||
@@ -22,7 +22,6 @@ Before you begin, ensure you have the following installed on your machine:
|
||||
Follow these steps to set up your development environment:
|
||||
|
||||
```bash
|
||||
cargo install tauri-cli --git https://github.com/modrinth/tauri.git --rev 5e2942876c2266594ed1db516c1d9975c873c36a
|
||||
pnpm install
|
||||
pnpm app:dev
|
||||
```
|
||||
|
||||
@@ -226,6 +226,9 @@ fn main() {
|
||||
"hide_ads_window",
|
||||
"scroll_ads_window",
|
||||
"show_ads_window",
|
||||
"record_ads_click",
|
||||
"open_link",
|
||||
"get_ads_personalization",
|
||||
])
|
||||
.default_permission(
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"ads-window"
|
||||
],
|
||||
"permissions": [
|
||||
"shell:allow-open",
|
||||
"ads:default"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
"identifier": "plugins",
|
||||
"description": "",
|
||||
"local": true,
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-confirm",
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"ads":{"identifier":"ads","description":"","remote":{"urls":["https://modrinth.com/*","http://localhost:3000/*"]},"local":false,"webviews":["ads-window"],"permissions":["shell:allow-open","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"]}}
|
||||
{"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"]}}
|
||||
3314
apps/app/gen/schemas/windows-schema.json
Normal file
@@ -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>
|
||||
@@ -1,15 +1,19 @@
|
||||
{
|
||||
"name": "@modrinth/app",
|
||||
"scripts": {
|
||||
"build": "cargo tauri build",
|
||||
"tauri": "cargo tauri",
|
||||
"dev": "cargo tauri dev",
|
||||
"build": "tauri build",
|
||||
"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.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/app-frontend": "workspace:*",
|
||||
"@modrinth/app-lib": "workspace:*"
|
||||
"@modrinth/app-lib": "workspace:*",
|
||||
"@modrinth/daedalus": "workspace:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
if (!window.modrinthClickListener) {
|
||||
window.modrinthClickListener = true
|
||||
document.addEventListener('click', function (e) {
|
||||
document.addEventListener(
|
||||
'click',
|
||||
function (e) {
|
||||
window.top.postMessage({ modrinthAdClick: true }, 'https://modrinth.com')
|
||||
|
||||
let target = e.target
|
||||
while (target != null) {
|
||||
if (target.matches('a')) {
|
||||
@@ -12,8 +14,9 @@ if (!window.modrinthClickListener) {
|
||||
}
|
||||
target = target.parentElement
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
window.open = (url, target, features) => {
|
||||
window.top.postMessage({ modrinthOpenUrl: url }, 'https://modrinth.com')
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
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| {
|
||||
@@ -16,6 +24,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
shown: true,
|
||||
size: None,
|
||||
position: None,
|
||||
last_click: None,
|
||||
malicious_origins: HashSet::new(),
|
||||
}));
|
||||
|
||||
// We refresh the ads window every 5 minutes for performance
|
||||
@@ -24,11 +34,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
loop {
|
||||
if let Some(webview) = app.webviews().get_mut("ads-window")
|
||||
{
|
||||
let _ = webview.navigate(
|
||||
"https://modrinth.com/wrapper/app-ads-cookie"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
let _ = webview.navigate(AD_LINK.parse().unwrap());
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60 * 5))
|
||||
@@ -43,6 +49,9 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
hide_ads_window,
|
||||
scroll_ads_window,
|
||||
show_ads_window,
|
||||
record_ads_click,
|
||||
open_link,
|
||||
get_ads_personalization,
|
||||
])
|
||||
.build()
|
||||
}
|
||||
@@ -79,7 +88,7 @@ pub async fn init_ads_window<R: Runtime>(
|
||||
tauri::webview::WebviewBuilder::new(
|
||||
"ads-window",
|
||||
WebviewUrl::External(
|
||||
"https://modrinth.com/wrapper/app-ads-cookie".parse().unwrap(),
|
||||
AD_LINK.parse().unwrap(),
|
||||
),
|
||||
)
|
||||
.initialization_script(LINK_SCRIPT)
|
||||
@@ -159,3 +168,49 @@ pub async fn scroll_ads_window<R: Runtime>(
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -317,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();
|
||||
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "http://timestamp.digicert.com",
|
||||
"wix": {
|
||||
"template": "./msi/main.wxs"
|
||||
},
|
||||
"nsis": {
|
||||
"installMode": "perMachine",
|
||||
"installerHooks": "./nsis/hooks.nsi"
|
||||
@@ -52,7 +49,8 @@
|
||||
]
|
||||
},
|
||||
"productName": "Modrinth App",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.9",
|
||||
"mainBinaryName": "Modrinth App",
|
||||
"identifier": "ModrinthApp",
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
|
||||
3
apps/app/tauri.linux.conf.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"mainBinaryName": "ModrinthApp"
|
||||
}
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
4
apps/docs/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
apps/docs/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
apps/docs/LICENSE
Normal file
@@ -0,0 +1,34 @@
|
||||
Creative Commons Legal Code
|
||||
CC0 1.0 Universal
|
||||
Official translations of this legal tool are available
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:
|
||||
|
||||
the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
|
||||
moral rights retained by the original author(s) and/or performer(s);
|
||||
publicity and privacy rights pertaining to a person's image or likeness depicted in a Work;
|
||||
rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below;
|
||||
rights protecting the extraction, dissemination, use and reuse of data in a Work;
|
||||
database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
|
||||
other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
|
||||
Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
|
||||
Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
|
||||
Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
|
||||
23
apps/docs/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Modrinth Documentation
|
||||
|
||||
Welcome to the Modrinth documentation!
|
||||
|
||||
## Development
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
Before you begin, ensure you have the following installed on your machine:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/)
|
||||
- [pnpm](https://pnpm.io/)
|
||||
|
||||
### Setup
|
||||
|
||||
Follow these steps to set up your development environment:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm docs:dev
|
||||
```
|
||||
|
||||
You should now have a development build of the documentation site running with hot-reloading enabled. Any changes you make to the code will automatically refresh the browser.
|
||||
52
apps/docs/astro.config.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
import starlight from '@astrojs/starlight'
|
||||
import { defineConfig } from 'astro/config'
|
||||
import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi'
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://docs.modrinth.com',
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'Modrinth Documentation',
|
||||
favicon: '/favicon.ico',
|
||||
editLink: {
|
||||
baseUrl: 'https://github.com/modrinth/code/edit/main/apps/docs/',
|
||||
},
|
||||
social: {
|
||||
github: 'https://github.com/modrinth/code',
|
||||
discord: 'https://discord.modrinth.com',
|
||||
'x.com': 'https://x.com/modrinth',
|
||||
mastodon: 'https://floss.social/@modrinth',
|
||||
threads: 'https://threads.net/@modrinth',
|
||||
},
|
||||
logo: {
|
||||
light: './src/assets/light-logo.svg',
|
||||
dark: './src/assets/dark-logo.svg',
|
||||
replacesTitle: true,
|
||||
},
|
||||
customCss: [
|
||||
'@modrinth/assets/styles/variables.scss',
|
||||
'@modrinth/assets/styles/inter.scss',
|
||||
'./src/styles/modrinth.css',
|
||||
],
|
||||
plugins: [
|
||||
// Generate the OpenAPI documentation pages.
|
||||
starlightOpenAPI([
|
||||
{
|
||||
base: 'api',
|
||||
label: 'Modrinth API',
|
||||
schema: './public/openapi.yaml',
|
||||
},
|
||||
]),
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Contributing to Modrinth',
|
||||
autogenerate: { directory: 'contributing' },
|
||||
},
|
||||
// Add the generated sidebar group to the sidebar.
|
||||
...openAPISidebarGroups,
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
21
apps/docs/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@modrinth/docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.3",
|
||||
"@astrojs/starlight": "^0.26.3",
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"astro": "^4.10.2",
|
||||
"sharp": "^0.32.5",
|
||||
"starlight-openapi": "^0.7.0",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
BIN
apps/docs/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 24 KiB |
3882
apps/docs/public/openapi.yaml
Normal file
79
apps/docs/public/welcome-channel.yaml
Normal file
@@ -0,0 +1,79 @@
|
||||
- type: text
|
||||
text: https://cdn.discordapp.com/attachments/734084240408444949/975414177550200902/welcome-channel.png
|
||||
|
||||
- type: embed
|
||||
embeds:
|
||||
- title: __Welcome to Modrinth's Discord server!__
|
||||
url: https://modrinth.com
|
||||
color: 0x1bd96a
|
||||
description: "Modrinth is the place for Minecraft mods, plugins, data packs, shaders, resource packs, and
|
||||
modpacks. Discover, play, and share Minecraft content through our open-source platform built for the community."
|
||||
|
||||
- type: embed
|
||||
embeds:
|
||||
- title: "**:scroll: __Rules__**"
|
||||
color: 0x4f9cff
|
||||
description: "Modrinth's rules are easy to follow. Despite this, please keep in mind that this is not an entirely
|
||||
open forum. First and foremost, this Discord server is intended to facilitate the development of Modrinth and
|
||||
for communication regarding Modrinth. Ultimately, it is up to the discretion of the moderators whether your
|
||||
messages are in violation of our rules.\n\n
|
||||
Modrinth's rules are split up into two categories: the **__DOs__** and the **__DO NOTs__**."
|
||||
- title: ":white_check_mark: Do:"
|
||||
color: 0x1bd96a
|
||||
description: >-
|
||||
1. Treat every user with respect and consider the opinions and viewpoints of others
|
||||
|
||||
2. Stay on-topic in all channels; all channels are only for discussion of **Modrinth itself** with the
|
||||
exceptions of <#783091855616901200>, <#1109517383074328686>, and <#1061855024252207167>
|
||||
|
||||
3. Follow Discord's rules, including the [Community Guidelines](https://discord.com/guidelines) and the [Terms
|
||||
of Service](https://discord.com/terms) (this also means that discussions regarding "cracked" launchers and
|
||||
Discord client modifications are **prohibited under all circumstances**)
|
||||
|
||||
4. Contact the moderators at any time via the <@&895382919772766219> ping
|
||||
|
||||
5. Respect the use of accessibility and self-identity tools such as [PluralKit](https://pluralkit.me)
|
||||
- title: ":no_entry: Do not:"
|
||||
color: 0xff496e
|
||||
description: >-
|
||||
6. Harass, bother, provoke, or insult anyone, including by sending unsolicited DMs or friend requests
|
||||
|
||||
7. Cause problems or impede Modrinth's development
|
||||
|
||||
8. Discuss drama from other places, including bashing or hating on other websites and platforms (though
|
||||
constructive criticism for the betterment of Modrinth is encouraged)
|
||||
|
||||
9. Report Modrinth content in the Discord (use the Report button on the website)
|
||||
|
||||
10. Assume staff member's opinions reflect those of Modrinth
|
||||
- title: ":pencil2: Nickname policy:"
|
||||
color: 0xffa347
|
||||
description: >-
|
||||
We want to keep this server clean and therefore require that display names of all members on the server are
|
||||
readable, accessible, and free of attention-seeking elements, which includes, but is not limited to, display
|
||||
names that begin with hoisting characters, have an excessive number of emojis in them, or use "fancy fonts",
|
||||
"glitch effects" and any other Unicode characters, which are either very inaccessible to screen readers or cause
|
||||
annoyance to other members.
|
||||
|
||||
When we find that your display name does not adhere to this policy, we will try to correct it by changing your
|
||||
nickname on the server. Repetitive attempts to revert to a violating display name may result in your removal
|
||||
from the server. We will also permanently remove any users whose profiles contain inappropriate content.
|
||||
- type: links
|
||||
color: 0x4f9cff
|
||||
title: "**:link: __Links__**"
|
||||
links:
|
||||
Website: https://modrinth.com
|
||||
Support: https://support.modrinth.com
|
||||
Status page: https://status.modrinth.com
|
||||
Roadmap: https://roadmap.modrinth.com
|
||||
Blog and newsletter: https://blog.modrinth.com/subscribe?utm_medium=social&utm_source=discord&utm_campaign=welcome
|
||||
API documentation: https://docs.modrinth.com
|
||||
Modrinth source code: https://github.com/modrinth
|
||||
Help translate Modrinth: https://crowdin.com/project/modrinth
|
||||
Follow Modrinth on Mastodon: https://floss.social/@modrinth
|
||||
Follow Modrinth on Twitter: https://twitter.com/modrinth
|
||||
|
||||
- type: text
|
||||
text: |
|
||||
**Main server:** <https://discord.modrinth.com>
|
||||
**Testing server:** <https://discord.modrinth.com/labs>
|
||||
21
apps/docs/src/assets/dark-logo.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1558 207.2">
|
||||
<!-- Generator: Adobe Illustrator 28.6.0, SVG Export Plug-In . SVG Version: 1.2.0 Build 709) -->
|
||||
<g>
|
||||
<g id="Layer_1">
|
||||
<g id="icon" fill="#1bd96a">
|
||||
<path d="M146.1,109.8l-14.4,17.6-24.1,7.6-10.8-12-61.9,37.1c-2.9-3.8-6.4-9.1-8.5-14.3l61.7-37-5.6-14.9,17.6-18.1,22.3-4.8,6.4,7.9-10.3,10.4-8.9,2.8-6.4,6.6,3.1,8.7,6.4,6.8,9-2.4,6.4-7,13.9-4.4s4.1,9.3,4.1,9.3Z" fill-rule="evenodd"/>
|
||||
<path d="M208.6,86.6l-48.9,13.2c-.8-4.7-1.6-8.9-4.3-16.1l49-13.2c2.1,5.5,3.5,10.9,4.2,16.1Z" fill-rule="evenodd"/>
|
||||
<path d="M33.7,110.5c3.5,41,37.9,73.2,79.7,73.2s59-18.4,72-45.1l15.9,5.5c-15.3,33.2-48.9,56.3-87.9,56.3S20.3,160.7,16.8,110.5c0,0,16.8,0,16.8,0ZM17.1,93.8C22,45,63.3,6.8,113.4,6.8s96.8,43.4,96.8,96.8-1.1,16.9-3.2,24.8l-15.9-5.5c1.6-6.3,2.4-12.8,2.3-19.3,0-44.2-35.9-80-80-80S38.8,54.3,34,93.8h-16.9,0Z" fill-rule="evenodd"/>
|
||||
<path d="M113.1,57c-25.7,0-46.6,20.9-46.6,46.7s20.9,46.7,46.7,46.7,2.6,0,3.9-.2l4.7,16.3c-2.8.4-5.7.6-8.5.6-35,0-63.4-28.4-63.4-63.4s28.4-63.4,63.4-63.4,1.7,0,2.6,0c0,0-2.6,16.7-2.6,16.7ZM132.2,43.2c25.7,8.1,44.4,32.1,44.4,60.5s-16,48.8-38.7,58.4l-4.6-16.2c15.8-7.5,26.7-23.6,26.7-42.2s-12.6-37.1-30.3-43.7l2.7-16.8h0Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<path id="modrinth" fill="#fff" d="M362,74.2c11.7,0,21.1,3.3,27.9,10.1,6.8,7,10.3,17.1,10.3,30.6v53.3h-21.8v-50.5c0-8.2-1.7-14.3-5.4-18.4-3.6-4-8.7-6.1-15.5-6.1s-13.2,2.4-17.6,7.1c-4.4,4.9-6.6,11.8-6.6,20.7v47.2h-21.8v-50.5c0-8.2-1.7-14.3-5.4-18.4-3.6-4-8.7-6.1-15.5-6.1s-13.2,2.4-17.6,7.1c-4.4,4.7-6.6,11.7-6.6,20.7v47.2h-21.8v-93h20.7v11.8c3.5-4.2,7.8-7.3,13.1-9.6,5.2-2.3,11-3.3,17.4-3.3s13.2,1.2,18.6,3.8c5.4,2.8,9.6,6.6,12.7,11.7,3.8-4.9,8.7-8.7,14.8-11.5,6.1-2.6,12.7-4,20-4h0ZM470.1,169.4c-9.4,0-17.9-2.1-25.4-6.1-7.4-3.9-13.5-9.8-17.6-17.1-4.4-7.1-6.4-15.3-6.4-24.6s2.1-17.4,6.4-24.5c4.2-7.2,10.3-13,17.6-16.9,7.5-4,16-6.1,25.4-6.1s18.1,2.1,25.6,6.1c7.5,4,13.4,9.8,17.8,16.9,4.2,7.1,6.3,15.3,6.3,24.5s-2.1,17.4-6.3,24.6c-4.4,7.3-10.3,13.1-17.8,17.1-7.5,4-16,6.1-25.6,6.1h0ZM470.1,150.8c8,0,14.6-2.6,19.9-8s7.8-12.4,7.8-21.1-2.6-15.7-7.8-21.1-11.8-8-19.9-8-14.6,2.6-19.7,8c-5.2,5.4-7.8,12.4-7.8,21.1s2.6,15.7,7.8,21.1c5,5.4,11.7,8,19.7,8ZM631.2,39v129.2h-20.9v-12c-3.6,4.3-8.1,7.7-13.2,9.9-5.4,2.3-11.1,3.3-17.6,3.3s-16.9-1.9-24-5.9-12.9-9.6-16.9-16.9c-4-7.1-6.1-15.5-6.1-24.9s2.1-17.8,6.1-24.9c4-7.1,9.8-12.7,16.9-16.7s15.1-5.9,24-5.9,11.8,1.1,16.9,3.1c5,2.1,9.5,5.3,13.1,9.4v-47.7h21.8,0ZM582.1,150.8c5.2,0,9.9-1.2,14.1-3.7,4.2-2.3,7.5-5.7,9.9-10.1,2.4-4.4,3.7-9.6,3.7-15.3s-1.2-11-3.7-15.3c-2.4-4.3-5.8-7.8-9.9-10.3-4.2-2.3-8.9-3.5-14.1-3.5s-9.9,1.2-14.1,3.5c-4.2,2.4-7.5,5.9-9.9,10.3-2.4,4.3-3.7,9.6-3.7,15.3s1.2,11,3.7,15.3c2.4,4.4,5.8,7.8,9.9,10.1,4.2,2.4,8.9,3.7,14.1,3.7ZM679.9,88.8c6.3-9.8,17.2-14.6,33.1-14.6v20.7c-1.7-.3-3.3-.5-5-.5-8.5,0-15.2,2.4-19.8,7.3-4.7,5.1-7.1,12.2-7.1,21.4v45.1h-21.8v-93h20.7v13.6h0ZM730.9,75.2h21.8v93h-21.8s0-93,0-93ZM741.9,59.9c-4,0-7.3-1.2-9.9-3.8-2.6-2.4-4-5.7-4-9.2,0-3.7,1.4-6.8,4-9.4,2.6-2.4,5.9-3.7,9.9-3.7s7.3,1.2,9.9,3.5c2.6,2.4,4,5.4,4,9s-1.2,7-3.8,9.6c-2.6,2.6-6.1,4-10.1,4ZM833.5,74.2c11.7,0,21.1,3.5,28.2,10.3s10.6,17.1,10.6,30.5v53.3h-21.8v-50.5c0-8.2-1.9-14.3-5.7-18.4-3.8-4-9.2-6.1-16.4-6.1s-14.3,2.4-19,7.1c-4.7,4.9-7,11.8-7,20.9v47h-21.8v-93h20.7v12c3.7-4.3,8.2-7.5,13.6-9.8s11.7-3.3,18.5-3.3h0ZM955.3,163.2c-2.7,2.2-5.9,3.8-9.2,4.7-3.8,1.1-7.7,1.6-11.7,1.6-10.3,0-18.1-2.6-23.7-8-5.6-5.4-8.4-13.1-8.4-23.3v-44.8h-15.3v-17.4h15.3v-21.2h21.8v21.2h24.9v17.4h-24.9v44.2c0,4.5,1,8,3.3,10.3,2.3,2.4,5.4,3.6,9.6,3.6s8.9-1.2,12.2-3.8l6.1,15.5h0ZM1025.5,74.2c11.7,0,21.1,3.5,28.2,10.3s10.6,17.1,10.6,30.5v53.3h-21.8v-50.5c0-8.2-1.9-14.3-5.8-18.4-3.8-4-9.2-6.1-16.4-6.1s-14.3,2.4-19,7.1c-4.7,4.9-7,11.8-7,20.9v47h-21.8V39h21.8v47c3.7-3.8,8-6.8,13.4-8.9,5.2-1.9,11.1-3,17.6-3h0Z"/>
|
||||
<g id="docs" fill="#1bd96a">
|
||||
<path d="M1177.8,169.7c-8.9,0-16.9-2-24-6-7.1-4-12.7-9.5-16.7-16.6-4-7.1-6.1-15.4-6.1-25s2-17.9,6.1-25c4-7.1,9.6-12.6,16.7-16.5,7.1-3.9,15.1-5.9,24-5.9s14.7,1.7,20.8,5.1c6.1,3.4,11,8.6,14.6,15.5,3.6,6.9,5.4,15.9,5.4,26.7s-1.7,19.6-5.2,26.6c-3.5,7-8.3,12.2-14.4,15.7-6.1,3.5-13.2,5.2-21.2,5.2ZM1180.4,151.2c5.2,0,9.9-1.2,14-3.5,4.1-2.3,7.4-5.7,9.9-10.1,2.5-4.4,3.7-9.5,3.7-15.4s-1.2-11.2-3.7-15.5c-2.5-4.3-5.8-7.7-9.9-10-4.1-2.3-8.8-3.5-14-3.5s-9.9,1.2-14,3.5c-4.1,2.3-7.4,5.6-9.9,10-2.5,4.3-3.7,9.5-3.7,15.5s1.2,11,3.7,15.4c2.5,4.4,5.8,7.8,9.9,10.1,4.1,2.3,8.8,3.5,14,3.5ZM1208.6,168.5v-21.9l.9-24.6-1.7-24.6v-57.6h21.5v128.8h-20.7Z"/>
|
||||
<path d="M1299.5,169.7c-9.5,0-17.9-2.1-25.3-6.2-7.4-4.1-13.2-9.7-17.5-16.9-4.3-7.2-6.4-15.3-6.4-24.5s2.1-17.4,6.4-24.6c4.3-7.1,10.1-12.7,17.5-16.7,7.4-4,15.8-6.1,25.3-6.1s18.1,2,25.6,6.1c7.5,4.1,13.3,9.6,17.5,16.7,4.2,7.1,6.3,15.3,6.3,24.6s-2.1,17.3-6.3,24.5c-4.2,7.2-10.1,12.8-17.5,16.9-7.5,4.1-16,6.2-25.6,6.2ZM1299.5,151.2c5.3,0,10.1-1.2,14.2-3.5,4.2-2.3,7.4-5.7,9.8-10.1,2.4-4.4,3.6-9.5,3.6-15.4s-1.2-11.2-3.6-15.5c-2.4-4.3-5.6-7.7-9.8-10-4.2-2.3-8.8-3.5-14.1-3.5s-10,1.2-14.1,3.5c-4.1,2.3-7.4,5.6-9.8,10-2.4,4.3-3.6,9.5-3.6,15.5s1.2,11,3.6,15.4c2.4,4.4,5.7,7.8,9.8,10.1,4.1,2.3,8.8,3.5,14,3.5Z"/>
|
||||
<path d="M1412.3,169.7c-9.7,0-18.4-2.1-25.9-6.2-7.6-4.1-13.5-9.7-17.8-16.9-4.3-7.2-6.4-15.3-6.4-24.5s2.1-17.4,6.4-24.6c4.3-7.1,10.2-12.7,17.8-16.7,7.6-4,16.2-6.1,25.9-6.1s17,1.8,23.9,5.5c6.9,3.6,12.1,9,15.7,16.1l-16.7,9.7c-2.8-4.4-6.2-7.6-10.2-9.7-4-2.1-8.3-3.1-12.9-3.1s-10.1,1.2-14.4,3.5c-4.3,2.3-7.6,5.6-10.1,10-2.4,4.3-3.6,9.5-3.6,15.5s1.2,11.2,3.6,15.5c2.4,4.3,5.8,7.7,10.1,10,4.3,2.3,9.1,3.5,14.4,3.5s8.9-1,12.9-3.1c4-2.1,7.4-5.3,10.2-9.7l16.7,9.7c-3.6,6.9-8.8,12.3-15.7,16.1-6.9,3.8-14.8,5.6-23.9,5.6Z"/>
|
||||
<path d="M1499.2,169.7c-7.8,0-15.2-1-22.3-3-7.1-2-12.8-4.5-16.9-7.4l8.3-16.5c4.2,2.7,9.1,4.9,14.9,6.6,5.8,1.7,11.6,2.6,17.4,2.6s11.8-.9,14.8-2.8c3.1-1.8,4.6-4.3,4.6-7.5s-1-4.5-3.1-5.8c-2.1-1.3-4.8-2.3-8.2-3-3.4-.7-7.1-1.3-11.2-1.9-4.1-.6-8.2-1.4-12.3-2.3-4.1-1-7.8-2.4-11.2-4.3-3.4-1.9-6.1-4.5-8.2-7.7-2.1-3.2-3.1-7.5-3.1-12.8s1.7-11,5-15.4c3.4-4.3,8.1-7.7,14.1-10.1,6.1-2.4,13.3-3.6,21.6-3.6s12.6.7,18.9,2.1c6.4,1.4,11.6,3.4,15.8,5.9l-8.3,16.5c-4.4-2.7-8.8-4.5-13.3-5.5-4.5-1-8.9-1.5-13.3-1.5-6.6,0-11.5,1-14.7,2.9-3.2,2-4.8,4.5-4.8,7.5s1,4.9,3.1,6.2c2.1,1.4,4.8,2.5,8.2,3.3,3.4.8,7.1,1.5,11.2,2,4.1.5,8.2,1.3,12.2,2.3,4,1,7.8,2.5,11.2,4.3,3.4,1.8,6.2,4.3,8.2,7.5,2.1,3.2,3.1,7.5,3.1,12.7s-1.7,10.8-5.1,15.1c-3.4,4.3-8.2,7.6-14.5,10-6.2,2.4-13.7,3.6-22.4,3.6Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
21
apps/docs/src/assets/light-logo.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1558 207.2">
|
||||
<!-- Generator: Adobe Illustrator 28.6.0, SVG Export Plug-In . SVG Version: 1.2.0 Build 709) -->
|
||||
<g>
|
||||
<g id="Layer_1">
|
||||
<g id="icon" fill="#00af5c">
|
||||
<path d="M146.1,109.8l-14.4,17.6-24.1,7.6-10.8-12-61.9,37.1c-2.9-3.8-6.4-9.1-8.5-14.3l61.7-37-5.6-14.9,17.6-18.1,22.3-4.8,6.4,7.9-10.3,10.4-8.9,2.8-6.4,6.6,3.1,8.7,6.4,6.8,9-2.4,6.4-7,13.9-4.4s4.1,9.3,4.1,9.3Z" fill-rule="evenodd"/>
|
||||
<path d="M208.6,86.6l-48.9,13.2c-.8-4.7-1.6-8.9-4.3-16.1l49-13.2c2.1,5.5,3.5,10.9,4.2,16.1Z" fill-rule="evenodd"/>
|
||||
<path d="M33.7,110.5c3.5,41,37.9,73.2,79.7,73.2s59-18.4,72-45.1l15.9,5.5c-15.3,33.2-48.9,56.3-87.9,56.3S20.3,160.7,16.8,110.5c0,0,16.8,0,16.8,0ZM17.1,93.8C22,45,63.3,6.8,113.4,6.8s96.8,43.4,96.8,96.8-1.1,16.9-3.2,24.8l-15.9-5.5c1.6-6.3,2.4-12.8,2.3-19.3,0-44.2-35.9-80-80-80S38.8,54.3,34,93.8h-16.9,0Z" fill-rule="evenodd"/>
|
||||
<path d="M113.1,57c-25.7,0-46.6,20.9-46.6,46.7s20.9,46.7,46.7,46.7,2.6,0,3.9-.2l4.7,16.3c-2.8.4-5.7.6-8.5.6-35,0-63.4-28.4-63.4-63.4s28.4-63.4,63.4-63.4,1.7,0,2.6,0c0,0-2.6,16.7-2.6,16.7ZM132.2,43.2c25.7,8.1,44.4,32.1,44.4,60.5s-16,48.8-38.7,58.4l-4.6-16.2c15.8-7.5,26.7-23.6,26.7-42.2s-12.6-37.1-30.3-43.7l2.7-16.8h0Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
<path id="modrinth" fill="#000" d="M362,74.2c11.7,0,21.1,3.3,27.9,10.1,6.8,7,10.3,17.1,10.3,30.6v53.3h-21.8v-50.5c0-8.2-1.7-14.3-5.4-18.4-3.6-4-8.7-6.1-15.5-6.1s-13.2,2.4-17.6,7.1c-4.4,4.9-6.6,11.8-6.6,20.7v47.2h-21.8v-50.5c0-8.2-1.7-14.3-5.4-18.4-3.6-4-8.7-6.1-15.5-6.1s-13.2,2.4-17.6,7.1c-4.4,4.7-6.6,11.7-6.6,20.7v47.2h-21.8v-93h20.7v11.8c3.5-4.2,7.8-7.3,13.1-9.6,5.2-2.3,11-3.3,17.4-3.3s13.2,1.2,18.6,3.8c5.4,2.8,9.6,6.6,12.7,11.7,3.8-4.9,8.7-8.7,14.8-11.5,6.1-2.6,12.7-4,20-4h0ZM470.1,169.4c-9.4,0-17.9-2.1-25.4-6.1-7.4-3.9-13.5-9.8-17.6-17.1-4.4-7.1-6.4-15.3-6.4-24.6s2.1-17.4,6.4-24.5c4.2-7.2,10.3-13,17.6-16.9,7.5-4,16-6.1,25.4-6.1s18.1,2.1,25.6,6.1c7.5,4,13.4,9.8,17.8,16.9,4.2,7.1,6.3,15.3,6.3,24.5s-2.1,17.4-6.3,24.6c-4.4,7.3-10.3,13.1-17.8,17.1-7.5,4-16,6.1-25.6,6.1h0ZM470.1,150.8c8,0,14.6-2.6,19.9-8s7.8-12.4,7.8-21.1-2.6-15.7-7.8-21.1-11.8-8-19.9-8-14.6,2.6-19.7,8c-5.2,5.4-7.8,12.4-7.8,21.1s2.6,15.7,7.8,21.1c5,5.4,11.7,8,19.7,8ZM631.2,39v129.2h-20.9v-12c-3.6,4.3-8.1,7.7-13.2,9.9-5.4,2.3-11.1,3.3-17.6,3.3s-16.9-1.9-24-5.9-12.9-9.6-16.9-16.9c-4-7.1-6.1-15.5-6.1-24.9s2.1-17.8,6.1-24.9c4-7.1,9.8-12.7,16.9-16.7s15.1-5.9,24-5.9,11.8,1.1,16.9,3.1c5,2.1,9.5,5.3,13.1,9.4v-47.7h21.8,0ZM582.1,150.8c5.2,0,9.9-1.2,14.1-3.7,4.2-2.3,7.5-5.7,9.9-10.1,2.4-4.4,3.7-9.6,3.7-15.3s-1.2-11-3.7-15.3c-2.4-4.3-5.8-7.8-9.9-10.3-4.2-2.3-8.9-3.5-14.1-3.5s-9.9,1.2-14.1,3.5c-4.2,2.4-7.5,5.9-9.9,10.3-2.4,4.3-3.7,9.6-3.7,15.3s1.2,11,3.7,15.3c2.4,4.4,5.8,7.8,9.9,10.1,4.2,2.4,8.9,3.7,14.1,3.7ZM679.9,88.8c6.3-9.8,17.2-14.6,33.1-14.6v20.7c-1.7-.3-3.3-.5-5-.5-8.5,0-15.2,2.4-19.8,7.3-4.7,5.1-7.1,12.2-7.1,21.4v45.1h-21.8v-93h20.7v13.6h0ZM730.9,75.2h21.8v93h-21.8s0-93,0-93ZM741.9,59.9c-4,0-7.3-1.2-9.9-3.8-2.6-2.4-4-5.7-4-9.2,0-3.7,1.4-6.8,4-9.4,2.6-2.4,5.9-3.7,9.9-3.7s7.3,1.2,9.9,3.5c2.6,2.4,4,5.4,4,9s-1.2,7-3.8,9.6c-2.6,2.6-6.1,4-10.1,4ZM833.5,74.2c11.7,0,21.1,3.5,28.2,10.3s10.6,17.1,10.6,30.5v53.3h-21.8v-50.5c0-8.2-1.9-14.3-5.7-18.4-3.8-4-9.2-6.1-16.4-6.1s-14.3,2.4-19,7.1c-4.7,4.9-7,11.8-7,20.9v47h-21.8v-93h20.7v12c3.7-4.3,8.2-7.5,13.6-9.8s11.7-3.3,18.5-3.3h0ZM955.3,163.2c-2.7,2.2-5.9,3.8-9.2,4.7-3.8,1.1-7.7,1.6-11.7,1.6-10.3,0-18.1-2.6-23.7-8-5.6-5.4-8.4-13.1-8.4-23.3v-44.8h-15.3v-17.4h15.3v-21.2h21.8v21.2h24.9v17.4h-24.9v44.2c0,4.5,1,8,3.3,10.3,2.3,2.4,5.4,3.6,9.6,3.6s8.9-1.2,12.2-3.8l6.1,15.5h0ZM1025.5,74.2c11.7,0,21.1,3.5,28.2,10.3s10.6,17.1,10.6,30.5v53.3h-21.8v-50.5c0-8.2-1.9-14.3-5.8-18.4-3.8-4-9.2-6.1-16.4-6.1s-14.3,2.4-19,7.1c-4.7,4.9-7,11.8-7,20.9v47h-21.8V39h21.8v47c3.7-3.8,8-6.8,13.4-8.9,5.2-1.9,11.1-3,17.6-3h0Z"/>
|
||||
<g id="docs" fill="#00af5c">
|
||||
<path d="M1177.8,169.7c-8.9,0-16.9-2-24-6-7.1-4-12.7-9.5-16.7-16.6-4-7.1-6.1-15.4-6.1-25s2-17.9,6.1-25c4-7.1,9.6-12.6,16.7-16.5,7.1-3.9,15.1-5.9,24-5.9s14.7,1.7,20.8,5.1c6.1,3.4,11,8.6,14.6,15.5,3.6,6.9,5.4,15.9,5.4,26.7s-1.7,19.6-5.2,26.6c-3.5,7-8.3,12.2-14.4,15.7-6.1,3.5-13.2,5.2-21.2,5.2ZM1180.4,151.2c5.2,0,9.9-1.2,14-3.5,4.1-2.3,7.4-5.7,9.9-10.1,2.5-4.4,3.7-9.5,3.7-15.4s-1.2-11.2-3.7-15.5c-2.5-4.3-5.8-7.7-9.9-10-4.1-2.3-8.8-3.5-14-3.5s-9.9,1.2-14,3.5c-4.1,2.3-7.4,5.6-9.9,10-2.5,4.3-3.7,9.5-3.7,15.5s1.2,11,3.7,15.4c2.5,4.4,5.8,7.8,9.9,10.1,4.1,2.3,8.8,3.5,14,3.5ZM1208.6,168.5v-21.9l.9-24.6-1.7-24.6v-57.6h21.5v128.8h-20.7Z"/>
|
||||
<path d="M1299.5,169.7c-9.5,0-17.9-2.1-25.3-6.2-7.4-4.1-13.2-9.7-17.5-16.9-4.3-7.2-6.4-15.3-6.4-24.5s2.1-17.4,6.4-24.6c4.3-7.1,10.1-12.7,17.5-16.7,7.4-4,15.8-6.1,25.3-6.1s18.1,2,25.6,6.1c7.5,4.1,13.3,9.6,17.5,16.7,4.2,7.1,6.3,15.3,6.3,24.6s-2.1,17.3-6.3,24.5c-4.2,7.2-10.1,12.8-17.5,16.9-7.5,4.1-16,6.2-25.6,6.2ZM1299.5,151.2c5.3,0,10.1-1.2,14.2-3.5,4.2-2.3,7.4-5.7,9.8-10.1,2.4-4.4,3.6-9.5,3.6-15.4s-1.2-11.2-3.6-15.5c-2.4-4.3-5.6-7.7-9.8-10-4.2-2.3-8.8-3.5-14.1-3.5s-10,1.2-14.1,3.5c-4.1,2.3-7.4,5.6-9.8,10-2.4,4.3-3.6,9.5-3.6,15.5s1.2,11,3.6,15.4c2.4,4.4,5.7,7.8,9.8,10.1,4.1,2.3,8.8,3.5,14,3.5Z"/>
|
||||
<path d="M1412.3,169.7c-9.7,0-18.4-2.1-25.9-6.2-7.6-4.1-13.5-9.7-17.8-16.9-4.3-7.2-6.4-15.3-6.4-24.5s2.1-17.4,6.4-24.6c4.3-7.1,10.2-12.7,17.8-16.7,7.6-4,16.2-6.1,25.9-6.1s17,1.8,23.9,5.5c6.9,3.6,12.1,9,15.7,16.1l-16.7,9.7c-2.8-4.4-6.2-7.6-10.2-9.7-4-2.1-8.3-3.1-12.9-3.1s-10.1,1.2-14.4,3.5c-4.3,2.3-7.6,5.6-10.1,10-2.4,4.3-3.6,9.5-3.6,15.5s1.2,11.2,3.6,15.5c2.4,4.3,5.8,7.7,10.1,10,4.3,2.3,9.1,3.5,14.4,3.5s8.9-1,12.9-3.1c4-2.1,7.4-5.3,10.2-9.7l16.7,9.7c-3.6,6.9-8.8,12.3-15.7,16.1-6.9,3.8-14.8,5.6-23.9,5.6Z"/>
|
||||
<path d="M1499.2,169.7c-7.8,0-15.2-1-22.3-3-7.1-2-12.8-4.5-16.9-7.4l8.3-16.5c4.2,2.7,9.1,4.9,14.9,6.6,5.8,1.7,11.6,2.6,17.4,2.6s11.8-.9,14.8-2.8c3.1-1.8,4.6-4.3,4.6-7.5s-1-4.5-3.1-5.8c-2.1-1.3-4.8-2.3-8.2-3-3.4-.7-7.1-1.3-11.2-1.9-4.1-.6-8.2-1.4-12.3-2.3-4.1-1-7.8-2.4-11.2-4.3-3.4-1.9-6.1-4.5-8.2-7.7-2.1-3.2-3.1-7.5-3.1-12.8s1.7-11,5-15.4c3.4-4.3,8.1-7.7,14.1-10.1,6.1-2.4,13.3-3.6,21.6-3.6s12.6.7,18.9,2.1c6.4,1.4,11.6,3.4,15.8,5.9l-8.3,16.5c-4.4-2.7-8.8-4.5-13.3-5.5-4.5-1-8.9-1.5-13.3-1.5-6.6,0-11.5,1-14.7,2.9-3.2,2-4.8,4.5-4.8,7.5s1,4.9,3.1,6.2c2.1,1.4,4.8,2.5,8.2,3.3,3.4.8,7.1,1.5,11.2,2,4.1.5,8.2,1.3,12.2,2.3,4,1,7.8,2.5,11.2,4.3,3.4,1.8,6.2,4.3,8.2,7.5,2.1,3.2,3.1,7.5,3.1,12.7s-1.7,10.8-5.1,15.1c-3.4,4.3-8.2,7.6-14.5,10-6.2,2.4-13.7,3.6-22.4,3.6Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
6
apps/docs/src/content/config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ schema: docsSchema() }),
|
||||
};
|
||||
4
apps/docs/src/content/docs/contributing/daedalus.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Daedalus (Metadata service)
|
||||
description: Guide for contributing to Modrinth's frontend
|
||||
---
|
||||
52
apps/docs/src/content/docs/contributing/getting-started.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Getting started
|
||||
description: How can I contribute to Modrinth?
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Contributing to Modrinth
|
||||
|
||||
Every public-facing aspect of Modrinth, including everything from our [API/backend][labrinth] and [frontend][knossos] to our [Gradle plugin][minotaur] and [launcher][theseus], is released under free and open source licenses on [GitHub]. As such, we love contributions from community members! Before proceeding to do so, though, there are a number of things you'll want to keep in mind throughout the process, as well as some details specific to certain projects.
|
||||
|
||||
## Things to keep in mind
|
||||
|
||||
### Consult people on Discord
|
||||
|
||||
There are a number of reasons to want to consult with people on our [Discord] before making a pull request. For example, if you're not sure whether something is a good idea or not, if you're not sure how to implement something, or if you can't get something working, these would all be good opportunities to create a thread in the `#development` forum channel.
|
||||
|
||||
If you intend to work on new features, to make significant codebase changes, or to make UI/design changes, please open a discussion thread first to ensure your work is put to its best use.
|
||||
|
||||
### Don't get discouraged
|
||||
|
||||
At times, pull requests may be rejected or left unmerged for a variation of reasons. Don't take it personally, and don't get discouraged! Sometimes a contribution just isn't the right fit for the time, or it might have just been lost in the mess of other things to do. Remember, the core Modrinth team are often busy, whether it be on a specific project/task or on external factors such as offline responsibilities. It all falls back to the same thing: don't get discouraged!
|
||||
|
||||
### Code isn't the only way to contribute
|
||||
|
||||
You don't need to know how to program to contribute to Modrinth. Quality assurance, supporting the community, coming up with feature ideas, and making sure your voice is heard in public decisions are all great ways to contribute to Modrinth. If you find bugs, reporting them on the appropriate issue tracker is your responsibility; however, remember that potential security breaches and exploits must instead be reported in accordance with our [security policy](https://modrinth.com/legal/security).
|
||||
|
||||
## Project-specific details
|
||||
|
||||
If you wish to contribute code to a specific project, here's the place to start. Most of Modrinth is written in the [Rust language](https://www.rust-lang.org), but some things are written in other languages/frameworks like [Nuxt.js](https://nuxtjs.org) or Java.
|
||||
|
||||
Most of Modrinth's code is in our monorepo, which you can find [here](https://github.com/modrinth/code). Our monorepo is powered by [Turborepo](https://turborepo.org).
|
||||
|
||||
Follow the project-specific instructions below to get started:
|
||||
- [Knossos (frontend)](/contributing/knossos)
|
||||
- [Theseus (Modrinth App)](/contributing/theseus)
|
||||
- [Minotaur (Gradle plugin)](/contributing/minotaur)
|
||||
- [Labrinth (API/backend)](/contributing/labrinth)
|
||||
- [Daedalus (Metadata service)](/contributing/daedalus)
|
||||
|
||||
### Documentation
|
||||
|
||||
The [documentation](https://github.com/modrinth/docs) (which you are reading right now!) is the place to find any and all general information about Modrinth and its API. The instructions are largely the same as [knossos](#knossos-frontend), except that the docs have no lint.
|
||||
|
||||
[Discord]: https://discord.modrinth.com
|
||||
[GitHub]: https://github.com/modrinth
|
||||
[knossos]: https://github.com/modrinth/code/tree/main/apps/frontend
|
||||
[labrinth]: https://github.com/modrinth/labrinth
|
||||
[theseus]: https://github.com/modrinth/theseus
|
||||
[minotaur]: https://github.com/modrinth/minotaur
|
||||
[Rust]: https://www.rust-lang.org/tools/install
|
||||
[pnpm]: https://pnpm.io
|
||||
35
apps/docs/src/content/docs/contributing/knossos.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Knossos (Frontend)
|
||||
description: Guide for contributing to Modrinth's frontend
|
||||
---
|
||||
|
||||
This project is our [monorepo](https://github.com/modrinth/code). You can find the frontend in the `apps/frontend` directory.
|
||||
|
||||
[knossos] is the Nuxt.js frontend. You will need to install [pnpm] and run the standard commands:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm run web:dev
|
||||
```
|
||||
|
||||
Once that's done, you'll be serving knossos on `localhost:3000` with hot reloading. You can replace the `dev` in `pnpm run dev` with `build` to build for a production server and `start` to start the server. You can also use `pnpm run lint` to find any eslint problems, and `pnpm run fix` to try automatically fixing those problems.
|
||||
|
||||
<details>
|
||||
<summary>.env variables & command line options</summary>
|
||||
|
||||
#### Basic configuration
|
||||
|
||||
`SITE_URL`: The URL of the site (used for auth redirects). Default: `http://localhost:3000`
|
||||
`BASE_URL`: The base URL for the API. Default: `https://staging-api.modrinth.com/v2/`
|
||||
`BROWSER_BASE_URL`: The base URL for the API used in the browser. Default: `https://staging-api.modrinth.com/v2/`
|
||||
|
||||
</details>
|
||||
|
||||
#### Ready to open a PR?
|
||||
|
||||
If you're prepared to contribute by submitting a pull request, ensure you have met the following criteria:
|
||||
|
||||
- `pnpm run fix` has been run.
|
||||
|
||||
[knossos]: https://github.com/modrinth/code/tree/main/apps/frontend
|
||||
[pnpm]: https://pnpm.io
|
||||
115
apps/docs/src/content/docs/contributing/labrinth.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Labrinth (API)
|
||||
description: Guide for contributing to Modrinth's backend
|
||||
---
|
||||
|
||||
|
||||
[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432 and a MeiliSearch instance on port 7700. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
|
||||
|
||||
Now, you'll have to install the sqlx CLI, which can be done with cargo:
|
||||
|
||||
```bash
|
||||
cargo install --git https://github.com/launchbadge/sqlx sqlx-cli --no-default-features --features postgres,rustls
|
||||
```
|
||||
|
||||
From there, you can create the database and perform all database migrations with one simple command:
|
||||
|
||||
```bash
|
||||
sqlx database setup
|
||||
```
|
||||
|
||||
Finally, if on Linux, you will need the OpenSSL library. On Debian-based systems, this involves the `pkg-config` and `libssl-dev` packages.
|
||||
|
||||
To enable labrinth to create a project, you need to add two things.
|
||||
|
||||
1. An entry in the `loaders` table.
|
||||
2. An entry in the `loaders_project_types` table.
|
||||
|
||||
A minimal setup can be done from the command line with [psql](https://www.postgresql.org/docs/current/app-psql.html):
|
||||
|
||||
```bash
|
||||
psql --host=localhost --port=5432 -U <username, default is labrinth> -W
|
||||
```
|
||||
|
||||
The default password for the database is `labrinth`. Once you've connected, run
|
||||
|
||||
```sql
|
||||
INSERT INTO loaders VALUES (0, 'placeholder_loader');
|
||||
INSERT INTO loaders_project_types VALUES (0, 1); -- modloader id, supported type id
|
||||
INSERT INTO categories VALUES (0, 'placeholder_category', 1); -- category id, category, project type id
|
||||
```
|
||||
|
||||
This will initialize your database with a modloader called 'placeholder_loader', with id 0, and marked as supporting mods only. It will also create a category called 'placeholder_category' that is marked as supporting mods only
|
||||
If you would like 'placeholder_loader' to be marked as supporting modpacks too, run
|
||||
|
||||
```sql
|
||||
INSERT INTO loaders_project_types VALUES (0, 2); -- modloader id, supported type id
|
||||
```
|
||||
|
||||
If you would like 'placeholder_category' to be marked as supporting modpacks too, run
|
||||
|
||||
```sql
|
||||
INSERT INTO categories VALUES (0, 'placeholder_category', 2); -- modloader id, supported type id
|
||||
```
|
||||
|
||||
The majority of configuration is done at runtime using [dotenvy](https://crates.io/crates/dotenvy) and the `.env` file. Each of the variables and what they do can be found in the dropdown below. Additionally, there are three command line options that can be used to specify to MeiliSearch what you want to do.
|
||||
|
||||
<details>
|
||||
<summary>.env variables & command line options</summary>
|
||||
|
||||
#### Basic configuration
|
||||
|
||||
`DEBUG`: Whether debugging tools should be enabled
|
||||
`RUST_LOG`: Specifies what information to log, from rust's [`env-logger`](https://github.com/env-logger-rs/env_logger); a reasonable default is `info,sqlx::query=warn`
|
||||
`SITE_URL`: The main URL to be used for CORS
|
||||
`CDN_URL`: The publicly accessible base URL for files uploaded to the CDN
|
||||
`MODERATION_DISCORD_WEBHOOK`: The URL for a Discord webhook where projects pending approval will be sent
|
||||
`CLOUDFLARE_INTEGRATION`: Whether labrinth should integrate with Cloudflare's spam protection
|
||||
`DATABASE_URL`: The URL for the PostgreSQL database
|
||||
`DATABASE_MIN_CONNECTIONS`: The minimum number of concurrent connections allowed to the database at the same time
|
||||
`DATABASE_MAX_CONNECTIONS`: The maximum number of concurrent connections allowed to the database at the same time
|
||||
`MEILISEARCH_ADDR`: The URL for the MeiliSearch instance used for search
|
||||
`MEILISEARCH_KEY`: The name that MeiliSearch is given
|
||||
`BIND_ADDR`: The bind address for the server. Supports both IPv4 and IPv6
|
||||
`MOCK_FILE_PATH`: The path used to store uploaded files; this has no default value and will panic if unspecified
|
||||
|
||||
#### CDN options
|
||||
|
||||
`STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local`, `backblaze`, or `s3`, but defaults to `local`
|
||||
|
||||
The Backblaze and S3 configuration options are fairly self-explanatory in name, so here's simply their names:
|
||||
`BACKBLAZE_KEY_ID`, `BACKBLAZE_KEY`, `BACKBLAZE_BUCKET_ID`
|
||||
`S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_BUCKET_NAME`
|
||||
|
||||
#### Search, OAuth, and miscellaneous options
|
||||
|
||||
`LOCAL_INDEX_INTERVAL`: The interval, in seconds, at which the local database is reindexed for searching. Defaults to `3600` seconds (1 hour).
|
||||
`VERSION_INDEX_INTERVAL`: The interval, in seconds, at which versions are reindexed for searching. Defaults to `1800` seconds (30 minutes).
|
||||
|
||||
The OAuth configuration options are fairly self-explanatory. For help setting up authentication, please contact us on [Discord].
|
||||
|
||||
`RATE_LIMIT_IGNORE_IPS`: An array of IPs that should have a lower rate limit factor. This can be useful for allowing the front-end to have a lower rate limit to prevent accidental timeouts.
|
||||
|
||||
#### Command line options
|
||||
|
||||
`--skip-first-index`: Skips indexing the local database on startup. This is useful to prevent doing unnecessary work when frequently restarting.
|
||||
`--reconfigure-indices`: Resets the MeiliSearch settings for the search indices and exits.
|
||||
`--reset-indices`: Resets the MeiliSearch indices and exits; this clears all previously indexed mods.
|
||||
|
||||
</details>
|
||||
|
||||
#### Ready to open a PR?
|
||||
|
||||
If you're prepared to contribute by submitting a pull request, ensure you have met the following criteria:
|
||||
|
||||
- `cargo fmt` has been run.
|
||||
- `cargo clippy` has been run.
|
||||
- `cargo check` has been run.
|
||||
- `cargo sqlx prepare` has been run.
|
||||
|
||||
> Note: If you encounter issues with `sqlx` saying 'no queries found' after running `cargo sqlx prepare`, you may need to ensure the installed version of `sqlx-cli` matches the current version of `sqlx` used [in labrinth](https://github.com/modrinth/labrinth/blob/master/Cargo.toml).
|
||||
|
||||
[Discord]: https://discord.modrinth.com
|
||||
[GitHub]: https://github.com/modrinth
|
||||
[labrinth]: https://github.com/modrinth/labrinth
|
||||
[Rust]: https://www.rust-lang.org/tools/install
|
||||
10
apps/docs/src/content/docs/contributing/minotaur.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Minotaur (Gradle plugin)
|
||||
description: Guide for contributing to Modrinth's gradle plugin
|
||||
---
|
||||
|
||||
[Minotaur][minotaur] is the Gradle plugin used to automatically publish artifacts to Modrinth. To run your copy of the plugin in a project, publish it to your local Maven with `./gradlew publishToMavenLocal` and add `mavenLocal()` to your buildscript.
|
||||
|
||||
Minotaur contains two test environments within it - one with ForgeGradle and one with Fabric Loom. You may tweak with these environments to test whatever you may be trying; just make sure that the `modrinth` task within each still functions properly. GitHub Actions will validate this if you're making a pull request, so you may want to use [`act pull_request`](https://github.com/nektos/act) to test them locally.
|
||||
|
||||
[minotaur]: https://github.com/modrinth/minotaur
|
||||
43
apps/docs/src/content/docs/contributing/theseus.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Theseus (Modrinth App)
|
||||
description: Guide for contributing to Modrinth's desktop app
|
||||
---
|
||||
|
||||
This project is our [monorepo](https://github.com/modrinth/code).
|
||||
|
||||
[theseus] is the Tauri-based launcher that lets users conveniently play any mod or modpack on Modrinth. It uses the Rust-based Tauri as the backend and Vue.js as the frontend. To get started, install [pnpm], [Rust], and the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for your system. Then, run the following commands:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm run app:dev
|
||||
```
|
||||
|
||||
Once the commands finish, you'll be viewing a Tauri window with Nuxt.js hot reloading.
|
||||
|
||||
You can use `pnpm run lint` to find any eslint problems, and `pnpm run fix` to try automatically fixing those problems.
|
||||
|
||||
### Theseus Architecture
|
||||
|
||||
Theseus is split up into three parts:
|
||||
- `apps/app-frontend`: The Vue.JS frontend for the app
|
||||
- `packages/app-lib`: The library holding all the core logic for the desktop app
|
||||
- `apps/app`: The Tauri-based Rust app. This primarily wraps around the library with some additional logic for Tauri.
|
||||
|
||||
The app's internal database is stored in SQLite. For production builds, this is found at <APPDIR>/app.db.
|
||||
|
||||
When running SQLX commands, be sure to set the `DATABASE_URL` environment variable to the path of the database.
|
||||
|
||||
You can edit the app's data directory using the `THESEUS_CONFIG_DIR` environment variable.
|
||||
|
||||
#### Ready to open a PR?
|
||||
|
||||
If you're prepared to contribute by submitting a pull request, ensure you have met the following criteria:
|
||||
|
||||
- Run `pnpm run fix` to address any fixable issues automatically.
|
||||
- Run `cargo fmt` to format Rust-related code.
|
||||
- Run `cargo clippy` to validate Rust-related code.
|
||||
- Run `cargo sqlx prepare --package theseus` if you've changed any SQL code to validate statements.
|
||||
|
||||
[theseus]: https://github.com/modrinth/code/tree/main/apps/app
|
||||
[Rust]: https://www.rust-lang.org/tools/install
|
||||
[pnpm]: https://pnpm.io
|
||||
15
apps/docs/src/content/docs/index.mdx
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Modrinth docs
|
||||
description: Developer documentation for Modrinth!
|
||||
template: splash
|
||||
hero:
|
||||
tagline: Developer documentation for Modrinth
|
||||
actions:
|
||||
- text: API documentation
|
||||
link: /api
|
||||
icon: right-arrow
|
||||
- text: Get support with Modrinth
|
||||
link: https://support.modrinth.com
|
||||
icon: external
|
||||
variant: minimal
|
||||
---
|
||||
2
apps/docs/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
54
apps/docs/src/styles/modrinth.css
Normal file
@@ -0,0 +1,54 @@
|
||||
:root,
|
||||
::backdrop,
|
||||
:root[data-theme='light'],
|
||||
[data-theme='light'] ::backdrop{
|
||||
--sl-font-system: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
|
||||
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
|
||||
--sl-color-white: var(--color-contrast); /* “white” */
|
||||
--sl-color-gray-1: var(--color-base);
|
||||
--sl-color-gray-2: var(--color-base);
|
||||
--sl-color-gray-3: var(--color-base);
|
||||
--sl-color-gray-4: var(--color-raised-bg);
|
||||
--sl-color-gray-5: var(--color-button-bg);
|
||||
--sl-color-gray-6: var(--color-raised-bg);
|
||||
--sl-color-black: var(--color-accent-contrast);
|
||||
|
||||
--sl-color-accent-low: var(--color-green-highlight);
|
||||
--sl-color-accent: var(--color-brand);
|
||||
--sl-color-accent-high: var(--color-brand-highlight);
|
||||
|
||||
--sl-color-orange-low: var(--color-orange-highlight);
|
||||
--sl-color-orange: var(--color-orange);
|
||||
--sl-color-orange-high: var(--color-orange-highlight);
|
||||
|
||||
--sl-color-green-low: var(--color-green-highlight);
|
||||
--sl-color-green: var(--color-green);
|
||||
--sl-color-green-high: var(--color-green-highlight);
|
||||
|
||||
--sl-color-blue-low: var(--color-blue-highlight);
|
||||
--sl-color-blue: var(--color-blue);
|
||||
--sl-color-blue-high: var(--color-blue-highlight);
|
||||
|
||||
--sl-color-purple-low: var(--color-purple-highlight);
|
||||
--sl-color-purple: var(--color-purple);
|
||||
--sl-color-purple-high: var(--color-purple-highlight);
|
||||
|
||||
--sl-color-red-low: var(--color-red-highlight);
|
||||
--sl-color-red: var(--color-red);
|
||||
--sl-color-red-high: var(--color-red-highlight);
|
||||
|
||||
--sl-color-text: var(--color-base);
|
||||
--sl-color-text-accent: var(--color-brand);
|
||||
--sl-color-text-invert: var(--color-accent-contrast);
|
||||
--sl-color-bg: var(--color-bg);
|
||||
--sl-color-bg-nav: var(--color-raised-bg);
|
||||
--sl-color-bg-sidebar: var(--color-raised-bg);
|
||||
--sl-color-bg-inline-code: var(--color-button-bg);
|
||||
--sl-color-bg-accent: var(--color-brand-highlight);
|
||||
}
|
||||
|
||||
:root[data-theme='light'],
|
||||
[data-theme='light'] ::backdrop{
|
||||
--sl-color-bg: var(--color-raised-bg);
|
||||
}
|
||||
3
apps/docs/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["../../packages/eslint-config-custom/nuxt.js"],
|
||||
rules: {
|
||||
"import/no-unresolved": "off",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -176,7 +176,6 @@ export default defineNuxtConfig({
|
||||
$fetch(`${API_URL}projects_random?count=60`, headers),
|
||||
$fetch(`${API_URL}search?limit=3&query=leave&index=relevance`, headers),
|
||||
$fetch(`${API_URL}search?limit=3&query=&index=updated`, headers),
|
||||
// TODO: dehardcode
|
||||
$fetch(`${API_URL.replace("/v2/", "/_internal/")}billing/products`, headers),
|
||||
]);
|
||||
|
||||
@@ -321,8 +320,10 @@ export default defineNuxtConfig({
|
||||
apiBaseUrl: process.env.BASE_URL ?? globalThis.BASE_URL ?? getApiUrl(),
|
||||
// @ts-ignore
|
||||
rateLimitKey: process.env.RATE_LIMIT_IGNORE_KEY ?? globalThis.RATE_LIMIT_IGNORE_KEY,
|
||||
pyroBaseUrl: process.env.PYRO_BASE_URL,
|
||||
public: {
|
||||
apiBaseUrl: getApiUrl(),
|
||||
pyroBaseUrl: process.env.PYRO_BASE_URL,
|
||||
siteUrl: getDomain(),
|
||||
production: isProduction(),
|
||||
featureFlagOverrides: getFeatureFlagOverrides(),
|
||||
@@ -361,7 +362,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
modules: ["@vintl/nuxt", "@nuxtjs/turnstile"],
|
||||
modules: ["@vintl/nuxt", "@nuxtjs/turnstile", "@pinia/nuxt"],
|
||||
vintl: {
|
||||
defaultLocale: "en-US",
|
||||
locales: [
|
||||
@@ -462,6 +463,7 @@ function getDomain() {
|
||||
return "https://modrinth.com";
|
||||
}
|
||||
} else {
|
||||
return "http://localhost:3000";
|
||||
const port = process.env.PORT || 3000;
|
||||
return `http://localhost:${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@nuxt/devtools": "^1.3.3",
|
||||
"@nuxtjs/turnstile": "^0.8.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/node": "^20.1.0",
|
||||
"@vintl/compact-number": "^2.0.5",
|
||||
"@vintl/how-ago": "^3.0.1",
|
||||
@@ -38,8 +39,13 @@
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"ace-builds": "^1.36.2",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"dompurify": "^3.1.7",
|
||||
"floating-vue": "2.0.0-beta.20",
|
||||
"fuse.js": "^6.6.2",
|
||||
"highlight.js": "^11.7.0",
|
||||
@@ -48,9 +54,12 @@
|
||||
"jszip": "^3.10.1",
|
||||
"markdown-it": "14.1.0",
|
||||
"pathe": "^1.1.2",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "^3.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"vue-multiselect": "3.0.0",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-typed-virtual-list": "^1.0.10",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-apexcharts": "^1.5.2",
|
||||
"xss": "^1.0.14"
|
||||
}
|
||||
|
||||
BIN
apps/frontend/src/assets/images/games/rinth.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
apps/frontend/src/assets/images/servers/this-is-fine.gif
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised">
|
||||
<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">90% of ad revenue goes to creators</p>
|
||||
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
|
||||
<nuxt-link to="/plus" class="mt-auto items-center gap-1 text-purple hover:underline">
|
||||
<span>
|
||||
Support creators and Modrinth ad-free with
|
||||
@@ -23,9 +23,19 @@ import { ChevronRightIcon } from "@modrinth/assets";
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
// Clean.io
|
||||
src: "https://cadmus.script.ac/d14pdm1b7fi5kh/script.js",
|
||||
},
|
||||
{
|
||||
// Aditude
|
||||
src: "https://dn0qt3r0xannq.cloudfront.net/modrinth-7JfmkEIXEp/modrinth-longform/prebid-load.js",
|
||||
async: true,
|
||||
},
|
||||
{
|
||||
// Optima
|
||||
src: "https://bservr.com/o.js?uid=8118d1fdb2e0d6f32180bd27",
|
||||
async: true,
|
||||
},
|
||||
{
|
||||
src: "/inmobi.js",
|
||||
async: true,
|
||||
@@ -42,11 +52,22 @@ useHead({
|
||||
|
||||
onMounted(() => {
|
||||
window.tude = window.tude || { cmd: [] };
|
||||
window.Raven = window.Raven || { cmd: [] };
|
||||
|
||||
window.Raven.cmd.push(({ config }) => {
|
||||
config.setCustom({
|
||||
param1: "web",
|
||||
});
|
||||
});
|
||||
|
||||
tude.cmd.push(function () {
|
||||
tude.refreshAdsViaDivMappings([
|
||||
{
|
||||
divId: "modrinth-rail-1",
|
||||
baseDivId: "pb-slot-square-2",
|
||||
targeting: {
|
||||
location: "web",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -119,7 +119,7 @@ export default {
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--color-brand-inverted);
|
||||
color: var(--color-accent-contrast, var(--color-brand-inverted));
|
||||
stroke-width: 0.2rem;
|
||||
height: 0.8rem;
|
||||
width: 0.8rem;
|
||||
|
||||
@@ -45,7 +45,7 @@ export default {
|
||||
margin: 0;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: var(--color-code-bg);
|
||||
width: min-content;
|
||||
width: fit-content;
|
||||
border-radius: 10px;
|
||||
user-select: text;
|
||||
transition:
|
||||
@@ -55,7 +55,6 @@ export default {
|
||||
outline 0.2s ease-in-out;
|
||||
|
||||
span {
|
||||
max-width: 10rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<nav
|
||||
class="experimental-styles-within relative flex w-fit overflow-clip rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||
ref="scrollContainer"
|
||||
class="experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||
>
|
||||
<NuxtLink
|
||||
v-for="(link, index) in filteredLinks"
|
||||
@@ -18,7 +19,9 @@
|
||||
<span class="text-nowrap">{{ link.label }}</span>
|
||||
</NuxtLink>
|
||||
<div
|
||||
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${subpageSelected ? 'bg-button-bg' : 'bg-brand-highlight'}`"
|
||||
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${
|
||||
subpageSelected ? 'bg-button-bg' : 'bg-brand-highlight'
|
||||
}`"
|
||||
:style="{
|
||||
left: sliderLeftPx,
|
||||
top: sliderTopPx,
|
||||
@@ -32,6 +35,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
|
||||
const route = useNativeRoute();
|
||||
|
||||
interface Tab {
|
||||
@@ -47,12 +52,13 @@ const props = defineProps<{
|
||||
query?: string;
|
||||
}>();
|
||||
|
||||
const scrollContainer = ref<HTMLElement | null>(null);
|
||||
|
||||
const sliderLeft = ref(4);
|
||||
const sliderTop = ref(4);
|
||||
const sliderRight = ref(4);
|
||||
const sliderBottom = ref(4);
|
||||
const activeIndex = ref(-1);
|
||||
const oldIndex = ref(-1);
|
||||
const subpageSelected = ref(false);
|
||||
|
||||
const filteredLinks = computed(() =>
|
||||
@@ -63,6 +69,8 @@ const sliderTopPx = computed(() => `${sliderTop.value}px`);
|
||||
const sliderRightPx = computed(() => `${sliderRight.value}px`);
|
||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`);
|
||||
|
||||
const tabLinkElements = ref();
|
||||
|
||||
function pickLink() {
|
||||
let index = -1;
|
||||
subpageSelected.value = false;
|
||||
@@ -86,16 +94,13 @@ function pickLink() {
|
||||
if (activeIndex.value !== -1) {
|
||||
startAnimation();
|
||||
} else {
|
||||
oldIndex.value = -1;
|
||||
sliderLeft.value = 0;
|
||||
sliderRight.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const tabLinkElements = ref();
|
||||
|
||||
function startAnimation() {
|
||||
const el = tabLinkElements.value[activeIndex.value].$el;
|
||||
const el = tabLinkElements.value[activeIndex.value]?.$el;
|
||||
|
||||
if (!el || !el.offsetParent) return;
|
||||
|
||||
@@ -141,21 +146,19 @@ function startAnimation() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", pickLink);
|
||||
pickLink();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", pickLink);
|
||||
});
|
||||
|
||||
watch(route, () => pickLink());
|
||||
watch(
|
||||
() => route.path,
|
||||
() => pickLink(),
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.navtabs-transition {
|
||||
/* Delay on opacity is to hide any jankiness as the page loads */
|
||||
transition:
|
||||
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
|
||||
all 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -571,6 +571,10 @@ function getMessages() {
|
||||
gap: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.notification__actions .iconified-button.square-button svg {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.unknown-type {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
@@ -384,6 +384,8 @@ const submitForReview = async () => {
|
||||
}
|
||||
|
||||
.author-actions {
|
||||
margin-top: var(--spacing-card-md);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,6 @@
|
||||
:data="analytics.formattedData.value.revenue.chart.data"
|
||||
:labels="analytics.formattedData.value.revenue.chart.labels"
|
||||
is-money
|
||||
suffix="<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><line x1='12' y1='2' x2='12' y2='22'></line><path d='M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6'></path></svg>"
|
||||
:colors="
|
||||
isUsingProjectColors
|
||||
? analytics.formattedData.value.revenue.chart.colors
|
||||
@@ -193,15 +192,20 @@
|
||||
class="country-value"
|
||||
>
|
||||
<div class="country-flag-container">
|
||||
<img
|
||||
:src="
|
||||
name.toLowerCase() === 'xx' || !name
|
||||
? 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
: countryCodeToFlag(name)
|
||||
"
|
||||
alt="Hidden country"
|
||||
class="country-flag"
|
||||
/>
|
||||
<template v-if="name.toLowerCase() === 'xx' || !name">
|
||||
<img
|
||||
src="https://cdn.modrinth.com/placeholder-banner.svg"
|
||||
alt="Placeholder flag"
|
||||
class="country-flag"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<img
|
||||
:src="countryCodeToFlag(name)"
|
||||
:alt="`${countryCodeToName(name)}'s flag`"
|
||||
class="country-flag"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="country-text">
|
||||
<strong class="country-name"
|
||||
@@ -247,15 +251,20 @@
|
||||
class="country-value"
|
||||
>
|
||||
<div class="country-flag-container">
|
||||
<img
|
||||
:src="
|
||||
name.toLowerCase() === 'xx' || !name
|
||||
? 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
: countryCodeToFlag(name)
|
||||
"
|
||||
alt="Hidden country"
|
||||
class="country-flag"
|
||||
/>
|
||||
<template v-if="name.toLowerCase() === 'xx' || !name">
|
||||
<img
|
||||
src="https://cdn.modrinth.com/placeholder-banner.svg"
|
||||
alt="Placeholder flag"
|
||||
class="country-flag"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<img
|
||||
:src="countryCodeToFlag(name)"
|
||||
:alt="`${countryCodeToName(name)}'s flag`"
|
||||
class="country-flag"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="country-text">
|
||||
<strong class="country-name">
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Creating backup" @show="focusInput">
|
||||
<div class="flex flex-col gap-2 md:w-[600px]">
|
||||
<div class="font-semibold text-contrast">Name</div>
|
||||
<input
|
||||
ref="input"
|
||||
v-model="backupName"
|
||||
type="text"
|
||||
class="bg-bg-input w-full rounded-lg p-4"
|
||||
placeholder="e.g. Before 1.21"
|
||||
maxlength="64"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<InfoIcon class="hidden sm:block" />
|
||||
<span class="text-sm text-secondary">
|
||||
If left empty, the backup name will default to
|
||||
<span class="font-semibold"> Backup #{{ newBackupAmount }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1 mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="isCreating" @click="createBackup">
|
||||
<PlusIcon />
|
||||
Create backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hideModal">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from "vue";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { PlusIcon, XIcon, InfoIcon } from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["backupCreated"]);
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const input = ref<HTMLInputElement>();
|
||||
const isCreating = ref(false);
|
||||
const backupError = ref<string | null>(null);
|
||||
const backupName = ref("");
|
||||
const newBackupAmount = computed(() =>
|
||||
props.server.backups?.data?.length === undefined ? 1 : props.server.backups?.data?.length + 1,
|
||||
);
|
||||
|
||||
const focusInput = () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
input.value?.focus();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
const hideModal = () => {
|
||||
modal.value?.hide();
|
||||
backupName.value = "";
|
||||
};
|
||||
|
||||
const createBackup = async () => {
|
||||
if (!backupName.value.trim()) {
|
||||
backupName.value = `Backup #${newBackupAmount.value}`;
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
try {
|
||||
await props.server.backups?.create(backupName.value);
|
||||
await props.server.refresh();
|
||||
hideModal();
|
||||
emit("backupCreated", { success: true, message: "Backup created successfully" });
|
||||
} catch (error) {
|
||||
backupError.value = error instanceof Error ? error.message : String(error);
|
||||
emit("backupCreated", { success: false, message: backupError.value });
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
hide: hideModal,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<NewModal ref="modal" danger header="Deleting backup">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="relative flex w-full flex-col gap-2 rounded-2xl bg-[#0e0e0ea4] p-6">
|
||||
<div class="text-2xl font-extrabold text-contrast">
|
||||
{{ backupName }}
|
||||
</div>
|
||||
<div class="flex gap-2 font-semibold text-contrast">
|
||||
<CalendarIcon />
|
||||
{{ formattedDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1 mt-4 flex justify-end gap-4">
|
||||
<ButtonStyled color="red">
|
||||
<button :disabled="isDeleting" @click="deleteBackup">
|
||||
<TrashIcon />
|
||||
Delete backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="hideModal">Cancel</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { TrashIcon, CalendarIcon } from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
|
||||
backupId: string;
|
||||
backupName: string;
|
||||
backupCreatedAt: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["backupDeleted"]);
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const isDeleting = ref(false);
|
||||
const backupError = ref<string | null>(null);
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
return new Date(props.backupCreatedAt).toLocaleString("en-US", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
year: "2-digit",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
});
|
||||
|
||||
const hideModal = () => {
|
||||
modal.value?.hide();
|
||||
};
|
||||
|
||||
const deleteBackup = async () => {
|
||||
if (!props.backupId) {
|
||||
emit("backupDeleted", { success: false, message: "No backup selected" });
|
||||
return;
|
||||
}
|
||||
|
||||
isDeleting.value = true;
|
||||
try {
|
||||
await props.server.backups?.delete(props.backupId);
|
||||
await props.server.refresh();
|
||||
hideModal();
|
||||
emit("backupDeleted", { success: true, message: "Backup deleted successfully" });
|
||||
} catch (error) {
|
||||
backupError.value = error instanceof Error ? error.message : String(error);
|
||||
emit("backupDeleted", { success: false, message: backupError.value });
|
||||
} finally {
|
||||
isDeleting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
hide: hideModal,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Renaming backup" @show="focusInput">
|
||||
<div class="flex flex-col gap-2 md:w-[600px]">
|
||||
<div class="font-semibold text-contrast">Name</div>
|
||||
<input
|
||||
ref="input"
|
||||
v-model="backupName"
|
||||
type="text"
|
||||
class="bg-bg-input w-full rounded-lg p-4"
|
||||
placeholder="e.g. Before 1.21"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-1 mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="isRenaming" @click="renameBackup">
|
||||
<SaveIcon />
|
||||
Rename backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hideModal">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from "vue";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { SaveIcon, XIcon } from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
|
||||
currentBackupId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["backupRenamed"]);
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const input = ref<HTMLInputElement>();
|
||||
const backupName = ref("");
|
||||
const isRenaming = ref(false);
|
||||
const backupError = ref<string | null>(null);
|
||||
|
||||
const focusInput = () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
input.value?.focus();
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
const hideModal = () => {
|
||||
backupName.value = "";
|
||||
modal.value?.hide();
|
||||
};
|
||||
|
||||
const renameBackup = async () => {
|
||||
if (!backupName.value.trim() || !props.currentBackupId) {
|
||||
emit("backupRenamed", { success: false, message: "Backup name cannot be empty" });
|
||||
return;
|
||||
}
|
||||
|
||||
isRenaming.value = true;
|
||||
try {
|
||||
await props.server.backups?.rename(props.currentBackupId, backupName.value);
|
||||
await props.server.refresh();
|
||||
hideModal();
|
||||
emit("backupRenamed", { success: true, message: "Backup renamed successfully" });
|
||||
} catch (error) {
|
||||
backupError.value = error instanceof Error ? error.message : String(error);
|
||||
emit("backupRenamed", { success: false, message: backupError.value });
|
||||
} finally {
|
||||
isRenaming.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
hide: hideModal,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Restoring backup">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="relative flex w-full flex-col gap-2 rounded-2xl bg-bg p-6">
|
||||
<div class="text-2xl font-extrabold text-contrast">
|
||||
{{ backupName }}
|
||||
</div>
|
||||
<div class="flex gap-2 font-semibold text-contrast">
|
||||
<CalendarIcon />
|
||||
{{ formattedDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1 mt-4 flex justify-end gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="isRestoring" @click="restoreBackup">Restore backup</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="hideModal">Cancel</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { CalendarIcon } from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "mods", "backups", "network", "startup", "ws", "fs"]>;
|
||||
backupId: string;
|
||||
backupName: string;
|
||||
backupCreatedAt: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["backupRestored"]);
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
const isRestoring = ref(false);
|
||||
const backupError = ref<string | null>(null);
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
return new Date(props.backupCreatedAt).toLocaleString("en-US", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
year: "2-digit",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
});
|
||||
|
||||
const hideModal = () => {
|
||||
modal.value?.hide();
|
||||
};
|
||||
|
||||
const restoreBackup = async () => {
|
||||
if (!props.backupId) {
|
||||
emit("backupRestored", { success: false, message: "No backup selected" });
|
||||
return;
|
||||
}
|
||||
|
||||
isRestoring.value = true;
|
||||
try {
|
||||
await props.server.backups?.restore(props.backupId);
|
||||
hideModal();
|
||||
emit("backupRestored", { success: true, message: "Backup restored successfully" });
|
||||
} catch (error) {
|
||||
backupError.value = error instanceof Error ? error.message : String(error);
|
||||
emit("backupRestored", { success: false, message: backupError.value });
|
||||
} finally {
|
||||
isRestoring.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
hide: hideModal,
|
||||
});
|
||||
</script>
|
||||
201
apps/frontend/src/components/ui/servers/BackupSettingsModal.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Editing auto backup settings">
|
||||
<div class="flex flex-col gap-4 md:w-[600px]">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="font-semibold text-contrast">Auto backup</div>
|
||||
<p class="m-0">
|
||||
Automatically create a backup of your server every
|
||||
<strong>{{ autoBackupInterval == 1 ? "hour" : `${autoBackupInterval} hours` }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingSettings" class="py-2 text-sm text-secondary">Loading settings...</div>
|
||||
<template v-else>
|
||||
<input
|
||||
id="auto-backup-toggle"
|
||||
v-model="autoBackupEnabled"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
:disabled="isSaving"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="font-semibold text-contrast">Interval</div>
|
||||
<p class="m-0">
|
||||
The amount of hours between each backup. This will only backup your server if it has
|
||||
been modified since the last backup.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-contrast">
|
||||
<div
|
||||
class="flex w-fit items-center rounded-xl border border-solid border-button-border bg-table-alternateRow"
|
||||
>
|
||||
<button
|
||||
class="rounded-l-xl p-3 text-secondary enabled:hover:text-contrast [&&]:bg-transparent enabled:[&&]:hover:bg-button-bg"
|
||||
:disabled="!autoBackupEnabled || isSaving"
|
||||
@click="autoBackupInterval = Math.max(autoBackupInterval - 1, 1)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="2" viewBox="-2 0 18 2">
|
||||
<path
|
||||
d="M18,12H6"
|
||||
transform="translate(-5 -11)"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
id="auto-backup-interval"
|
||||
v-model="autoBackupInterval"
|
||||
class="w-16 !appearance-none text-center [&&]:bg-transparent [&&]:focus:shadow-none"
|
||||
type="number"
|
||||
style="-moz-appearance: textfield; appearance: none"
|
||||
min="1"
|
||||
max="24"
|
||||
step="1"
|
||||
:disabled="!autoBackupEnabled || isSaving"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="rounded-r-xl p-3 text-secondary enabled:hover:text-contrast [&&]:bg-transparent enabled:[&&]:hover:bg-button-bg"
|
||||
:disabled="!autoBackupEnabled || isSaving"
|
||||
@click="autoBackupInterval = Math.min(autoBackupInterval + 1, 24)"
|
||||
>
|
||||
<PlusIcon />
|
||||
</button>
|
||||
</div>
|
||||
{{ autoBackupInterval == 1 ? "hour" : "hours" }}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!hasChanges || isSaving" @click="saveSettings">
|
||||
<SaveIcon class="h-5 w-5" />
|
||||
{{ isSaving ? "Saving..." : "Save changes" }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button :disabled="isSaving" @click="modal?.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { PlusIcon, XIcon, SaveIcon } from "@modrinth/assets";
|
||||
import { ref, computed } from "vue";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["backups"]>;
|
||||
}>();
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
|
||||
const initialSettings = ref<{ interval: number; enabled: boolean } | null>(null);
|
||||
const autoBackupEnabled = ref(false);
|
||||
const autoBackupInterval = ref(1);
|
||||
const isLoadingSettings = ref(true);
|
||||
const isSaving = ref(false);
|
||||
|
||||
const validatedBackupInterval = computed(() => {
|
||||
const roundedValue = Math.round(autoBackupInterval.value);
|
||||
|
||||
if (roundedValue < 1) {
|
||||
return 1;
|
||||
} else if (roundedValue > 24) {
|
||||
return 24;
|
||||
}
|
||||
return roundedValue;
|
||||
});
|
||||
|
||||
const hasChanges = computed(() => {
|
||||
if (!initialSettings.value) return false;
|
||||
|
||||
return (
|
||||
autoBackupEnabled.value !== initialSettings.value.enabled ||
|
||||
autoBackupInterval.value !== initialSettings.value.interval
|
||||
);
|
||||
});
|
||||
|
||||
const fetchSettings = async () => {
|
||||
isLoadingSettings.value = true;
|
||||
try {
|
||||
const settings = await props.server.backups?.getAutoBackup();
|
||||
initialSettings.value = settings as { interval: number; enabled: boolean };
|
||||
autoBackupEnabled.value = settings?.enabled ?? false;
|
||||
autoBackupInterval.value = settings?.interval || 1;
|
||||
} catch (error) {
|
||||
console.error("Error fetching backup settings:", error);
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Error",
|
||||
text: "Failed to load backup settings",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
isLoadingSettings.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await props.server.backups?.updateAutoBackup(
|
||||
autoBackupEnabled.value ? "enable" : "disable",
|
||||
autoBackupInterval.value,
|
||||
);
|
||||
|
||||
initialSettings.value = {
|
||||
enabled: autoBackupEnabled.value,
|
||||
interval: autoBackupInterval.value,
|
||||
};
|
||||
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Success",
|
||||
text: "Backup settings updated successfully",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
modal.value?.hide();
|
||||
} catch (error) {
|
||||
console.error("Error saving backup settings:", error);
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Error",
|
||||
text: "Failed to save backup settings",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(autoBackupInterval, () => {
|
||||
autoBackupInterval.value = validatedBackupInterval.value;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
show: async () => {
|
||||
await fetchSettings();
|
||||
modal.value?.show();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stylized-toggle:checked::after {
|
||||
background: var(--color-accent-contrast) !important;
|
||||
}
|
||||
</style>
|
||||
229
apps/frontend/src/components/ui/servers/FileItem.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<li
|
||||
role="button"
|
||||
data-pyro-file
|
||||
:class="containerClasses"
|
||||
tabindex="0"
|
||||
@click="selectItem"
|
||||
@contextmenu="openContextMenu"
|
||||
@keydown="(e) => e.key === 'Enter' && selectItem()"
|
||||
>
|
||||
<div data-pyro-file-metadata class="flex w-full items-center gap-4 truncate">
|
||||
<div
|
||||
class="flex size-8 items-center justify-center rounded-full bg-bg-raised p-[6px] group-hover:bg-brand-highlight group-hover:text-brand group-focus:bg-brand-highlight group-focus:text-brand"
|
||||
>
|
||||
<component :is="iconComponent" class="size-6" />
|
||||
</div>
|
||||
<div class="flex w-full flex-col truncate">
|
||||
<span
|
||||
class="w-[98%] truncate font-bold group-hover:text-contrast group-focus:text-contrast"
|
||||
>{{ name }}</span
|
||||
>
|
||||
<span class="text-xs text-secondary group-hover:text-primary">
|
||||
{{ subText }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-pyro-file-actions class="flex w-fit flex-shrink-0 items-center gap-4 md:gap-12">
|
||||
<span class="w-[160px] text-nowrap text-right font-mono text-sm text-secondary">{{
|
||||
formattedDate
|
||||
}}</span>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<UiServersTeleportOverflowMenu :options="menuOptions" direction="left" position="bottom">
|
||||
<MoreHorizontalIcon class="h-5 w-5 bg-transparent" />
|
||||
<template #rename> <EditIcon /> Rename </template>
|
||||
<template #move> <RightArrowIcon /> Move </template>
|
||||
<template #download> <DownloadIcon /> Download </template>
|
||||
<template #delete> <TrashIcon /> Delete </template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import {
|
||||
MoreHorizontalIcon,
|
||||
EditIcon,
|
||||
DownloadIcon,
|
||||
TrashIcon,
|
||||
FolderOpenIcon,
|
||||
FileIcon,
|
||||
RightArrowIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { computed, shallowRef, ref } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import {
|
||||
UiServersIconsCogFolderIcon,
|
||||
UiServersIconsEarthIcon,
|
||||
UiServersIconsCodeFileIcon,
|
||||
UiServersIconsTextFileIcon,
|
||||
UiServersIconsImageFileIcon,
|
||||
} from "#components";
|
||||
import PaletteIcon from "~/assets/icons/palette.svg?component";
|
||||
|
||||
interface FileItemProps {
|
||||
name: string;
|
||||
type: "directory" | "file";
|
||||
size?: number;
|
||||
count?: number;
|
||||
modified: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const props = defineProps<FileItemProps>();
|
||||
|
||||
const emit = defineEmits(["rename", "download", "delete", "move", "edit", "contextmenu"]);
|
||||
|
||||
const codeExtensions = Object.freeze([
|
||||
"json",
|
||||
"json5",
|
||||
"jsonc",
|
||||
"java",
|
||||
"kt",
|
||||
"kts",
|
||||
"sh",
|
||||
"bat",
|
||||
"ps1",
|
||||
"yml",
|
||||
"yaml",
|
||||
"toml",
|
||||
"js",
|
||||
"ts",
|
||||
"py",
|
||||
"rb",
|
||||
"php",
|
||||
"html",
|
||||
"css",
|
||||
"cpp",
|
||||
"c",
|
||||
"h",
|
||||
"rs",
|
||||
"go",
|
||||
]);
|
||||
|
||||
const textExtensions = Object.freeze(["txt", "md", "log", "cfg", "conf", "properties", "ini"]);
|
||||
const imageExtensions = Object.freeze(["png", "jpg", "jpeg", "gif", "svg", "webp"]);
|
||||
const units = Object.freeze(["B", "KB", "MB", "GB", "TB", "PB", "EB"]);
|
||||
|
||||
const route = shallowRef(useRoute());
|
||||
const router = useRouter();
|
||||
|
||||
const containerClasses = computed(() => [
|
||||
"group m-0 p-0 focus:!outline-none flex w-full select-none items-center justify-between overflow-hidden border-0 border-b border-solid border-bg-raised p-3 last:border-none hover:bg-bg-raised focus:bg-bg-raised",
|
||||
isEditableFile.value ? "cursor-pointer" : props.type === "directory" ? "cursor-pointer" : "",
|
||||
]);
|
||||
|
||||
const fileExtension = computed(() => props.name.split(".").pop()?.toLowerCase() || "");
|
||||
|
||||
const menuOptions = computed(() => [
|
||||
{
|
||||
id: "rename",
|
||||
action: () => emit("rename", { name: props.name, type: props.type, path: props.path }),
|
||||
},
|
||||
{
|
||||
id: "move",
|
||||
action: () => emit("move", { name: props.name, type: props.type, path: props.path }),
|
||||
},
|
||||
{
|
||||
id: "download",
|
||||
action: () => emit("download", { name: props.name, type: props.type, path: props.path }),
|
||||
shown: props.type !== "directory",
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
action: () => emit("delete", { name: props.name, type: props.type, path: props.path }),
|
||||
color: "red" as const,
|
||||
},
|
||||
]);
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
if (props.type === "directory") {
|
||||
if (props.name === "config") return UiServersIconsCogFolderIcon;
|
||||
if (props.name === "world") return UiServersIconsEarthIcon;
|
||||
if (props.name === "resourcepacks") return PaletteIcon;
|
||||
return FolderOpenIcon;
|
||||
}
|
||||
|
||||
const ext = fileExtension.value;
|
||||
if (codeExtensions.includes(ext)) return UiServersIconsCodeFileIcon;
|
||||
if (textExtensions.includes(ext)) return UiServersIconsTextFileIcon;
|
||||
if (imageExtensions.includes(ext)) return UiServersIconsImageFileIcon;
|
||||
return FileIcon;
|
||||
});
|
||||
|
||||
const subText = computed(() => {
|
||||
if (props.type === "directory") {
|
||||
return `${props.count} ${props.count === 1 ? "item" : "items"}`;
|
||||
}
|
||||
return formattedSize.value;
|
||||
});
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
const date = new Date(props.modified * 1000);
|
||||
return `${date.toLocaleDateString("en-US", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
year: "2-digit",
|
||||
})}, ${date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
})}`;
|
||||
});
|
||||
|
||||
const isEditableFile = computed(() => {
|
||||
if (props.type === "file") {
|
||||
const ext = fileExtension.value;
|
||||
return (
|
||||
!props.name.includes(".") ||
|
||||
textExtensions.includes(ext) ||
|
||||
codeExtensions.includes(ext) ||
|
||||
imageExtensions.includes(ext)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const formattedSize = computed(() => {
|
||||
if (props.size === undefined) return "";
|
||||
const bytes = props.size;
|
||||
if (bytes === 0) return "0 B";
|
||||
|
||||
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||
const size = (bytes / Math.pow(1024, exponent)).toFixed(2);
|
||||
return `${size} ${units[exponent]}`;
|
||||
});
|
||||
|
||||
const openContextMenu = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
emit("contextmenu", event.clientX, event.clientY);
|
||||
};
|
||||
|
||||
const navigateToFolder = () => {
|
||||
const currentPath = route.value.query.path?.toString() || "";
|
||||
const newPath = currentPath.endsWith("/")
|
||||
? `${currentPath}${props.name}`
|
||||
: `${currentPath}/${props.name}`;
|
||||
router.push({ query: { path: newPath, page: 1 } });
|
||||
};
|
||||
|
||||
const isNavigating = ref(false);
|
||||
|
||||
const selectItem = () => {
|
||||
if (isNavigating.value) return;
|
||||
isNavigating.value = true;
|
||||
|
||||
if (props.type === "directory") {
|
||||
navigateToFolder();
|
||||
} else if (props.type === "file" && isEditableFile.value) {
|
||||
emit("edit", { name: props.name, type: props.type, path: props.path });
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
isNavigating.value = false;
|
||||
}, 500);
|
||||
};
|
||||
</script>
|
||||
40
apps/frontend/src/components/ui/servers/FileManagerError.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="flex h-full w-full items-center justify-center gap-6 p-20">
|
||||
<FileIcon class="size-28" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-red-500 m-0 text-2xl font-bold">{{ title }}</h3>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
{{ message }}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled>
|
||||
<button size="sm" @click="$emit('refetch')">
|
||||
<UiServersIconsLoadingIcon class="h-5 w-5" />
|
||||
Try again
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button size="sm" @click="$emit('home')">
|
||||
<HomeIcon class="h-5 w-5" />
|
||||
Go to home folder
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FileIcon, HomeIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
message: string;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: "refetch"): void;
|
||||
(e: "home"): void;
|
||||
}>();
|
||||
</script>
|
||||