mirror of
https://github.com/vrc-get/vrc-get.git
synced 2026-06-21 09:58:08 +00:00
Compare commits
855 commits
apple-scip
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a1044df51 |
||
|
|
e71c48141b |
||
|
|
467434903a | ||
|
|
ba8606d312 | ||
|
|
ed42cdfca4 | ||
|
|
dbd479fbc9 |
||
|
|
db54f0c8ae |
||
|
|
da419a0374 |
||
|
|
8351270552 |
||
|
|
50fccc9a50 |
||
|
|
cf7075a6d5 |
||
|
|
5e4709209f |
||
|
|
269cb8aeda |
||
|
|
ba9ad1f59c |
||
|
|
23b1bb2515 |
||
|
|
2c36473e6e | ||
|
|
791e50d94f | ||
|
|
a77ab4a0a3 |
||
|
|
aa14673468 |
||
|
|
36aa59b304 |
||
|
|
b61c44c64a |
||
|
|
0a5ab490b8 |
||
|
|
cfc1b627eb |
||
|
|
1b2065afe0 |
||
|
|
4634c42410 |
||
|
|
fd8b735ac9 |
||
|
|
580c8da047 |
||
|
|
0ebfb25263 |
||
|
|
9eaf99b3d1 |
||
|
|
87753ceffa |
||
|
|
0fa954cef0 |
||
|
|
41e25d4e86 |
||
|
|
3c499f9d6a |
||
|
|
65e37ff7af |
||
|
|
48e1866ff3 |
||
|
|
968ae63a4c |
||
|
|
805be35ac6 |
||
|
|
901368c9bf |
||
|
|
09c8190634 | ||
|
|
5eed047fa4 |
||
|
|
056073834c |
||
|
|
c4117c7457 |
||
|
|
3bf6b96b93 |
||
|
|
00fb4d8546 |
||
|
|
4d4c0c27d3 | ||
|
|
a31c947c1a |
||
|
|
f6e677909c | ||
|
|
8b58270235 | ||
|
|
c1b297e02a | ||
|
|
a0ec779fab |
||
|
|
800278a0c3 | ||
|
|
334492e5c6 | ||
|
|
1a947b7a6c | ||
|
|
514f52a419 | ||
|
|
3e42f0de4e | ||
|
|
6dfdd9c5f3 | ||
|
|
21a0e19722 | ||
|
|
6ab9c49c94 | ||
|
|
04adf285b5 | ||
|
|
665513d0fc |
||
|
|
6f8cd53159 |
||
|
|
ec6454b0d4 |
||
|
|
533a40b1eb | ||
|
|
90c7e249cd | ||
|
|
e057838795 | ||
|
|
437a63bcc6 | ||
|
|
836bf82351 |
||
|
|
316c73a6b6 | ||
|
|
ec04492af8 | ||
|
|
ebd117afd2 | ||
|
|
253a0266e6 | ||
|
|
0cf635e892 | ||
|
|
f8923dce1b |
||
|
|
9fc228eed0 |
||
|
|
54113144a8 | ||
|
|
0ab6c9434b | ||
|
|
9d94d8b929 | ||
|
|
d1887eb467 | ||
|
|
45ed34f573 | ||
|
|
930715960e | ||
|
|
8db24b7b7c | ||
|
|
44ed683c23 | ||
|
|
c248b0ed55 | ||
|
|
9d11a5e816 |
||
|
|
81d66143c9 | ||
|
|
6f143ac5e3 |
||
|
|
f44633a2ad |
||
|
|
3fb8281a47 |
||
|
|
244946c234 |
||
|
|
ccd252f8aa |
||
|
|
c2b2b9b491 |
||
|
|
9decfe400c |
||
|
|
35e7537a90 |
||
|
|
1800951c12 | ||
|
|
76b0f05651 |
||
|
|
f45ee6514e |
||
|
|
d421ee04b2 |
||
|
|
bd92649248 |
||
|
|
68be14aa90 |
||
|
|
dd9673c2cb |
||
|
|
73cfa96f45 |
||
|
|
fa22fc3611 |
||
|
|
8b61a0c806 |
||
|
|
c37758d4c1 |
||
|
|
37688599ae | ||
|
|
fcb0c5263d |
||
|
|
a7e19a6479 | ||
|
|
f0a7cbd050 |
||
|
|
a9322f4d64 |
||
|
|
0c895bf4a5 |
||
|
|
07e7279c31 |
||
|
|
a9e135b940 |
||
|
|
d2aca1554c |
||
|
|
5be2c13984 |
||
|
|
1b1a0eefae |
||
|
|
3e94514b13 |
||
|
|
56a32bad04 |
||
|
|
fb6a6538d7 |
||
|
|
ab64f403a5 |
||
|
|
96d8ced3bb |
||
|
|
bde10abd3b |
||
|
|
e3313f0327 |
||
|
|
bb0b8cda55 |
||
|
|
11b6b1cad4 |
||
|
|
92cff628c2 |
||
|
|
3f3465d74b |
||
|
|
d1c28f6479 |
||
|
|
089427cbfa |
||
|
|
375894c6c3 |
||
|
|
eed54886b8 |
||
|
|
11c1b7048b |
||
|
|
b613f834ea |
||
|
|
5d9c6ce802 |
||
|
|
f15e489515 |
||
|
|
687904918d |
||
|
|
c5d38a1f6e |
||
|
|
55635bbe9c |
||
|
|
ca30b889f1 |
||
|
|
0a17c9a1cd |
||
|
|
50aba0530e |
||
|
|
33be110c7a |
||
|
|
2aad318387 |
||
|
|
43ca6755ff |
||
|
|
92bc6cf38e |
||
|
|
8d293e5e69 |
||
|
|
1e9a100d9c |
||
|
|
8624950ca5 |
||
|
|
57c9a54d37 |
||
|
|
6470169cf1 |
||
|
|
795699baf0 |
||
|
|
7fbf8c60d1 |
||
|
|
a5c5d6e128 |
||
|
|
a3aa50256f |
||
|
|
498eedc1d6 |
||
|
|
c863bc4461 |
||
|
|
8fd964fa83 |
||
|
|
c7012c5e81 |
||
|
|
cf6c82adbd |
||
|
|
e0931be79a | ||
|
|
e0dce679d7 |
||
|
|
f9ca613378 |
||
|
|
0fa0c7ca00 |
||
|
|
9c1465868e |
||
|
|
874492f3fc |
||
|
|
8588a7276c |
||
|
|
0a5b9c38eb |
||
|
|
17a28fa609 |
||
|
|
099883a33d |
||
|
|
5cb60c4002 |
||
|
|
cdb1b8294d |
||
|
|
0de963736f | ||
|
|
0026f5029c |
||
|
|
987357354e |
||
|
|
f13add3409 |
||
|
|
2d7fefb34c |
||
|
|
e625630856 |
||
|
|
4943a671e0 |
||
|
|
6e63cdba25 |
||
|
|
e04d1ee919 |
||
|
|
a4992e3973 |
||
|
|
185b7aa8b7 |
||
|
|
24ae2b33de |
||
|
|
1b928b150b |
||
|
|
2d859d993e |
||
|
|
4018f718a7 |
||
|
|
de73063b98 |
||
|
|
d6ea9b3079 |
||
|
|
eaf09ecb6a |
||
|
|
d74c7b16b8 |
||
|
|
6c46983cae |
||
|
|
36b76933fc |
||
|
|
ad7efa259e |
||
|
|
97042ebc6a |
||
|
|
e135092745 |
||
|
|
5607daf7b5 | ||
|
|
2861fcaa68 | ||
|
|
8a71e426d6 |
||
|
|
cd242e389b |
||
|
|
95dc46666b |
||
|
|
f0c4de7d78 |
||
|
|
e9a994a932 |
||
|
|
803fbf3395 |
||
|
|
20d24b496b |
||
|
|
a1ff52fed4 |
||
|
|
4a99cfe4c9 |
||
|
|
8b13244b74 | ||
|
|
875a989d52 |
||
|
|
973c91c9c0 |
||
|
|
3d2dd2e058 |
||
|
|
16d8a787ec |
||
|
|
4ccc7d7bc8 |
||
|
|
8c8315f57d |
||
|
|
4cac964713 |
||
|
|
fb32bd97cd |
||
|
|
1a70d2db82 |
||
|
|
cacb9a5e9a |
||
|
|
4700bfa2c0 |
||
|
|
223be38368 |
||
|
|
523ede22ea |
||
|
|
38a7649fbd |
||
|
|
d61b46ded8 |
||
|
|
4756f6c115 |
||
|
|
e14806c72c |
||
|
|
8324a4ec04 |
||
|
|
a9f2a9713f |
||
|
|
40cd4487d5 |
||
|
|
56a2139df6 |
||
|
|
175008956c |
||
|
|
00a928e58e |
||
|
|
7037a758bc |
||
|
|
f457ffcc69 |
||
|
|
9291853c19 |
||
|
|
0e6d011ee5 |
||
|
|
be5c9aa765 |
||
|
|
b090c440ac |
||
|
|
3c51a2de31 |
||
|
|
04fa1f8244 |
||
|
|
35b26d9322 |
||
|
|
478c25b03a |
||
|
|
20e5836985 |
||
|
|
ce8de700a7 |
||
|
|
3076713ea6 |
||
|
|
75974f4ad9 |
||
|
|
4eadf2bdcd |
||
|
|
c43ed4a828 |
||
|
|
f675d29173 |
||
|
|
c45e4e399f |
||
|
|
f5f7ac794e |
||
|
|
88fd6a06a7 |
||
|
|
c3020225f3 |
||
|
|
d2f9a39b67 |
||
|
|
776bd4b4e8 |
||
|
|
4ae3507f96 |
||
|
|
7b105639f9 |
||
|
|
fc177add7c |
||
|
|
4063021e2a |
||
|
|
463b4c5df7 |
||
|
|
bbbbd19926 |
||
|
|
3d7b2b8e22 |
||
|
|
b24bcf1c38 |
||
|
|
19bde29c6b |
||
|
|
f18bc14777 |
||
|
|
07a5eb27b3 |
||
|
|
6a5c60a79e |
||
|
|
2b04760299 |
||
|
|
1845e8083d |
||
|
|
faf432fd61 |
||
|
|
45942829df | ||
|
|
b64e09dc55 |
||
|
|
dee9a2fb78 |
||
|
|
508d62b3e6 |
||
|
|
f119b203f9 |
||
|
|
a64cb3af9b |
||
|
|
70f7f8c78e |
||
|
|
b64058cab0 |
||
|
|
3e8d5cc9d9 |
||
|
|
2809d94996 |
||
|
|
dccd85be31 |
||
|
|
726ca0c068 |
||
|
|
5887c5631c |
||
|
|
a57de0d7f1 |
||
|
|
9bc7d26fff |
||
|
|
20c20a49b0 |
||
|
|
6a4648ddbb |
||
|
|
4783761910 |
||
|
|
1fdb97d04a |
||
|
|
b3430d926b |
||
|
|
4edda28776 |
||
|
|
b8903da155 |
||
|
|
38ac981d22 |
||
|
|
9fa4d25637 |
||
|
|
490190606c |
||
|
|
fd6eb27b09 |
||
|
|
5fd5109f3b |
||
|
|
496a1c91c1 |
||
|
|
42f1835360 |
||
|
|
4b89aec67a |
||
|
|
422b4b1b8d |
||
|
|
98cde02985 |
||
|
|
b6cb3f51f7 |
||
|
|
50bc72d662 |
||
|
|
9655865ba9 |
||
|
|
827042c827 |
||
|
|
e265b8fcdb |
||
|
|
3394e59e49 |
||
|
|
dccfbc2867 |
||
|
|
1cbc34bb30 |
||
|
|
61832aa154 |
||
|
|
b696c7cc31 |
||
|
|
847d76cb5c | ||
|
|
a29a9bc9e2 |
||
|
|
72103a67a5 |
||
|
|
7f1be1818e |
||
|
|
6a7d60d89c |
||
|
|
7df14594d1 |
||
|
|
001a6a330e |
||
|
|
9ff7c8e1e7 |
||
|
|
17f315a44a |
||
|
|
d2cdc980e2 |
||
|
|
9c9e8b8f9c |
||
|
|
b7dc45c2c3 |
||
|
|
ebb17f10e3 |
||
|
|
da26f04917 |
||
|
|
930c79d649 |
||
|
|
f19b9d0084 |
||
|
|
c771de380a |
||
|
|
bb26d062de |
||
|
|
41b1514f66 |
||
|
|
09aebbcb8b |
||
|
|
df8174341e |
||
|
|
93012d8bd1 |
||
|
|
9a739177bc |
||
|
|
34ef49a629 |
||
|
|
647799180a |
||
|
|
e00162180f |
||
|
|
1567b9dec2 |
||
|
|
2e5962422a |
||
|
|
fb292da426 |
||
|
|
98b8df7bb2 |
||
|
|
a5355dd287 |
||
|
|
8915177909 |
||
|
|
ad232e3506 |
||
|
|
a8aed0a011 |
||
|
|
79b7776e40 |
||
|
|
d96ae23a60 |
||
|
|
c44bbe2584 |
||
|
|
fa05c2bc30 |
||
|
|
9b26a5fdb3 |
||
|
|
c8b0cf89c8 |
||
|
|
911c0a5e51 |
||
|
|
8773f3d3e6 |
||
|
|
974a195a3c |
||
|
|
9b63d1919f |
||
|
|
04b65168b8 |
||
|
|
f2ce2829eb |
||
|
|
d3d127efb8 |
||
|
|
98a9bf6938 |
||
|
|
56f586974e | ||
|
|
2f6e52032e |
||
|
|
09d60c804f |
||
|
|
8c210c248f | ||
|
|
3f6ba916c0 | ||
|
|
3213f9288a |
||
|
|
2411f7482b |
||
|
|
7ac0b41800 |
||
|
|
03f1140d71 |
||
|
|
3e58fc6d1a |
||
|
|
fe0f711d62 |
||
|
|
dbd5fbbca8 |
||
|
|
da12ba3cad |
||
|
|
1178e151ef |
||
|
|
0cbbaacd12 |
||
|
|
bd7f84196b |
||
|
|
8babbb0e04 |
||
|
|
63d40420c9 |
||
|
|
6afa1e07b5 |
||
|
|
6f5359c7a3 |
||
|
|
60f20b2b22 |
||
|
|
25e19296d0 |
||
|
|
fd66eee442 |
||
|
|
f2809b0d19 |
||
|
|
91d8b73fad |
||
|
|
5ac23e1ca3 |
||
|
|
0961bcad1d |
||
|
|
4c8ccea7c3 |
||
|
|
68ff4c3205 |
||
|
|
b30df5621a |
||
|
|
02759caf1c |
||
|
|
9df7055ee1 |
||
|
|
60636fb8c3 |
||
|
|
1b7263e35e |
||
|
|
5937594af1 |
||
|
|
4da483032e |
||
|
|
93cb37ff77 |
||
|
|
0ae87b7b49 |
||
|
|
dfe9e97b00 |
||
|
|
51b7f9ccee |
||
|
|
44a5f5eadf |
||
|
|
a89bcbfa16 |
||
|
|
3436711467 |
||
|
|
dd160c5199 |
||
|
|
4e31d23b0c |
||
|
|
6fb9f631a1 |
||
|
|
fca8805043 |
||
|
|
9d533a2051 |
||
|
|
f7a0099b53 |
||
|
|
d98339017f |
||
|
|
6bc973dd66 |
||
|
|
7fca14868f |
||
|
|
a9378ac341 |
||
|
|
09bca3b5fd |
||
|
|
001880b49d |
||
|
|
bcb7131054 |
||
|
|
b5d93a3bab |
||
|
|
6c411dd7a2 |
||
|
|
3e3b2155cd |
||
|
|
95d8af8559 |
||
|
|
358ca77692 |
||
|
|
8ec535701c |
||
|
|
07472f8622 |
||
|
|
0680a57066 |
||
|
|
88fa1c2b3d |
||
|
|
b38f2393fd |
||
|
|
b00beaae70 |
||
|
|
d349e16411 |
||
|
|
13e53bfba0 |
||
|
|
281669e6c5 |
||
|
|
30d49406e4 |
||
|
|
3fce6e45ea |
||
|
|
2c8eeec473 |
||
|
|
ee527cf95c |
||
|
|
02fe4e64ac |
||
|
|
99d526e0cf |
||
|
|
16b48bb1db |
||
|
|
3e3ffabf7e |
||
|
|
fcbcf3a504 |
||
|
|
370dc0f3d8 |
||
|
|
8619197e92 |
||
|
|
205a60aafc |
||
|
|
5c0d2789d8 |
||
|
|
3790d10f96 |
||
|
|
72e927edd3 |
||
|
|
506b389e40 |
||
|
|
22a40d64cd |
||
|
|
4586b5f5b3 |
||
|
|
9a46a49c36 |
||
|
|
bf53c697a3 |
||
|
|
96fd94e2a1 |
||
|
|
bbda611499 |
||
|
|
083d320cf1 |
||
|
|
d79cd05fe6 |
||
|
|
1411a7405f |
||
|
|
7639563544 |
||
|
|
4c8f24da3f | ||
|
|
1ae45e3b47 |
||
|
|
19ef64ef32 | ||
|
|
650d0bf8ff | ||
|
|
7ba6106a63 | ||
|
|
52234179f3 | ||
|
|
976fd95517 | ||
|
|
ee2a73994e | ||
|
|
439a639d15 | ||
|
|
9c57629211 | ||
|
|
fa7dc36c83 |
||
|
|
06cdd8d5cc |
||
|
|
d28d8dcdc6 |
||
|
|
23d087e6cc |
||
|
|
a657a77633 |
||
|
|
6e0b28ee2c |
||
|
|
727f963923 |
||
|
|
7229201ad3 |
||
|
|
042dfa9f69 |
||
|
|
bdd312152c |
||
|
|
00051c6e53 |
||
|
|
4f6cee33c2 | ||
|
|
02fcb00bb3 |
||
|
|
0e62041ab4 |
||
|
|
06fe826379 |
||
|
|
57ae7c7df8 |
||
|
|
40b3edc0e2 |
||
|
|
7f1d7f36e8 |
||
|
|
dd645a5b5b |
||
|
|
da81afe549 |
||
|
|
e29fafa6d2 |
||
|
|
571a6e71fb | ||
|
|
27835f9ce0 |
||
|
|
b0a1f5b6d9 |
||
|
|
a9474fae66 |
||
|
|
301ae004b6 |
||
|
|
d32d7f854a | ||
|
|
7eda8cedde | ||
|
|
0f63d3bc4e | ||
|
|
9cb256d4ec | ||
|
|
2a87edcaa3 |
||
|
|
e543d5329f | ||
|
|
0943241884 |
||
|
|
d0a90046b7 |
||
|
|
9922e0da2c |
||
|
|
2cc9c692d7 |
||
|
|
93609bb42d |
||
|
|
22f50c3e05 |
||
|
|
f0854c9a78 |
||
|
|
82d4babc13 |
||
|
|
8b7df92640 |
||
|
|
ed26112f7e |
||
|
|
40a1f6fef7 |
||
|
|
7b259d47f4 |
||
|
|
faf15cd44a |
||
|
|
40d0786b19 |
||
|
|
7fdd1192e1 |
||
|
|
d09244291f |
||
|
|
eae4745ce6 |
||
|
|
9554373332 |
||
|
|
22d757ed52 |
||
|
|
9fbce2a575 |
||
|
|
9257f1fed5 |
||
|
|
b52b04a81e |
||
|
|
3d4757f267 |
||
|
|
9dd4a13596 |
||
|
|
2e2ee22add |
||
|
|
f852312336 |
||
|
|
0d82e0e5ec |
||
|
|
d5434497da |
||
|
|
78d6ff1a3c |
||
|
|
205c9e66d7 |
||
|
|
e5eee65f66 |
||
|
|
714b1b3b8a |
||
|
|
fa0a5e4e37 |
||
|
|
9a430541df |
||
|
|
20489da9d6 |
||
|
|
2f1755cb03 |
||
|
|
a3b44389ee |
||
|
|
ca4cacbfff |
||
|
|
fc0b40a27f |
||
|
|
063639e389 |
||
|
|
b0aafd0411 |
||
|
|
87f784b613 |
||
|
|
9f0ba1abfb |
||
|
|
a4a86b0ab1 |
||
|
|
a69ecb0cf1 |
||
|
|
bb44461bab |
||
|
|
4332876651 |
||
|
|
50e38cebbe |
||
|
|
88a0b77e5d |
||
|
|
fe7d9d776c |
||
|
|
9e3171b56c |
||
|
|
df7d7e0f56 |
||
|
|
bfe8041047 |
||
|
|
96248ade9d |
||
|
|
c8e8ab51d5 |
||
|
|
d746140499 |
||
|
|
ce829ccae4 |
||
|
|
b231c500a3 |
||
|
|
911541bac4 |
||
|
|
5c2385ea96 |
||
|
|
29c076dc9a |
||
|
|
fbe3d42492 |
||
|
|
7d74bcec30 |
||
|
|
e8505ace12 |
||
|
|
2faca4c5a2 |
||
|
|
5252b0e0e6 |
||
|
|
8d914e3cf4 |
||
|
|
bdc5a8ac36 |
||
|
|
483f8dd0fd |
||
|
|
b570f01931 |
||
|
|
d5ae35c5c1 |
||
|
|
345006328d |
||
|
|
d34a1ff529 |
||
|
|
408916325b |
||
|
|
072e401d82 |
||
|
|
a7dbf1e85e |
||
|
|
42031f1716 |
||
|
|
3ad5ccb200 |
||
|
|
86e5529b0e |
||
|
|
f0440d3c40 |
||
|
|
4695d60b56 |
||
|
|
f846d2c41f |
||
|
|
cc55272841 |
||
|
|
8c85a9cb13 |
||
|
|
21ed073c99 |
||
|
|
1544967281 |
||
|
|
d0d8cdaf19 |
||
|
|
c3291d9993 |
||
|
|
1de47356ad |
||
|
|
9a6475f001 |
||
|
|
7c6ee29928 |
||
|
|
2753ffa3db |
||
|
|
2228b08b3a |
||
|
|
aa9dbb0057 |
||
|
|
61e2a9a78d |
||
|
|
b6ac50cc74 |
||
|
|
377b4ce340 |
||
|
|
18fc1fa4bd |
||
|
|
6e2caedc2d |
||
|
|
b39242c710 |
||
|
|
30c7fb2c1b |
||
|
|
03771c8be3 |
||
|
|
b332d12e0b |
||
|
|
df1fc5a591 |
||
|
|
28be7e8202 |
||
|
|
23f09836a0 |
||
|
|
a9ff68b47a |
||
|
|
edb8c2d2d9 |
||
|
|
3ce9fd58d3 |
||
|
|
73998e7f2c |
||
|
|
51aa98ab2b |
||
|
|
df3efbcf65 |
||
|
|
3f28dbe371 |
||
|
|
0abcadb979 |
||
|
|
e5d7c492d2 |
||
|
|
670642d8cb | ||
|
|
d2cabbdc0b | ||
|
|
257a479f3e | ||
|
|
add44e04a9 | ||
|
|
c1eb9ffc16 | ||
|
|
6104fc017d | ||
|
|
be4d898668 |
||
|
|
a21606f011 |
||
|
|
5859403cad |
||
|
|
b9596cb56a | ||
|
|
9c2963b21c | ||
|
|
194ae0b96d | ||
|
|
d370a5195c |
||
|
|
85edddbf0d |
||
|
|
ceaf3a9cce |
||
|
|
ad5f28ce6a |
||
|
|
6251e87fbe |
||
|
|
a239686702 |
||
|
|
6f4ded7ed7 |
||
|
|
912609d681 |
||
|
|
a64426984c |
||
|
|
7a30129796 |
||
|
|
c706ce7b27 |
||
|
|
933933e287 | ||
|
|
f5fa8a5e2f | ||
|
|
d120156b50 |
||
|
|
0c50a14ba7 |
||
|
|
274ef9f33d |
||
|
|
94971631a3 |
||
|
|
3aadcf21d7 |
||
|
|
beffbeae47 |
||
|
|
8b0956c6df |
||
|
|
4db0e64e81 |
||
|
|
c679ed69fa |
||
|
|
1394691f4a |
||
|
|
229118fb9e |
||
|
|
54e4ad3756 |
||
|
|
ed9a12ee80 | ||
|
|
dbe60c6a6e | ||
|
|
caec80ce05 |
||
|
|
3c3dc35ea3 | ||
|
|
03cac441dd | ||
|
|
45a9ab02fb |
||
|
|
35bc7ff4a5 |
||
|
|
447f371598 |
||
|
|
2f71bcf9d3 |
||
|
|
b42e4c29a1 |
||
|
|
033d8ea05c | ||
|
|
0a64ce7194 |
||
|
|
2362b55407 |
||
|
|
420e640f00 | ||
|
|
3e60bf15e9 |
||
|
|
7e240df2c5 |
||
|
|
da4efddb1c |
||
|
|
a4e447021c |
||
|
|
693fc96583 |
||
|
|
68b63967db |
||
|
|
50e458c5c6 |
||
|
|
3204c31447 |
||
|
|
416448ddec |
||
|
|
b1ab5c46c0 |
||
|
|
b4eb2e4116 |
||
|
|
3ecd463963 |
||
|
|
bcb2c9253b |
||
|
|
874800f371 |
||
|
|
18105daae5 |
||
|
|
1713fb6f1c |
||
|
|
c675d975bd |
||
|
|
6c9900ed69 |
||
|
|
2d83e0dade |
||
|
|
048ad943d4 |
||
|
|
a1b16b6ac6 |
||
|
|
518a8c671b |
||
|
|
a7170b42c5 |
||
|
|
ed70604b38 |
||
|
|
30fa3e7bef |
||
|
|
a0e08ce48f | ||
|
|
d185db333c | ||
|
|
86f1afa140 | ||
|
|
8485f35e17 | ||
|
|
abcffbd849 | ||
|
|
b7e6d97484 | ||
|
|
9026343cdd | ||
|
|
74062136fd |
||
|
|
ec66e5c2f0 |
||
|
|
2e5c5da3cb |
||
|
|
0f0f0aaddb |
||
|
|
5b7861a91e | ||
|
|
c90c495a31 | ||
|
|
c477121bc5 |
||
|
|
7ff3b16c8e | ||
|
|
d7b126f82a |
||
|
|
6af3d96cf2 | ||
|
|
6a258a19b3 |
||
|
|
544a9bd505 | ||
|
|
40ce7de7b7 |
||
|
|
6866b6f08c | ||
|
|
bcdb94861b | ||
|
|
c32b59b9bf |
||
|
|
b097013eba | ||
|
|
d7d369b5fe | ||
|
|
520b79e0dd | ||
|
|
52d3e158d3 | ||
|
|
0a599c806b | ||
|
|
16f65ec950 | ||
|
|
09ccfb9f4f |
||
|
|
4d7a923eaf | ||
|
|
3c6385e75f | ||
|
|
8331d1d978 | ||
|
|
b1d28f4efc | ||
|
|
54fbb7ceff | ||
|
|
dce7c74f45 | ||
|
|
4abb31287f | ||
|
|
a7bf336674 | ||
|
|
0cb111bba8 | ||
|
|
68b1ed245b | ||
|
|
f769dd45a6 |
||
|
|
261f3a97cb | ||
|
|
3399b0fa00 | ||
|
|
e89e1340a0 | ||
|
|
70b0c7c7e6 | ||
|
|
6eb2422362 | ||
|
|
7b9f009e17 | ||
|
|
12f4244814 |
||
|
|
cc139a07e1 |
||
|
|
42e3ba176d | ||
|
|
0925dcbca6 |
||
|
|
2edaa8c846 | ||
|
|
f1c0afd6cc |
||
|
|
1a03dea7c9 |
||
|
|
1ef62e6f79 |
||
|
|
ef33365884 |
||
|
|
b27a9ce01d | ||
|
|
13174eba98 |
||
|
|
0dbf59dd85 | ||
|
|
7a5aed016d |
||
|
|
8e703a718b |
||
|
|
a78ebd4f52 |
||
|
|
3342e069dd |
||
|
|
bde51b52e3 |
||
|
|
357ead18ce |
||
|
|
7c8a40bd2b |
||
|
|
56d883fa6d |
||
|
|
f69d968680 |
||
|
|
d5b4c28b34 |
||
|
|
d7c8cea78a |
||
|
|
aebaf83dde |
||
|
|
13feb4e889 |
||
|
|
cbcd0a5268 | ||
|
|
88b8043084 | ||
|
|
fe95e0f893 |
||
|
|
d741270d43 |
||
|
|
ba826a9856 | ||
|
|
73a802103e |
||
|
|
ecc94f9771 |
||
|
|
92e71a4929 |
||
|
|
43869ae594 |
||
|
|
9e23a3f794 |
||
|
|
5f57cd6954 |
||
|
|
19fc2ea44f |
||
|
|
77fc446770 |
||
|
|
28b585a754 |
||
|
|
f0599e37f6 |
||
|
|
08b4b83cf6 |
||
|
|
eddf19d989 |
||
|
|
8830701a43 |
||
|
|
8cc0f2c865 |
||
|
|
50376cd537 |
||
|
|
acc97794c7 |
||
|
|
4e85bd674b |
||
|
|
6085d0c8da |
||
|
|
49d2383d0b |
||
|
|
3a059b8076 |
||
|
|
3ea86ca01a |
||
|
|
b06d839f6f |
||
|
|
889abbf080 |
||
|
|
e27c81527b |
||
|
|
2afa04625a |
||
|
|
264e7264d2 |
||
|
|
fed69c51ae |
||
|
|
be449abcb0 |
||
|
|
d36ad4d5f6 |
||
|
|
52bf5045d1 |
||
|
|
62d65154ac |
||
|
|
a18ee8d27b |
||
|
|
926549727b |
||
|
|
990c7a4792 |
||
|
|
13eb2f2d80 |
||
|
|
f0d77b3c4d |
||
|
|
6c7e362853 |
||
|
|
f229e4fcfe |
||
|
|
99416f5530 |
||
|
|
b2dc61a903 |
||
|
|
44d6669d3c |
||
|
|
19636c3ee0 |
||
|
|
f717df3e52 |
||
|
|
bd79f1a3d7 |
||
|
|
ab9f9a4ae5 |
||
|
|
4bcaa4e564 |
||
|
|
3c79f0c4c1 |
||
|
|
bd934ed698 |
||
|
|
97be14fb5c |
||
|
|
08309f1e80 |
||
|
|
b8412ea607 |
||
|
|
7f0afbf3af |
||
|
|
63e4ff482b |
||
|
|
ff7f8a5dab |
||
|
|
c398d70326 |
||
|
|
4e30f45a3b |
||
|
|
4133a7550b |
||
|
|
9c99b7d19e |
||
|
|
0d97b76156 |
||
|
|
e0288af16c |
||
|
|
8b2e029df8 |
||
|
|
5fb465b9e8 |
||
|
|
b75d4772bf |
||
|
|
45260b65bb |
||
|
|
4b05207f5c |
||
|
|
3ea727bc7d |
||
|
|
de7941e238 | ||
|
|
2f17a3f71e | ||
|
|
9c7d6e188d | ||
|
|
09425bb82e |
||
|
|
e2918bd233 |
||
|
|
803b7ca0e3 |
||
|
|
1577bdff55 |
||
|
|
d190956c0f |
||
|
|
b0d32afb04 |
||
|
|
e2c20bb741 |
||
|
|
d02734aab8 |
||
|
|
627f1bb565 |
||
|
|
942cf708e5 |
||
|
|
963ed90aec |
||
|
|
454dd753f3 |
||
|
|
62d3ad81f1 |
||
|
|
6a7bf4187e |
||
|
|
e22519b94a | ||
|
|
7d20291448 | ||
|
|
d00609643c |
||
|
|
22e5ed0eb0 |
||
|
|
70000416e0 | ||
|
|
7e4a235dca |
||
|
|
48699d9a88 |
||
|
|
1207ad493f |
||
|
|
3e9f1f2814 |
268 changed files with 17103 additions and 9674 deletions
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[alias]
|
||||
xtask = "run --profile xtask --target host-tuple -p xtask --"
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
github: [anatawa12]
|
||||
custom: [https://booth.pm/ja/items/6448396]
|
||||
50
.github/actions/sign-windows/action.yml
vendored
Normal file
50
.github/actions/sign-windows/action.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: 'Sign in place'
|
||||
description: 'Signs exe in-place'
|
||||
inputs:
|
||||
signpath-api-token:
|
||||
description: SignPath REST API access token.
|
||||
required: true
|
||||
signing-policy-slug:
|
||||
description: SignPath signing policy slug
|
||||
version:
|
||||
required: true
|
||||
description: Version number of artifact we're signing
|
||||
|
||||
path:
|
||||
description: The path of artifact we'll sign
|
||||
required: true
|
||||
artifact-name:
|
||||
description: The name of artifact on github
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: upload unsigned artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
name: ${{ inputs.artifact-name }}
|
||||
|
||||
- name: Sign on signpath
|
||||
id: sign
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ inputs.signpath-api-token }}
|
||||
organization-id: 'c2d4dbf9-920f-4318-9017-7306e0fc7590'
|
||||
project-slug: 'vrc-get'
|
||||
signing-policy-slug: ${{ inputs.signing-policy-slug }}
|
||||
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: .github/actions/sign-windows/signed/${{ inputs.artifact-name }}
|
||||
parameters: |
|
||||
version: ${{ toJSON(inputs.version) }}
|
||||
|
||||
- name: Copy artifact
|
||||
env:
|
||||
DOWNLOADED_PATH: .github/actions/sign-windows/signed/${{ inputs.artifact-name }}
|
||||
REPLACE_TO: ${{ inputs.path }}
|
||||
shell: bash
|
||||
run:
|
||||
cp -f "$DOWNLOADED_PATH/$(basename "$REPLACE_TO")" "$REPLACE_TO"
|
||||
8
.github/copilot-instructions.md
vendored
Normal file
8
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
If you're writing code:
|
||||
- Please don't make localization for locales other than en / ja. I cannot review those locales.
|
||||
- Run cargo clippy for lints and cargo fmt for format before commit.
|
||||
- After completing the code and commit, please add a changelog entry. Please note that the numbers in the changelog file are pull request numbers, not issue numbers.
|
||||
- Please add it to the bottom of the change list.
|
||||
- Please use the proper section for each change. "Fix" should be used only for bug fixes. UX improvements typically belong under "Change", and new features typically under "Add". These are not strict rules, so use them flexibly.
|
||||
- You should use Conventional Commits (chore:, fix:, dev:, build:, docs:, style:, lint:, and others).
|
||||
- Please split commits for implementation and changelog updates.
|
||||
189
.github/scripts/localization-updates.js
vendored
189
.github/scripts/localization-updates.js
vendored
|
|
@ -55,9 +55,21 @@ module.exports = async ({github, context}) => {
|
|||
},
|
||||
];
|
||||
|
||||
/** @type {{missingCount: number, extraCount: number, id: string, discussionNumber: number}[]} */
|
||||
const localeData = [];
|
||||
|
||||
for (const locale of locales) {
|
||||
await processOneLocale(github, owner, repo, locale.discussionNumber, locale.replyId, locale.id);
|
||||
const proceed = await processOneLocale(github, owner, repo, locale.discussionNumber, locale.replyId, locale.id);
|
||||
localeData.push({
|
||||
id: locale.id,
|
||||
discussionNumber: locale.discussionNumber,
|
||||
missingCount: proceed.missingCount,
|
||||
extraCount: proceed.extraCount,
|
||||
})
|
||||
}
|
||||
|
||||
// 894 is English Localization and Text Representation
|
||||
await updateRootLocale(github, owner, repo, 894, localeData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -68,9 +80,113 @@ module.exports = async ({github, context}) => {
|
|||
* @param number {number}
|
||||
* @param replyToId {string}
|
||||
* @param localeId {string}
|
||||
* @return {Promise<void>}
|
||||
* @return {Promise<{missingCount: number, extraCount: number}>}
|
||||
*/
|
||||
async function processOneLocale(github, owner, repo, number, replyToId, localeId) {
|
||||
const enJson = json5.parse(await fs.readFile(`vrc-get-gui/locales/en.json5`, "utf8"));
|
||||
const enKeys = normalizeKeys(Object.keys(enJson.translation));
|
||||
const transJson = json5.parse(await fs.readFile(`vrc-get-gui/locales/${localeId}.json5`, "utf8"));
|
||||
const transKeys = normalizeKeys(Object.keys(transJson.translation));
|
||||
|
||||
const {missingList: missingKeys, extraList: extraKeys} = missingAndExtras(enKeys, transKeys);
|
||||
|
||||
const newData = {
|
||||
missingKeys,
|
||||
extraKeys,
|
||||
};
|
||||
|
||||
const newAutoPart = `**Missing Keys:**\n${listToMarkdown(missingKeys)}\n\n**Excess Keys:**\n${listToMarkdown(extraKeys)}\n`;
|
||||
|
||||
const {discussionId, previousJson: dataJson} = await updateComment(github, owner, repo, number, newAutoPart, newData);
|
||||
dataJson.missingKeys ??= [];
|
||||
dataJson.extraKeys ??= [];
|
||||
|
||||
// create comment if there are new missing / extra keys
|
||||
const {extraList: newlyAddedMissingKeys} = missingAndExtras(normalizeKeys(dataJson.missingKeys), missingKeys);
|
||||
const {extraList: newlyAddedExtraKeys} = missingAndExtras(normalizeKeys(dataJson.extraKeys), extraKeys);
|
||||
if (newlyAddedMissingKeys.length > 0 || newlyAddedExtraKeys.length > 0) {
|
||||
const text = `
|
||||
There are new missing / excess keys in the translation. Please update the translation!
|
||||
|
||||
**New Missing Keys:**
|
||||
|
||||
${listToMarkdown(newlyAddedMissingKeys)}
|
||||
|
||||
**New Excess Keys:**
|
||||
|
||||
${listToMarkdown(newlyAddedExtraKeys)}
|
||||
`
|
||||
|
||||
await github.graphql(`
|
||||
mutation($discussionId: ID!, $replyToId: ID!, $body: String!) {
|
||||
addDiscussionComment(input: {discussionId: $discussionId, replyToId: $replyToId, body: $body}) {
|
||||
comment {
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {discussionId, replyToId, body: text});
|
||||
}
|
||||
|
||||
return {
|
||||
missingCount: missingKeys.length,
|
||||
extraCount: extraKeys.length,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the root locale configuration for a specified repository.
|
||||
*
|
||||
* @param {import('@octokit/rest').Octokit} github - The GitHub API client instance used to interact with the GitHub API.
|
||||
* @param {string} owner - The owner of the repository where the root locale is to be updated.
|
||||
* @param {string} repo - The name of the repository where the root locale is to be updated.
|
||||
* @param {number} number
|
||||
* @param {{missingCount: number, extraCount: number, id: string, discussionNumber: number}[]} localeData - The locale data object containing the updated root locale configuration.
|
||||
* @return {Promise<void>} A promise that resolves to the API response for the update operation.
|
||||
*/
|
||||
async function updateRootLocale(github, owner, repo, number, localeData) {
|
||||
let table = "| locale | missing count | exceeding count | link |\n" +
|
||||
"| -- | -- | -- | -- |\n"
|
||||
for (let {missingCount, extraCount, id, discussionNumber} of localeData) {
|
||||
table += `| ${id} | ${missingCount} | ${extraCount} | [link](https://github.com/${owner}/${repo}/discussions/${discussionNumber}) |\n`;
|
||||
}
|
||||
|
||||
await updateComment(github, owner, repo, number, table, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param beforeList {T[]}
|
||||
* @param afterList {T[]}
|
||||
* @return {{missingList: T[], extraList: T[]}}
|
||||
*/
|
||||
function missingAndExtras(beforeList, afterList) {
|
||||
const missingList = beforeList.filter(key => !afterList.includes(key)).filter(key => !optionalKeys.includes(key));
|
||||
const extraList = afterList.filter(key => !beforeList.includes(key)).filter(key => !optionalKeys.includes(key));
|
||||
|
||||
return {missingList, extraList};
|
||||
}
|
||||
|
||||
function listToMarkdown(values) {
|
||||
return values.length === 0 ? 'nothing' : values.map(key => `- \`${key}\``).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param github {import('@octokit/rest').Octokit}
|
||||
* @param owner {string}
|
||||
* @param repo {string}
|
||||
* @param number {number}
|
||||
* @param content {string} the updated content
|
||||
* @param newData {object} data stored in the comment
|
||||
* @return {Promise<{previousJson: object, discussionId:string}>}
|
||||
*/
|
||||
async function updateComment(
|
||||
github,
|
||||
owner, repo, number,
|
||||
content,
|
||||
newData,
|
||||
) {
|
||||
/** @type {{data: {repository: {discussion: {body: string}}}}} */
|
||||
const result = await github.graphql(`
|
||||
query($owner: String!, $repo: String!, $number: Int!) {
|
||||
|
|
@ -98,42 +214,14 @@ async function processOneLocale(github, owner, repo, number, replyToId, localeId
|
|||
const postAutoPart = split1[1] ?? '';
|
||||
|
||||
const dataJsonLine = autoPart.split(/\r?\n/).find(l => l.startsWith(dataJsonLinePrefix));
|
||||
// the dataJson is for computing the difference and create new comment if there are changes
|
||||
const dataJson = dataJsonLine ? JSON.parse(dataJsonLine.slice(dataJsonLinePrefix.length)) : {};
|
||||
dataJson.missingKeys ??= [];
|
||||
dataJson.extraKeys ??= [];
|
||||
|
||||
const enJson = json5.parse(await fs.readFile(`vrc-get-gui/locales/en.json5`, "utf8"));
|
||||
const enKeys = normalizeKeys(Object.keys(enJson.translation));
|
||||
const transJson = json5.parse(await fs.readFile(`vrc-get-gui/locales/${localeId}.json5`, "utf8"));
|
||||
const transKeys = normalizeKeys(Object.keys(transJson.translation));
|
||||
|
||||
const missingKeys = enKeys.filter(key => !transKeys.includes(key)).filter(key => !optionalKeys.includes(key));
|
||||
const extraKeys = transKeys.filter(key => !enKeys.includes(key)).filter(key => !optionalKeys.includes(key));
|
||||
|
||||
const missingKeysStr = missingKeys.length === 0 ? 'nothing' : missingKeys.map(key => `- \`${key}\``).join('\n');
|
||||
const excessKeysStr = extraKeys.length === 0 ? 'nothing' : extraKeys.map(key => `- \`${key}\``).join('\n');
|
||||
|
||||
const newData = {
|
||||
missingKeys,
|
||||
extraKeys,
|
||||
};
|
||||
|
||||
const newAutoPart = `
|
||||
**Missing Keys:**
|
||||
|
||||
${missingKeysStr}
|
||||
|
||||
**Excess Keys:**
|
||||
|
||||
${excessKeysStr}
|
||||
const previousJson = dataJsonLine ? JSON.parse(dataJsonLine.slice(dataJsonLinePrefix.length)) : {};
|
||||
|
||||
const newBody = `${manualPart}${autoPartStart}
|
||||
${content}
|
||||
<!-- data part
|
||||
${dataJsonLinePrefix}${JSON.stringify(newData)}
|
||||
-->
|
||||
`;
|
||||
|
||||
const newBody = `${manualPart}${autoPartStart}${newAutoPart}${autoPartEnd}${postAutoPart}`;
|
||||
${autoPartEnd}${postAutoPart}`;
|
||||
|
||||
await github.graphql(`
|
||||
mutation($discussionId: ID!, $body: String!) {
|
||||
|
|
@ -145,36 +233,9 @@ ${dataJsonLinePrefix}${JSON.stringify(newData)}
|
|||
}
|
||||
`, {discussionId, body: newBody});
|
||||
|
||||
// create comment if there are new missing / extra keys
|
||||
const oldMissingKeys = new Set(normalizeKeys(dataJson.missingKeys));
|
||||
const oldExtraKeys = new Set(normalizeKeys(dataJson.extraKeys));
|
||||
const newlyAddedMissingKeys = missingKeys.filter(key => !oldMissingKeys.has(key));
|
||||
const newlyAddedExtraKeys = extraKeys.filter(key => !oldExtraKeys.has(key));
|
||||
if (newlyAddedMissingKeys.length > 0 || newlyAddedExtraKeys.length > 0) {
|
||||
const newMissingKeysStr = newlyAddedMissingKeys.length === 0 ? 'nothing' : newlyAddedMissingKeys.map(key => `- \`${key}\``).join('\n');
|
||||
const newExcessKeysStr = newlyAddedExtraKeys.length === 0 ? 'nothing' : newlyAddedExtraKeys.map(key => `- \`${key}\``).join('\n');
|
||||
|
||||
const text = `
|
||||
There are new missing / excess keys in the translation. Please update the translation!
|
||||
|
||||
**New Missing Keys:**
|
||||
|
||||
${newMissingKeysStr}
|
||||
|
||||
**New Excess Keys:**
|
||||
|
||||
${newExcessKeysStr}
|
||||
`
|
||||
|
||||
await github.graphql(`
|
||||
mutation($discussionId: ID!, $replyToId: ID!, $body: String!) {
|
||||
addDiscussionComment(input: {discussionId: $discussionId, replyToId: $replyToId, body: $body}) {
|
||||
comment {
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {discussionId, replyToId, body: text});
|
||||
return {
|
||||
previousJson,
|
||||
discussionId,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
232
.github/workflows/ci-gui.yml
vendored
232
.github/workflows/ci-gui.yml
vendored
|
|
@ -4,12 +4,9 @@ on:
|
|||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# workaround of https://github.com/tauri-apps/tauri-action/issues/1091
|
||||
TAURI_BUNDLER_DMG_IGNORE_CI: false
|
||||
|
||||
jobs:
|
||||
build-gui:
|
||||
|
|
@ -19,6 +16,7 @@ jobs:
|
|||
include:
|
||||
- triple: x86_64-unknown-linux-gnu
|
||||
on: ubuntu-22.04
|
||||
bundles: appimage,appimage-updater
|
||||
setup: |
|
||||
sudo apt update && sudo apt install -y lld
|
||||
ld.lld --version
|
||||
|
|
@ -28,12 +26,14 @@ jobs:
|
|||
|
||||
- triple: x86_64-pc-windows-msvc
|
||||
on: windows-latest
|
||||
bundles: setup-exe,setup-exe-zip,exe-updater
|
||||
|
||||
- triple: universal-apple-darwin
|
||||
on: macos-14
|
||||
setup: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
bundles: app,dmg,app-updater
|
||||
triple:
|
||||
- x86_64-unknown-linux-gnu
|
||||
#- aarch64-unknown-linux-gnu
|
||||
|
|
@ -46,7 +46,22 @@ jobs:
|
|||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: samypr100/setup-dev-drive@v4
|
||||
with:
|
||||
drive-size: 12GB # github actions grantees 14 GB of disk space. we have few GB for action environment
|
||||
drive-path: "/dev_drive.vhdx"
|
||||
mount-path: ${{ github.workspace }}
|
||||
trusted-dev-drive: true
|
||||
- name: Preinitialize git repository
|
||||
shell: bash
|
||||
run: |
|
||||
# hopefully for non-cleaning environments, actions/checkout will tries to clean existing repository
|
||||
# if existing dir is not git repository nor for specified repository.
|
||||
# this step creates git directory to workaround the behavior
|
||||
git init
|
||||
git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- run: rustup update stable
|
||||
|
|
@ -59,7 +74,7 @@ jobs:
|
|||
with:
|
||||
key: ci-build-gui-${{ matrix.triple }}
|
||||
- name: Cache javascript essentials
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
|
|
@ -87,20 +102,16 @@ jobs:
|
|||
cp vrc-get-gui/Tauri.toml vrc-get-gui/Tauri.toml.bak
|
||||
grep -v "remove if ci" < vrc-get-gui/Tauri.toml.bak > vrc-get-gui/Tauri.toml
|
||||
|
||||
- name: Enable Devtools Feature
|
||||
shell: bash
|
||||
run: |
|
||||
cargo add --package vrc-get-gui tauri --features devtools
|
||||
- name: Build ALCOM binary
|
||||
run:
|
||||
cargo xtask build-alcom --release --target ${{ matrix.triple }} --devtools ${{ (secrets.ACTIONS_STEP_DEBUG || vars.ACTIONS_STEP_DEBUG) == 'true' && '--verbose' || '' }}
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
with:
|
||||
projectPath: vrc-get-gui
|
||||
tauriScript: npm run tauri
|
||||
args: |
|
||||
--target ${{ matrix.triple }} -c '{"version":"${{ steps.version.outputs.version }}", "bundle":{"windows":{"certificateThumbprint": null}}}' ${{ (secrets.ACTIONS_STEP_DEBUG || vars.ACTIONS_STEP_DEBUG) == 'true' && '--verbose' || '' }}
|
||||
- name: Build installer
|
||||
shell: bash
|
||||
run: cargo xtask bundle-alcom --release --target ${{ matrix.triple }} --bundles ${{ matrix.bundles }}
|
||||
|
||||
- name: Upload built binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.triple }}
|
||||
path: |
|
||||
|
|
@ -111,6 +122,195 @@ jobs:
|
|||
target/${{ matrix.triple }}/release/bundle/*/ALCOM*
|
||||
target/${{ matrix.triple }}/release/bundle/*/alcom*
|
||||
|
||||
build-rpm:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- install_rust: false
|
||||
- no_dist: false
|
||||
- mock-env: fedora-40-x86_64
|
||||
install_rust: true
|
||||
no_dist: true
|
||||
mock-env:
|
||||
- fedora-40-x86_64
|
||||
- fedora-rawhide-x86_64
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 'fedora:latest'
|
||||
options: --privileged
|
||||
env:
|
||||
MOCK_ENV: ${{ matrix.mock-env }}
|
||||
RPMBUILD_OPTS: ${{ case(matrix.no_dist, '-D "dist %{nil}"', '') }} ${{ case(matrix.install_rust, '-D "install_rust 1"', '') }}
|
||||
steps:
|
||||
- name: Install CI dependencies
|
||||
run: dnf install -y git tar curl
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
# https://github.com/actions/checkout/issues/1169
|
||||
- run: git config --system --add safe.directory $GITHUB_WORKSPACE
|
||||
- name: install dependencies
|
||||
run: dnf install -y mock rpmbuild
|
||||
- name: prepare rpm build environment
|
||||
run: mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||
- name: update version name in spec file
|
||||
run: |
|
||||
COMMIT_HASH="$(git rev-parse HEAD)"
|
||||
SHORT_HASH="$(git rev-parse --short HEAD)"
|
||||
PKG_VERSION="$(<vrc-get-gui/Cargo.toml sed -En -e '/^version/{s/.*"(.*)".*/\1/p;}')+${SHORT_HASH}"
|
||||
|
||||
echo "PKG_VERSION=$PKG_VERSION" >> $GITHUB_ENV
|
||||
|
||||
cp vrc-get-gui/Cargo.toml vrc-get-gui/Cargo.toml.bak
|
||||
sed -E "/^version/s/\"$/+$(git rev-parse --short HEAD)\"/" < vrc-get-gui/Cargo.toml.bak > vrc-get-gui/Cargo.toml
|
||||
rm vrc-get-gui/Cargo.toml.bak
|
||||
git add vrc-get-gui/Cargo.toml
|
||||
|
||||
sed -i vrc-get-gui/bundle/alcom.spec -e "/^Version:/c\Version: ${PKG_VERSION//-/\~}"
|
||||
- name: build source rpm package
|
||||
run: |
|
||||
git archive --format=tar --prefix=vrc-get-gui-v$PKG_VERSION/ $(git write-tree) | gzip > ~/rpmbuild/SOURCES/gui-v$PKG_VERSION.tar.gz
|
||||
eval "rpmbuild -bs vrc-get-gui/bundle/alcom.spec $RPMBUILD_OPTS"
|
||||
- name: build rpm package
|
||||
run: eval "mock -v -r '$(ls -1 /etc/mock{/eol,}/$MOCK_ENV.cfg 2>/dev/null)' --enable-network $RPMBUILD_OPTS rebuild ~/rpmbuild/SRPMS/alcom-${PKG_VERSION//-/\~}-1*.src.rpm"
|
||||
- name: copy built binaries
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp ~/rpmbuild/SRPMS/alcom-${PKG_VERSION//-/\~}-1*.src.rpm artifacts/
|
||||
cp /var/lib/mock/$MOCK_ENV/result/alcom-${PKG_VERSION//-/\~}-1*.${MOCK_ENV##*-}.rpm artifacts/
|
||||
|
||||
- name: Upload built binary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: rpm-${{ matrix.mock-env }}
|
||||
path: artifacts/*
|
||||
|
||||
build-deb:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- install_rust: false
|
||||
- install_nodejs: false
|
||||
- apt-components: main
|
||||
- apt-with-updates: false
|
||||
|
||||
# Old distributions have older tools than we need. Download tools in build process
|
||||
- pbuilder-distribution: bookworm
|
||||
install_rust: true
|
||||
install_nodejs: true
|
||||
- pbuilder-distribution: jammy
|
||||
install_rust: true
|
||||
install_nodejs: true
|
||||
|
||||
# Debian uses mirror from debian-archive.trafficmanager.net which is managed by microsoft on azure
|
||||
- pbuilder-distribution: bookworm
|
||||
mirror: http://debian-archive.trafficmanager.net/debian/
|
||||
keyring: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
- pbuilder-distribution: sid
|
||||
mirror: http://debian-archive.trafficmanager.net/debian/
|
||||
keyring: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
# Ubuntu legacy release
|
||||
- pbuilder-distribution: jammy
|
||||
mirror: http://archive.ubuntu.com/ubuntu/
|
||||
apt-components: main universe
|
||||
apt-with-updates: true
|
||||
keyring: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
# For note,
|
||||
# bookworm: libc6@2.36
|
||||
# sid: libc6@2.39 as of 2026/06/14
|
||||
# jammy: libc6@2.35
|
||||
pbuilder-distribution:
|
||||
# debian distribution
|
||||
- bookworm
|
||||
- sid
|
||||
# ubuntu distribution
|
||||
- jammy # jammy is the oldest ubuntu release with libwebkit2gtk-4.1 >= 2.41 (but requires -updates and universe)
|
||||
target-arch:
|
||||
- amd64
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.target-arch }}
|
||||
PBUILDER_DISTRIBUTION: ${{ matrix.pbuilder-distribution }}
|
||||
PBUILDER_MIRROR: ${{ matrix.mirror }}
|
||||
PBUILDER_KEYRING: ${{ matrix.keyring }}
|
||||
PBUILDER_COMPONENTS: ${{ matrix.apt-components }}
|
||||
PBUILDER_OTHERMIRROR: ${{ case(matrix.apt-with-updates, format('deb {0} {1}-updates {2}', matrix.mirror, matrix.pbuilder-distribution, matrix.apt-components), '') }}
|
||||
INSTALL_RUST: ${{ case(matrix.install_rust, '1', '0') }}
|
||||
INSTALL_NODEJS: ${{ case(matrix.install_nodejs, '1', '0') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
path: vrc-get
|
||||
submodules: recursive
|
||||
- name: install dependencies
|
||||
run: sudo apt update && sudo apt install -y pbuilder debian-archive-keyring debhelper-compat=13
|
||||
- name: prepare deb build environment
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
cp -r vrc-get-gui/bundle/debian debian
|
||||
sudo pbuilder create \
|
||||
--architecture "$TARGET_ARCH" \
|
||||
--keyring "$PBUILDER_KEYRING" \
|
||||
--mirror "$PBUILDER_MIRROR" \
|
||||
--distribution "$PBUILDER_DISTRIBUTION" \
|
||||
--components "$PBUILDER_COMPONENTS" \
|
||||
--othermirror "$PBUILDER_OTHERMIRROR"
|
||||
- name: update changelog
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
COMMIT_HASH="$(git rev-parse HEAD)"
|
||||
SHORT_HASH="$(git rev-parse --short HEAD)"
|
||||
PKG_VERSION="$(<vrc-get-gui/Cargo.toml sed -En -e '/^version/{s/.*"(.*)".*/\1/p;}')+${SHORT_HASH}"
|
||||
|
||||
echo "PKG_VERSION=$PKG_VERSION" >> $GITHUB_ENV
|
||||
|
||||
cp debian/changelog debian/changelog.bak
|
||||
cat - debian/changelog.bak <<CHANGELOG > debian/changelog
|
||||
alcom (${PKG_VERSION//-/\~}-1) experimental;
|
||||
|
||||
* Upgraded version to ${PKG_VERSION}
|
||||
|
||||
-- anatawa12 <i@anatawa12.com> $(date -u +"%a, %d %b %Y %H:%M:%S +0000")
|
||||
|
||||
CHANGELOG
|
||||
rm debian/changelog.bak
|
||||
|
||||
cp vrc-get-gui/Cargo.toml vrc-get-gui/Cargo.toml.bak
|
||||
sed -E "/^version/s/\"$/+$(git rev-parse --short HEAD)\"/" < vrc-get-gui/Cargo.toml.bak > vrc-get-gui/Cargo.toml
|
||||
rm vrc-get-gui/Cargo.toml.bak
|
||||
git add vrc-get-gui/Cargo.toml
|
||||
|
||||
echo cat debian/changelog
|
||||
cat debian/changelog
|
||||
dpkg-parsechangelog
|
||||
- name: build source deb package
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
git archive --format=tar $(git write-tree) | xz > ../alcom_${PKG_VERSION//-/\~}.orig.tar.xz
|
||||
dpkg-buildpackage -d -S
|
||||
- name: build deb package
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
sudo --preserve-env=INSTALL_RUST,INSTALL_NODEJS pbuilder build --use-network yes ../alcom_${PKG_VERSION//-/\~}-1.dsc
|
||||
- name: copy built binaries
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp vrc-get/debian/changelog artifacts/
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}* artifacts/
|
||||
ls artifacts
|
||||
- name: Print information about built package
|
||||
run: dpkg-deb -I artifacts/alcom_${PKG_VERSION//-/\~}-1_$TARGET_ARCH.deb
|
||||
|
||||
- name: Upload built binary
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deb-${{ matrix.pbuilder-distribution }}-${{ matrix.target-arch }}
|
||||
path: artifacts/*
|
||||
|
||||
conclude-gui:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() }}
|
||||
|
|
|
|||
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
|
|
@ -4,7 +4,6 @@ on:
|
|||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
|
@ -72,11 +71,9 @@ jobs:
|
|||
runs-on: ${{ matrix.on }}
|
||||
env:
|
||||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
# workaround of https://github.com/tauri-apps/tauri-action/issues/1091
|
||||
TAURI_BUNDLER_DMG_IGNORE_CI: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
|
@ -102,7 +99,7 @@ jobs:
|
|||
- name: Build
|
||||
run: cargo build --verbose --target ${{ matrix.triple }}
|
||||
- name: Upload built binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.triple }}
|
||||
path: |
|
||||
|
|
@ -116,10 +113,10 @@ jobs:
|
|||
- name: Check binary is statically linked
|
||||
shell: bash
|
||||
if: ${{ matrix.static-linked }}
|
||||
env:
|
||||
RUSTFLAGS: ''
|
||||
run: |
|
||||
# https://github.com/taiki-e/setup-cross-toolchain-action/issues/18
|
||||
unset CARGO_BUILD_TARGET
|
||||
cargo run -p build-check-static-link target/${{ matrix.triple }}/debug/vrc-get*
|
||||
cargo xtask check-static-link target/${{ matrix.triple }}/debug/vrc-get${WINDIR:+.exe}
|
||||
|
||||
conclude:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
4
.github/workflows/labeler.yml
vendored
4
.github/workflows/labeler.yml
vendored
|
|
@ -9,11 +9,11 @@ jobs:
|
|||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v2
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
- uses: actions/labeler@v5
|
||||
- uses: actions/labeler@v6
|
||||
with:
|
||||
repo-token: ${{ steps.app-token.outputs.token }}
|
||||
|
|
|
|||
14
.github/workflows/lint.yml
vendored
14
.github/workflows/lint.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
run:
|
||||
sudo apt update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
|
|
@ -50,10 +50,10 @@ jobs:
|
|||
target/
|
||||
key: lints-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- uses: auguwu/clippy-action@1.4.0
|
||||
- uses: auguwu/clippy-action@1.5.0
|
||||
with:
|
||||
token: ${{secrets.GITHUB_TOKEN}}
|
||||
check-args: --all --all-targets --all-features
|
||||
check-args: --all --all-targets --all-features --exclude windows-installer-wrapper
|
||||
args: -Dclippy::todo -Dwarnings
|
||||
|
||||
biome:
|
||||
|
|
@ -63,7 +63,7 @@ jobs:
|
|||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: run Biome
|
||||
working-directory: vrc-get-gui
|
||||
run: npm run biome -- ci
|
||||
|
|
@ -75,7 +75,7 @@ jobs:
|
|||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: run Type Check
|
||||
working-directory: vrc-get-gui
|
||||
run: npm ci && npx tsc
|
||||
|
|
|
|||
6
.github/workflows/localization-updates.yml
vendored
6
.github/workflows/localization-updates.yml
vendored
|
|
@ -12,13 +12,13 @@ jobs:
|
|||
contents: read
|
||||
discussions: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20.x'
|
||||
- run: npm i --omit=dev
|
||||
working-directory: .github/scripts
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/localization-updates.js')
|
||||
|
|
|
|||
642
.github/workflows/publish-gui.yml
vendored
642
.github/workflows/publish-gui.yml
vendored
|
|
@ -1,12 +1,642 @@
|
|||
name: Apple Script Test
|
||||
name: Publish (GUI)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_kind:
|
||||
type: choice
|
||||
description: The type of release.
|
||||
default: prerelease
|
||||
required: true
|
||||
options:
|
||||
- prerelease
|
||||
- start-rc
|
||||
- stable
|
||||
dry-run:
|
||||
type: boolean
|
||||
description: Dry Run, If true, do not publish release to GitHub.
|
||||
default: true
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
group: releasing
|
||||
|
||||
jobs:
|
||||
run-test:
|
||||
runs-on: macos-latest
|
||||
pre-build:
|
||||
name: Update version name
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
gui-version: ${{ env.GUI_VERSION }}
|
||||
prerelease: ${{ steps.update-version.outputs.prerelease }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run test AppleScript
|
||||
run: osascript test.applescript "$(pwd)"
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: anatawa12/something-releaser@v3
|
||||
- uses: snow-actions/git-config-user@v1.0.0
|
||||
- run: rustup update stable
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Update Version Name
|
||||
id: update-version
|
||||
run: |
|
||||
# set version name in properties file
|
||||
case "$RELEASE_KIND_IN" in
|
||||
"prerelease" )
|
||||
get-version -t gui | version-next | set-version -t gui
|
||||
gh-export-variable PRERELEASE true
|
||||
gh-set-output prerelease true
|
||||
;;
|
||||
"start-rc" )
|
||||
get-version -t gui | version-set-channel - rc 0 | set-version -t gui
|
||||
gh-export-variable PRERELEASE true
|
||||
gh-set-output prerelease true
|
||||
;;
|
||||
"stable" )
|
||||
get-version -t gui | version-set-channel - stable | set-version -t gui
|
||||
gh-export-variable PRERELEASE false
|
||||
gh-set-output prerelease '' # empty string for false
|
||||
;;
|
||||
* )
|
||||
echo "invalid release kind: $RELEASE_KIND_IN"
|
||||
exit 255
|
||||
;;
|
||||
esac
|
||||
|
||||
GUI_VERSION="$(get-version -t gui)"
|
||||
# update debian package
|
||||
cp vrc-get-gui/bundle/debian/changelog vrc-get-gui/bundle/debian/changelog.bak
|
||||
cat - vrc-get-gui/bundle/debian/changelog.bak <<CHANGELOG > vrc-get-gui/bundle/debian/changelog
|
||||
alcom (${GUI_VERSION//-/\~}-1) stable;
|
||||
|
||||
* Upgraded version to ${GUI_VERSION}
|
||||
|
||||
-- anatawa12 <i@anatawa12.com> $(date -u +"%a, %d %b %Y %H:%M:%S +0000")
|
||||
|
||||
CHANGELOG
|
||||
rm vrc-get-gui/bundle/debian/changelog.bak
|
||||
# update rpm package
|
||||
sed -i vrc-get-gui/bundle/alcom.spec -e "/^Version:/c\Version: ${GUI_VERSION//-/\~}"
|
||||
|
||||
case "$GITHUB_REF_NAME" in
|
||||
master | master-* )
|
||||
echo "head is master or master-*"
|
||||
;;
|
||||
* )
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo "head is not master, but DRY_RUN is true"
|
||||
else
|
||||
echo "head is not master, but DRY_RUN is false"
|
||||
exit 255
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
gh-export-variable GUI_VERSION "${GUI_VERSION}"
|
||||
env:
|
||||
RELEASE_KIND_IN: ${{ inputs.release_kind }}
|
||||
DRY_RUN: ${{ inputs.dry-run }}
|
||||
|
||||
# region changelog
|
||||
- name: Create Changelog
|
||||
id: changelog
|
||||
uses: anatawa12/sh-actions/changelog/prepare-release@master
|
||||
with:
|
||||
path: CHANGELOG-gui.md
|
||||
version: ${{ env.GUI_VERSION }}
|
||||
prerelease: ${{ env.PRERELEASE }}
|
||||
tag-prefix: gui-v
|
||||
prerelease-note-heading: |
|
||||
Version ${{ env.GUI_VERSION }}
|
||||
---
|
||||
release-note-heading: |
|
||||
Version ${{ env.GUI_VERSION }}
|
||||
---
|
||||
- name: Upload CHANGELOG.md
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: CHANGELOG
|
||||
path: CHANGELOG.md
|
||||
- name: copy release note
|
||||
run: cp "${{ steps.changelog.outputs.release-note }}" release-note.md
|
||||
- name: Upload release note
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-note-for-release
|
||||
path: release-note.md
|
||||
- name: remove temp release note file
|
||||
run: rm release-note.md
|
||||
# endregion changelog
|
||||
|
||||
- name: Commit
|
||||
id: update
|
||||
run: |-
|
||||
# commit & tag
|
||||
git commit -am "gui v$GUI_VERSION"
|
||||
git branch releasing
|
||||
git push -f -u origin releasing
|
||||
|
||||
build-rust:
|
||||
name: Build rust
|
||||
environment:
|
||||
name: actions-code-signing
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# note: when you changed paths for tauri updater (which are files with .sig),
|
||||
# remember keep in sync with build-updater-json
|
||||
- name: x86_64-linux-appimage
|
||||
triple: x86_64-unknown-linux-gnu
|
||||
on: ubuntu-22.04
|
||||
setup: |
|
||||
sudo apt update && sudo apt install -y lld
|
||||
ld.lld --version
|
||||
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
rustflags: "-C link-arg=-fuse-ld=lld"
|
||||
last-bundles: appimage,appimage-updater
|
||||
updater-bundle: bundle/appimage/ALCOM_${GUI_VERSION}_x86_64.AppImage.tar.gz
|
||||
dist-path: |
|
||||
bundle/appimage/ALCOM_${GUI_VERSION}_x86_64.AppImage:alcom-${GUI_VERSION}-x86_64.AppImage
|
||||
|
||||
bundle/appimage/ALCOM_${GUI_VERSION}_x86_64.AppImage.tar.gz:alcom-${GUI_VERSION}-x86_64.AppImage.tar.gz
|
||||
bundle/appimage/ALCOM_${GUI_VERSION}_x86_64.AppImage.tar.gz.sig:alcom-${GUI_VERSION}-x86_64.AppImage.tar.gz.sig
|
||||
|
||||
- name: x86_64-windows-all
|
||||
triple: x86_64-pc-windows-msvc
|
||||
on: windows-2022
|
||||
last-bundles: setup-exe-zip,exe-updater
|
||||
updater-bundle: bundle/setup/alcom-updater.exe
|
||||
dist-path: |
|
||||
ALCOM.exe:ALCOM-${GUI_VERSION}-x86_64.exe
|
||||
bundle/setup/alcom-setup.exe:ALCOM-${GUI_VERSION}-x86_64-setup.exe
|
||||
bundle/setup/alcom-setup.exe.zip:ALCOM-${GUI_VERSION}-x86_64-setup.exe.zip
|
||||
bundle/setup/alcom-updater.exe:ALCOM-${GUI_VERSION}-x86_64-updater.exe
|
||||
bundle/setup/alcom-updater.exe.sig:ALCOM-${GUI_VERSION}-x86_64-updater.exe.sig
|
||||
|
||||
- name: universal-macos-all
|
||||
triple: universal-apple-darwin
|
||||
on: macos-14
|
||||
setup: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
last-bundles: dmg,app-updater
|
||||
updater-bundle: bundle/macos/ALCOM.app.tar.gz
|
||||
dist-path: |
|
||||
bundle/dmg/ALCOM_${GUI_VERSION}_universal.dmg:ALCOM-${GUI_VERSION}-universal.dmg
|
||||
|
||||
bundle/macos/ALCOM.app.tar.gz:ALCOM-${GUI_VERSION}-universal.app.tar.gz
|
||||
bundle/macos/ALCOM.app.tar.gz.sig:ALCOM-${GUI_VERSION}-universal.app.tar.gz.sig
|
||||
|
||||
name:
|
||||
- x86_64-linux-appimage
|
||||
#- aarch64-unknown-linux-musl
|
||||
- x86_64-windows-all
|
||||
#- aarch64-windows-all
|
||||
- universal-macos-all
|
||||
|
||||
runs-on: ${{ matrix.on }}
|
||||
env:
|
||||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
|
||||
needs: [ pre-build ]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
submodules: recursive
|
||||
- run: rustup update stable
|
||||
- name: Install cross-compilation tools
|
||||
uses: taiki-e/setup-cross-toolchain-action@v1
|
||||
if: ${{ matrix.triple != 'universal-apple-darwin' }}
|
||||
with:
|
||||
target: ${{ matrix.triple }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-targets: false # for release build, do not cache build artifacts
|
||||
key: release-gui # there are no elements about build result, so it's ok to share between all builds
|
||||
|
||||
- name: Setup
|
||||
run: ${{ matrix.setup }}
|
||||
|
||||
- name: Build ALCOM binary
|
||||
run: |
|
||||
cargo xtask build-alcom --target ${{ matrix.triple }} --release ${{ matrix.alcom-build-options }}
|
||||
|
||||
- name: pre-sign Bundle ALCOM app (macOS)
|
||||
if: ${{ contains(matrix.name, 'macos') }}
|
||||
run: cargo xtask bundle-alcom --target ${{ matrix.triple }} --release --bundles app
|
||||
|
||||
- name: Sign ALCOM app (macOS)
|
||||
if: ${{ contains(matrix.name, 'macos') }}
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: cargo xtask sign-alcom-app --target ${{ matrix.triple }}
|
||||
|
||||
- name: Sign ALCOM exe (Windows)
|
||||
if: ${{ contains(matrix.name, 'windows') }}
|
||||
uses: ./.github/actions/sign-windows
|
||||
with:
|
||||
artifact-name: alcom-exe-unsigned
|
||||
path: target/${{ matrix.triple }}/release/ALCOM.exe
|
||||
|
||||
signpath-api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
signing-policy-slug: ${{ case(inputs.dry-run, 'test-signing', 'release-signing') }}
|
||||
version: ${{ needs.pre-build.outputs.gui-version }}
|
||||
|
||||
- name: Bundle Setup exe (Windows)
|
||||
if: ${{ contains(matrix.name, 'windows') }}
|
||||
run: cargo xtask bundle-alcom --target ${{ matrix.triple }} --release --bundles setup-exe
|
||||
|
||||
- name: Sign Setup exe (Windows)
|
||||
if: ${{ contains(matrix.name, 'windows') }}
|
||||
uses: ./.github/actions/sign-windows
|
||||
with:
|
||||
artifact-name: alcom-setup-exe-unsigned
|
||||
path: target/${{ matrix.triple }}/release/bundle/setup/alcom-setup.exe
|
||||
|
||||
signpath-api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
signing-policy-slug: ${{ case(inputs.dry-run, 'test-signing', 'release-signing') }}
|
||||
version: ${{ needs.pre-build.outputs.gui-version }}
|
||||
|
||||
- name: Bundle ALCOM (${{ matrix.last-bundles }})
|
||||
run: cargo xtask bundle-alcom --target ${{ matrix.triple }} --release --bundles ${{ matrix.last-bundles }}
|
||||
|
||||
- name: Sign updater artifacts (All Platforms)
|
||||
shell: bash
|
||||
if: ${{ matrix.updater-bundle }}
|
||||
env:
|
||||
GUI_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
UPDATER_BUNDLE: ${{ matrix.updater-bundle }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: |
|
||||
UPDATER_BUNDLE="${UPDATER_BUNDLE//\$\{GUI_VERSION\}/$GUI_VERSION}"
|
||||
cargo xtask sign-alcom-updater "target/${{ matrix.triple }}/release/${UPDATER_BUNDLE}"
|
||||
|
||||
- name: Move artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
env:
|
||||
GUI_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
DIST_PATH: ${{ matrix.dist-path }}
|
||||
run: |-
|
||||
mkdir artifacts
|
||||
|
||||
echo "$DIST_PATH" | while IFS=: read -r src dst; do
|
||||
src="${src//\$\{GUI_VERSION\}/$GUI_VERSION}"
|
||||
dst="${dst//\$\{GUI_VERSION\}/$GUI_VERSION}"
|
||||
if [ -z "$dst" ]; then
|
||||
continue
|
||||
fi
|
||||
printf "mv %s %s\n" "target/${{ matrix.triple }}/release/$src" "artifacts/$dst"
|
||||
mv "target/${{ matrix.triple }}/release/$src" "artifacts/$dst"
|
||||
done
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: artifacts-${{ matrix.name }}
|
||||
path: artifacts/*
|
||||
|
||||
build-rpm:
|
||||
needs: [ pre-build ]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- install_rust: false
|
||||
- no_dist: false
|
||||
- mock-env: fedora-40-x86_64
|
||||
install_rust: true
|
||||
no_dist: true
|
||||
mock-env:
|
||||
- fedora-40-x86_64
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 'fedora:latest'
|
||||
options: --privileged
|
||||
env:
|
||||
MOCK_ENV: ${{ matrix.mock-env }}
|
||||
RPMBUILD_OPTS: ${{ case(matrix.no_dist, '-D "dist %{nil}"', '') }} ${{ case(matrix.install_rust, '-D "install_rust 1"', '') }}
|
||||
|
||||
PKG_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
steps:
|
||||
- name: Install CI dependencies
|
||||
run: dnf install -y git tar curl
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
submodules: recursive
|
||||
# https://github.com/actions/checkout/issues/1169
|
||||
- run: git config --system --add safe.directory $GITHUB_WORKSPACE
|
||||
- name: install dependencies
|
||||
run: dnf install -y mock rpmbuild
|
||||
- name: prepare rpm build environment
|
||||
run: mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||
- name: build source rpm package
|
||||
run: |
|
||||
git archive --format=tar --prefix=vrc-get-gui-v$PKG_VERSION/ $(git write-tree) | gzip > ~/rpmbuild/SOURCES/gui-v$PKG_VERSION.tar.gz
|
||||
eval "rpmbuild -bs vrc-get-gui/bundle/alcom.spec $RPMBUILD_OPTS"
|
||||
- name: build rpm package
|
||||
run: eval "mock -v -r '$(ls -1 /etc/mock{/eol,}/$MOCK_ENV.cfg 2>/dev/null)' --enable-network $RPMBUILD_OPTS rebuild ~/rpmbuild/SRPMS/alcom-${PKG_VERSION//-/\~}-1*.src.rpm"
|
||||
- name: copy built binaries
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp ~/rpmbuild/SRPMS/alcom-${PKG_VERSION//-/\~}-1*.src.rpm artifacts/
|
||||
cp /var/lib/mock/$MOCK_ENV/result/alcom-${PKG_VERSION//-/\~}-1*.${MOCK_ENV##*-}.rpm artifacts/
|
||||
|
||||
- name: Upload built binary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-rpm-${{ matrix.mock-env }}
|
||||
path: artifacts/*
|
||||
|
||||
build-deb:
|
||||
needs: [ pre-build ]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- install_rust: false
|
||||
- install_nodejs: false
|
||||
- apt-components: main
|
||||
- apt-with-updates: false
|
||||
|
||||
# Old distributions have older tools than we need. Download tools in build process
|
||||
- pbuilder-distribution: jammy
|
||||
install_rust: true
|
||||
install_nodejs: true
|
||||
|
||||
# Debian uses mirror from debian-archive.trafficmanager.net which is managed by microsoft on azure
|
||||
- pbuilder-distribution: jammy
|
||||
mirror: http://archive.ubuntu.com/ubuntu/
|
||||
apt-components: main universe
|
||||
apt-with-updates: true
|
||||
keyring: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
# We build on jammy since it's the distribution with a) libwebkit2gtk-4.1 >= 2.41 and 2) oldest libc version required.
|
||||
# bookworm: libc6@2.36
|
||||
# sid: libc6@2.39 as of 2026/06/14
|
||||
# jammy: libc6@2.35
|
||||
pbuilder-distribution:
|
||||
- jammy # jammy is the oldest ubuntu release with libwebkit2gtk-4.1 >= 2.41 (but requires -updates and universe)
|
||||
target-arch:
|
||||
- amd64
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TARGET_ARCH: ${{ matrix.target-arch }}
|
||||
PBUILDER_DISTRIBUTION: ${{ matrix.pbuilder-distribution }}
|
||||
PBUILDER_MIRROR: ${{ matrix.mirror }}
|
||||
PBUILDER_KEYRING: ${{ matrix.keyring }}
|
||||
PBUILDER_COMPONENTS: ${{ matrix.apt-components }}
|
||||
PBUILDER_OTHERMIRROR: ${{ case(matrix.apt-with-updates, format('deb {0} {1}-updates {2}', matrix.mirror, matrix.pbuilder-distribution, matrix.apt-components), '') }}
|
||||
INSTALL_RUST: ${{ case(matrix.install_rust, '1', '0') }}
|
||||
INSTALL_NODEJS: ${{ case(matrix.install_nodejs, '1', '0') }}
|
||||
|
||||
PKG_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
path: vrc-get
|
||||
submodules: recursive
|
||||
- name: install dependencies
|
||||
run: sudo apt update && sudo apt install -y pbuilder debian-archive-keyring debhelper-compat=13
|
||||
- name: prepare deb build environment
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
( mkdir debian && cd debian && ln -s ../vrc-get-gui/bundle/debian/* . )
|
||||
sudo pbuilder create \
|
||||
--architecture "$TARGET_ARCH" \
|
||||
--keyring "$PBUILDER_KEYRING" \
|
||||
--mirror "$PBUILDER_MIRROR" \
|
||||
--distribution "$PBUILDER_DISTRIBUTION" \
|
||||
--components "$PBUILDER_COMPONENTS" \
|
||||
--othermirror "$PBUILDER_OTHERMIRROR"
|
||||
- name: build source deb package
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
git archive --format=tar HEAD | xz > ../alcom_${PKG_VERSION//-/\~}.orig.tar.xz
|
||||
dpkg-buildpackage -d -S
|
||||
- name: build deb package
|
||||
working-directory: vrc-get
|
||||
run: |
|
||||
sudo --preserve-env=INSTALL_RUST,INSTALL_NODEJS pbuilder build --use-network yes ../alcom_${PKG_VERSION//-/\~}-1.dsc
|
||||
- name: copy built binaries
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}-1_$TARGET_ARCH.deb artifacts/
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}-1_$TARGET_ARCH.buildinfo artifacts/
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}-1_$TARGET_ARCH.changes artifacts/
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}-1.debian.tar.xz artifacts/
|
||||
cp /var/cache/pbuilder/result/alcom_${PKG_VERSION//-/\~}-1.dsc artifacts/
|
||||
ls artifacts
|
||||
- name: Print information about built package
|
||||
run: dpkg-deb -I artifacts/alcom_${PKG_VERSION//-/\~}-1_$TARGET_ARCH.deb
|
||||
|
||||
- name: Upload built binary
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-deb-${{ matrix.pbuilder-distribution }}-${{ matrix.target-arch }}
|
||||
path: artifacts/*
|
||||
|
||||
build-updater-json:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre-build, build-rust ]
|
||||
steps:
|
||||
# use release
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Download All Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: assets
|
||||
pattern: artifacts-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Run updater-json
|
||||
env:
|
||||
GUI_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
run: cargo xtask alcom-updater-json --version "$GUI_VERSION" --assets assets updater.json
|
||||
|
||||
- name: Upload updater-json
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: updater.json
|
||||
path: |
|
||||
updater.json
|
||||
|
||||
publish-to-github:
|
||||
name: Publish to GitHub
|
||||
if: ${{ !inputs.dry-run }}
|
||||
environment:
|
||||
name: actions-github-app
|
||||
url: https://github.com/anatawa12/vrc-get/releases/gui-v${{ needs.pre-build.outputs.gui-version }}
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre-build, build-rust, build-rpm, build-deb, build-updater-json ]
|
||||
env:
|
||||
GUI_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
fetch-depth: 2
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
# tools
|
||||
- uses: anatawa12/something-releaser@v3
|
||||
- uses: snow-actions/git-config-user@v1.0.0
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Download All Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: assets
|
||||
pattern: artifacts-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download changelog
|
||||
# if: ${{ !needs.pre-build.outputs.prerelease }}
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: release-note-for-release
|
||||
path: changelog
|
||||
|
||||
- name: Push tag
|
||||
run: |-
|
||||
# set tag and publish current version
|
||||
git tag "gui-v$GUI_VERSION"
|
||||
git push --tags
|
||||
# create master and push
|
||||
git switch -c master
|
||||
git fetch origin master --depth=1
|
||||
git log --all --graph
|
||||
git push -u origin master
|
||||
sleep 1
|
||||
|
||||
- name: create release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |-
|
||||
# latest = false because we need to have vrc-get cli in the latest release
|
||||
# always generating notes file for now
|
||||
# ${{ !needs.pre-build.outputs.prerelease && '--notes-file changelog/release-note.md' || '' }} \
|
||||
gh release create \
|
||||
${{ needs.pre-build.outputs.prerelease && '--prerelease' || '' }} \
|
||||
--latest=false \
|
||||
--notes-file changelog/release-note.md \
|
||||
--verify-tag "gui-v$GUI_VERSION" \
|
||||
assets/*
|
||||
|
||||
rm -rf outputs assets
|
||||
|
||||
- name: prepare next release & push
|
||||
if: ${{ !needs.pre-build.outputs.prerelease }}
|
||||
run: |
|
||||
get-version -t gui | version-next | version-set-channel - beta 0 | set-version -t gui
|
||||
GUI_NEXT="$(get-version -t gui | version-stable)"
|
||||
git commit -am "chore: prepare for next version: gui $GUI_NEXT"
|
||||
git push
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
if: ${{ !failure() && !cancelled() }}
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre-build
|
||||
- build-rust
|
||||
- publish-to-github
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
fetch-depth: 2
|
||||
- name: remove releasing branch
|
||||
run: git push --delete origin releasing
|
||||
|
||||
pull-request-to-website:
|
||||
name: Create PullRequest to vrc-get.anatawa12.com for updater.json
|
||||
if: ${{ !inputs.dry-run }}
|
||||
environment:
|
||||
name: actions-github-app
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre-build, build-updater-json ]
|
||||
env:
|
||||
GUI_VERSION: ${{ needs.pre-build.outputs.gui-version }}
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
owner: vrc-get
|
||||
repositories: vrc-get.anatawa12.com
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: 'vrc-get/vrc-get.anatawa12.com'
|
||||
ref: 'master'
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: snow-actions/git-config-user@v1.0.0
|
||||
|
||||
- name: Download updater.json
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: updater.json
|
||||
path: .
|
||||
- name: Move updater.json
|
||||
env:
|
||||
STABLE: ${{ !needs.pre-build.outputs.prerelease }}
|
||||
run: |
|
||||
mkdir -p public/api/gui
|
||||
if $STABLE; then
|
||||
rm public/api/gui/tauri-updater.json || true # remove old file if exists
|
||||
mv updater.json public/api/gui/tauri-updater.json
|
||||
fi
|
||||
rm public/api/gui/tauri-updater-beta.json || true
|
||||
mv updater.json public/api/gui/tauri-updater-beta.json
|
||||
|
||||
- name: Commit
|
||||
run: |-
|
||||
BRANCH_NAME="update-tauri-updater-json-v$GUI_VERSION"
|
||||
git switch -c "$BRANCH_NAME"
|
||||
git add public/api/gui/tauri-updater.json public/api/gui/tauri-updater-beta.json
|
||||
git commit -m "chore: update tauri-updater.json to v$GUI_VERSION"
|
||||
git push -u origin "$BRANCH_NAME"
|
||||
|
||||
- name: Create Pull Request
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
BRANCH_NAME="update-tauri-updater-json-v$GUI_VERSION"
|
||||
|
||||
gh pr create \
|
||||
--title "chore: update tauri-updater.json to v$GUI_VERSION" \
|
||||
--body "update tauri-updater.json to v$GUI_VERSION" \
|
||||
--base master \
|
||||
--head "$BRANCH_NAME" \
|
||||
--assignee anatawa12 \
|
||||
|
|
|
|||
53
.github/workflows/publish.yml
vendored
53
.github/workflows/publish.yml
vendored
|
|
@ -12,6 +12,14 @@ on:
|
|||
- prerelease
|
||||
- start-rc
|
||||
- stable
|
||||
dry-run:
|
||||
type: boolean
|
||||
description: Dry Run, If true, do not publish release to GitHub.
|
||||
default: true
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
group: releasing
|
||||
|
||||
jobs:
|
||||
pre-build:
|
||||
|
|
@ -24,7 +32,7 @@ jobs:
|
|||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: anatawa12/something-releaser@v3
|
||||
|
|
@ -67,8 +75,12 @@ jobs:
|
|||
echo "head is master, master-*, or hotfix-*"
|
||||
;;
|
||||
* )
|
||||
echo "invalid release kind: $RELEASE_KIND_IN is not allowd for $GITHUB_REF_NAME"
|
||||
exit 255
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo "head is not master, but DRY_RUN is true"
|
||||
else
|
||||
echo "head is not master, but DRY_RUN is false"
|
||||
exit 255
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
|
|
@ -76,6 +88,7 @@ jobs:
|
|||
gh-export-variable VPM_VERSION "$(get-version -t vpm)"
|
||||
env:
|
||||
RELEASE_KIND_IN: ${{ github.event.inputs.release_kind }}
|
||||
DRY_RUN: ${{ inputs.dry-run }}
|
||||
|
||||
# region changelog
|
||||
- name: Create Changelog
|
||||
|
|
@ -94,7 +107,7 @@ jobs:
|
|||
---
|
||||
- name: Upload CHANGELOG.md
|
||||
if: ${{ !steps.update-version.outputs.prerelease }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: CHANGELOG
|
||||
path: CHANGELOG.md
|
||||
|
|
@ -103,7 +116,7 @@ jobs:
|
|||
run: cp "${{ steps.changelog.outputs.release-note }}" release-note.md
|
||||
- name: Upload release note
|
||||
if: ${{ !steps.update-version.outputs.prerelease }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-note-for-release
|
||||
path: release-note.md
|
||||
|
|
@ -164,7 +177,7 @@ jobs:
|
|||
|
||||
needs: [ pre-build ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
submodules: recursive
|
||||
|
|
@ -186,12 +199,13 @@ jobs:
|
|||
run: cargo build --target ${{ matrix.triple }} --release --verbose
|
||||
- name: Check binary is statically linked
|
||||
shell: bash
|
||||
env:
|
||||
RUSTFLAGS: ''
|
||||
run: |
|
||||
# https://github.com/taiki-e/setup-cross-toolchain-action/issues/18
|
||||
unset CARGO_BUILD_TARGET
|
||||
cargo run -p build-check-static-link target/${{ matrix.triple }}/release/vrc-get*
|
||||
cargo xtask check-static-link target/${{ matrix.triple }}/release/vrc-get${WINDIR:+.exe}
|
||||
|
||||
- name: Move artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
run: |-
|
||||
mkdir artifacts
|
||||
|
|
@ -201,20 +215,22 @@ jobs:
|
|||
done
|
||||
popd
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: artifacts-${{ matrix.triple }}
|
||||
path: artifacts/*
|
||||
|
||||
publish-crates-io:
|
||||
name: Publish to crates.io
|
||||
if: ${{ !inputs.dry-run }}
|
||||
environment:
|
||||
name: crates.io
|
||||
url: https://crates.io/crates/vrc-get
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre-build, build-rust ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
fetch-depth: 1
|
||||
|
|
@ -232,6 +248,7 @@ jobs:
|
|||
|
||||
publish-to-github:
|
||||
name: Publish to GitHub
|
||||
if: ${{ !inputs.dry-run }}
|
||||
environment:
|
||||
name: actions-github-app
|
||||
url: https://github.com/anatawa12/vrc-get/releases/v${{ needs.pre-build.outputs.cli-version }}
|
||||
|
|
@ -243,12 +260,12 @@ jobs:
|
|||
CLI_VERSION: ${{ needs.pre-build.outputs.cli-version }}
|
||||
VPM_VERSION: ${{ needs.pre-build.outputs.vpm-version }}
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v2
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
fetch-depth: 2
|
||||
|
|
@ -261,7 +278,7 @@ jobs:
|
|||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Download All Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: assets
|
||||
pattern: artifacts-*
|
||||
|
|
@ -269,7 +286,7 @@ jobs:
|
|||
|
||||
- name: Download changelog
|
||||
if: ${{ !needs.pre-build.outputs.prerelease }}
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: release-note-for-release
|
||||
path: changelog
|
||||
|
|
@ -326,7 +343,7 @@ jobs:
|
|||
CLI_VERSION: ${{ needs.pre-build.outputs.cli-version }}
|
||||
VPM_VERSION: ${{ needs.pre-build.outputs.vpm-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'releasing'
|
||||
fetch-depth: 2
|
||||
|
|
@ -336,7 +353,7 @@ jobs:
|
|||
publish-to-homebrew:
|
||||
name: Publish to homebrew
|
||||
# vrc-get is on autobump list https://github.com/Homebrew/homebrew-core/blame/master/.github/autobump.txt
|
||||
if: false # ${{ !needs.pre-build.outputs.prerelease }}
|
||||
if: false # ${{ !inputs.dry-run && !needs.pre-build.outputs.prerelease }}
|
||||
environment:
|
||||
name: homebrew-core
|
||||
url: https://github.com/homebrew/homebrew-core
|
||||
|
|
@ -350,7 +367,7 @@ jobs:
|
|||
|
||||
publish-to-winget:
|
||||
name: Publish to winget
|
||||
if: ${{ !needs.pre-build.outputs.prerelease }}
|
||||
if: ${{ !inputs.dry-run && !needs.pre-build.outputs.prerelease }}
|
||||
needs: [ pre-build, publish-to-github ]
|
||||
|
||||
uses: vrc-get/vrc-get/.github/workflows/publish-cli-winget.yml@master
|
||||
|
|
|
|||
128
CHANGELOG-gui.md
128
CHANGELOG-gui.md
|
|
@ -8,6 +8,7 @@ The format is based on [Keep a Changelog].
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Implement project sorting by creation date `#2941`
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
@ -16,10 +17,127 @@ The format is based on [Keep a Changelog].
|
|||
### Removed
|
||||
|
||||
### Fixed
|
||||
- Unity can be duplicated `#2321`
|
||||
|
||||
### Security
|
||||
|
||||
## [1.1.6] - 2026-06-02
|
||||
### Added
|
||||
- The package list can show hidden packages. [`#2731`](https://github.com/vrc-get/vrc-get/pull/2731)
|
||||
- Build-time option to disable auto updater [`#2759`](https://github.com/vrc-get/vrc-get/pull/2759)
|
||||
- Please read README for new build instruction.
|
||||
- User repositories can now be reordered by drag and drop [`#2935`](https://github.com/vrc-get/vrc-get/pull/2935)
|
||||
|
||||
### Changed
|
||||
- The "Clear Selection" button in the package management screen is now red (destructive style) to distinguish it from the "Install Selected" button [`#2803`](https://github.com/vrc-get/vrc-get/pull/2803)
|
||||
- File filled with '\0' or whitespace will be treated as empty file [`#2710`](https://github.com/vrc-get/vrc-get/pull/2710)
|
||||
- This should prevent `syntax error loading settings.json: expected value at line 1 column 1` if settings.json is broken
|
||||
- We also added a backup file to recover from settings.json corruption [`#2933`](https://github.com/vrc-get/vrc-get/pull/2933)
|
||||
- Completely changed how do we build ALCOM and how do we self-update ALCOM [`#2759`](https://github.com/vrc-get/vrc-get/pull/2759) [`#2828`](https://github.com/vrc-get/vrc-get/pull/2828) [`#2881`](https://github.com/vrc-get/vrc-get/pull/2881) [`#2882`](https://github.com/vrc-get/vrc-get/pull/2882) [`#2885`](https://github.com/vrc-get/vrc-get/pull/2885)
|
||||
- This fixes few problems relates to auto update
|
||||
- Please read README for new build instruction.
|
||||
- Improved backup speed by parallelizing the process [`#2746`](https://github.com/vrc-get/vrc-get/pull/2746)
|
||||
- Along with this change, the default compression level has been changed to `zip-fast`
|
||||
- We added dialog on enabling "Show Prerelease Packages" [`#2795`](https://github.com/vrc-get/vrc-get/pull/2795)
|
||||
- I hope this prevents users unexpectedly adding prerelease packages
|
||||
- Path for unitypackage on Template Editor now can be reselected [`#2635`](https://github.com/vrc-get/vrc-get/pull/2635)
|
||||
- ALCOM now refuses launching project if project is on noexec mount points [`#2814`](https://github.com/vrc-get/vrc-get/pull/2814)
|
||||
- This would cause problems with several native plugins
|
||||
- Already-added packages are now excluded from the package name suggestions in the Template Editor [`#2828`](https://github.com/vrc-get/vrc-get/pull/2828)
|
||||
- Extended some timeouts to 1 minute [`#2826`](https://github.com/vrc-get/vrc-get/pull/2826)
|
||||
- Prevents timeouts in slow DNS environments
|
||||
- Improved robustness for package installation errors [`#2844`](https://github.com/vrc-get/vrc-get/pull/2844)
|
||||
- It is now unlikely that vrc-get will leave the project directory corrupted if an I/O error occurs while installing a package
|
||||
- Backslashes in path in zip file are now treated as path separator on unix [`#2845`](https://github.com/vrc-get/vrc-get/pull/2845)
|
||||
- This fixes problem with Gesture Manager 3.9.7
|
||||
- Empty string for `documentationUrl` and `changelogUrl` are now allowed and ignored [`#2930`](https://github.com/vrc-get/vrc-get/pull/2930)
|
||||
- They are formerly rejected as invalid url
|
||||
|
||||
### Fixed
|
||||
- Fixed an issue where the progress bar flickered and did not display correct progress in environments using WebKit as the renderer. [`#2641`](https://github.com/vrc-get/vrc-get/pull/2641)
|
||||
- Fails to import UnityPackages with files in `Packages` directory [`#2679`](https://github.com/vrc-get/vrc-get/pull/2679)
|
||||
- null as vpmDependencies value is not allowed [`#2709`](https://github.com/vrc-get/vrc-get/pull/2709)
|
||||
- It's not recommended, but we allow null for `vpmDependencies` as a alias of `{}`
|
||||
- ALCOM cannot detect per-user flatpak installation of unity hub [`#2812`](https://github.com/vrc-get/vrc-get/pull/2812)
|
||||
- Unabled to import some untypackages [`#2821`](https://github.com/vrc-get/vrc-get/pull/2821)
|
||||
- It's hard to say but some older unitypackages ware unsupported.
|
||||
- Panic when resolving projects where dependency packages depend on newer versions of locked packages [`#2822`](https://github.com/vrc-get/vrc-get/pull/2822)
|
||||
- Missing glibc and libgcc_s dependency notation in .deb / .rpm distributon [`#2828`](https://github.com/vrc-get/vrc-get/pull/2828)
|
||||
- Unclear error message for invalid version name or version range [`#2842`](https://github.com/vrc-get/vrc-get/pull/2842)
|
||||
- Default file names in save dialogs now include the appropriate file extension [`#2846`](https://github.com/vrc-get/vrc-get/pull/2846)
|
||||
- Template export now defaults to `{template name}.alcomtemplate`
|
||||
- Repository list export now defaults to `repositories.txt`
|
||||
- Uninformative `[object Object]` appearing as an error message [`#2848`](https://github.com/vrc-get/vrc-get/pull/2848)
|
||||
- New Unity Hub loading method may not load manually added Unity Editors [`#2850`](https://github.com/vrc-get/vrc-get/pull/2850)
|
||||
- New Unity Hub loading method does load unity hub configuration on Linux [`#2850`](https://github.com/vrc-get/vrc-get/pull/2850)
|
||||
- Too many open files when copying project `#2867
|
||||
- Added workaround for VRCDefaultWorldScene generation issue in SDK 3.10.2 or later [`#2916`](https://github.com/vrc-get/vrc-get/pull/2916)
|
||||
- See [this][default-scene-canny] canny for bug in VRCSDK and issue [#2913][issue-2913] for our decision.
|
||||
|
||||
### Security
|
||||
- Package hash checks are now enforced when installing packages [`#2849`](https://github.com/vrc-get/vrc-get/pull/2849)
|
||||
- It has been about two years since the error message for package hash mismatches was introduced.
|
||||
- It is now enforced for security.
|
||||
|
||||
[default-scene-canny]: https://feedback.vrchat.com/sdk-bug-reports/p/3102-3103-vrcscenetemplateinitializer-does-not-create-sample-scene-if-udon-prepr
|
||||
[issue-2913]: https://github.com/vrc-get/vrc-get/issues/2913
|
||||
|
||||
## [1.1.5] - 2025-11-16
|
||||
- Fix package version selector dropdown exceeding window height [`#2589`](https://github.com/vrc-get/vrc-get/pull/2589)
|
||||
- The dropdown list now has a maximum height of 50% of the viewport or 24rem, whichever is smaller
|
||||
- This prevents the version selector from overflowing the window on small screens
|
||||
- Fix muted-foreground color [`#2516`](https://github.com/vrc-get/vrc-get/pull/2516) [`#2517`](https://github.com/vrc-get/vrc-get/pull/2517)
|
||||
- Remove `DialogDescription` not in `DialogHeader` to fix text color
|
||||
- Fix 'Detected Loop' panic with valid database file [`#2607`](https://github.com/vrc-get/vrc-get/pull/2607)
|
||||
|
||||
## [1.1.4] - 2025-09-02
|
||||
### Added
|
||||
- Add compact gui option [`#2436`](https://github.com/vrc-get/vrc-get/pull/2436) [`#2450`](https://github.com/vrc-get/vrc-get/pull/2450) [`#2470`](https://github.com/vrc-get/vrc-get/pull/2470)
|
||||
|
||||
### Changed
|
||||
- Improved saving interacting with setting files [`#2485`](https://github.com/vrc-get/vrc-get/pull/2485)
|
||||
- This should reduce "EOF while parsing a value at line 1 column 0" error on launch.
|
||||
- This should reduce losing settings after crashing ALCOM or PC.
|
||||
|
||||
### Fixed
|
||||
- Specifying a single unityversion doesn't work properly in alcomtemplate [`#2452`](https://github.com/vrc-get/vrc-get/pull/2452)
|
||||
- For example, if you'd like to specify `2022.3.22f1`, you need to set `2022.3.22`, not `2022.3.22f1`
|
||||
- You can now see correct validation and suggestions for this.
|
||||
- Home/End and Up/Down keys now consistently control cursor position in autocomplete fields [`#2466`](https://github.com/vrc-get/vrc-get/pull/2466)
|
||||
- Home/End keys now always move the text cursor regardless of autocomplete state
|
||||
- Up/Down keys move the text cursor when suggestions are not visible, and navigate suggestions when they are visible
|
||||
- Previously, these keys would sometimes be captured for suggestion navigation when autocomplete was open
|
||||
|
||||
## [1.1.3] - 2025-07-28
|
||||
### Added
|
||||
- Add support for `keywords` UPM manifest field [`#2375`](https://github.com/vrc-get/vrc-get/pull/2375)
|
||||
- You now can specifiy search keywords for package with `keywords` UPM manifest field
|
||||
- Favorites for templates [`#2376`](https://github.com/vrc-get/vrc-get/pull/2376)
|
||||
- It's much easier to select project templates you likely to use.
|
||||
|
||||
### Changed
|
||||
- Improved the Template Editor with AutoComplete [`#2371`](https://github.com/vrc-get/vrc-get/pull/2371)
|
||||
- You no longer need to remember the package name (id) and version associated with the package.
|
||||
- You now can search package by display name, name (id), aliases to enter package name, and ALCOM shows common version range for you.
|
||||
- Updated project settings of templates to include Item layer [`#2373`](https://github.com/vrc-get/vrc-get/pull/2373)
|
||||
- You should no longer need to update layers and collision matrix before uploading world
|
||||
- Improved behavior about `settings.json` to `vcc.litedb` migration [`#2327`](https://github.com/vrc-get/vrc-get/pull/2327)
|
||||
- See [`vrchat-community/creator-companion#492`](https://github.com/vrchat-community/creator-companion/issues/492) and the PR for details
|
||||
- Last used template is now preserved [`#2376`](https://github.com/vrc-get/vrc-get/pull/2376)
|
||||
- When you generally create project with custom template, you no longer need to change template every time.
|
||||
|
||||
### Fixed
|
||||
- Packages are not deselected after installing packages [`#2372`](https://github.com/vrc-get/vrc-get/pull/2372)
|
||||
|
||||
## [1.1.2] - 2025-06-30
|
||||
### Fixed
|
||||
- Fixed `a - b` version range is not correctly serialized on the `vpm-manifest.json`
|
||||
- Frontend error on package list update [`#2341`](https://github.com/vrc-get/vrc-get/pull/2341)
|
||||
|
||||
## [1.1.1] - 2025-06-21
|
||||
### Fixed
|
||||
- Unity can be duplicated [`#2321`](https://github.com/vrc-get/vrc-get/pull/2321)
|
||||
- Crash on creating a new project on Windows [`#2326`](https://github.com/vrc-get/vrc-get/pull/2326)
|
||||
|
||||
## [1.1.0] - 2025-06-19
|
||||
### Added
|
||||
- Support for Projects with Unity 2018 or older [`#2106`](https://github.com/vrc-get/vrc-get/pull/2106)
|
||||
|
|
@ -585,7 +703,13 @@ Release pipeline fixes
|
|||
- Apple code signing [`#422`](https://github.com/anatawa12/vrc-get/pull/422)
|
||||
- Migrate vpm 2019 project to 2022 [`#435`](https://github.com/anatawa12/vrc-get/pull/435)
|
||||
|
||||
[Unreleased]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.0...HEAD
|
||||
[Unreleased]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.6...HEAD
|
||||
[1.1.6]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.5...gui-v1.1.6
|
||||
[1.1.5]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.4...gui-v1.1.5
|
||||
[1.1.4]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.3...gui-v1.1.4
|
||||
[1.1.3]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.2...gui-v1.1.3
|
||||
[1.1.2]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.1...gui-v1.1.2
|
||||
[1.1.1]: https://github.com/vrc-get/vrc-get/compare/gui-v1.1.0...gui-v1.1.1
|
||||
[1.1.0]: https://github.com/vrc-get/vrc-get/compare/gui-v1.0.1...gui-v1.1.0
|
||||
[1.0.1]: https://github.com/vrc-get/vrc-get/compare/gui-v1.0.0...gui-v1.0.1
|
||||
[1.0.0]: https://github.com/vrc-get/vrc-get/compare/gui-v0.1.17...gui-v1.0.0
|
||||
|
|
|
|||
55
CHANGELOG.md
55
CHANGELOG.md
|
|
@ -10,28 +10,52 @@ The format is based on [Keep a Changelog].
|
|||
### Added
|
||||
|
||||
### Changed
|
||||
- Changed how we read VCC's project information `#1997`
|
||||
- Along with this, building this project no longer needs dotnet SDK to build.
|
||||
- Migrated the project to Rust 2024 `#1956`
|
||||
- This is internal changes should not cause behavior changes
|
||||
- This would require Rust 1.85 for building this project
|
||||
- Removed `cargo-about` from build-time dependency `#1961`
|
||||
- This is internal changes should not cause behavior changes
|
||||
- I listed here since this may need update on package metadata of some package managers
|
||||
- The method to retrieve the list of Unity from Unity Hub `#1808` `#1971`
|
||||
- You now can select multiple folders at once to adding project `#2018`
|
||||
- I didn't know official VCC had such a feature. Sorry for lack of feature!
|
||||
- The requirements for unity project `#2106`
|
||||
- Since this version, `Projectsettings/ProjectVersion.txt` is required.
|
||||
- Improved saving interacting with setting files `#2485` `#2710`
|
||||
- This should reduce "EOF while parsing a value at line 1 column 0" error on launch.
|
||||
- This should reduce losing settings after crashing ALCOM or PC.
|
||||
- null as vpmDependencies value is not allowed `#2709`
|
||||
- It's not recommended, but we allow null for `vpmDependencies` as a alias of `{}`
|
||||
- Improved robustness for package installation errors `#2844`
|
||||
- It is now unlikely that vrc-get will leave the project directory corrupted if an I/O error occurs while installing a package
|
||||
- Backslashes in path in zip file are now treated as path separator on unix `#2845`
|
||||
- This fixes problem with Gesture Manager 3.9.7
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
- Uninstall package is not reverted successfully if removing package is prevented by `ERROR_SHARING_VIOLATION` `#2209`
|
||||
- Fix 'Detected Loop' panic with valid database file `#2607`
|
||||
- Panic when resolving projects where dependency packages depend on newer versions of locked packages `#2822`
|
||||
- Warning for backup/project path in AppData folder not shown when path is in Roaming or LocalLow [`#2827`](https://github.com/vrc-get/vrc-get/pull/2827)
|
||||
- Unclear error message for invalid version name or version range `#2842`
|
||||
- Empty string for `documentationUrl` and `changelogUrl` are now allowed and ignored `#2930`
|
||||
- They are formerly rejected as invalid url
|
||||
|
||||
### Security
|
||||
- Package hash checks are now enforced when installing packages `#2849`
|
||||
- It has been about two years since the error message for package hash mismatches was introduced.
|
||||
- It is now enforced for security.
|
||||
|
||||
## [1.9.1] - 2025-07-28
|
||||
### Changed
|
||||
- Changed how we read VCC's project information [`#1997`](https://github.com/vrc-get/vrc-get/pull/1997)
|
||||
- Along with this, building this project no longer needs dotnet SDK to build.
|
||||
- Migrated the project to Rust 2024 [`#1956`](https://github.com/vrc-get/vrc-get/pull/1956)
|
||||
- This is internal changes should not cause behavior changes
|
||||
- This would require Rust 1.85 for building this project
|
||||
- Removed `cargo-about` from build-time dependency [`#1961`](https://github.com/vrc-get/vrc-get/pull/1961)
|
||||
- This is internal changes should not cause behavior changes
|
||||
- I listed here since this may need update on package metadata of some package managers
|
||||
- The method to retrieve the list of Unity from Unity Hub [`#1808`](https://github.com/vrc-get/vrc-get/pull/1808) [`#1971`](https://github.com/vrc-get/vrc-get/pull/1971)
|
||||
- You now can select multiple folders at once to adding project [`#2018`](https://github.com/vrc-get/vrc-get/pull/2018)
|
||||
- I didn't know official VCC had such a feature. Sorry for lack of feature!
|
||||
- The requirements for unity project [`#2106`](https://github.com/vrc-get/vrc-get/pull/2106)
|
||||
- Since this version, `Projectsettings/ProjectVersion.txt` is required.
|
||||
|
||||
### Fixed
|
||||
- Uninstall package is not reverted successfully if removing package is prevented by `ERROR_SHARING_VIOLATION` [`#2209`](https://github.com/vrc-get/vrc-get/pull/2209)
|
||||
- Fixed `a - b` version range is not correctly serialized on the `vpm-manifest.json`
|
||||
|
||||
## [1.9.0] - 2025-01-01
|
||||
### Added
|
||||
|
|
@ -482,7 +506,8 @@ The format is based on [Keep a Changelog].
|
|||
## [0.1.0] - 2023-01-25
|
||||
Initial Release
|
||||
|
||||
[Unreleased]: https://github.com/vrc-get/vrc-get/compare/v1.9.0...HEAD
|
||||
[Unreleased]: https://github.com/vrc-get/vrc-get/compare/v1.9.1...HEAD
|
||||
[1.9.1]: https://github.com/vrc-get/vrc-get/compare/v1.9.0...v1.9.1
|
||||
[1.9.0]: https://github.com/vrc-get/vrc-get/compare/v1.8.2...v1.9.0
|
||||
[1.8.2]: https://github.com/vrc-get/vrc-get/compare/v1.8.1...v1.8.2
|
||||
[1.8.1]: https://github.com/vrc-get/vrc-get/compare/v1.8.0...v1.8.1
|
||||
|
|
|
|||
3627
Cargo.lock
generated
3627
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
11
Cargo.toml
11
Cargo.toml
|
|
@ -2,10 +2,10 @@
|
|||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"build-check-static-link",
|
||||
"build-updater-json",
|
||||
"xtask",
|
||||
"vrc-get",
|
||||
"vrc-get-gui",
|
||||
"vrc-get-gui/windows-installer-wrapper",
|
||||
"vrc-get-vpm",
|
||||
]
|
||||
|
||||
|
|
@ -24,3 +24,10 @@ authors = ["anatawa12 <anatawa12@icloud.com>"]
|
|||
homepage = "https://github.com/anatawa12/vrc-get#readme"
|
||||
repository = "https://github.com/anatawa12/vrc-get"
|
||||
readme = "README.md"
|
||||
|
||||
[profile.xtask]
|
||||
inherits = "dev"
|
||||
incremental = false
|
||||
debug = 1
|
||||
opt-level = 0
|
||||
lto = "off"
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
/*
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
[package]
|
||||
name = "build-check-static-link"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dependencies.object]
|
||||
version = "0.37"
|
||||
default-features = false
|
||||
features = [
|
||||
"read_core",
|
||||
"macho",
|
||||
"pe",
|
||||
"elf",
|
||||
]
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
This crate is to ensure built binary is statically linked or dynamically linked to the system libraries.
|
||||
|
||||
this crate use `object` crate to read the binary file.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
cargo run -p build-check-static-link <path/to/binary>
|
||||
```
|
||||
|
||||
exits with zero if statically linked or linked with allowed dynamic libraries, otherwise exits with non-zero.
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
use object::{Endian, Endianness, FileKind, Object};
|
||||
use std::fs;
|
||||
use std::process::exit;
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args();
|
||||
let _ = args.next();
|
||||
let mut success = true;
|
||||
for arg in args {
|
||||
if arg.ends_with(".d") {
|
||||
println!("skipping .d file: {}", arg);
|
||||
continue;
|
||||
}
|
||||
let binary = std::path::Path::new(&arg);
|
||||
let binary = fs::read(binary).unwrap();
|
||||
|
||||
success |= match FileKind::parse(binary.as_slice()).expect("detecting type") {
|
||||
FileKind::MachO64 => process_mach_64::<Endianness>(&binary),
|
||||
FileKind::Pe64 => process_pe_64(&binary),
|
||||
FileKind::Elf64 => process_elf_64::<Endianness>(&binary),
|
||||
unknown => panic!("unknown file type: {:?}", unknown),
|
||||
};
|
||||
}
|
||||
|
||||
if success { exit(0) } else { exit(1) }
|
||||
}
|
||||
|
||||
fn process_mach_64<E: Endian>(binary: &[u8]) -> bool {
|
||||
use object::macho::*;
|
||||
use object::read::macho::*;
|
||||
|
||||
let mut success = true;
|
||||
|
||||
let parsed = MachHeader64::<E>::parse(binary, 0).expect("failed to parse binary");
|
||||
let endian = parsed.endian().unwrap();
|
||||
|
||||
let mut commands = parsed
|
||||
.load_commands(endian, binary, 0)
|
||||
.expect("parsing binary");
|
||||
while let Some(command) = commands.next().expect("reading binary") {
|
||||
if let Some(dylib) = command.dylib().unwrap() {
|
||||
let dylib = command.string(endian, dylib.dylib.name).unwrap();
|
||||
match dylib {
|
||||
| b"/System/Library/Frameworks/Security.framework/Versions/A/Security"
|
||||
| b"/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration"
|
||||
| b"/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
| b"/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation"
|
||||
| b"/usr/lib/libobjc.A.dylib"
|
||||
| b"/usr/lib/libiconv.2.dylib"
|
||||
| b"/usr/lib/libSystem.B.dylib"
|
||||
=> {
|
||||
// known system library
|
||||
println!("system dylib: {}", std::str::from_utf8(dylib).unwrap());
|
||||
}
|
||||
unknown => {
|
||||
println!("ERROR: unknown dylib: {:?}", std::str::from_utf8(unknown).unwrap_or("unable to parse with utf8"));
|
||||
success = false;
|
||||
},
|
||||
}
|
||||
} else if command.cmd() == LC_LOAD_DYLINKER {
|
||||
let data: &DylinkerCommand<E> = command.data().expect("parse LC_LOAD_DYLINKER");
|
||||
if command.string(endian, data.name).unwrap() != b"/usr/lib/dyld" {
|
||||
println!("ERROR: dylinker is not /usr/lib/dyld");
|
||||
success = false;
|
||||
} else {
|
||||
println!("dylinker: /usr/lib/dyld");
|
||||
}
|
||||
}
|
||||
}
|
||||
success
|
||||
}
|
||||
|
||||
fn process_pe_64(binary: &[u8]) -> bool {
|
||||
use object::LittleEndian as LE;
|
||||
use object::read::pe::*;
|
||||
|
||||
let mut success = true;
|
||||
let parsed = PeFile64::parse(binary).expect("failed to parse binary");
|
||||
|
||||
let table = parsed.import_table().unwrap().unwrap();
|
||||
let mut iter = table.descriptors().unwrap();
|
||||
while let Some(x) = iter.next().unwrap() {
|
||||
let dll = table.name(x.name.get(LE)).unwrap();
|
||||
match dll.to_ascii_lowercase().as_slice() {
|
||||
| b"advapi32.dll"
|
||||
| b"kernel32.dll"
|
||||
| b"bcrypt.dll" // TODO: check if this is a system library
|
||||
| b"ntdll.dll"
|
||||
| b"shell32.dll"
|
||||
| b"ole32.dll"
|
||||
| b"ws2_32.dll"
|
||||
| b"crypt32.dll"
|
||||
=> {
|
||||
println!("system dll: {}", std::str::from_utf8(dll).unwrap());
|
||||
// known system library
|
||||
}
|
||||
unknown => {
|
||||
println!("ERROR: unknown dll: {:?}", std::str::from_utf8(unknown).unwrap_or("unable to parse with utf8"));
|
||||
success = false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
|
||||
fn process_elf_64<E: Endian>(binary: &[u8]) -> bool {
|
||||
use object::elf::*;
|
||||
use object::read::elf::*;
|
||||
|
||||
let mut success = true;
|
||||
|
||||
let parsed = ElfFile64::<E>::parse(binary).expect("failed to parse binary");
|
||||
|
||||
for x in parsed.imports().unwrap() {
|
||||
println!(
|
||||
"dynamic importing symbol: {}",
|
||||
std::str::from_utf8(x.name()).unwrap()
|
||||
);
|
||||
success = false;
|
||||
}
|
||||
|
||||
for segment in parsed.elf_program_headers() {
|
||||
if segment.p_type.get(parsed.endian()) == PT_INTERP {
|
||||
let data = segment.data(parsed.endian(), parsed.data()).unwrap();
|
||||
println!("interpreter: {:?}", std::str::from_utf8(data).unwrap());
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
/*
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
[package]
|
||||
name = "build-updater-json"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", default-features = false, features = ["now", "serde"] }
|
||||
indexmap = { version = "2", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
// see https://tauri.app/v1/guides/distribution/updater/ for json format
|
||||
|
||||
use chrono::{Timelike, Utc};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Serialize;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdaterJson {
|
||||
version: String,
|
||||
notes: String,
|
||||
pub_date: chrono::DateTime<Utc>,
|
||||
platforms: IndexMap<String, Platform>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Platform {
|
||||
signature: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// consts
|
||||
const DOWNLOAD_URL_BASE: &str =
|
||||
"https://github.com/vrc-get/vrc-get/releases/download/gui-v{version}";
|
||||
let platform_file_name = [
|
||||
("darwin-x86_64", "ALCOM-{version}-universal.app.tar.gz"),
|
||||
("darwin-aarch64", "ALCOM-{version}-universal.app.tar.gz"),
|
||||
("linux-x86_64", "alcom-{version}-x86_64.AppImage.tar.gz"),
|
||||
//("linux-aarch64", "alcom-{version}-aarch64.AppImage.tar.gz"),
|
||||
("windows-x86_64", "ALCOM-{version}-x86_64-setup.nsis.zip"),
|
||||
//("windows-aarch64", "ALCOM-{version}-aarch64-setup.nsis.zip"),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<IndexMap<_, _>>();
|
||||
|
||||
let version = std::env::var("GUI_VERSION").expect("GUI_VERSION not set");
|
||||
|
||||
let base_url = DOWNLOAD_URL_BASE.replace("{version}", &version);
|
||||
|
||||
// create platforms info
|
||||
let mut platforms = IndexMap::new();
|
||||
for (platform, file_name) in platform_file_name {
|
||||
let file_name = file_name.replace("{version}", &version);
|
||||
|
||||
std::fs::metadata(format!("assets/{file_name}"))
|
||||
.unwrap_or_else(|e| panic!("{}: {}", file_name, e));
|
||||
|
||||
let signature = std::fs::read_to_string(format!("assets/{file_name}.sig"))
|
||||
.unwrap_or_else(|e| panic!("{}.sig: {}", file_name, e));
|
||||
|
||||
let url = format!("{}/{}", base_url, file_name);
|
||||
platforms.insert(platform.to_string(), Platform { signature, url });
|
||||
}
|
||||
|
||||
let is_beta = version.contains('-');
|
||||
let notes = if is_beta {
|
||||
// https://github.com/vrc-get/vrc-get/blob/master/CHANGELOG-gui.md#unreleased
|
||||
"Please read changelog at https://github.com/vrc-get/vrc-get/blob/master/CHANGELOG-gui.md#unreleased".into()
|
||||
} else {
|
||||
// https://github.com/vrc-get/vrc-get/blob/master/CHANGELOG-gui.md#101---2025-02-05
|
||||
let version = version.replace('.', "");
|
||||
let date = Utc::now().format("%Y-%m-%d").to_string();
|
||||
format!(
|
||||
"Please read changelog at https://github.com/vrc-get/vrc-get/blob/master/CHANGELOG-gui.md#{version}---{date}"
|
||||
)
|
||||
};
|
||||
|
||||
let updater = UpdaterJson {
|
||||
version,
|
||||
notes,
|
||||
pub_date: Utc::now().with_nanosecond(0).unwrap(),
|
||||
platforms,
|
||||
};
|
||||
|
||||
if !is_beta {
|
||||
write_json("updater.json", &updater);
|
||||
}
|
||||
write_json("updater-beta.json", &updater);
|
||||
}
|
||||
|
||||
fn write_json(path: impl AsRef<Path>, json: impl Serialize) {
|
||||
let json = serde_json::to_string_pretty(&json).unwrap();
|
||||
std::fs::write(path, json).expect("write updater.json");
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/env osascript
|
||||
|
||||
on run (folderName)
|
||||
tell application "Finder"
|
||||
tell folder folderName
|
||||
log "opening folder " & folderName
|
||||
open
|
||||
end tell
|
||||
end tell
|
||||
end run
|
||||
1
vrc-get-gui/.npmrc
Normal file
1
vrc-get-gui/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
ignore-scripts=true
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "vrc-get-gui"
|
||||
version = "1.1.1-rc.0"
|
||||
version = "1.1.7-beta.0"
|
||||
description = "A fast open-source alternative of VRChat Creator Companion"
|
||||
|
||||
homepage.workspace = true
|
||||
|
|
@ -22,17 +22,17 @@ tauri-build = { version = "2", features = [ "config-toml" ] }
|
|||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_with = { version = "3", features = ["base64"] }
|
||||
tauri = { version = "2", features = [ "config-toml" ] }
|
||||
tauri = { version = "=2.11.2", features = [ "config-toml" ] } # = for sync version between npm and cargo
|
||||
vrc-get-vpm = { path = "../vrc-get-vpm", features = ["experimental-project-management", "experimental-unity-management"] }
|
||||
reqwest = { version = "0.12", features = ["gzip", "brotli"] }
|
||||
specta = { version = "2.0.0-rc.20", features = [ "chrono", "url", "indexmap" ] }
|
||||
tauri-specta = { version = "2.0.0-rc.20", features = ["typescript"] }
|
||||
specta-typescript = "0.0.7"
|
||||
reqwest = { version = "0.13", features = ["gzip", "brotli", "json"] }
|
||||
specta = { version = "2.0.0-rc.24", features = [ "chrono", "url", "indexmap" ] }
|
||||
tauri-specta = { version = "2.0.0-rc.24", features = ["typescript"] }
|
||||
specta-typescript = "0.0.11"
|
||||
open = "5"
|
||||
arc-swap = "1"
|
||||
log = { version = "0.4", features = [ "std", "kv" ] }
|
||||
chrono = { version = "0.4", features = [ "serde" ] }
|
||||
ringbuffer = "0.15"
|
||||
ringbuffer = "0.16"
|
||||
tokio = { version = "1", features = ["process"] }
|
||||
tokio-util = "0.7"
|
||||
fs_extra = "1"
|
||||
|
|
@ -42,12 +42,17 @@ tar = "0.4"
|
|||
flate2 = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
trash = "5"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "tokio"] }
|
||||
async_zip = { version = "0.0.18", features = ["tokio", "deflate"] }
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-stream = "0.3"
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
minisign-verify = "0.2"
|
||||
base64 = "0.22"
|
||||
semver = "1"
|
||||
tempfile = "3"
|
||||
sha2 = "0.11"
|
||||
hex = "0.4"
|
||||
sys-locale = "0.3"
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
url = "2"
|
||||
|
|
@ -56,11 +61,12 @@ yoke = { version = "0.8", features = ["derive"] }
|
|||
atomicbox = "0.4"
|
||||
stable_deref_trait = "1"
|
||||
itertools = "0.14"
|
||||
sysinfo = "0.39.3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61", features = ["Win32_Storage_FileSystem", "Win32_System_IO", "Win32_NetworkManagement_IpHelper", "Wdk_System_SystemServices", "Win32_System_SystemInformation"] }
|
||||
winreg = "0.55"
|
||||
wmi = "0.17"
|
||||
windows = { version = "0.62", features = ["Win32_Storage_FileSystem", "Win32_System_IO", "Win32_NetworkManagement_IpHelper", "Wdk_System_SystemServices", "Win32_System_SystemInformation", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||
winreg = "0.56"
|
||||
wmi = "0.18"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
plist = { version = "1" }
|
||||
|
|
@ -69,12 +75,22 @@ objc2-foundation = "0.3.0"
|
|||
block2 = "0.6.0"
|
||||
objc2 = "0.6.0"
|
||||
dispatch2 = "0.3.0"
|
||||
rlimit = "0.11.0"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { version = "0.30", features = ["fs"] }
|
||||
nix = { version = "0.31", features = ["fs", "mount"] }
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
|
||||
no-self-updater = []
|
||||
|
||||
# Devtools
|
||||
#
|
||||
# Enables browser devtools for debugging javascript part.
|
||||
# It's not recommended to enable this for production builds,
|
||||
# development use only!
|
||||
devtools = ["tauri/devtools"]
|
||||
|
|
|
|||
|
|
@ -75,9 +75,9 @@ To build ALCOM, you need to have the following installed:
|
|||
- [cargo] latest — to build the most part of the project
|
||||
- And other requirements for tauri, see [tauri requirements](https://v2.tauri.app/start/prerequisites/#system-dependencies)
|
||||
|
||||
Please note that ALCOM requires the latest version of cargo at that time.
|
||||
Please note that ALCOM requires the latest version of rust toolchain at that time.
|
||||
We update the required version of cargo without notice.
|
||||
Therefore, you may need to update them before building the project.
|
||||
Therefore, It's recommended to update rust toolchain before building the project.
|
||||
|
||||
[Node.js]: https://nodejs.org/en
|
||||
[npm]: https://www.npmjs.com
|
||||
|
|
@ -88,9 +88,27 @@ Therefore, you may need to update them before building the project.
|
|||
To build the project, run the following command:
|
||||
|
||||
```bash
|
||||
npm run tauri build
|
||||
cargo xtask build-alcom --release
|
||||
```
|
||||
|
||||
This command builds the main ALCOM executable for the current platform.
|
||||
For cross-compilation, add the `--target` command-line parameter.
|
||||
The executable will be created in the `target/release` directory.
|
||||
|
||||
There are a few build options available when building ALCOM.
|
||||
Most notably, you can disable the self-updater with the `--no-self-updater` option.
|
||||
Note that this does not disable update checks.
|
||||
ALCOM will show a message when a newer release is available instead of offering a self-update.
|
||||
|
||||
Directly distributing the executable may be suitable for some environments, but we also provide bundled distributions.
|
||||
To bundle ALCOM, run the following command after building it.
|
||||
|
||||
```bash
|
||||
cargo xtask bundle-alcom --release --bundles <bundles>
|
||||
```
|
||||
|
||||
Check `--help` for the list of supported bundle types.
|
||||
|
||||
## Development
|
||||
|
||||
ALCOM is currently based on tauri and next.js.
|
||||
|
|
|
|||
|
|
@ -6,65 +6,3 @@ beforeBuildCommand = "npm run build"
|
|||
beforeDevCommand = "npm run dev"
|
||||
devUrl = "http://localhost:3030"
|
||||
frontendDist = "out"
|
||||
|
||||
[bundle]
|
||||
active = true
|
||||
targets = [
|
||||
"appimage",
|
||||
"deb",
|
||||
"rpm",
|
||||
"nsis", #-setup.exe
|
||||
"app", # needs for dmg
|
||||
"dmg",
|
||||
]
|
||||
longDescription = "ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri."
|
||||
shortDescription = "ALCOM - Alternative Creator Companion"
|
||||
category = "DeveloperTool"
|
||||
copyright = "(c) anatawa12 and other contributors"
|
||||
|
||||
externalBin = []
|
||||
icon = [
|
||||
"icons/32x32.png",
|
||||
"icons/64x64.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico",
|
||||
]
|
||||
resources = []
|
||||
publisher = "anatawa12"
|
||||
|
||||
createUpdaterArtifacts = "v1Compatible" # remove if ci # we do not generate updater artifacts in CI
|
||||
|
||||
[[bundle.fileAssociations]]
|
||||
# note: for macOS we directory use info.plist for registering file association.
|
||||
description = "ALCOM Project Template"
|
||||
ext = ['alcomtemplate']
|
||||
mimeType = "application/x-alcom-template+json"
|
||||
name = "ALCOM Project Template"
|
||||
|
||||
[bundle.linux.deb]
|
||||
desktopTemplate = "alcom.desktop"
|
||||
|
||||
[bundle.linux.rpm]
|
||||
desktopTemplate = "alcom.desktop"
|
||||
|
||||
[bundle.macOS]
|
||||
exceptionDomain = ""
|
||||
frameworks = []
|
||||
providerShortName = "anatawa12"
|
||||
|
||||
[bundle.windows]
|
||||
nsis.template = "installer.nsi"
|
||||
|
||||
# signing
|
||||
certificateThumbprint = "0D17F6395EC64A2B1D341BB7AC5B3163EB148BB7"
|
||||
timestampUrl = "http://ts.ssl.com"
|
||||
digestAlgorithm = "sha256"
|
||||
tsp = true
|
||||
|
||||
[plugins.updater]
|
||||
endpoints = []
|
||||
pubkey = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDkyMjAzMkU2Q0ZGQjQ0MjYKUldRbVJQdlA1aklna2d2NnRoM3ZsT3lzWEQ3MC9zTGpaWVR4NGdQOXR0UGJaOHBlY2xCcFY5bHcK"
|
||||
|
||||
[app.security]
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
[Desktop Entry]
|
||||
Categories={{categories}}
|
||||
{{#if comment}}
|
||||
Comment={{comment}}
|
||||
{{/if}}
|
||||
Exec={{exec}} %u
|
||||
Icon={{icon}}
|
||||
Name={{name}}
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/vcc
|
||||
|
|
@ -1,20 +1,25 @@
|
|||
"use client"; // Error components must be Client Components
|
||||
import { useEffect } from "react";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import globalInfo from "@/lib/global-info";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function ErrorPage({
|
||||
error,
|
||||
}: {
|
||||
error: Error;
|
||||
error: object;
|
||||
reset?: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
const errorMessage = `${error}`;
|
||||
const errorStack = `${error.stack}`;
|
||||
// When there is overridden toString, use it. if not, use stringify
|
||||
const errorMessage =
|
||||
error.toString === Object.prototype.toString
|
||||
? JSON.stringify(error)
|
||||
: error.toString();
|
||||
const errorStack =
|
||||
"stack" in error ? `${error.stack}` : "No stacktrace provided";
|
||||
|
||||
const openIssue = () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import { LoaderCircle } from "lucide-react";
|
|||
|
||||
export default function Loading({
|
||||
loadingText = "Loading...",
|
||||
}: { loadingText?: React.ReactNode }) {
|
||||
}: {
|
||||
loadingText?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full w-full space-y-4">
|
||||
<LoaderCircle className="h-10 w-10 animate-spin" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||
import ErrorPage from "@/app/-error";
|
||||
import { Providers } from "@/components/providers";
|
||||
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
||||
import "./globals.css";
|
||||
import React, { Suspense } from "react";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { ScrollPageContainer } from "@/components/ScrollPageContainer";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { HNavBar, HNavBarText, VStack } from "@/components/layout";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { ScrollPageContainer } from "@/components/ScrollPageContainer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
|
@ -17,8 +18,13 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { toastError, toastInfo, toastNormal, toastSuccess } from "@/lib/toast";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
toastError,
|
||||
toastInfo,
|
||||
toastNormal,
|
||||
toastSuccess,
|
||||
toastWarning,
|
||||
} from "@/lib/toast";
|
||||
|
||||
export const Route = createFileRoute("/_main/dev-palette/")({
|
||||
component: Page,
|
||||
|
|
@ -28,12 +34,8 @@ function Page() {
|
|||
return (
|
||||
<VStack>
|
||||
<HNavBar
|
||||
className={"shrink-0"}
|
||||
leading={
|
||||
<p className="cursor-pointer py-1.5 font-bold grow-0">
|
||||
UI Palette (dev only)
|
||||
</p>
|
||||
}
|
||||
className="shrink-0"
|
||||
leading={<HNavBarText>UI Palette (dev only)</HNavBarText>}
|
||||
/>
|
||||
<ScrollPageContainer>
|
||||
<main className="flex flex-col gap-2 shrink grow">
|
||||
|
|
@ -117,6 +119,12 @@ function Page() {
|
|||
>
|
||||
Error
|
||||
</Button>
|
||||
<Button
|
||||
variant={"warning"}
|
||||
onClick={() => toastWarning("Warning Toast Body")}
|
||||
>
|
||||
Warning
|
||||
</Button>
|
||||
<Button
|
||||
variant={"success"}
|
||||
onClick={() => toastSuccess("Success Toast Body")}
|
||||
|
|
@ -177,7 +185,7 @@ function UnityTableBody() {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{unityPaths.map(([path, version, isFromHub]) => (
|
||||
{unityPaths.map(([path, version, _isFromHub]) => (
|
||||
<tr key={path} className="even:bg-secondary/30">
|
||||
<td className={"p-2.5"}>{version}</td>
|
||||
<td className={"p-2.5"}>{path}</td>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { BugOff, CircleX, Info, OctagonAlert } from "lucide-react";
|
||||
import { memo, useEffect, useMemo, useRef } from "react";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import type { LogEntry, LogLevel } from "@/lib/bindings";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { BugOff, CircleX, Info, OctagonAlert } from "lucide-react";
|
||||
import { memo, useEffect, useMemo, useRef } from "react";
|
||||
|
||||
export const LogsListCard = memo(function LogsListCard({
|
||||
logEntry,
|
||||
|
|
@ -81,12 +81,8 @@ export const LogsListCard = memo(function LogsListCard({
|
|||
);
|
||||
});
|
||||
|
||||
const LogRow = memo(function LogRow({
|
||||
log,
|
||||
}: {
|
||||
log: LogEntry;
|
||||
}) {
|
||||
const cellClass = "p-2.5";
|
||||
const LogRow = memo(function LogRow({ log }: { log: LogEntry }) {
|
||||
const cellClass = "p-2.5 compact:py-1";
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ArrowDownFromLine } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { HNavBar, HNavBarText, VStack } from "@/components/layout";
|
||||
import { SearchBox } from "@/components/SearchBox";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
|
|
@ -23,15 +32,6 @@ import { tc } from "@/lib/i18n";
|
|||
import { toastThrownError } from "@/lib/toast";
|
||||
import { useTauriListen } from "@/lib/use-tauri-listen";
|
||||
import { useSessionStorage } from "@/lib/useSessionStorage";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ArrowDownFromLine } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { LogsListCard } from "./-logs-list-card";
|
||||
|
||||
export const Route = createFileRoute("/_main/log/")({
|
||||
|
|
@ -152,10 +152,10 @@ function ManageLogsHeading({
|
|||
|
||||
return (
|
||||
<HNavBar
|
||||
className={"shrink-0"}
|
||||
className="shrink-0"
|
||||
leading={
|
||||
<>
|
||||
<p className="cursor-pointer py-1.5 font-bold grow-0">{tc("logs")}</p>
|
||||
<HNavBarText>{tc("logs")}</HNavBarText>
|
||||
|
||||
<SearchBox
|
||||
className={"w-max grow"}
|
||||
|
|
@ -169,7 +169,7 @@ function ManageLogsHeading({
|
|||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className={"shrink-0 p-3"}>
|
||||
<Button className={"shrink-0 p-3 compact:h-10"}>
|
||||
{tc("logs:manage:select logs level")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
|
@ -207,6 +207,7 @@ function ManageLogsHeading({
|
|||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
className={"compact:h-10"}
|
||||
onClick={() =>
|
||||
commands.utilOpen(
|
||||
`${globalInfo.vpmHomeFolder}/vrc-get/gui-logs`,
|
||||
|
|
@ -222,11 +223,11 @@ function ManageLogsHeading({
|
|||
<Button
|
||||
variant={"ghost"}
|
||||
onClick={() => handleLogAutoScrollChange(!autoScroll)}
|
||||
className={
|
||||
className={`compact:h-10 ${
|
||||
autoScroll
|
||||
? "bg-secondary border border-primary"
|
||||
: "bg-transparent"
|
||||
}
|
||||
}`}
|
||||
>
|
||||
<ArrowDownFromLine className={"w-5 h-5"} />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tc } from "@/lib/i18n";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { tc } from "@/lib/i18n";
|
||||
|
||||
type PageType =
|
||||
| "/packages/user-packages"
|
||||
|
|
@ -8,15 +8,17 @@ type PageType =
|
|||
|
||||
// Note: For historical reasons, templates page are under packages in route.
|
||||
|
||||
export function HeadingPageName({
|
||||
pageType,
|
||||
}: {
|
||||
pageType: PageType;
|
||||
}) {
|
||||
export function HeadingPageName({ pageType }: { pageType: PageType }) {
|
||||
// Note for p-1 rounded-md -m-1 compact:m-0
|
||||
// For normal mode, we use 1-unit of the outer padding for selector rectangle, so we use negative margin to eat padding.
|
||||
// For compact mode, the height of the button is 2 units shorter than normal with the height of the navbar is remaining.
|
||||
// Therefore we use the 1 unit space for outer padding for selector rectangle.
|
||||
return (
|
||||
<div className={"-ml-1.5"}>
|
||||
<div className={"flex compact:h-10 items-center"}>
|
||||
<div
|
||||
className={"grid grid-cols-3 gap-1.5 bg-secondary p-1 -m-1 rounded-md"}
|
||||
className={
|
||||
"grid grid-cols-3 gap-1.5 bg-secondary p-1 rounded-md -m-1 compact:m-0"
|
||||
}
|
||||
>
|
||||
<HeadingButton
|
||||
currentPage={pageType}
|
||||
|
|
@ -51,7 +53,7 @@ function HeadingButton({
|
|||
children: React.ReactNode;
|
||||
}) {
|
||||
const button =
|
||||
"cursor-pointer px-3 py-2 font-bold grow-0 hover:bg-background rounded-sm text-center p-2";
|
||||
"cursor-pointer px-3 py-2 font-bold grow-0 hover:bg-background rounded-sm text-center p-2 compact:h-8 compact:py-1";
|
||||
|
||||
if (currentPage === targetPage) {
|
||||
return <div className={`${button} bg-background`}>{children}</div>;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { queryOptions } from "@tanstack/react-query";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ReorderableList,
|
||||
useReorderableList,
|
||||
} from "@/components/ReorderableList";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
||||
import { DialogFooter } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
import type {
|
||||
|
|
@ -15,41 +18,6 @@ import { type DialogApi, type DialogContext, showDialog } from "@/lib/dialog";
|
|||
import { tc, tt } from "@/lib/i18n";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toastError, toastSuccess } from "@/lib/toast";
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
type State =
|
||||
| {
|
||||
type: "normal";
|
||||
}
|
||||
| {
|
||||
type: "enteringRepositoryInfo";
|
||||
}
|
||||
| {
|
||||
type: "loadingRepository";
|
||||
}
|
||||
| {
|
||||
type: "duplicated";
|
||||
reason: TauriDuplicatedReason;
|
||||
duplicatedName: string;
|
||||
}
|
||||
| {
|
||||
type: "confirming";
|
||||
repo: TauriRemoteRepositoryInfo;
|
||||
url: string;
|
||||
headers: { [key: string]: string };
|
||||
};
|
||||
|
||||
interface AddRepository {
|
||||
dialog: React.ReactNode;
|
||||
openAddDialog: () => void;
|
||||
inProgress: boolean;
|
||||
addRepository: (
|
||||
url: string,
|
||||
headers: { [p: string]: string },
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
const environmentRepositoriesInfo = queryOptions({
|
||||
queryKey: ["environmentRepositoriesInfo"],
|
||||
|
|
@ -188,7 +156,7 @@ function EnteringRepositoryInfo({
|
|||
|
||||
return (
|
||||
<>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p className={"font-normal"}>
|
||||
{tc("vpm repositories:dialog:enter repository info")}
|
||||
</p>
|
||||
|
|
@ -274,7 +242,7 @@ function EnteringRepositoryInfo({
|
|||
{tc("vpm repositories:hint:duplicate headers")}
|
||||
</p>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close(null)}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -291,16 +259,12 @@ function EnteringRepositoryInfo({
|
|||
);
|
||||
}
|
||||
|
||||
function LoadingRepository({
|
||||
cancel,
|
||||
}: {
|
||||
cancel: () => void;
|
||||
}) {
|
||||
function LoadingRepository({ cancel }: { cancel: () => void }) {
|
||||
return (
|
||||
<>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("vpm repositories:dialog:downloading...")}</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={cancel}>{tc("general:button:cancel")}</Button>
|
||||
</DialogFooter>
|
||||
|
|
@ -339,10 +303,10 @@ function Duplicated({
|
|||
|
||||
return (
|
||||
<>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("vpm repositories:dialog:already added")}</p>
|
||||
<p>{message}</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close()}>
|
||||
{tc("general:button:ok")}
|
||||
|
|
@ -364,7 +328,7 @@ function Confirming({
|
|||
return (
|
||||
<>
|
||||
{/* TODO: use ScrollArea (I failed to use it inside dialog) */}
|
||||
<DialogDescription className={"max-h-[50vh] overflow-y-auto font-normal"}>
|
||||
<div className={"max-h-[50vh] overflow-y-auto font-normal"}>
|
||||
<p className={"font-normal"}>
|
||||
{tc("vpm repositories:dialog:name", { name: repo.display_name })}
|
||||
</p>
|
||||
|
|
@ -377,7 +341,7 @@ function Confirming({
|
|||
{tc("vpm repositories:dialog:headers")}
|
||||
</p>
|
||||
<ul className={"list-disc pl-6"}>
|
||||
{Object.entries(headers).map(([key, value], idx) => (
|
||||
{Object.entries(headers).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
{key}: {value}
|
||||
</li>
|
||||
|
|
@ -389,11 +353,11 @@ function Confirming({
|
|||
{tc("vpm repositories:dialog:packages")}
|
||||
</p>
|
||||
<ul className={"list-disc pl-6"}>
|
||||
{repo.packages.map((info, idx) => (
|
||||
{repo.packages.map((info) => (
|
||||
<li key={info.name}>{info.display_name ?? info.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close(false)}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { queryOptions } from "@tanstack/react-query";
|
||||
import type React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
|
|
@ -5,7 +8,7 @@ import {
|
|||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
||||
import { DialogFooter } from "@/components/ui/dialog";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
import type {
|
||||
|
|
@ -19,10 +22,6 @@ import { tc, tt } from "@/lib/i18n";
|
|||
import { queryClient } from "@/lib/query-client";
|
||||
import { toastSuccess } from "@/lib/toast";
|
||||
import { useEffectEvent } from "@/lib/use-effect-event";
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import type React from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
type ParsedRepositories = {
|
||||
repositories: TauriRepositoryDescriptor[];
|
||||
|
|
@ -94,7 +93,7 @@ function ConfirmingRepositoryList({
|
|||
return (
|
||||
<>
|
||||
{/* TODO: use ScrollArea (I failed to use it inside dialog) */}
|
||||
<DialogDescription className={"max-h-[50vh] overflow-y-auto font-normal"}>
|
||||
<div className={"max-h-[50vh] overflow-y-auto font-normal"}>
|
||||
<p className={"font-normal whitespace-normal"}>
|
||||
{tc("vpm repositories:dialog:confirm repository list")}
|
||||
</p>
|
||||
|
|
@ -120,7 +119,7 @@ function ConfirmingRepositoryList({
|
|||
</ul>
|
||||
</>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter className={"gap-2"}>
|
||||
<Button onClick={() => dialog.close(null)}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -160,7 +159,7 @@ function LoadingRepositories({
|
|||
|
||||
return (
|
||||
<>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("vpm repositories:dialog:downloading repositories...")}</p>
|
||||
<Progress value={downloaded} max={totalCount} />
|
||||
<div className={"text-center"}>
|
||||
|
|
@ -169,7 +168,7 @@ function LoadingRepositories({
|
|||
totalCount,
|
||||
})}
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => cancelRef.current?.()}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -197,7 +196,7 @@ function ConfirmingPackages({
|
|||
return (
|
||||
<>
|
||||
{/* TODO: use ScrollArea (I failed to use it inside dialog) */}
|
||||
<DialogDescription className={"font-normal"}>
|
||||
<div className={"font-normal"}>
|
||||
<p className={"whitespace-normal"}>
|
||||
{tc("vpm repositories:dialog:confirm packages list")}
|
||||
</p>
|
||||
|
|
@ -228,7 +227,7 @@ function ConfirmingPackages({
|
|||
error = false;
|
||||
content = (
|
||||
<ul className={"list-disc pl-6"}>
|
||||
{download.value.packages.map((info, idx) => (
|
||||
{download.value.packages.map((info) => (
|
||||
<li key={info.name}>{info.display_name ?? info.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -250,7 +249,7 @@ function ConfirmingPackages({
|
|||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close(null)}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -265,10 +264,8 @@ function ConfirmingPackages({
|
|||
|
||||
function AddingRepositories() {
|
||||
return (
|
||||
<>
|
||||
<DialogDescription>
|
||||
<p>{tc("vpm repositories:dialog:adding repositories...")}</p>
|
||||
</DialogDescription>
|
||||
</>
|
||||
<div>
|
||||
<p>{tc("vpm repositories:dialog:adding repositories...")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,48 @@
|
|||
"use client";
|
||||
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import {
|
||||
type CollisionDetection,
|
||||
closestCenter,
|
||||
DndContext,
|
||||
type DragEndEvent,
|
||||
type DragOverEvent,
|
||||
DragOverlay,
|
||||
type DragStartEvent,
|
||||
defaultDropAnimation,
|
||||
defaultDropAnimationSideEffects,
|
||||
type Modifier,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ChevronDown, CircleX, GripVertical } from "lucide-react";
|
||||
import {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -28,15 +62,6 @@ import { usePrevPathName } from "@/lib/prev-page";
|
|||
import { toastThrownError } from "@/lib/toast";
|
||||
import { useTauriListen } from "@/lib/use-tauri-listen";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ChevronDown, CircleX } from "lucide-react";
|
||||
import { Suspense, useCallback, useEffect, useId, useMemo } from "react";
|
||||
import { HeadingPageName } from "../-tab-selector";
|
||||
import { addRepository, openAddRepositoryDialog } from "./-use-add-repository";
|
||||
import { importRepositories } from "./-use-import-repositories";
|
||||
|
|
@ -45,6 +70,8 @@ export const Route = createFileRoute("/_main/packages/repositories/")({
|
|||
component: Page,
|
||||
});
|
||||
|
||||
type UserRepoWithListId = TauriUserRepository & { listId: string };
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
|
|
@ -53,11 +80,99 @@ function Page() {
|
|||
);
|
||||
}
|
||||
|
||||
const restrictToVerticalAxis: Modifier = ({ transform }) => ({
|
||||
...transform,
|
||||
x: 0,
|
||||
});
|
||||
|
||||
const DRAG_OVERLAY_MODIFIERS = [restrictToVerticalAxis];
|
||||
|
||||
const customDropAnimation: typeof defaultDropAnimation = {
|
||||
...defaultDropAnimation,
|
||||
sideEffects: defaultDropAnimationSideEffects({
|
||||
styles: {
|
||||
active: { opacity: "0" },
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const TABLE_HEAD = [
|
||||
"", // checkbox
|
||||
"general:name",
|
||||
"vpm repositories:url",
|
||||
"", // actions
|
||||
"", // grip handle
|
||||
] as const;
|
||||
|
||||
const environmentRepositoriesInfo = queryOptions({
|
||||
queryKey: ["environmentRepositoriesInfo"],
|
||||
queryFn: commands.environmentRepositoriesInfo,
|
||||
});
|
||||
|
||||
// Scrolls the given viewport element when the pointer is near the top or bottom
|
||||
// edge during drag. dnd-kit's built-in autoscroll is disabled because it causes
|
||||
// jitter with Radix UI ScrollArea (wrong container detection + double-smoothing).
|
||||
function useDragAutoScroll(
|
||||
viewportRef: React.RefObject<HTMLElement | null>,
|
||||
isActive: boolean,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!isActive) return;
|
||||
|
||||
const THRESHOLD = 80; // px from edge to begin scrolling
|
||||
const MAX_SPEED = 15; // px/frame at the very edge
|
||||
|
||||
let pointerY = 0;
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
pointerY = e.clientY;
|
||||
};
|
||||
window.addEventListener("pointermove", onPointerMove, { passive: true });
|
||||
|
||||
let rafId: number;
|
||||
const tick = () => {
|
||||
const viewport = viewportRef.current;
|
||||
if (viewport) {
|
||||
const { top, bottom } = viewport.getBoundingClientRect();
|
||||
const distFromTop = pointerY - top;
|
||||
const distFromBottom = bottom - pointerY;
|
||||
|
||||
let delta = 0;
|
||||
if (distFromTop >= 0 && distFromTop < THRESHOLD) {
|
||||
delta = -MAX_SPEED * (1 - distFromTop / THRESHOLD);
|
||||
} else if (distFromBottom >= 0 && distFromBottom < THRESHOLD) {
|
||||
delta = MAX_SPEED * (1 - distFromBottom / THRESHOLD);
|
||||
}
|
||||
|
||||
if (delta !== 0) {
|
||||
viewport.scrollTo({
|
||||
top: viewport.scrollTop + delta,
|
||||
behavior: "instant",
|
||||
});
|
||||
}
|
||||
}
|
||||
rafId = requestAnimationFrame(tick);
|
||||
};
|
||||
rafId = requestAnimationFrame(tick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointermove", onPointerMove);
|
||||
cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [isActive, viewportRef]);
|
||||
}
|
||||
|
||||
function computeSlotKey(repo: TauriUserRepository, used: Set<string>): string {
|
||||
const base = `${repo.id} ${repo.url ?? ""}`;
|
||||
let key = base;
|
||||
let counter = 0;
|
||||
while (used.has(key)) {
|
||||
counter++;
|
||||
key = `${base} ${counter}`;
|
||||
}
|
||||
used.add(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
function PageBody() {
|
||||
const result = useQuery(environmentRepositoriesInfo);
|
||||
|
||||
|
|
@ -99,140 +214,110 @@ function PageBody() {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const bodyAnimation = usePrevPathName().startsWith("/packages")
|
||||
? "slide-right"
|
||||
: "";
|
||||
const guiAnimation = useQuery({
|
||||
queryKey: ["environmentGuiAnimation"],
|
||||
queryFn: commands.environmentGuiAnimation,
|
||||
initialData: true,
|
||||
}).data;
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<HNavBar
|
||||
className={"shrink-0"}
|
||||
leading={<HeadingPageName pageType={"/packages/repositories"} />}
|
||||
trailing={
|
||||
<DropdownMenu>
|
||||
<div className={"flex divide-x"}>
|
||||
<Button
|
||||
className={"rounded-r-none"}
|
||||
onClick={() => openAddRepositoryDialog()}
|
||||
>
|
||||
{tc("vpm repositories:button:add repository")}
|
||||
</Button>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={"rounded-l-none pl-2 pr-2"}
|
||||
>
|
||||
<Button>
|
||||
<ChevronDown className={"w-4 h-4"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => importRepositoriesMutation.mutate()}
|
||||
>
|
||||
{tc("vpm repositories:button:import repositories")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => exportRepositories.mutate()}>
|
||||
{tc("vpm repositories:button:export repositories")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
/>
|
||||
<main
|
||||
className={`shrink overflow-hidden flex w-full h-full ${bodyAnimation}`}
|
||||
>
|
||||
<ScrollableCardTable className={"h-full w-full"}>
|
||||
<RepositoryTableBody
|
||||
userRepos={result.data?.user_repositories || []}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
/>
|
||||
</ScrollableCardTable>
|
||||
</main>
|
||||
</VStack>
|
||||
const userRepos = result.data?.user_repositories;
|
||||
|
||||
const listIdMapRef = useRef<Map<string, string>>(new Map());
|
||||
|
||||
const augmentedUserRepos = useMemo<UserRepoWithListId[]>(() => {
|
||||
if (!userRepos) {
|
||||
listIdMapRef.current = new Map();
|
||||
return [];
|
||||
}
|
||||
const prev = listIdMapRef.current;
|
||||
const next = new Map<string, string>();
|
||||
const usedKeys = new Set<string>();
|
||||
const result: UserRepoWithListId[] = [];
|
||||
|
||||
for (const r of userRepos) {
|
||||
const key = computeSlotKey(r, usedKeys);
|
||||
const listId = prev.get(key) ?? crypto.randomUUID();
|
||||
next.set(key, listId);
|
||||
result.push({ ...r, listId });
|
||||
}
|
||||
|
||||
listIdMapRef.current = next;
|
||||
return result;
|
||||
}, [userRepos]);
|
||||
|
||||
const [orderedListIds, setOrderedListIds] = useState<string[]>(() =>
|
||||
augmentedUserRepos.map((r) => r.listId),
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryTableBody({
|
||||
userRepos,
|
||||
hiddenUserRepos,
|
||||
}: {
|
||||
userRepos: TauriUserRepository[];
|
||||
hiddenUserRepos: Set<string>;
|
||||
}) {
|
||||
const TABLE_HEAD = [
|
||||
"", // checkbox
|
||||
"general:name",
|
||||
"vpm repositories:url",
|
||||
"", // actions
|
||||
];
|
||||
useEffect(() => {
|
||||
setOrderedListIds(augmentedUserRepos.map((r) => r.listId));
|
||||
}, [augmentedUserRepos]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<thead>
|
||||
<tr>
|
||||
{TABLE_HEAD.map((head, index) => (
|
||||
<th
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static array
|
||||
key={index}
|
||||
className={
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground p-2.5"
|
||||
}
|
||||
>
|
||||
<small className="font-normal leading-none">{tc(head)}</small>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<RepositoryRow
|
||||
repoId={"com.vrchat.repos.official"}
|
||||
url={"https://packages.vrchat.com/official?download"}
|
||||
displayName={tt("vpm repositories:source:official")}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
canRemove={false}
|
||||
/>
|
||||
<RepositoryRow
|
||||
repoId={"com.vrchat.repos.curated"}
|
||||
url={"https://packages.vrchat.com/curated?download"}
|
||||
displayName={tt("vpm repositories:source:curated")}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
className={"border-b border-primary/10"}
|
||||
canRemove={false}
|
||||
/>
|
||||
{userRepos.map((repo) => (
|
||||
<RepositoryRow
|
||||
key={repo.id}
|
||||
repoId={repo.id}
|
||||
displayName={repo.display_name}
|
||||
url={repo.url}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
const userRepoByListId = useMemo(
|
||||
() => new Map(augmentedUserRepos.map((r) => [r.listId, r])),
|
||||
[augmentedUserRepos],
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryRow({
|
||||
repoId,
|
||||
displayName,
|
||||
url,
|
||||
hiddenUserRepos,
|
||||
className,
|
||||
canRemove = true,
|
||||
}: {
|
||||
repoId: TauriUserRepository["id"];
|
||||
displayName: TauriUserRepository["display_name"];
|
||||
url: TauriUserRepository["url"];
|
||||
hiddenUserRepos: Set<string>;
|
||||
className?: string;
|
||||
canRemove?: boolean;
|
||||
}) {
|
||||
const cellClass = "p-2.5";
|
||||
const id = useId();
|
||||
const userRepoByListIdRef =
|
||||
useRef<Map<string, UserRepoWithListId>>(userRepoByListId);
|
||||
useEffect(() => {
|
||||
userRepoByListIdRef.current = userRepoByListId;
|
||||
}, [userRepoByListId]);
|
||||
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [overId, setOverId] = useState<string | null>(null);
|
||||
const [columnWidths, setColumnWidths] = useState<number[]>([]);
|
||||
const theadRowRef = useRef<HTMLTableRowElement>(null);
|
||||
const scrollViewportRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
const orderedListIdsSet = useMemo(
|
||||
() => new Set(orderedListIds),
|
||||
[orderedListIds],
|
||||
);
|
||||
|
||||
const collisionDetection = useCallback<CollisionDetection>(
|
||||
(args) =>
|
||||
closestCenter({
|
||||
...args,
|
||||
droppableContainers: args.droppableContainers.filter((c) =>
|
||||
orderedListIdsSet.has(c.id as string),
|
||||
),
|
||||
}),
|
||||
[orderedListIdsSet],
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const reorderMutation = useMutation({
|
||||
mutationFn: (listIds: string[]) => {
|
||||
const repos = listIds
|
||||
.map((lid) => userRepoByListId.get(lid))
|
||||
.filter((r): r is UserRepoWithListId => r !== undefined)
|
||||
.map((r) => ({ index: r.index, id: r.id }));
|
||||
return commands.environmentReorderRepositories(repos);
|
||||
},
|
||||
// Pin listIds to the new positions so duplicate-keyed rows don't swap their listIds on refetch.
|
||||
onMutate: (newListIds: string[]) => {
|
||||
const prevMap = new Map(listIdMapRef.current);
|
||||
const rebuilt = new Map<string, string>();
|
||||
const usedKeys = new Set<string>();
|
||||
for (const lid of newListIds) {
|
||||
const repo = userRepoByListIdRef.current.get(lid);
|
||||
if (!repo) continue;
|
||||
const key = computeSlotKey(repo, usedKeys);
|
||||
rebuilt.set(key, lid);
|
||||
}
|
||||
listIdMapRef.current = rebuilt;
|
||||
return { prevMap };
|
||||
},
|
||||
onSettled: () => queryClient.invalidateQueries(environmentRepositoriesInfo),
|
||||
onError: (e, _newListIds, ctx) => {
|
||||
if (ctx?.prevMap) listIdMapRef.current = ctx.prevMap;
|
||||
toastThrownError(e);
|
||||
},
|
||||
});
|
||||
|
||||
const setHideRepository = useMutation({
|
||||
mutationFn: async ({ id, shown }: { id: string; shown: boolean }) => {
|
||||
if (shown) {
|
||||
|
|
@ -277,68 +362,483 @@ function RepositoryRow({
|
|||
},
|
||||
});
|
||||
|
||||
const activeVisualIndex = useMemo(() => {
|
||||
if (!activeId) return 0;
|
||||
const effectiveId = overId ?? activeId;
|
||||
return orderedListIds.indexOf(effectiveId) + 2; // +2 for the 2 fixed rows
|
||||
}, [activeId, overId, orderedListIds]);
|
||||
|
||||
function handleDragStart(event: DragStartEvent) {
|
||||
setActiveId(event.active.id as string);
|
||||
if (theadRowRef.current) {
|
||||
const widths = Array.from(
|
||||
theadRowRef.current.querySelectorAll("th"),
|
||||
(th) => th.getBoundingClientRect().width,
|
||||
);
|
||||
setColumnWidths(widths);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver(event: DragOverEvent) {
|
||||
setOverId((event.over?.id as string | null) ?? null);
|
||||
}
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
setActiveId(null);
|
||||
setOverId(null);
|
||||
const { active, over } = event;
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = orderedListIds.indexOf(active.id as string);
|
||||
const newIndex = orderedListIds.indexOf(over.id as string);
|
||||
const newListIds = arrayMove(orderedListIds, oldIndex, newIndex);
|
||||
setOrderedListIds(newListIds);
|
||||
reorderMutation.mutate(newListIds);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragCancel() {
|
||||
setActiveId(null);
|
||||
setOverId(null);
|
||||
}
|
||||
|
||||
useDragAutoScroll(scrollViewportRef, activeId !== null);
|
||||
|
||||
const bodyAnimation = usePrevPathName().startsWith("/packages")
|
||||
? "slide-right"
|
||||
: "";
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={collisionDetection}
|
||||
autoScroll={false}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<VStack>
|
||||
<div style={activeId !== null ? { pointerEvents: "none" } : undefined}>
|
||||
<HNavBar
|
||||
className="shrink-0"
|
||||
leading={<HeadingPageName pageType={"/packages/repositories"} />}
|
||||
trailing={
|
||||
<DropdownMenu>
|
||||
<div className={"flex divide-x"}>
|
||||
<Button
|
||||
className={"rounded-r-none compact:h-10"}
|
||||
onClick={() => openAddRepositoryDialog()}
|
||||
>
|
||||
{tc("vpm repositories:button:add repository")}
|
||||
</Button>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={"rounded-l-none pl-2 pr-2 compact:h-10"}
|
||||
>
|
||||
<Button>
|
||||
<ChevronDown className={"w-4 h-4"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => importRepositoriesMutation.mutate()}
|
||||
>
|
||||
{tc("vpm repositories:button:import repositories")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => exportRepositories.mutate()}>
|
||||
{tc("vpm repositories:button:export repositories")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<main
|
||||
className={`shrink overflow-hidden flex w-full h-full ${bodyAnimation}`}
|
||||
>
|
||||
<ScrollableCardTable
|
||||
className={"h-full w-full"}
|
||||
viewportRef={scrollViewportRef}
|
||||
>
|
||||
<RepositoryTableBody
|
||||
orderedListIds={orderedListIds}
|
||||
userRepoByListId={userRepoByListId}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
theadRowRef={theadRowRef}
|
||||
guiAnimation={guiAnimation}
|
||||
onToggleVisibility={(id, shown) =>
|
||||
setHideRepository.mutate({ id, shown })
|
||||
}
|
||||
isDragActive={activeId !== null}
|
||||
/>
|
||||
</ScrollableCardTable>
|
||||
</main>
|
||||
</VStack>
|
||||
<DragOverlay
|
||||
modifiers={DRAG_OVERLAY_MODIFIERS}
|
||||
dropAnimation={guiAnimation ? customDropAnimation : null}
|
||||
>
|
||||
{activeId ? (
|
||||
<RepositoryDragOverlay
|
||||
repo={userRepoByListId.get(activeId)}
|
||||
selected={
|
||||
!hiddenUserRepos.has(userRepoByListId.get(activeId)?.id ?? "")
|
||||
}
|
||||
columnWidths={columnWidths}
|
||||
visualIndex={activeVisualIndex}
|
||||
guiAnimation={guiAnimation}
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryTableBody({
|
||||
orderedListIds,
|
||||
userRepoByListId,
|
||||
hiddenUserRepos,
|
||||
theadRowRef,
|
||||
guiAnimation,
|
||||
onToggleVisibility,
|
||||
isDragActive,
|
||||
}: {
|
||||
orderedListIds: string[];
|
||||
userRepoByListId: Map<string, UserRepoWithListId>;
|
||||
hiddenUserRepos: Set<string>;
|
||||
theadRowRef: React.RefObject<HTMLTableRowElement | null>;
|
||||
guiAnimation: boolean;
|
||||
onToggleVisibility: (id: string, shown: boolean) => void;
|
||||
isDragActive: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<thead>
|
||||
<tr ref={theadRowRef}>
|
||||
{TABLE_HEAD.map((head, index) => (
|
||||
<th
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static array
|
||||
key={index}
|
||||
className={
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground px-2.5 py-1.5"
|
||||
}
|
||||
>
|
||||
<small className="font-normal leading-none">{tc(head)}</small>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<RepositoryRow
|
||||
repoId={"com.vrchat.repos.official"}
|
||||
url={"https://packages.vrchat.com/official?download"}
|
||||
displayName={tt("vpm repositories:source:official")}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
canRemove={false}
|
||||
rowIndex={0}
|
||||
guiAnimation={guiAnimation}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
isDragActive={isDragActive}
|
||||
/>
|
||||
<RepositoryRow
|
||||
repoId={"com.vrchat.repos.curated"}
|
||||
url={"https://packages.vrchat.com/curated?download"}
|
||||
displayName={tt("vpm repositories:source:curated")}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
className={"border-b border-primary/10"}
|
||||
canRemove={false}
|
||||
rowIndex={1}
|
||||
guiAnimation={guiAnimation}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
isDragActive={isDragActive}
|
||||
/>
|
||||
<SortableContext
|
||||
items={orderedListIds}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{orderedListIds.map((listId, index) => {
|
||||
const repo = userRepoByListId.get(listId);
|
||||
if (!repo) return null;
|
||||
return (
|
||||
<RepositoryRow
|
||||
key={listId}
|
||||
listId={listId}
|
||||
repoId={repo.id}
|
||||
repoIndex={repo.index}
|
||||
displayName={repo.display_name}
|
||||
url={repo.url}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
rowIndex={2 + index}
|
||||
guiAnimation={guiAnimation}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
isDragActive={isDragActive}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</tbody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const CELL_CLASS = "p-2.5 compact:py-1 align-middle";
|
||||
|
||||
function RepositoryRowCells({
|
||||
labelId,
|
||||
displayName,
|
||||
url,
|
||||
canRemove,
|
||||
selected,
|
||||
onCheckedChange,
|
||||
onRemove,
|
||||
dragListeners,
|
||||
dragAttributes,
|
||||
}: {
|
||||
labelId?: string;
|
||||
displayName: string;
|
||||
url: string | null | undefined;
|
||||
canRemove: boolean;
|
||||
selected: boolean;
|
||||
onCheckedChange?: (shown: boolean) => void;
|
||||
onRemove?: () => void;
|
||||
dragListeners?: ReturnType<typeof useSortable>["listeners"];
|
||||
dragAttributes?: ReturnType<typeof useSortable>["attributes"];
|
||||
}) {
|
||||
const interactive = onCheckedChange !== undefined;
|
||||
return (
|
||||
<>
|
||||
<td className={CELL_CLASS}>
|
||||
{interactive ? (
|
||||
<div className="flex">
|
||||
<Checkbox
|
||||
id={labelId}
|
||||
checked={selected}
|
||||
onCheckedChange={(x) => onCheckedChange(x === true)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pointer-events-none flex">
|
||||
<Checkbox checked={selected} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className={CELL_CLASS}>
|
||||
{interactive ? (
|
||||
<label htmlFor={labelId}>
|
||||
<p className="font-normal">{displayName}</p>
|
||||
</label>
|
||||
) : (
|
||||
<p className="font-normal">{displayName}</p>
|
||||
)}
|
||||
</td>
|
||||
<td className={CELL_CLASS}>
|
||||
<p className="font-normal">{url}</p>
|
||||
</td>
|
||||
<td className={`${CELL_CLASS} w-0`}>
|
||||
{interactive ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={canRemove}>
|
||||
<Button
|
||||
disabled={!canRemove}
|
||||
onClick={onRemove}
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
>
|
||||
<CircleX className={"size-5 text-destructive"} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{canRemove
|
||||
? tc("vpm repositories:remove repository")
|
||||
: tc(
|
||||
"vpm repositories:tooltip:remove curated or official repository",
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button variant={"ghost"} size={"icon"} disabled>
|
||||
<CircleX className={"size-5 text-destructive"} />
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
className={cn(
|
||||
CELL_CLASS,
|
||||
"w-0",
|
||||
canRemove ? "cursor-move" : "cursor-not-allowed",
|
||||
)}
|
||||
{...(canRemove ? dragListeners : undefined)}
|
||||
{...(canRemove ? dragAttributes : undefined)}
|
||||
>
|
||||
<GripVertical
|
||||
className={cn(
|
||||
"size-5 text-muted-foreground",
|
||||
!canRemove && "opacity-50",
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryRow({
|
||||
listId,
|
||||
repoId,
|
||||
repoIndex,
|
||||
displayName,
|
||||
url,
|
||||
hiddenUserRepos,
|
||||
className,
|
||||
canRemove = true,
|
||||
rowIndex,
|
||||
guiAnimation,
|
||||
onToggleVisibility,
|
||||
isDragActive,
|
||||
}: {
|
||||
listId?: string;
|
||||
repoId: TauriUserRepository["id"];
|
||||
repoIndex?: number;
|
||||
displayName: TauriUserRepository["display_name"];
|
||||
url: TauriUserRepository["url"];
|
||||
hiddenUserRepos: Set<string>;
|
||||
className?: string;
|
||||
canRemove?: boolean;
|
||||
rowIndex: number;
|
||||
guiAnimation: boolean;
|
||||
onToggleVisibility: (id: string, shown: boolean) => void;
|
||||
isDragActive: boolean;
|
||||
}) {
|
||||
const labelId = useId();
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: listId ?? repoId, disabled: !canRemove });
|
||||
|
||||
const visualIndex = useMemo(() => {
|
||||
if (isDragging) return rowIndex;
|
||||
const dy = transform?.y ?? 0;
|
||||
if (dy < 0) return rowIndex - 1;
|
||||
if (dy > 0) return rowIndex + 1;
|
||||
return rowIndex;
|
||||
}, [rowIndex, transform?.y, isDragging]);
|
||||
|
||||
const dragStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
transform: transform ? `translateY(${transform.y}px)` : undefined,
|
||||
transition: guiAnimation
|
||||
? [transition, isDragActive ? undefined : "background-color 200ms ease"]
|
||||
.filter(Boolean)
|
||||
.join(", ") || undefined
|
||||
: undefined,
|
||||
opacity: isDragging ? 0 : 1,
|
||||
position: "relative",
|
||||
}),
|
||||
[transform, transition, isDragging, guiAnimation, isDragActive],
|
||||
);
|
||||
|
||||
const selected = !hiddenUserRepos.has(repoId);
|
||||
|
||||
return (
|
||||
<tr className={cn("even:bg-secondary/30", className)}>
|
||||
<td className={cellClass}>
|
||||
<Checkbox
|
||||
id={id}
|
||||
checked={selected}
|
||||
onCheckedChange={(x) =>
|
||||
setHideRepository.mutate({ id: repoId, shown: x === true })
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className={cellClass}>
|
||||
<label htmlFor={id}>
|
||||
<p className="font-normal">{displayName}</p>
|
||||
</label>
|
||||
</td>
|
||||
<td className={cellClass}>
|
||||
<p className="font-normal">{url}</p>
|
||||
</td>
|
||||
<td className={`${cellClass} w-0`}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={canRemove}>
|
||||
<Button
|
||||
disabled={!canRemove}
|
||||
onClick={() => {
|
||||
void openSingleDialog(RemoveRepositoryDialog, {
|
||||
displayName,
|
||||
id: repoId,
|
||||
});
|
||||
}}
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
>
|
||||
<CircleX className={"size-5 text-destructive"} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{canRemove
|
||||
? tc("vpm repositories:remove repository")
|
||||
: tc(
|
||||
"vpm repositories:tooltip:remove curated or official repository",
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<tr
|
||||
ref={setNodeRef}
|
||||
style={dragStyle}
|
||||
className={cn(visualIndex % 2 === 1 ? "bg-secondary/30" : "", className)}
|
||||
>
|
||||
<RepositoryRowCells
|
||||
labelId={labelId}
|
||||
displayName={displayName}
|
||||
url={url}
|
||||
canRemove={canRemove}
|
||||
selected={selected}
|
||||
onCheckedChange={(shown) => onToggleVisibility(repoId, shown)}
|
||||
onRemove={() =>
|
||||
void openSingleDialog(RemoveRepositoryDialog, {
|
||||
displayName,
|
||||
index: repoIndex ?? 0,
|
||||
id: repoId,
|
||||
})
|
||||
}
|
||||
dragListeners={listeners}
|
||||
dragAttributes={attributes}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryDragOverlay({
|
||||
repo,
|
||||
selected,
|
||||
columnWidths,
|
||||
visualIndex,
|
||||
guiAnimation,
|
||||
}: {
|
||||
repo: TauriUserRepository | undefined;
|
||||
selected: boolean;
|
||||
columnWidths: number[];
|
||||
visualIndex: number;
|
||||
guiAnimation: boolean;
|
||||
}) {
|
||||
const style = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
transition: guiAnimation ? "background-color 200ms ease" : undefined,
|
||||
}),
|
||||
[guiAnimation],
|
||||
);
|
||||
|
||||
if (!repo) return null;
|
||||
return (
|
||||
<table
|
||||
className={cn(
|
||||
"w-full table-fixed text-left",
|
||||
visualIndex % 2 === 1 ? "bg-secondary/30" : "",
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{columnWidths.length > 0 && (
|
||||
<colgroup>
|
||||
{columnWidths.map((w, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: fixed column order
|
||||
<col key={i} style={{ width: w }} />
|
||||
))}
|
||||
</colgroup>
|
||||
)}
|
||||
<tbody>
|
||||
<tr>
|
||||
<RepositoryRowCells
|
||||
displayName={repo.display_name}
|
||||
url={repo.url}
|
||||
canRemove={true}
|
||||
selected={selected}
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function RemoveRepositoryDialog({
|
||||
dialog,
|
||||
displayName,
|
||||
index,
|
||||
id,
|
||||
}: { dialog: DialogContext<void>; displayName: string; id: string }) {
|
||||
}: {
|
||||
dialog: DialogContext<void>;
|
||||
displayName: string;
|
||||
index: number;
|
||||
id: string;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const removeRepository = useMutation({
|
||||
mutationFn: async (id: string) =>
|
||||
await commands.environmentRemoveRepository(id),
|
||||
onMutate: async (id) => {
|
||||
mutationFn: async (args: { index: number; id: string }) =>
|
||||
await commands.environmentRemoveRepository(args.index, args.id),
|
||||
onMutate: async ({ index }) => {
|
||||
await queryClient.cancelQueries(environmentRepositoriesInfo);
|
||||
const data = queryClient.getQueryData(
|
||||
environmentRepositoriesInfo.queryKey,
|
||||
|
|
@ -346,22 +846,30 @@ function RemoveRepositoryDialog({
|
|||
if (data !== undefined) {
|
||||
queryClient.setQueryData(environmentRepositoriesInfo.queryKey, {
|
||||
...data,
|
||||
user_repositories: data.user_repositories.filter((x) => x.id !== id),
|
||||
user_repositories: data.user_repositories.filter(
|
||||
(x) => x.index !== index,
|
||||
),
|
||||
});
|
||||
}
|
||||
return data;
|
||||
},
|
||||
onError: (e, _args, ctx) => {
|
||||
queryClient.setQueryData(environmentRepositoriesInfo.queryKey, ctx);
|
||||
toastThrownError(e);
|
||||
},
|
||||
onSettled: () => queryClient.invalidateQueries(environmentRepositoriesInfo),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogTitle>{tc("vpm repositories:remove repository")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p className={"whitespace-normal font-normal"}>
|
||||
{tc("vpm repositories:dialog:confirm remove description", {
|
||||
name: displayName,
|
||||
})}
|
||||
</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close()}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -369,7 +877,7 @@ function RemoveRepositoryDialog({
|
|||
<Button
|
||||
onClick={() => {
|
||||
dialog.close();
|
||||
removeRepository.mutate(id);
|
||||
removeRepository.mutate({ index, id });
|
||||
}}
|
||||
className={"ml-2"}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,33 @@
|
|||
import Loading from "@/app/-loading";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
useSuspenseQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ChevronDown, CircleX, Ellipsis, Star } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Suspense, useId, useMemo, useState } from "react";
|
||||
import { HeadingPageName } from "@/app/_main/packages/-tab-selector";
|
||||
import Loading from "@/app/-loading";
|
||||
import { FilePathRow } from "@/components/common-setting-parts";
|
||||
import { FavoriteStarToggleButton } from "@/components/FavoriteStarButton";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { Overlay } from "@/components/Overlay";
|
||||
import {
|
||||
ReorderableList,
|
||||
type ReorderableListId,
|
||||
useReorderableList,
|
||||
} from "@/components/ReorderableList";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TemplateSelect } from "@/components/TemplateSelect";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
type AutoCompleteOption,
|
||||
Autocomplete,
|
||||
} from "@/components/ui/autocomplete";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -20,45 +35,32 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
import {
|
||||
commands,
|
||||
type TauriAlcomTemplate,
|
||||
type TauriProjectTemplateInfo,
|
||||
commands,
|
||||
type TauriVersion,
|
||||
} from "@/lib/bindings";
|
||||
import { dateToString, formatDateOffset } from "@/lib/dateToString";
|
||||
import { type DialogContext, openSingleDialog } from "@/lib/dialog";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
import { processResult } from "@/lib/import-templates";
|
||||
import { usePrevPathName } from "@/lib/prev-page";
|
||||
import {
|
||||
type ProjectTemplateCategory,
|
||||
projectTemplateCategory,
|
||||
projectTemplateDisplayId,
|
||||
projectTemplateName,
|
||||
} from "@/lib/project-template";
|
||||
import { toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
queryOptions,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
useSuspenseQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ChevronDown, CircleX, Ellipsis } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Suspense, useId, useState } from "react";
|
||||
import { compareVersion } from "@/lib/version";
|
||||
|
||||
export const Route = createFileRoute("/_main/packages/templates/")({
|
||||
component: RouteComponent,
|
||||
|
|
@ -81,15 +83,15 @@ function RouteComponent() {
|
|||
return (
|
||||
<VStack>
|
||||
<HNavBar
|
||||
className={"shrink-0"}
|
||||
className="shrink-0"
|
||||
leading={<HeadingPageName pageType={"/packages/templates"} />}
|
||||
trailing={
|
||||
<DropdownMenu>
|
||||
<div className={"flex divide-x"}>
|
||||
<CreateTemplateButton className={"rounded-r-none"} />
|
||||
<CreateTemplateButton className={"rounded-r-none compact:h-10"} />
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={"rounded-l-none pl-2 pr-2"}
|
||||
className={"rounded-l-none pl-2 pr-2 compact:h-10"}
|
||||
>
|
||||
<Button>
|
||||
<ChevronDown className={"w-4 h-4"} />
|
||||
|
|
@ -139,6 +141,7 @@ function TemplatesTableBody() {
|
|||
await openSingleDialog(TemplateEditor, {
|
||||
templates: information.data.templates,
|
||||
template: { ...alcomTemplate, id },
|
||||
favoriteTemplates: information.data.favorite_templates,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
@ -161,16 +164,45 @@ function TemplatesTableBody() {
|
|||
}
|
||||
};
|
||||
|
||||
const templatesOrdered = useMemo(() => {
|
||||
const perCategoryFav: {
|
||||
[K in `${boolean}-${ProjectTemplateCategory}`]: TauriProjectTemplateInfo[];
|
||||
} = {
|
||||
"true-builtin": [],
|
||||
"false-builtin": [],
|
||||
"true-alcom": [],
|
||||
"false-alcom": [],
|
||||
"true-vcc": [],
|
||||
"false-vcc": [],
|
||||
};
|
||||
for (const template of information.data.templates) {
|
||||
const category = projectTemplateCategory(template.id);
|
||||
const favorite = information.data.favorite_templates.includes(
|
||||
template.id,
|
||||
);
|
||||
perCategoryFav[`${favorite}-${category}`].push(template);
|
||||
}
|
||||
return (["builtin", "alcom", "vcc"] as const).flatMap((category) => [
|
||||
...perCategoryFav[`true-${category}`],
|
||||
...perCategoryFav[`false-${category}`],
|
||||
]);
|
||||
}, [information.data.templates, information.data.favorite_templates]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
className={`sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground px-2.5 py-1.5`}
|
||||
>
|
||||
<Star className={"size-4"} />
|
||||
</th>
|
||||
{TABLE_HEAD.map((head, index) => (
|
||||
<th
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static array
|
||||
key={index}
|
||||
className={
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground p-2.5"
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground px-2.5 py-1.5"
|
||||
}
|
||||
>
|
||||
<small className="font-normal leading-none">{tc(head)}</small>
|
||||
|
|
@ -179,12 +211,13 @@ function TemplatesTableBody() {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{information.data.templates.map((template) => (
|
||||
{templatesOrdered.map((template) => (
|
||||
<TemplateRow
|
||||
key={template.id}
|
||||
template={template}
|
||||
remove={removeTemplate}
|
||||
edit={editTemplate}
|
||||
favorite={information.data.favorite_templates.includes(template.id)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
@ -196,12 +229,14 @@ function TemplateRow({
|
|||
template,
|
||||
remove,
|
||||
edit,
|
||||
favorite,
|
||||
}: {
|
||||
template: TauriProjectTemplateInfo;
|
||||
remove?: (id: string) => void;
|
||||
edit?: (id: string) => void;
|
||||
favorite: boolean;
|
||||
}) {
|
||||
const cellClass = "p-2.5";
|
||||
const cellClass = "p-2.5 compact:py-1";
|
||||
const id = useId();
|
||||
|
||||
const category = projectTemplateCategory(template.id);
|
||||
|
|
@ -218,8 +253,64 @@ function TemplateRow({
|
|||
}
|
||||
};
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setTemplateFavorite = useMutation({
|
||||
mutationFn: (params: { id: string; favorite: boolean }) =>
|
||||
commands.environmentSetTemplateFavorite(params.id, params.favorite),
|
||||
|
||||
onMutate: async (params) => {
|
||||
await queryClient.cancelQueries(environmentProjectCreationInformation);
|
||||
|
||||
const previousData = queryClient.getQueryData(
|
||||
environmentProjectCreationInformation.queryKey,
|
||||
);
|
||||
|
||||
if (previousData !== undefined) {
|
||||
queryClient.setQueryData(
|
||||
environmentProjectCreationInformation.queryKey,
|
||||
{
|
||||
...previousData,
|
||||
favorite_templates: params.favorite
|
||||
? previousData.favorite_templates.includes(params.id)
|
||||
? previousData.favorite_templates
|
||||
: [...previousData.favorite_templates, params.id]
|
||||
: previousData.favorite_templates.filter((x) => x !== params.id),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return previousData;
|
||||
},
|
||||
|
||||
onError: (error, _, context) => {
|
||||
console.error("Error favoriting project", error);
|
||||
toastThrownError(error);
|
||||
if (context) {
|
||||
queryClient.setQueryData(
|
||||
environmentProjectCreationInformation.queryKey,
|
||||
context,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<tr className="even:bg-secondary/30">
|
||||
<tr className="even:bg-secondary/30 group">
|
||||
<td className={`${cellClass} w-3`}>
|
||||
<div className={"relative flex"}>
|
||||
<FavoriteStarToggleButton
|
||||
favorite={favorite}
|
||||
disabled={category === "vcc"}
|
||||
onToggle={() =>
|
||||
setTemplateFavorite.mutate({
|
||||
id: template.id,
|
||||
favorite: !favorite,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className={`${cellClass} w-full`}>
|
||||
<label htmlFor={id}>
|
||||
<p className="font-normal">{projectTemplateName(template)}</p>
|
||||
|
|
@ -286,13 +377,16 @@ function TemplateRow({
|
|||
function RemoveTemplateConfirmDialog({
|
||||
dialog,
|
||||
displayName,
|
||||
}: { dialog: DialogContext<boolean>; displayName: string }) {
|
||||
}: {
|
||||
dialog: DialogContext<boolean>;
|
||||
displayName: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<DialogTitle>{tc("templates:dialog:remove template")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
{tc("templates:dialog:confirm remove template", { displayName })}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close(false)}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -413,6 +507,7 @@ function CreateTemplateButton({ className }: { className: string }) {
|
|||
void openSingleDialog(TemplateEditor, {
|
||||
templates: information.data.templates,
|
||||
template: null,
|
||||
favoriteTemplates: information.data.favorite_templates,
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
@ -428,18 +523,25 @@ const prereleaseSegment = regexp`(?:0|[1-9]\d*|[0-9a-z-]*[a-z-][0-9a-z-]*)`;
|
|||
const prerelease = regexp`(?:-?${prereleaseSegment}(?:\.${prereleaseSegment})*)`;
|
||||
const buildSegment = regexp`(?:[0-9a-z-]+)`;
|
||||
const build = regexp`(?:${buildSegment}(?:\.${buildSegment})*)`;
|
||||
const rangeRegex = new RegExp(
|
||||
const packageRangeRegex = new RegExp(
|
||||
regexp`^\s*(?:(?:>|<|>=|<=|=|\^|~)\s*)?v?${versionSegment}(?:\.${versionSegment}(?:\.${versionSegment}${prerelease}?${build}?)?)?\s*$`,
|
||||
"i",
|
||||
);
|
||||
// Currently, the unity version channel part and increment part is ignored and not allowed to include
|
||||
const unityRangeRegex = new RegExp(
|
||||
regexp`^\s*(?:(?:>|<|>=|<=|=|\^|~)\s*)?v?${versionSegment}(?:\.${versionSegment}(?:\.${versionSegment})?)?\s*$`,
|
||||
"i",
|
||||
);
|
||||
|
||||
function TemplateEditor({
|
||||
templates,
|
||||
template,
|
||||
favoriteTemplates,
|
||||
dialog,
|
||||
}: {
|
||||
templates: TauriProjectTemplateInfo[];
|
||||
template: (TauriAlcomTemplate & { id: string }) | null;
|
||||
favoriteTemplates: string[];
|
||||
dialog: DialogContext<boolean>;
|
||||
}) {
|
||||
const [baseTemplate, setBaseTemplate] = useState<string>(
|
||||
|
|
@ -448,6 +550,183 @@ function TemplateEditor({
|
|||
const [name, setName] = useState(template?.display_name ?? "");
|
||||
const [unityRange, setUnityRange] = useState(template?.unity_version ?? "");
|
||||
|
||||
const allPackages = useQuery({
|
||||
queryKey: ["environmentPackages"],
|
||||
queryFn: () => commands.environmentPackages(),
|
||||
});
|
||||
|
||||
const { packageCandidates, versionCandidatePerPackage } = useMemo(() => {
|
||||
type PackageInfo = {
|
||||
dataSourceVersion: TauriVersion;
|
||||
displayName: string | null;
|
||||
keywords: string[];
|
||||
versions: TauriVersion[];
|
||||
};
|
||||
const packages = new Map<string, PackageInfo>();
|
||||
for (const pkg of allPackages.data ?? []) {
|
||||
if (pkg.is_yanked) continue;
|
||||
let rowInfo = packages.get(pkg.name);
|
||||
if (
|
||||
rowInfo == null ||
|
||||
compareVersion(pkg.version, rowInfo.dataSourceVersion) > 0
|
||||
) {
|
||||
packages.set(
|
||||
pkg.name,
|
||||
(rowInfo = {
|
||||
dataSourceVersion: pkg.version,
|
||||
displayName: pkg.display_name,
|
||||
keywords: pkg.keywords,
|
||||
versions: rowInfo?.versions ?? [],
|
||||
}),
|
||||
);
|
||||
}
|
||||
rowInfo.versions.push(pkg.version);
|
||||
}
|
||||
return {
|
||||
packageCandidates: Array.from(packages.entries()).map(
|
||||
([id, pkg]) =>
|
||||
({
|
||||
value: id,
|
||||
label: (
|
||||
<AutocompletePackageLabel displayName={pkg.displayName} id={id} />
|
||||
),
|
||||
keywords: [pkg.displayName, ...pkg.keywords].filter(
|
||||
(x) => x != null,
|
||||
),
|
||||
}) satisfies AutoCompleteOption,
|
||||
),
|
||||
versionCandidatePerPackage: new Map(
|
||||
Array.from(packages.entries()).map(([id, pkg]) => {
|
||||
// we generate few candidates for version per package
|
||||
// - '*' for any version
|
||||
// - '>=latestStable' and '>=latestPrerelease'
|
||||
// - '^latestStable' and '^latestPrerelease'
|
||||
// - '1.x' '1.2.x' (or something like this) for stable release
|
||||
|
||||
const latestStable = pkg.versions
|
||||
.filter((x) => x.pre === "")
|
||||
.sort(compareVersion)
|
||||
.at(-1);
|
||||
const latestPrerelease = pkg.versions.sort(compareVersion).at(-1);
|
||||
|
||||
const candidates: AutoCompleteOption[] = [];
|
||||
|
||||
function addCandidate(value: string, description: React.ReactNode) {
|
||||
candidates.push({
|
||||
value,
|
||||
label: (
|
||||
<AutocompleteVersionLabel
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
addCandidate("*", tc("templates:dialog:any version"));
|
||||
|
||||
if (latestStable != null) {
|
||||
addCandidate(
|
||||
`${latestStable.major}.x`,
|
||||
`${latestStable.major}.0.0 ≤ v < ${latestStable.major + 1}.0.0`,
|
||||
);
|
||||
addCandidate(
|
||||
`${latestStable.major}.${latestStable.minor}.x`,
|
||||
`${latestStable.major}.${latestStable.minor}.0 ≤ v < ${latestStable.major}.${latestStable.minor + 1}.0`,
|
||||
);
|
||||
addCandidate(
|
||||
`${latestStable.major}.${latestStable.minor}.${latestStable.patch}`,
|
||||
`v = ${latestStable.major}.${latestStable.minor}.${latestStable.patch}`,
|
||||
);
|
||||
addCandidate(
|
||||
`>=${latestStable.major}.${latestStable.minor}.${latestStable.patch}`,
|
||||
`v ≥ ${latestStable.major}.${latestStable.minor}.${latestStable.patch}`,
|
||||
);
|
||||
addCandidate(
|
||||
`^${latestStable.major}.${latestStable.minor}.${latestStable.patch}`,
|
||||
`${latestStable.major}.${latestStable.minor}.${latestStable.patch} ≤ v < ${hatEndVersion(latestStable)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (latestPrerelease != null && latestPrerelease !== latestStable) {
|
||||
addCandidate(
|
||||
`${latestPrerelease.major}.${latestPrerelease.minor}.${latestPrerelease.patch}-${latestPrerelease.pre}`,
|
||||
`v = ${latestPrerelease.major}.${latestPrerelease.minor}.${latestPrerelease.patch}-${latestPrerelease.pre}`,
|
||||
);
|
||||
addCandidate(
|
||||
`>=${latestPrerelease.major}.${latestPrerelease.minor}.${latestPrerelease.patch}-${latestPrerelease.pre}`,
|
||||
`v ≥ ${latestPrerelease.major}.${latestPrerelease.minor}.${latestPrerelease.patch}-${latestPrerelease.pre}`,
|
||||
);
|
||||
addCandidate(
|
||||
`^${latestPrerelease.major}.${latestPrerelease.minor}.${latestPrerelease.patch}-${latestPrerelease.pre}`,
|
||||
`${latestPrerelease.major}.${latestPrerelease.minor}.${latestPrerelease.patch}-${latestPrerelease.pre} ≤ v < ${hatEndVersion(latestPrerelease)}`,
|
||||
);
|
||||
}
|
||||
|
||||
function hatEndVersion(version: TauriVersion): string {
|
||||
return version.major === 0 && version.minor === 0
|
||||
? `${version.major}.${version.minor}.${version.patch + 1}`
|
||||
: version.major === 0
|
||||
? `${version.major}.${version.minor + 1}.0`
|
||||
: `${version.major + 1}.0.0`;
|
||||
}
|
||||
|
||||
return [id, candidates];
|
||||
}),
|
||||
),
|
||||
};
|
||||
}, [allPackages.data]);
|
||||
|
||||
const unityCandidates = useMemo(() => {
|
||||
const templateInfo = templates.find((x) => x.id === baseTemplate);
|
||||
if (templateInfo == null) return [];
|
||||
// unityVersions is in order
|
||||
// currently, ignore the unity version channel part and increment part
|
||||
const unityVersions = templateInfo.unity_versions.map(
|
||||
(x) => x.split(/[^\d.]/, 2)[0],
|
||||
);
|
||||
const candidates: AutoCompleteOption[] = [];
|
||||
|
||||
function addCandidate(value: string, description: React.ReactNode) {
|
||||
candidates.push({
|
||||
value,
|
||||
label: (
|
||||
<AutocompleteVersionLabel value={value} description={description} />
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
candidates.push(...unityVersions);
|
||||
|
||||
addCandidate("*", tc("templates:dialog:any version"));
|
||||
|
||||
// create something like 2022.x and 2022.3.x
|
||||
const addedRange = new Set<string>();
|
||||
for (const unityVersion of unityVersions) {
|
||||
const majorOnly = unityVersion.match(/^\d+/)?.[0];
|
||||
const minor = unityVersion.match(/^\d+\.\d+/)?.[0];
|
||||
if (majorOnly && !addedRange.has(majorOnly)) {
|
||||
addedRange.add(majorOnly);
|
||||
addCandidate(
|
||||
`${majorOnly}.x`,
|
||||
tc("templates:dialog:any unity specified version", {
|
||||
version: majorOnly,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (minor && !addedRange.has(minor)) {
|
||||
addedRange.add(minor);
|
||||
addCandidate(
|
||||
`${minor}.x`,
|
||||
tc("templates:dialog:any unity specified version", {
|
||||
version: minor,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}, [templates, baseTemplate]);
|
||||
|
||||
type Package = { name: string; range: string };
|
||||
const packagesListContext = useReorderableList<Package>({
|
||||
defaultValue: { name: "", range: "" },
|
||||
|
|
@ -462,6 +741,11 @@ function TemplateEditor({
|
|||
reorderable: false,
|
||||
});
|
||||
|
||||
const addedPackageNames = useMemo(
|
||||
() => new Set(packagesListContext.value.map((p) => p.name)),
|
||||
[packagesListContext.value],
|
||||
);
|
||||
|
||||
const unityPackagesListContext = useReorderableList<string>({
|
||||
defaultValue: "",
|
||||
defaultArray: template?.unity_packages ?? [],
|
||||
|
|
@ -472,7 +756,7 @@ function TemplateEditor({
|
|||
|
||||
const addUnityPackages = async () => {
|
||||
try {
|
||||
const packages = await commands.environmentPickUnityPackage();
|
||||
const packages = await commands.environmentPickUnityPackages();
|
||||
for (const pkg of packages) {
|
||||
unityPackagesListContext.add(pkg);
|
||||
}
|
||||
|
|
@ -482,6 +766,31 @@ function TemplateEditor({
|
|||
}
|
||||
};
|
||||
|
||||
const pickUnityPackage = async (
|
||||
currentValue: string,
|
||||
currentId: ReorderableListId,
|
||||
) => {
|
||||
try {
|
||||
const result = await commands.environmentPickUnityPackage(currentValue);
|
||||
switch (result.type) {
|
||||
case "NoFolderSelected":
|
||||
// no-op
|
||||
break;
|
||||
case "InvalidSelection":
|
||||
toastError(tt("general:toast:invalid file"));
|
||||
break;
|
||||
case "Successful":
|
||||
unityPackagesListContext.update(currentId, result.new_path);
|
||||
break;
|
||||
default:
|
||||
assertNever(result);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toastThrownError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const saveTemplate = async () => {
|
||||
try {
|
||||
|
|
@ -507,10 +816,10 @@ function TemplateEditor({
|
|||
|
||||
const validVersion = (p: Package) =>
|
||||
(p.name === "" && p.range === "") || // the empty (non-set) row
|
||||
(p.name !== "" && p.range.match(rangeRegex)); // ready to create
|
||||
(p.name !== "" && p.range.match(packageRangeRegex)); // ready to create
|
||||
const readyToCreate =
|
||||
packagesListContext.value.every(validVersion) &&
|
||||
unityRange.match(rangeRegex) &&
|
||||
unityRange.match(unityRangeRegex) &&
|
||||
name.length !== 0;
|
||||
|
||||
return (
|
||||
|
|
@ -520,182 +829,170 @@ function TemplateEditor({
|
|||
? tc("templates:dialog:edit template")
|
||||
: tc("templates:dialog:create template")}
|
||||
</DialogTitle>
|
||||
<DialogDescription asChild>
|
||||
<div className={"flex flex-col gap-4 shrink min-h-0"}>
|
||||
<section>
|
||||
<h3 className={"font-bold w-full text-center content-center"}>
|
||||
{tc("templates:dialog:general information")}
|
||||
</h3>
|
||||
<table
|
||||
className={"grid grid-cols-[min-content_1fr] gap-x-4 gap-y-1"}
|
||||
>
|
||||
<tbody className={"contents"}>
|
||||
<tr className={"contents"}>
|
||||
<th className={"content-center text-start whitespace-nowrap"}>
|
||||
{tc("general:name")}:
|
||||
</th>
|
||||
<td className={"flex"}>
|
||||
<Input
|
||||
className={cn(
|
||||
"grow",
|
||||
name.length === 0 &&
|
||||
"border-destructive ring-destructive text-destructive",
|
||||
)}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={"Your New Template"}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={"contents"}>
|
||||
<th className={"content-center text-start whitespace-nowrap"}>
|
||||
{tc("templates:dialog:base template")}:
|
||||
</th>
|
||||
<td className={"flex"}>
|
||||
<Select
|
||||
value={baseTemplate}
|
||||
onValueChange={setBaseTemplate}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue className={"grow"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates.map((template) => {
|
||||
const id = projectTemplateDisplayId(template.id);
|
||||
if (id == null) return null;
|
||||
return (
|
||||
<SelectItem key={id} value={id}>
|
||||
{projectTemplateName(template)}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={"contents"}>
|
||||
<th className={"content-center text-start whitespace-nowrap"}>
|
||||
{tc("templates:dialog:unity version")}:
|
||||
</th>
|
||||
<td className={"flex"}>
|
||||
<Input
|
||||
className={cn(
|
||||
"grow",
|
||||
unityRange.match(rangeRegex) ||
|
||||
"border-destructive ring-destructive text-destructive",
|
||||
)}
|
||||
value={unityRange}
|
||||
onChange={(e) => setUnityRange(e.target.value)}
|
||||
placeholder={">=2022 * =2022.3.22"}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section className={"shrink overflow-hidden flex flex-col"}>
|
||||
<h3 className={"font-bold w-full text-center content-center"}>
|
||||
{tc("general:packages")}
|
||||
</h3>
|
||||
<div className={"w-full max-h-[30vh] overflow-y-auto shrink"}>
|
||||
<table className={"w-full"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={"sticky top-0 z-10 bg-background"}>
|
||||
{tc("general:name")}
|
||||
</th>
|
||||
<th className={"sticky top-0 z-10 bg-background"}>
|
||||
{tc("general:version")}
|
||||
</th>
|
||||
<th className={"sticky top-0 z-10 bg-background"} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ReorderableList
|
||||
context={packagesListContext}
|
||||
renderItem={(value, id) => (
|
||||
<>
|
||||
<td>
|
||||
<div className={"flex"}>
|
||||
<Input
|
||||
type={"text"}
|
||||
value={value.name}
|
||||
className={"grow"}
|
||||
onChange={(e) =>
|
||||
packagesListContext.update(id, (old) => ({
|
||||
...old,
|
||||
name: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className={"flex"}>
|
||||
<Input
|
||||
type={"text"}
|
||||
value={value.range}
|
||||
className={cn(
|
||||
"grow",
|
||||
validVersion(value) ||
|
||||
"border-destructive ring-destructive text-destructive",
|
||||
)}
|
||||
onChange={(e) =>
|
||||
packagesListContext.update(id, (old) => ({
|
||||
...old,
|
||||
range: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</>
|
||||
<div className={"flex flex-col gap-4 shrink min-h-0"}>
|
||||
<section>
|
||||
<h3 className={"font-bold w-full text-center content-center"}>
|
||||
{tc("templates:dialog:general information")}
|
||||
</h3>
|
||||
<table className={"grid grid-cols-[min-content_1fr] gap-x-4 gap-y-1"}>
|
||||
<tbody className={"contents"}>
|
||||
<tr className={"contents"}>
|
||||
<th className={"content-center text-start whitespace-nowrap"}>
|
||||
{tc("general:name")}:
|
||||
</th>
|
||||
<td className={"flex"}>
|
||||
<Input
|
||||
className={cn(
|
||||
"grow",
|
||||
name.length === 0 &&
|
||||
"border-destructive ring-destructive text-destructive",
|
||||
)}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={"Your New Template"}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section className={"shrink overflow-hidden flex flex-col"}>
|
||||
<Overlay>
|
||||
<h3 className={"font-bold w-full text-center content-center"}>
|
||||
{tc("templates:dialog:unitypackages")}
|
||||
</h3>
|
||||
<div className={"text-right mb-2"}>
|
||||
<Button onClick={addUnityPackages}>
|
||||
{tc("general:button:add")}
|
||||
</Button>
|
||||
</div>
|
||||
</Overlay>
|
||||
<div className={"w-full max-h-[30vh] overflow-y-auto shrink"}>
|
||||
<table className={"w-full"}>
|
||||
<tbody>
|
||||
<ReorderableList
|
||||
context={unityPackagesListContext}
|
||||
ifEmpty={() => (
|
||||
<td className={"text-center"}>
|
||||
{tc("templates:dialog:no unitypackages")}
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={"contents"}>
|
||||
<th className={"content-center text-start whitespace-nowrap"}>
|
||||
{tc("templates:dialog:base template")}:
|
||||
</th>
|
||||
<td className={"flex"}>
|
||||
<TemplateSelect
|
||||
value={baseTemplate}
|
||||
onValueChange={setBaseTemplate}
|
||||
templates={templates}
|
||||
favoriteTemplates={favoriteTemplates}
|
||||
className={"grow"}
|
||||
excludeNoIdTemplates
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={"contents"}>
|
||||
<th className={"content-center text-start whitespace-nowrap"}>
|
||||
{tc("templates:dialog:unity version")}:
|
||||
</th>
|
||||
<td className={"flex"}>
|
||||
<Autocomplete
|
||||
className={cn(
|
||||
"grow",
|
||||
unityRange.match(unityRangeRegex) ||
|
||||
"border-destructive ring-destructive text-destructive",
|
||||
)}
|
||||
renderItem={(value) => (
|
||||
value={unityRange}
|
||||
onChange={(value) => setUnityRange(value)}
|
||||
options={unityCandidates}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section className={"shrink overflow-hidden flex flex-col"}>
|
||||
<h3 className={"font-bold w-full text-center content-center"}>
|
||||
{tc("general:packages")}
|
||||
</h3>
|
||||
<div className={"w-full max-h-[30vh] overflow-y-auto shrink"}>
|
||||
<table className={"w-full"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={"sticky top-0 z-10 bg-background"}>
|
||||
{tc("general:name")}
|
||||
</th>
|
||||
<th className={"sticky top-0 z-10 bg-background"}>
|
||||
{tc("general:version")}
|
||||
</th>
|
||||
<th className={"sticky top-0 z-10 bg-background"} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ReorderableList
|
||||
context={packagesListContext}
|
||||
renderItem={(value, id) => (
|
||||
<>
|
||||
<td>
|
||||
<div className={"flex"}>
|
||||
<Input
|
||||
type={"text"}
|
||||
value={value}
|
||||
<Autocomplete
|
||||
value={value.name}
|
||||
className={"grow"}
|
||||
disabled
|
||||
options={packageCandidates.filter(
|
||||
(c) =>
|
||||
c.value === value.name ||
|
||||
!addedPackageNames.has(c.value),
|
||||
)}
|
||||
onChange={(value) =>
|
||||
packagesListContext.update(id, (old) => ({
|
||||
...old,
|
||||
name: value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<td>
|
||||
<div className={"flex"}>
|
||||
<Autocomplete
|
||||
value={value.range}
|
||||
className={cn(
|
||||
"grow",
|
||||
validVersion(value) ||
|
||||
"border-destructive ring-destructive text-destructive",
|
||||
)}
|
||||
options={
|
||||
versionCandidatePerPackage.get(value.name) ?? []
|
||||
}
|
||||
onChange={(value) =>
|
||||
packagesListContext.update(id, (old) => ({
|
||||
...old,
|
||||
range: value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section className={"shrink overflow-hidden flex flex-col"}>
|
||||
<Overlay>
|
||||
<h3 className={"font-bold w-full text-center content-center"}>
|
||||
{tc("templates:dialog:unitypackages")}
|
||||
</h3>
|
||||
<div className={"text-right mb-2"}>
|
||||
<Button onClick={addUnityPackages}>
|
||||
{tc("general:button:add")}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</Overlay>
|
||||
<div className={"w-full max-h-[30vh] overflow-y-auto shrink"}>
|
||||
<table className={"w-full"}>
|
||||
<tbody>
|
||||
<ReorderableList
|
||||
context={unityPackagesListContext}
|
||||
ifEmpty={() => (
|
||||
<td className={"text-center"}>
|
||||
{tc("templates:dialog:no unitypackages")}
|
||||
</td>
|
||||
)}
|
||||
renderItem={(value, id) => (
|
||||
<td>
|
||||
<FilePathRow
|
||||
path={value}
|
||||
pick={() => pickUnityPackage(value, id).finally()}
|
||||
withOpen={false}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<DialogFooter className={"mt-2"}>
|
||||
<Button onClick={() => dialog.close(false)}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -711,3 +1008,34 @@ function TemplateEditor({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AutocompletePackageLabel({
|
||||
displayName,
|
||||
id,
|
||||
}: {
|
||||
displayName: string | null;
|
||||
id: string;
|
||||
}) {
|
||||
if (displayName == null) return id;
|
||||
return (
|
||||
<div className={"flex flex-col"}>
|
||||
<div>{displayName}</div>
|
||||
<div className={"text-xs text-muted-foreground"}>{id}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AutocompleteVersionLabel({
|
||||
value,
|
||||
description,
|
||||
}: {
|
||||
value: string;
|
||||
description: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className={"flex flex-row justify-between w-full"}>
|
||||
<div>{value}</div>
|
||||
<div className={"text-xs text-muted-foreground"}>{description}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CircleX } from "lucide-react";
|
||||
import { Suspense, useId } from "react";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
|
|
@ -23,15 +31,6 @@ import { tc } from "@/lib/i18n";
|
|||
import { usePrevPathName } from "@/lib/prev-page";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { toVersionString } from "@/lib/version";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CircleX } from "lucide-react";
|
||||
import { Suspense, useId } from "react";
|
||||
import { HeadingPageName } from "../-tab-selector";
|
||||
|
||||
export const Route = createFileRoute("/_main/packages/user-packages/")({
|
||||
|
|
@ -87,10 +86,13 @@ function PageBody() {
|
|||
return (
|
||||
<VStack>
|
||||
<HNavBar
|
||||
className={"shrink-0"}
|
||||
className="shrink-0"
|
||||
leading={<HeadingPageName pageType={"/packages/user-packages"} />}
|
||||
trailing={
|
||||
<Button onClick={() => addUserPackageWithPicker.mutate()}>
|
||||
<Button
|
||||
className={"compact:h-10"}
|
||||
onClick={() => addUserPackageWithPicker.mutate()}
|
||||
>
|
||||
{tc("user packages:button:add package")}
|
||||
</Button>
|
||||
}
|
||||
|
|
@ -154,7 +156,7 @@ function RepositoryTableBody({
|
|||
// biome-ignore lint/suspicious/noArrayIndexKey: static array
|
||||
key={index}
|
||||
className={
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground p-2.5"
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground px-2.5 py-1.5"
|
||||
}
|
||||
>
|
||||
<small className="font-normal leading-none">{tc(head)}</small>
|
||||
|
|
@ -182,7 +184,7 @@ function PackageRow({
|
|||
pkg: TauriUserPackage;
|
||||
remove: () => void;
|
||||
}) {
|
||||
const cellClass = "p-2.5";
|
||||
const cellClass = "p-2.5 compact:py-1";
|
||||
const id = useId();
|
||||
|
||||
const pkgDisplayNames = pkg.package.display_name ?? pkg.package.name;
|
||||
|
|
@ -217,14 +219,14 @@ function PackageRow({
|
|||
<DialogTitle>
|
||||
{tc("user packages:dialog:remove package")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p className={"whitespace-normal font-normal"}>
|
||||
{tc("user packages:dialog:confirm remove description", {
|
||||
name: pkgDisplayNames,
|
||||
path: pkg.path,
|
||||
})}
|
||||
</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button>{tc("general:button:cancel")}</Button>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,19 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useEffect, useId, useMemo, useState } from "react";
|
||||
import { VStack } from "@/components/layout";
|
||||
import { TemplateSelect } from "@/components/TemplateSelect";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
import type { TauriProjectTemplateInfo } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
|
|
@ -32,20 +25,8 @@ import {
|
|||
ProjectNameCheckResult,
|
||||
useProjectNameCheck,
|
||||
} from "@/lib/project-name-check";
|
||||
import {
|
||||
type ProjectTemplateCategory,
|
||||
projectTemplateCategory,
|
||||
projectTemplateName,
|
||||
} from "@/lib/project-template";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useId } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export async function createProject() {
|
||||
const information = await commands.environmentProjectCreationInformation();
|
||||
|
|
@ -53,6 +34,8 @@ export async function createProject() {
|
|||
using dialog = showDialog();
|
||||
const result = await dialog.ask(EnteringInformation, {
|
||||
templates: information.templates,
|
||||
favoriteTemplates: information.favorite_templates,
|
||||
lastUsedTemplate: information.last_used_template,
|
||||
projectLocation: information.default_path,
|
||||
recentProjectLocations: information.recent_project_locations,
|
||||
});
|
||||
|
|
@ -70,7 +53,6 @@ export async function createProject() {
|
|||
);
|
||||
dialog.close();
|
||||
toastSuccess(tt("projects:toast:project created"));
|
||||
close?.();
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["environmentProjects"],
|
||||
});
|
||||
|
|
@ -93,7 +75,7 @@ function DialogBase({
|
|||
return (
|
||||
<>
|
||||
<DialogTitle>{tc("projects:create new project")}</DialogTitle>
|
||||
<DialogDescription>{children}</DialogDescription>
|
||||
<div>{children}</div>
|
||||
<DialogFooter className={"gap-2"}>
|
||||
<Button onClick={close} disabled={!close}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -117,23 +99,39 @@ function EnteringInformation({
|
|||
templates,
|
||||
projectLocation: projectLocationFirst,
|
||||
recentProjectLocations: recentProjectLocationsReversed,
|
||||
favoriteTemplates,
|
||||
lastUsedTemplate,
|
||||
dialog,
|
||||
}: {
|
||||
templates: TauriProjectTemplateInfo[];
|
||||
projectLocation: string;
|
||||
favoriteTemplates: string[];
|
||||
lastUsedTemplate: string | null;
|
||||
recentProjectLocations: string[];
|
||||
dialog: DialogContext<null | ProjectCreationInformation>;
|
||||
}) {
|
||||
const [unityVersion, setUnityVersion] = useState<string>(
|
||||
templates[0].unity_versions[0],
|
||||
);
|
||||
const [templateId, setTemplateId] = useState<string>(templates[0].id);
|
||||
|
||||
const templateById = useMemo(
|
||||
() => new Map(templates.map((t) => [t.id, t])),
|
||||
[templates],
|
||||
);
|
||||
|
||||
const [templateId, setTemplateId] = useState<string>(() => {
|
||||
const template = lastUsedTemplate
|
||||
? templateById.get(lastUsedTemplate)
|
||||
: undefined;
|
||||
return template?.available &&
|
||||
template.unity_versions.length !== 0 &&
|
||||
lastUsedTemplate != null
|
||||
? lastUsedTemplate
|
||||
: templates[0].id;
|
||||
});
|
||||
|
||||
const [unityVersion, setUnityVersion] = useState<string>(
|
||||
() =>
|
||||
templateById.get(templateId)?.unity_versions?.[0] ??
|
||||
templates[0].unity_versions[0],
|
||||
);
|
||||
|
||||
const [projectNameRaw, setProjectName] = useState("New Project");
|
||||
const projectName = projectNameRaw.trim();
|
||||
const [projectLocation, setProjectLocation] = useState(projectLocationFirst);
|
||||
|
|
@ -180,28 +178,6 @@ function EnteringInformation({
|
|||
const templateInputId = useId();
|
||||
const unityInputId = useId();
|
||||
|
||||
const templatesByCategory = useMemo(() => {
|
||||
const byCategory: {
|
||||
[k in ProjectTemplateCategory]: TauriProjectTemplateInfo[];
|
||||
} = {
|
||||
builtin: [],
|
||||
alcom: [],
|
||||
vcc: [],
|
||||
};
|
||||
|
||||
for (const template of templates) {
|
||||
byCategory[projectTemplateCategory(template.id)].push(template);
|
||||
}
|
||||
|
||||
return (
|
||||
[
|
||||
["builtin", byCategory.builtin],
|
||||
["alcom", byCategory.alcom],
|
||||
["vcc", byCategory.vcc],
|
||||
] satisfies [ProjectTemplateCategory, TauriProjectTemplateInfo[]][]
|
||||
).filter((x) => x[1].length > 0);
|
||||
}, [templates]);
|
||||
|
||||
const unityVersions = templateById.get(templateId)?.unity_versions ?? [];
|
||||
|
||||
const badProjectName = ["AlreadyExists", "InvalidNameForFolderName"].includes(
|
||||
|
|
@ -231,59 +207,13 @@ function EnteringInformation({
|
|||
<div className={"flex items-center whitespace-nowrap"}>
|
||||
<label htmlFor={templateInputId}>{tc("projects:template")}</label>
|
||||
</div>
|
||||
<Select
|
||||
<TemplateSelect
|
||||
value={templateId}
|
||||
onValueChange={(value) => setTemplateId(value)}
|
||||
>
|
||||
<SelectTrigger id={templateInputId}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templatesByCategory.map(([category, templates], index) => (
|
||||
<SelectGroup key={category}>
|
||||
{index !== 0 && <SelectSeparator />}
|
||||
<SelectLabel>
|
||||
{tc(`projects:template-category:${category}`)}
|
||||
</SelectLabel>
|
||||
{templates.map((template) => {
|
||||
const disabled =
|
||||
!template.available ||
|
||||
template.unity_versions.length === 0;
|
||||
const contents = (
|
||||
<SelectItem
|
||||
value={template.id}
|
||||
disabled={disabled}
|
||||
key={template.id}
|
||||
>
|
||||
{projectTemplateName(template)}
|
||||
</SelectItem>
|
||||
);
|
||||
if (!template.available) {
|
||||
return (
|
||||
<Tooltip key={template.id}>
|
||||
<TooltipTrigger>{contents}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{tc("projects:tooltip:template-unavailable")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
} else if (template.unity_versions.length === 0) {
|
||||
return (
|
||||
<Tooltip key={template.id}>
|
||||
<TooltipTrigger>{contents}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{tc("projects:tooltip:template-no-unity")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return contents;
|
||||
}
|
||||
})}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
onValueChange={setTemplateId}
|
||||
templates={templates}
|
||||
favoriteTemplates={favoriteTemplates}
|
||||
selectTriggerId={templateInputId}
|
||||
/>
|
||||
</div>
|
||||
<div className={"flex items-center gap-1 whitespace-nowrap"}>
|
||||
<label htmlFor={unityInputId}>
|
||||
|
|
@ -386,8 +316,10 @@ function UnityVersion({
|
|||
function CreatingProject() {
|
||||
return (
|
||||
<DialogBase>
|
||||
<RefreshCw className={"w-5 h-5 animate-spin"} />
|
||||
<p>{tc("projects:creating project...")}</p>
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<RefreshCw className={"w-5 h-5 animate-spin"} />
|
||||
<p>{tc("projects:creating project...")}</p>
|
||||
</div>
|
||||
</DialogBase>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { CircleHelp, CircleUserRound, Ellipsis, Globe } from "lucide-react";
|
||||
import {
|
||||
ButtonDisabledIfInvalid,
|
||||
FavoriteToggleButton,
|
||||
getProjectDisplayInfo,
|
||||
ManageOrMigrateButton,
|
||||
ProjectContext,
|
||||
TooltipTriggerIfInvalid,
|
||||
TooltipTriggerIfValid,
|
||||
getProjectDisplayInfo,
|
||||
useSetProjectFavoriteMutation,
|
||||
} from "@/app/_main/projects/-project-row";
|
||||
import { copyProject } from "@/app/_main/projects/manage/-copy-project";
|
||||
import { BackupProjectDialog } from "@/components/BackupProjectDialog";
|
||||
import { FavoriteStarToggleButton } from "@/components/FavoriteStarButton";
|
||||
import { OpenUnityButton } from "@/components/OpenUnityButton";
|
||||
import { RemoveProjectDialog } from "@/components/RemoveProjectDialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -28,17 +29,14 @@ import {
|
|||
} from "@/components/ui/tooltip";
|
||||
import type { TauriProject } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { dateToString, formatDateOffset } from "@/lib/dateToString";
|
||||
import {
|
||||
dateToString,
|
||||
dayToString,
|
||||
formatDateOffset,
|
||||
} from "@/lib/dateToString";
|
||||
import { openSingleDialog } from "@/lib/dialog";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { toastThrownError } from "@/lib/toast";
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
import { CircleHelp, CircleUserRound, Ellipsis, Globe } from "lucide-react";
|
||||
|
||||
const environmentProjects = queryOptions({
|
||||
queryKey: ["environmentProjects"],
|
||||
queryFn: commands.environmentProjects,
|
||||
});
|
||||
|
||||
export function ProjectGridItem({
|
||||
project,
|
||||
|
|
@ -51,7 +49,7 @@ export function ProjectGridItem({
|
|||
|
||||
const typeIconClass = "w-5 h-5";
|
||||
|
||||
const { projectTypeKind, displayType, isLegacy, lastModified } =
|
||||
const { projectTypeKind, displayType, isLegacy, createdAt, lastModified } =
|
||||
getProjectDisplayInfo(project);
|
||||
|
||||
const removed = !project.is_exists;
|
||||
|
|
@ -61,11 +59,11 @@ export function ProjectGridItem({
|
|||
<ProjectContext.Provider
|
||||
value={{ removed, is_valid, loading: Boolean(loading) }}
|
||||
>
|
||||
<Card className="relative p-4 bg-card flex flex-col gap-2 group">
|
||||
<Card className="relative p-4 bg-card flex flex-col gap-2 group compact:p-2 compact:pl-3 compact:gap-1">
|
||||
<div className={"absolute top-2 right-2 gap-2 flex"}>
|
||||
<div className="relative content-center">
|
||||
<FavoriteToggleButton
|
||||
project={project}
|
||||
<FavoriteStarToggleButton
|
||||
favorite={project.favorite}
|
||||
disabled={removed || loading}
|
||||
onToggle={() =>
|
||||
setProjectFavorite.mutate({
|
||||
|
|
@ -128,14 +126,7 @@ export function ProjectGridItem({
|
|||
<p className="font-normal whitespace-pre overflow-ellipsis overflow-hidden">
|
||||
{project.name}
|
||||
</p>
|
||||
</TooltipTriggerIfValid>
|
||||
<TooltipContent>{project.name}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTriggerIfValid
|
||||
className={"text-left select-text cursor-auto w-full"}
|
||||
>
|
||||
<p className="font-normal opacity-50 text-sm whitespace-pre overflow-ellipsis overflow-hidden">
|
||||
<p className="font-normal opacity-50 text-sm whitespace-pre overflow-ellipsis overflow-hidden compact:hidden">
|
||||
{project.path}
|
||||
</p>
|
||||
</TooltipTriggerIfValid>
|
||||
|
|
@ -178,25 +169,47 @@ export function ProjectGridItem({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{tc("general:last modified")}:{" "}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<time dateTime={lastModified.toISOString()}>
|
||||
<time className="font-normal">
|
||||
{formatDateOffset(project.last_modified)}
|
||||
<div className="flex flex-row gap-1">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{tc("general:created at")}:{" "}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<time dateTime={createdAt.toISOString()}>
|
||||
<time className="font-normal">
|
||||
{dayToString(project.created_at)}
|
||||
</time>
|
||||
</time>
|
||||
</time>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{dateToString(project.last_modified)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{dateToString(project.created_at)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">/</p>
|
||||
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{tc("general:last modified")}:{" "}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<time dateTime={lastModified.toISOString()}>
|
||||
<time className="font-normal">
|
||||
{formatDateOffset(project.last_modified)}
|
||||
</time>
|
||||
</time>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{dateToString(project.last_modified)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex flex-wrap gap-2 justify-end">
|
||||
<div className="mt-2 flex flex-wrap gap-2 justify-end compact:gap-1">
|
||||
<ButtonDisabledIfInvalid asChild>
|
||||
<OpenUnityButton
|
||||
projectPath={project.path}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { CircleHelp, CircleUserRound, Ellipsis, Globe } from "lucide-react";
|
||||
import React, { type ComponentProps, useContext } from "react";
|
||||
import { copyProject } from "@/app/_main/projects/manage/-copy-project";
|
||||
import { MigrationCopyingDialog } from "@/app/_main/projects/manage/-unity-migration";
|
||||
import { BackupProjectDialog } from "@/components/BackupProjectDialog";
|
||||
import { FavoriteStarToggleButton } from "@/components/FavoriteStarButton";
|
||||
import { OpenUnityButton } from "@/components/OpenUnityButton";
|
||||
import { RemoveProjectDialog } from "@/components/RemoveProjectDialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -24,28 +29,17 @@ import {
|
|||
import { assertNever } from "@/lib/assert-never";
|
||||
import type { TauriProject, TauriProjectType } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { dateToString, formatDateOffset } from "@/lib/dateToString";
|
||||
import {
|
||||
dateToString,
|
||||
dayToString,
|
||||
formatDateOffset,
|
||||
} from "@/lib/dateToString";
|
||||
import { type DialogContext, openSingleDialog, showDialog } from "@/lib/dialog";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
import { router } from "@/lib/main";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { compareUnityVersionString } from "@/lib/version";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
CircleHelp,
|
||||
CircleUserRound,
|
||||
Ellipsis,
|
||||
Globe,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import React, { type ComponentProps, useContext } from "react";
|
||||
|
||||
export const ProjectDisplayType: Record<
|
||||
TauriProjectType,
|
||||
|
|
@ -84,11 +78,11 @@ export function ProjectRow({
|
|||
project: TauriProject;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const cellClass = "p-2.5";
|
||||
const cellClass = "p-2.5 compact:py-1";
|
||||
const noGrowCellClass = `${cellClass} w-1`;
|
||||
const typeIconClass = "w-5 h-5";
|
||||
|
||||
const { projectTypeKind, displayType, isLegacy, lastModified } =
|
||||
const { projectTypeKind, displayType, isLegacy, createdAt, lastModified } =
|
||||
getProjectDisplayInfo(project);
|
||||
|
||||
const openProjectFolder = () =>
|
||||
|
|
@ -115,10 +109,10 @@ export function ProjectRow({
|
|||
<tr
|
||||
className={`group even:bg-secondary/30 ${removed || loading || !(project.is_valid ?? true) ? "opacity-50" : ""}`}
|
||||
>
|
||||
<td className={`${cellClass} w-3`}>
|
||||
<td className={noGrowCellClass}>
|
||||
<div className={"relative flex"}>
|
||||
<FavoriteToggleButton
|
||||
project={project}
|
||||
<FavoriteStarToggleButton
|
||||
favorite={project.favorite}
|
||||
disabled={removed || loading}
|
||||
onToggle={() =>
|
||||
setProjectFavorite.mutate({
|
||||
|
|
@ -140,14 +134,7 @@ export function ProjectRow({
|
|||
className={"text-left select-text cursor-auto w-full"}
|
||||
>
|
||||
<p className="font-normal whitespace-pre">{project.name}</p>
|
||||
</TooltipTriggerIfValid>
|
||||
<TooltipContent>{project.name}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTriggerIfValid
|
||||
className={"text-left select-text cursor-auto w-full"}
|
||||
>
|
||||
<p className="font-normal opacity-50 text-sm whitespace-pre">
|
||||
<p className="font-normal opacity-50 text-sm whitespace-pre compact:hidden">
|
||||
{project.path}
|
||||
</p>
|
||||
</TooltipTriggerIfValid>
|
||||
|
|
@ -164,7 +151,7 @@ export function ProjectRow({
|
|||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td className={`${cellClass} w-[8em] min-w-[8em]`}>
|
||||
<td className={noGrowCellClass}>
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="flex items-center">
|
||||
{projectTypeKind === "avatars" ? (
|
||||
|
|
@ -188,6 +175,22 @@ export function ProjectRow({
|
|||
<td className={noGrowCellClass}>
|
||||
<p className="font-normal">{project.unity}</p>
|
||||
</td>
|
||||
<td className={noGrowCellClass}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<time dateTime={createdAt.toISOString()}>
|
||||
<time className="font-normal">
|
||||
{dayToString(project.created_at)}
|
||||
</time>
|
||||
</time>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{dateToString(project.created_at)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td className={noGrowCellClass}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
|
|
@ -205,7 +208,7 @@ export function ProjectRow({
|
|||
</Tooltip>
|
||||
</td>
|
||||
<td className={noGrowCellClass}>
|
||||
<div className="flex flex-row gap-2 max-w-min">
|
||||
<div className="flex flex-row gap-2 max-w-min items-center">
|
||||
<ButtonDisabledIfInvalid asChild>
|
||||
<OpenUnityButton
|
||||
projectPath={project.path}
|
||||
|
|
@ -272,11 +275,9 @@ export function ProjectRow({
|
|||
);
|
||||
}
|
||||
|
||||
export function ManageOrMigrateButton({
|
||||
project,
|
||||
}: {
|
||||
project: TauriProject;
|
||||
}) {
|
||||
export function ManageOrMigrateButton({ project }: { project: TauriProject }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (compareUnityVersionString(project.unity, "2018.0.0f0") < 0) {
|
||||
// No UPM is supported in unity 2017 or older
|
||||
return (
|
||||
|
|
@ -293,7 +294,6 @@ export function ManageOrMigrateButton({
|
|||
);
|
||||
}
|
||||
|
||||
const navigate = useNavigate();
|
||||
switch (project.project_type) {
|
||||
case "LegacySdk2":
|
||||
return (
|
||||
|
|
@ -415,9 +415,9 @@ function ConfirmVpmMigrationDialog({
|
|||
return (
|
||||
<div className={"contents whitespace-normal"}>
|
||||
<DialogTitle>{tc("projects:dialog:vpm migrate header")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("projects:dialog:vpm migrate description")}</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter className={"gap-1"}>
|
||||
<Button onClick={() => dialog.close(null)}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -440,9 +440,9 @@ function VpmMigrationUpdating() {
|
|||
return (
|
||||
<div className={"contents whitespace-normal"}>
|
||||
<DialogTitle>{tc("projects:dialog:vpm migrate header")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("projects:migrating...")}</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -517,49 +517,18 @@ export const TooltipTriggerIfValid = ({
|
|||
}
|
||||
};
|
||||
|
||||
export function FavoriteToggleButton({
|
||||
project,
|
||||
disabled,
|
||||
onToggle,
|
||||
className,
|
||||
}: {
|
||||
project: { favorite: boolean };
|
||||
disabled?: boolean;
|
||||
onToggle: () => void;
|
||||
className?: string;
|
||||
}) {
|
||||
if (disabled) return null;
|
||||
|
||||
return (
|
||||
<Star
|
||||
strokeWidth={project.favorite ? 1.5 : 3}
|
||||
className={cn(
|
||||
"size-4 transition-colors cursor-pointer",
|
||||
project.favorite ? "text-foreground" : "text-foreground/30",
|
||||
!project.favorite && "opacity-0 group-hover:opacity-100",
|
||||
"hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
fill={project.favorite ? "currentColor" : "none"}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
onToggle();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function getProjectDisplayInfo(project: TauriProject) {
|
||||
const projectTypeKind = ProjectDisplayType[project.project_type] ?? "unknown";
|
||||
const displayType = tc(`projects:type:${projectTypeKind}`);
|
||||
const isLegacy = LegacyProjectTypes.includes(project.project_type);
|
||||
const createdAt = new Date(project.created_at);
|
||||
const lastModified = new Date(project.last_modified);
|
||||
|
||||
return {
|
||||
projectTypeKind,
|
||||
displayType,
|
||||
isLegacy,
|
||||
createdAt,
|
||||
lastModified,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
|
@ -12,14 +15,11 @@ import {
|
|||
import type { TauriProject } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { ProjectGridItem } from "./-project-grid-item";
|
||||
import {
|
||||
isSorting,
|
||||
sortSearchProjects,
|
||||
type sortings,
|
||||
sortSearchProjects,
|
||||
useSetProjectSortingMutation,
|
||||
} from "./-projects-list-card";
|
||||
|
||||
|
|
@ -30,6 +30,7 @@ const sortingOptions: { key: SimpleSorting; label: string }[] = [
|
|||
{ key: "name", label: "general:name" },
|
||||
{ key: "type", label: "projects:type" },
|
||||
{ key: "unity", label: "projects:unity" },
|
||||
{ key: "createdAt", label: "general:created at" },
|
||||
{ key: "lastModified", label: "general:last modified" },
|
||||
];
|
||||
|
||||
|
|
@ -77,29 +78,27 @@ export function ProjectsGridCard({
|
|||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full overflow-hidden">
|
||||
<Card className="flex items-center mb-3 flex-wrap">
|
||||
<div className="flex items-center gap-1 m-2 ml-4">
|
||||
<p className="grow-0 whitespace-pre mb-0 leading-tight">
|
||||
{tc("projects:sort by")}
|
||||
</p>
|
||||
<Select
|
||||
value={currentKey}
|
||||
onValueChange={(value) =>
|
||||
handleChangeSortingKey(value as SimpleSorting)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sortingOptions.map((option) => (
|
||||
<SelectItem key={option.key} value={option.key}>
|
||||
{tc(option.label)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Card className="flex items-center mb-3 flex-wrap p-2 gap-2 compact:p-1 compact:gap-1">
|
||||
<p className="grow-0 whitespace-pre pl-2 leading-tight">
|
||||
{tc("projects:sort by")}
|
||||
</p>
|
||||
<Select
|
||||
value={currentKey}
|
||||
onValueChange={(value) =>
|
||||
handleChangeSortingKey(value as SimpleSorting)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sortingOptions.map((option) => (
|
||||
<SelectItem key={option.key} value={option.key}>
|
||||
{tc(option.label)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button variant="ghost" size="icon" onClick={toggleOrder}>
|
||||
{isReversed ? (
|
||||
|
|
@ -114,7 +113,10 @@ export function ProjectsGridCard({
|
|||
className="h-full w-full vrc-get-scrollable-card rounded-l-xl"
|
||||
scrollBarClassName="bg-background rounded-full border-l-0 p-[1.5px]"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-3 overflow-x-hidden mr-4">
|
||||
<div
|
||||
className="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-3 overflow-x-hidden mr-4
|
||||
compact:grid-cols-2 compact:lg:grid-cols-3 compact:2xl:grid-cols-4 compact:gap-1.5"
|
||||
>
|
||||
{projectsShown.map((project) => (
|
||||
<ProjectGridItem
|
||||
key={project.path}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
"use client";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ChevronDown, ChevronsUpDown, ChevronUp, Star } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
import type { TauriProject, TauriProjectType } from "@/lib/bindings";
|
||||
|
|
@ -6,12 +9,15 @@ import { commands } from "@/lib/bindings";
|
|||
import { tc } from "@/lib/i18n";
|
||||
import { toastThrownError } from "@/lib/toast";
|
||||
import { compareUnityVersionString } from "@/lib/version";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ChevronDown, ChevronUp, ChevronsUpDown, Star } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { ProjectRow } from "./-project-row";
|
||||
|
||||
export const sortings = ["lastModified", "name", "unity", "type"] as const;
|
||||
export const sortings = [
|
||||
"createdAt",
|
||||
"lastModified",
|
||||
"name",
|
||||
"unity",
|
||||
"type",
|
||||
] as const;
|
||||
|
||||
type SimpleSorting = (typeof sortings)[number];
|
||||
type Sorting = SimpleSorting | `${SimpleSorting}Reversed`;
|
||||
|
|
@ -158,6 +164,18 @@ export function ProjectsTableCard({
|
|||
</small>
|
||||
</button>
|
||||
</th>
|
||||
<th className={`${thClass} ${headerBg("createdAt")}`}>
|
||||
<button
|
||||
type="button"
|
||||
className={"flex w-full project-table-button"}
|
||||
onClick={() => setSorting("createdAt")}
|
||||
>
|
||||
{icon("createdAt")}
|
||||
<small className="font-normal leading-none">
|
||||
{tc("general:created at")}
|
||||
</small>
|
||||
</button>
|
||||
</th>
|
||||
<th className={`${thClass} ${headerBg("lastModified")}`}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -194,6 +212,12 @@ export function sortSearchProjects(
|
|||
searched.sort((a, b) => b.last_modified - a.last_modified);
|
||||
|
||||
switch (sorting) {
|
||||
case "createdAt":
|
||||
searched.sort((a, b) => b.created_at - a.created_at);
|
||||
break;
|
||||
case "createdAtReversed":
|
||||
searched.sort((a, b) => a.created_at - b.created_at);
|
||||
break;
|
||||
case "lastModified":
|
||||
searched.sort((a, b) => b.last_modified - a.last_modified);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import Loading from "@/app/-loading";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ChevronDown, LayoutGrid, LayoutList, RefreshCw } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { createProject } from "@/app/_main/projects/-create-project";
|
||||
import { ProjectsGridCard } from "@/app/_main/projects/-projects-grid-card";
|
||||
import Loading from "@/app/-loading";
|
||||
import { HNavBar, HNavBarText, VStack } from "@/components/layout";
|
||||
import { SearchBox } from "@/components/SearchBox";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
|
|
@ -24,15 +33,6 @@ import { isFindKey, useDocumentEvent } from "@/lib/events";
|
|||
import { useProjectUpdateInProgress } from "@/lib/global-events";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ChevronDown, LayoutGrid, LayoutList, RefreshCw } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { ProjectsTableCard } from "./-projects-list-card";
|
||||
|
||||
export const Route = createFileRoute("/_main/projects/")({
|
||||
|
|
@ -188,15 +188,14 @@ function ProjectViewHeader({
|
|||
|
||||
return (
|
||||
<HNavBar
|
||||
className={"shrink-0"}
|
||||
className="shrink-0"
|
||||
leading={
|
||||
<>
|
||||
<p className="cursor-pointer font-bold grow-0 whitespace-pre mb-0 leading-tight">
|
||||
{tc("projects")}
|
||||
</p>
|
||||
<HNavBarText>{tc("projects")}</HNavBarText>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={"compact:h-10 compact:w-10"}
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
onClick={() =>
|
||||
|
|
@ -215,13 +214,14 @@ function ProjectViewHeader({
|
|||
</Tooltip>
|
||||
|
||||
<SearchBox
|
||||
className={"w-max grow"}
|
||||
className={"w-max grow compact:h-10"}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
ref={searchRef}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className={"compact:h-10"}
|
||||
variant={"ghost"}
|
||||
onClick={() => {
|
||||
if (viewMode === "List") {
|
||||
|
|
@ -254,12 +254,15 @@ function ProjectViewHeader({
|
|||
<DropdownMenu>
|
||||
<div className={"flex divide-x"}>
|
||||
<Button
|
||||
className={"rounded-r-none pl-4 pr-3"}
|
||||
className={"rounded-r-none pl-4 pr-3 compact:h-10"}
|
||||
onClick={startCreateProject}
|
||||
>
|
||||
{tc("projects:create new project")}
|
||||
</Button>
|
||||
<DropdownMenuTrigger asChild className={"rounded-l-none pl-2 pr-2"}>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={"rounded-l-none pl-2 pr-2 compact:h-10"}
|
||||
>
|
||||
<Button>
|
||||
<ChevronDown className={"w-4 h-4"} />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -39,11 +39,12 @@ export interface PackageRowInfo {
|
|||
infoSource: TauriVersion;
|
||||
displayName: string;
|
||||
description: string;
|
||||
aliases: string[];
|
||||
keywords: string[];
|
||||
unityCompatible: Map<string, TauriPackage>;
|
||||
unityIncompatible: Map<string, TauriPackage>;
|
||||
sources: Set<string>;
|
||||
isThereSource: boolean; // this will be true even if all sources are hidden
|
||||
visibleSources: Set<string>;
|
||||
installed: null | {
|
||||
version: TauriVersion;
|
||||
yanked: boolean;
|
||||
|
|
@ -92,7 +93,9 @@ export function combinePackagesAndProjectDetails(
|
|||
const yankedVersions = new Set<`${string}:${string}`>();
|
||||
const knownPackages = new Set<string>();
|
||||
const packagesPerRepository = new Map<string, TauriPackage[]>();
|
||||
const hiddenPackagesPerRepository = new Map<string, TauriPackage[]>();
|
||||
const userPackages: TauriPackage[] = [];
|
||||
const hiddenUserPackages: TauriPackage[] = [];
|
||||
|
||||
for (const pkg of packages) {
|
||||
if (!showPrereleasePackages && pkg.version.pre) continue;
|
||||
|
|
@ -107,13 +110,19 @@ export function combinePackagesAndProjectDetails(
|
|||
let packages: TauriPackage[];
|
||||
// check the repository is visible
|
||||
if (pkg.source === "LocalUser") {
|
||||
if (hideLocalUserPackages) continue;
|
||||
packages = userPackages;
|
||||
if (hideLocalUserPackages) {
|
||||
packages = hiddenUserPackages;
|
||||
} else {
|
||||
packages = userPackages;
|
||||
}
|
||||
} else if ("Remote" in pkg.source) {
|
||||
if (hiddenRepositoriesSet.has(pkg.source.Remote.id)) continue;
|
||||
|
||||
packages = packagesPerRepository.get(pkg.source.Remote.id) ?? [];
|
||||
packagesPerRepository.set(pkg.source.Remote.id, packages);
|
||||
if (hiddenRepositoriesSet.has(pkg.source.Remote.id)) {
|
||||
packages = hiddenPackagesPerRepository.get(pkg.source.Remote.id) ?? [];
|
||||
hiddenPackagesPerRepository.set(pkg.source.Remote.id, packages);
|
||||
} else {
|
||||
packages = packagesPerRepository.get(pkg.source.Remote.id) ?? [];
|
||||
packagesPerRepository.set(pkg.source.Remote.id, packages);
|
||||
}
|
||||
} else {
|
||||
assertNever(pkg.source);
|
||||
}
|
||||
|
|
@ -132,12 +141,13 @@ export function combinePackagesAndProjectDetails(
|
|||
id: pkg.name,
|
||||
displayName: pkg.display_name ?? pkg.name,
|
||||
description: pkg.description ?? "",
|
||||
aliases: pkg.aliases,
|
||||
keywords: pkg.keywords,
|
||||
infoSource: pkg.version,
|
||||
unityCompatible: new Map(),
|
||||
unityIncompatible: new Map(),
|
||||
sources: new Set(),
|
||||
isThereSource: false,
|
||||
visibleSources: new Set(),
|
||||
installed: null,
|
||||
latest: { status: "none" },
|
||||
stableLatest: { status: "none" },
|
||||
|
|
@ -167,7 +177,7 @@ export function combinePackagesAndProjectDetails(
|
|||
packageRowInfo.displayName = pkg.display_name ?? pkg.name;
|
||||
packageRowInfo.description =
|
||||
pkg.description || packageRowInfo.description;
|
||||
packageRowInfo.aliases = pkg.aliases;
|
||||
packageRowInfo.keywords = pkg.keywords;
|
||||
}
|
||||
|
||||
if (project == null || isUnityCompatible(pkg, project.unity)) {
|
||||
|
|
@ -178,8 +188,14 @@ export function combinePackagesAndProjectDetails(
|
|||
|
||||
if (pkg.source === "LocalUser") {
|
||||
packageRowInfo.sources.add("User");
|
||||
if (!hideLocalUserPackages) {
|
||||
packageRowInfo.visibleSources.add("User");
|
||||
}
|
||||
} else if ("Remote" in pkg.source) {
|
||||
packageRowInfo.sources.add(pkg.source.Remote.display_name);
|
||||
if (!hiddenRepositoriesSet.has(pkg.source.Remote.id)) {
|
||||
packageRowInfo.visibleSources.add(pkg.source.Remote.display_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,6 +203,13 @@ export function combinePackagesAndProjectDetails(
|
|||
packagesPerRepository.get("com.vrchat.repos.official")?.forEach(addPackage);
|
||||
packagesPerRepository.get("com.vrchat.repos.curated")?.forEach(addPackage);
|
||||
userPackages.forEach(addPackage);
|
||||
hiddenUserPackages.forEach((pkg) => {
|
||||
const packageRowInfo = getRowInfo(pkg);
|
||||
packageRowInfo.isThereSource = true;
|
||||
if (pkg.source === "LocalUser") {
|
||||
packageRowInfo.sources.add("User");
|
||||
}
|
||||
});
|
||||
packagesPerRepository.delete("com.vrchat.repos.official");
|
||||
packagesPerRepository.delete("com.vrchat.repos.curated");
|
||||
|
||||
|
|
@ -201,6 +224,17 @@ export function combinePackagesAndProjectDetails(
|
|||
packages.forEach(addPackage);
|
||||
}
|
||||
|
||||
// process hidden repositories - only add to sources, not to version calculations
|
||||
for (const packages of hiddenPackagesPerRepository.values()) {
|
||||
packages.forEach((pkg) => {
|
||||
const packageRowInfo = getRowInfo(pkg);
|
||||
packageRowInfo.isThereSource = true;
|
||||
if (pkg.source !== "LocalUser") {
|
||||
packageRowInfo.sources.add(pkg.source.Remote.display_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// sort versions
|
||||
for (const value of packagesTable.values()) {
|
||||
value.unityCompatible = new Map(
|
||||
|
|
@ -292,14 +326,14 @@ export function combinePackagesAndProjectDetails(
|
|||
|
||||
// if installed, use the installed version to get the display name
|
||||
packageRowInfo.displayName = pkg.display_name ?? pkg.name;
|
||||
packageRowInfo.aliases = [...pkg.aliases, ...packageRowInfo.aliases];
|
||||
packageRowInfo.keywords = [...pkg.keywords, ...packageRowInfo.keywords];
|
||||
packageRowInfo.installed = {
|
||||
version: pkg.version,
|
||||
yanked:
|
||||
pkg.is_yanked ||
|
||||
yankedVersions.has(`${pkg.name}:${toVersionString(pkg.version)}`),
|
||||
};
|
||||
packageRowInfo.isThereSource = knownPackages.has(pkg.name);
|
||||
packageRowInfo.isThereSource = true;
|
||||
|
||||
// if we have the latest version, check if it's upgradable
|
||||
if (packageRowInfo.latest.status !== "none") {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { NavigateFn } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { VStack } from "@/components/layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
import { type TauriCopyProjectProgress, commands } from "@/lib/bindings";
|
||||
import { commands, type TauriCopyProjectProgress } from "@/lib/bindings";
|
||||
import { callAsyncCommand } from "@/lib/call-async-command";
|
||||
import { type DialogContext, showDialog } from "@/lib/dialog";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
|
|
@ -19,11 +18,6 @@ import {
|
|||
} from "@/lib/project-name-check";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { NavigateFn } from "@tanstack/react-router";
|
||||
import type React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export async function copyProject(existingPath: string, navigate?: NavigateFn) {
|
||||
using dialog = showDialog();
|
||||
|
|
@ -58,31 +52,6 @@ export async function copyProject(existingPath: string, navigate?: NavigateFn) {
|
|||
});
|
||||
}
|
||||
|
||||
function DialogBase({
|
||||
children,
|
||||
close,
|
||||
createProject,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
close?: () => void;
|
||||
createProject?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<DialogTitle>{tc("projects:dialog:copy project")}</DialogTitle>
|
||||
<DialogDescription>{children}</DialogDescription>
|
||||
<DialogFooter className={"gap-2"}>
|
||||
<Button onClick={close} disabled={!close}>
|
||||
{tc("general:button:cancel")}
|
||||
</Button>
|
||||
<Button onClick={createProject} disabled={!createProject}>
|
||||
{tc("projects:button:create")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyProjectNameDialog({
|
||||
dialog,
|
||||
projectPath,
|
||||
|
|
@ -140,7 +109,7 @@ function CopyProjectNameDialog({
|
|||
<DialogTitle>
|
||||
{tc("projects:dialog:copy project", { name: oldName })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<VStack>
|
||||
<Input
|
||||
value={projectNameRaw}
|
||||
|
|
@ -176,7 +145,7 @@ function CopyProjectNameDialog({
|
|||
projectNameCheckState={projectNameCheckState}
|
||||
/>
|
||||
</VStack>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter className={"gap-2"}>
|
||||
<Button onClick={() => dialog.close(null)}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -226,7 +195,7 @@ export function CopyingDialog({
|
|||
<DialogTitle>
|
||||
{tc("projects:dialog:copy project", { name: oldName })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("projects:dialog:copying...")}</p>
|
||||
<p>
|
||||
{tc("projects:dialog:proceed k/n", {
|
||||
|
|
@ -236,7 +205,7 @@ export function CopyingDialog({
|
|||
</p>
|
||||
<Progress value={progress.proceed} max={progress.total} />
|
||||
<p>{tc("projects:do not close")}</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter className={"gap-2"}>
|
||||
<Button disabled>{tc("general:button:cancel")}</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,28 @@
|
|||
// noinspection ExceptionCaughtLocallyJS
|
||||
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleArrowUp,
|
||||
CircleMinus,
|
||||
CirclePlus,
|
||||
Ellipsis,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import type React from "react";
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { applyChangesMutation } from "@/app/_main/projects/manage/-use-package-change";
|
||||
import { Route } from "@/app/_main/projects/manage/index";
|
||||
import { ExternalLink } from "@/components/ExternalLink";
|
||||
|
|
@ -7,6 +30,7 @@ import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
|||
import { SearchBox } from "@/components/SearchBox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
|
|
@ -34,27 +58,12 @@ import {
|
|||
import { assertNever } from "@/lib/assert-never";
|
||||
import type { TauriPackage, TauriRepositoriesInfo } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { type DialogContext, openSingleDialog } from "@/lib/dialog";
|
||||
import { isFindKey, useDocumentEvent } from "@/lib/events";
|
||||
import { usePackageUpdateInProgress } from "@/lib/global-events";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
import { toastThrownError } from "@/lib/toast";
|
||||
import { toVersionString } from "@/lib/version";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
CircleArrowUp,
|
||||
CircleMinus,
|
||||
CirclePlus,
|
||||
Ellipsis,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useLayoutEffect } from "react";
|
||||
import { useRef } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type {
|
||||
PackageLatestInfo,
|
||||
PackageRowInfo,
|
||||
|
|
@ -81,9 +90,23 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
onRefresh: () => void;
|
||||
}) {
|
||||
const [search, setSearch] = useState("");
|
||||
const [bulkUpdatePackageIds, setBulkUpdatePackageIds] = useState<string[]>(
|
||||
const [bulkUpdatePackageIdsRaw, setBulkUpdatePackageIds] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
const [showHiddenPackages, setShowHiddenPackages] = useState(false);
|
||||
|
||||
const bulkUpdatePackageIds = useMemo(() => {
|
||||
const packageIds = new Set(packageRowsData.map((p) => p.id));
|
||||
|
||||
return bulkUpdatePackageIdsRaw.filter((pkgId) => packageIds.has(pkgId));
|
||||
}, [packageRowsData, bulkUpdatePackageIdsRaw]);
|
||||
|
||||
useDocumentEvent(
|
||||
"post-package-changes",
|
||||
() => setBulkUpdatePackageIds([]),
|
||||
[],
|
||||
);
|
||||
|
||||
const bulkUpdateMode = useMemo(() => {
|
||||
const packageRowByPackageId = new Map(
|
||||
packageRowsData.map((row) => [row.id, row]),
|
||||
|
|
@ -106,7 +129,7 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
(row) =>
|
||||
row.displayName.toLowerCase().includes(searchLower) ||
|
||||
row.id.toLowerCase().includes(searchLower) ||
|
||||
row.aliases.some((alias) =>
|
||||
row.keywords.some((alias) =>
|
||||
alias.toLowerCase().includes(searchLower),
|
||||
),
|
||||
)
|
||||
|
|
@ -114,19 +137,27 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
);
|
||||
}, [packageRowsData, search]);
|
||||
|
||||
const hiddenPackages = useMemo(() => {
|
||||
return packageRowsData.filter(
|
||||
(pkg) =>
|
||||
pkg.visibleSources.size === 0 && pkg.isThereSource && !pkg.installed,
|
||||
);
|
||||
}, [packageRowsData]);
|
||||
|
||||
const visibleHiddenPackagesCount = useMemo(() => {
|
||||
return hiddenPackages.filter((pkg) => filteredPackageIds.has(pkg.id))
|
||||
.length;
|
||||
}, [hiddenPackages, filteredPackageIds]);
|
||||
|
||||
const toggleShowHiddenPackages = useCallback(() => {
|
||||
setShowHiddenPackages((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const hiddenUserRepositories = useMemo(
|
||||
() => new Set(repositoriesInfo?.hidden_user_repositories ?? []),
|
||||
[repositoriesInfo],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setBulkUpdatePackageIds((ids) => {
|
||||
if (ids.length === 0) return [];
|
||||
const packageIds = new Set(packageRowsData.map((p) => p.id));
|
||||
return ids.filter((x) => packageIds.has(x));
|
||||
});
|
||||
}, [packageRowsData]);
|
||||
|
||||
const addBulkUpdatePackage = useCallback((row: PackageRowInfo) => {
|
||||
setBulkUpdatePackageIds((prev) => {
|
||||
return prev.some((id) => id === row.id) ? prev : [...prev, row.id];
|
||||
|
|
@ -168,7 +199,7 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
|
||||
return (
|
||||
<Card className="grow shrink flex shadow-none w-full">
|
||||
<CardContent className="w-full p-2 flex flex-col gap-2">
|
||||
<CardContent className="w-full p-2 flex flex-col gap-2 compact:p-1 compact:gap-1.5">
|
||||
<ManagePackagesHeading
|
||||
packageRowsData={packageRowsData}
|
||||
hiddenUserRepositories={hiddenUserRepositories}
|
||||
|
|
@ -184,7 +215,7 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
cancel={() => setBulkUpdatePackageIds([])}
|
||||
/>
|
||||
<ScrollableCardTable
|
||||
className={"h-full"}
|
||||
className={"h-full rounded-md"}
|
||||
ref={scrollTableOuterRef}
|
||||
viewportRef={scrollTableScrollAreaRef}
|
||||
>
|
||||
|
|
@ -200,7 +231,7 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
// biome-ignore lint/suspicious/noArrayIndexKey: static array
|
||||
key={index}
|
||||
className={
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground p-2.5"
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground px-2.5 py-1.5"
|
||||
}
|
||||
>
|
||||
<small className="font-normal leading-none">{tc(head)}</small>
|
||||
|
|
@ -208,32 +239,85 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
))}
|
||||
<th
|
||||
className={
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground p-2.5"
|
||||
"sticky top-0 z-10 border-b border-primary bg-secondary text-secondary-foreground px-2.5 py-1.5"
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{packageRowsData.map((row) => (
|
||||
<tr
|
||||
className="even:bg-secondary/30 anchor-none"
|
||||
hidden={!filteredPackageIds.has(row.id)}
|
||||
key={row.id}
|
||||
>
|
||||
<PackageRow
|
||||
pkg={row}
|
||||
bulkUpdateSelected={bulkUpdatePackageIds.some(
|
||||
(id) => id === row.id,
|
||||
)}
|
||||
bulkUpdateAvailable={canBulkUpdate(
|
||||
bulkUpdateMode,
|
||||
bulkUpdateModeForPackage(row),
|
||||
)}
|
||||
addBulkUpdatePackage={addBulkUpdatePackage}
|
||||
removeBulkUpdatePackage={removeBulkUpdatePackage}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
{packageRowsData.map((row) => {
|
||||
if (
|
||||
row.visibleSources.size === 0 &&
|
||||
row.isThereSource &&
|
||||
!row.installed
|
||||
)
|
||||
return null;
|
||||
return (
|
||||
<tr
|
||||
className="even:bg-secondary/30 anchor-none"
|
||||
hidden={!filteredPackageIds.has(row.id)}
|
||||
key={row.id}
|
||||
>
|
||||
<PackageRow
|
||||
pkg={row}
|
||||
bulkUpdateSelected={bulkUpdatePackageIds.some(
|
||||
(id) => id === row.id,
|
||||
)}
|
||||
bulkUpdateAvailable={canBulkUpdate(
|
||||
bulkUpdateMode,
|
||||
bulkUpdateModeForPackage(row),
|
||||
)}
|
||||
addBulkUpdatePackage={addBulkUpdatePackage}
|
||||
removeBulkUpdatePackage={removeBulkUpdatePackage}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{/* Hidden packages section */}
|
||||
{hiddenPackages.length > 0 && (
|
||||
<>
|
||||
<tr
|
||||
className="bg-secondary/50 hover:bg-secondary/70 cursor-pointer"
|
||||
onClick={toggleShowHiddenPackages}
|
||||
>
|
||||
<td className="p-3.5 compact:py-1 w-1">
|
||||
{showHiddenPackages ? (
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
colSpan={TABLE_HEAD.length + 1}
|
||||
className="p-3.5 compact:py-1 font-medium text-sm text-muted-foreground"
|
||||
>
|
||||
{tc("projects:manage:hidden packages")} (
|
||||
{visibleHiddenPackagesCount})
|
||||
</td>
|
||||
</tr>
|
||||
{showHiddenPackages &&
|
||||
hiddenPackages.map((row) => (
|
||||
<tr
|
||||
className="even:bg-secondary/30 anchor-none"
|
||||
hidden={!filteredPackageIds.has(row.id)}
|
||||
key={row.id}
|
||||
>
|
||||
<PackageRow
|
||||
pkg={row}
|
||||
bulkUpdateSelected={bulkUpdatePackageIds.some(
|
||||
(id) => id === row.id,
|
||||
)}
|
||||
bulkUpdateAvailable={canBulkUpdate(
|
||||
bulkUpdateMode,
|
||||
bulkUpdateModeForPackage(row),
|
||||
)}
|
||||
addBulkUpdatePackage={addBulkUpdatePackage}
|
||||
removeBulkUpdatePackage={removeBulkUpdatePackage}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</ScrollableCardTable>
|
||||
</CardContent>
|
||||
|
|
@ -242,6 +326,29 @@ export const PackageListCard = memo(function PackageListCard({
|
|||
);
|
||||
});
|
||||
|
||||
function ShowPrereleaseConfirmDialog({
|
||||
dialog,
|
||||
}: {
|
||||
dialog: DialogContext<boolean>;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<DialogTitle>
|
||||
{tc("settings:dialog:show prerelease packages")}
|
||||
</DialogTitle>
|
||||
<div>{tc("settings:dialog:show prerelease packages description")}</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close(false)}>
|
||||
{tc("general:button:cancel")}
|
||||
</Button>
|
||||
<Button variant="warning" onClick={() => dialog.close(true)}>
|
||||
{tc("settings:dialog:enable show prerelease packages")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ManagePackagesHeading({
|
||||
packageRowsData,
|
||||
hiddenUserRepositories,
|
||||
|
|
@ -336,9 +443,11 @@ function ManagePackagesHeading({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={"flex flex-wrap shrink-0 grow-0 flex-row gap-2 items-center"}
|
||||
className={
|
||||
"flex flex-wrap shrink-0 grow-0 flex-row gap-2 items-center gap-y-1"
|
||||
}
|
||||
>
|
||||
<p className="cursor-pointer font-bold py-1.5 grow-0 shrink-0 pl-2">
|
||||
<p className="cursor-pointer font-bold grow-0 shrink-0 pl-2">
|
||||
{tc("projects:manage:manage packages")}
|
||||
</p>
|
||||
|
||||
|
|
@ -348,7 +457,7 @@ function ManagePackagesHeading({
|
|||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
onClick={onRefresh}
|
||||
className={"shrink-0"}
|
||||
className="shrink-0"
|
||||
disabled={isLoading || backgroundLoading}
|
||||
>
|
||||
{isLoading || backgroundLoading ? (
|
||||
|
|
@ -372,7 +481,7 @@ function ManagePackagesHeading({
|
|||
|
||||
{upgradableToLatest && (
|
||||
<Button
|
||||
className={"shrink-0"}
|
||||
className="shrink-0"
|
||||
onClick={() => onUpgradeAllRequest(false)}
|
||||
disabled={isLoading}
|
||||
variant={"success"}
|
||||
|
|
@ -384,7 +493,7 @@ function ManagePackagesHeading({
|
|||
{/* show this button only if some packages are upgradable to prerelease and there is different stable */}
|
||||
{upgradableToStable && (
|
||||
<Button
|
||||
className={"shrink-0"}
|
||||
className="shrink-0"
|
||||
onClick={() => onUpgradeAllRequest(true)}
|
||||
disabled={isLoading}
|
||||
variant={"success"}
|
||||
|
|
@ -455,9 +564,21 @@ function ManagePackagesHeading({
|
|||
checked={repositoriesInfo?.show_prerelease_packages}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowPrereleasePackages.mutate(
|
||||
!repositoriesInfo?.show_prerelease_packages,
|
||||
);
|
||||
const newValue = !repositoriesInfo?.show_prerelease_packages;
|
||||
if (newValue) {
|
||||
void openSingleDialog(ShowPrereleaseConfirmDialog, {})
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
setShowPrereleasePackages.mutate(true);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toastThrownError(e);
|
||||
});
|
||||
} else {
|
||||
setShowPrereleasePackages.mutate(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tc("settings:show prerelease")}
|
||||
|
|
@ -530,13 +651,6 @@ function bulkUpdateModeForPackage(pkg: PackageRowInfo): PackageBulkUpdateMode {
|
|||
};
|
||||
}
|
||||
|
||||
function hasAnyUpdate(pkg: PackageBulkUpdateMode): boolean {
|
||||
for (const kind of possibleUpdateKind) {
|
||||
if (pkg[kind]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function canBulkUpdate(
|
||||
bulkUpdateMode: BulkUpdateMode,
|
||||
possibleUpdate: PackageBulkUpdateMode,
|
||||
|
|
@ -559,8 +673,6 @@ function BulkUpdateCard({
|
|||
packageRowsData: PackageRowInfo[];
|
||||
cancel?: () => void;
|
||||
}) {
|
||||
if (!bulkUpdateMode.hasPackages) return null;
|
||||
|
||||
const count = bulkUpdatePackageIds.length;
|
||||
const { projectPath } = Route.useSearch();
|
||||
const packageChange = useMutation(applyChangesMutation(projectPath));
|
||||
|
|
@ -617,10 +729,11 @@ function BulkUpdateCard({
|
|||
});
|
||||
};
|
||||
|
||||
if (!bulkUpdateMode.hasPackages) return null;
|
||||
return (
|
||||
<Card
|
||||
className={
|
||||
"shrink-0 p-2 flex flex-row gap-2 bg-secondary text-secondary-foreground flex-wrap"
|
||||
"shrink-0 p-2 compact:p-1 flex flex-row gap-2 compact:gap-1 bg-secondary text-secondary-foreground flex-wrap"
|
||||
}
|
||||
>
|
||||
{bulkUpdateMode.canInstallOrUpgrade && (
|
||||
|
|
@ -650,7 +763,7 @@ function BulkUpdateCard({
|
|||
{tc("projects:manage:button:uninstall selected")}
|
||||
</ButtonDisabledIfLoading>
|
||||
)}
|
||||
<ButtonDisabledIfLoading onClick={cancel}>
|
||||
<ButtonDisabledIfLoading onClick={cancel} variant={"warning"}>
|
||||
{tc("projects:manage:button:clear selection")}
|
||||
{" ("}
|
||||
{tc("projects:manage:n packages selected", { count })}
|
||||
|
|
@ -792,7 +905,7 @@ const PackageRow = memo(function PackageRow({
|
|||
addBulkUpdatePackage: (pkg: PackageRowInfo) => void;
|
||||
removeBulkUpdatePackage: (pkg: PackageRowInfo) => void;
|
||||
}) {
|
||||
const cellClass = "p-2.5";
|
||||
const cellClass = "p-3.5 compact:py-1";
|
||||
const noGrowCellClass = `${cellClass} w-1`;
|
||||
const versionNames = [...pkg.unityCompatible.keys()];
|
||||
const latestVersion: string | undefined = versionNames[0];
|
||||
|
|
@ -841,26 +954,26 @@ const PackageRow = memo(function PackageRow({
|
|||
|
||||
return (
|
||||
<>
|
||||
<td className={`${cellClass} w-1`}>
|
||||
<CheckboxDisabledIfLoading
|
||||
checked={bulkUpdateSelected}
|
||||
onCheckedChange={onClickBulkUpdate}
|
||||
disabled={!bulkUpdateAvailable}
|
||||
className="hover:before:content-none"
|
||||
/>
|
||||
<td className={`${cellClass} w-1 compact:px-2`}>
|
||||
<div className={"flex items-center justify-center aspect-square"}>
|
||||
<CheckboxDisabledIfLoading
|
||||
checked={bulkUpdateSelected}
|
||||
onCheckedChange={onClickBulkUpdate}
|
||||
disabled={!bulkUpdateAvailable}
|
||||
className="hover:before:content-none"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className={`${cellClass} overflow-hidden max-w-80 text-ellipsis`}>
|
||||
<Tooltip
|
||||
open={
|
||||
pkg.description ? undefined /* auto */ : false /* disable tooltip */
|
||||
}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`flex flex-col ${pkg.installed ? "" : "opacity-50"}`}
|
||||
>
|
||||
<p className="font-normal">{pkg.displayName}</p>
|
||||
<p className="font-normal opacity-50 text-sm">{pkg.id}</p>
|
||||
<p className="font-normal opacity-50 text-sm compact:hidden">
|
||||
{pkg.id}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={"max-w-[80dvw]"}>
|
||||
|
|
@ -869,6 +982,7 @@ const PackageRow = memo(function PackageRow({
|
|||
>
|
||||
{pkg.description}
|
||||
</p>
|
||||
<p className="font-normal opacity-50 text-sm">{pkg.id}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</td>
|
||||
|
|
@ -879,27 +993,29 @@ const PackageRow = memo(function PackageRow({
|
|||
<LatestPackageInfo info={pkg.latest} />
|
||||
</td>
|
||||
<td className={`${noGrowCellClass} max-w-32 overflow-hidden`}>
|
||||
{pkg.sources.size === 0 ? (
|
||||
{pkg.visibleSources.size === 0 ? (
|
||||
pkg.isThereSource ? (
|
||||
<p>{tc("projects:manage:source not selected")}</p>
|
||||
) : (
|
||||
<p>{tc("projects:manage:none")}</p>
|
||||
)
|
||||
) : pkg.sources.size === 1 ? (
|
||||
) : pkg.visibleSources.size === 1 ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<p className="overflow-hidden text-ellipsis">
|
||||
{[...pkg.sources][0]}
|
||||
{[...pkg.visibleSources][0]}
|
||||
</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{[...pkg.sources][0]}</TooltipContent>
|
||||
<TooltipContent>{[...pkg.visibleSources][0]}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<p>{tc("projects:manage:multiple sources")}</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{[...pkg.sources].join(", ")}</TooltipContent>
|
||||
<TooltipContent>
|
||||
{[...pkg.visibleSources].join(", ")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
|
|
@ -936,9 +1052,24 @@ const PackageRow = memo(function PackageRow({
|
|||
</ButtonDisabledIfLoading>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{!latestVersion
|
||||
? tc("projects:manage:tooltip:incompatible with unity")
|
||||
: tc("projects:manage:tooltip:add package")}
|
||||
{pkg.visibleSources.size === 0 && pkg.isThereSource ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p>
|
||||
{tc(
|
||||
"projects:manage:tooltip:select repository to install",
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs opacity-75">
|
||||
{[...pkg.sources]
|
||||
.filter((source) => !pkg.visibleSources.has(source))
|
||||
.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
) : !latestVersion ? (
|
||||
tc("projects:manage:tooltip:incompatible with unity")
|
||||
) : (
|
||||
tc("projects:manage:tooltip:add package")
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
|
@ -1023,7 +1154,7 @@ const PackageVersionSelector = memo(function PackageVersionSelector({
|
|||
<PackageInstalledInfo pkg={pkg} />
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent className="max-h-[min(24rem,45vh)]">
|
||||
{/* PackageVersionList is extremely heavy */}
|
||||
{isOpen && (
|
||||
<PackageVersionList
|
||||
|
|
@ -1065,11 +1196,7 @@ function PackageVersionList({
|
|||
);
|
||||
}
|
||||
|
||||
function PackageInstalledInfo({
|
||||
pkg,
|
||||
}: {
|
||||
pkg: PackageRowInfo;
|
||||
}) {
|
||||
function PackageInstalledInfo({ pkg }: { pkg: PackageRowInfo }) {
|
||||
if (pkg.installed) {
|
||||
const version = toVersionString(pkg.installed.version);
|
||||
if (pkg.installed.yanked) {
|
||||
|
|
@ -1088,11 +1215,7 @@ function PackageInstalledInfo({
|
|||
}
|
||||
}
|
||||
|
||||
function LatestPackageInfo({
|
||||
info,
|
||||
}: {
|
||||
info: PackageLatestInfo;
|
||||
}) {
|
||||
function LatestPackageInfo({ info }: { info: PackageLatestInfo }) {
|
||||
const { projectPath } = Route.useSearch();
|
||||
const packageChange = useMutation(applyChangesMutation(projectPath));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { type ComponentProps, createContext, useContext } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { type ComponentProps, createContext, useContext } from "react";
|
||||
|
||||
interface PageContext {
|
||||
isLoading: boolean;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import type { NavigateFn } from "@tanstack/react-router";
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { BackupProjectDialog } from "@/components/BackupProjectDialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { UnitySelectorDialog } from "@/components/unity-selector-dialog";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
|
|
@ -21,8 +19,6 @@ import { tc, tt } from "@/lib/i18n";
|
|||
import { queryClient } from "@/lib/query-client";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { compareUnityVersionString, parseUnityVersion } from "@/lib/version";
|
||||
import type { NavigateFn } from "@tanstack/react-router";
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
|
||||
export async function unityVersionChange({
|
||||
version: targetUnityVersion,
|
||||
|
|
@ -206,14 +202,14 @@ function NoExactUnity2022Dialog({
|
|||
return (
|
||||
<>
|
||||
<DialogTitle>{header}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>
|
||||
{tc(
|
||||
"projects:manage:dialog:exact version unity not found for patch migration description",
|
||||
{ unity: expectedVersion },
|
||||
)}
|
||||
</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter className={"gap-2"}>
|
||||
{installWithUnityHubLink && (
|
||||
<Button
|
||||
|
|
@ -242,11 +238,11 @@ function MigrationConfirmMigrationPatchDialog({
|
|||
return (
|
||||
<>
|
||||
<DialogTitle>{header}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p className={"text-destructive"}>
|
||||
{tc("projects:dialog:migrate unity2022 patch description", { unity })}
|
||||
</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close(null)} className="mr-1">
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -269,9 +265,9 @@ function MigrationConfirmMigrationDialog({
|
|||
return (
|
||||
<>
|
||||
<DialogTitle>{header}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("projects:dialog:vpm migrate description")}</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter className={"gap-1"}>
|
||||
<Button onClick={() => dialog.close(null)}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -330,9 +326,9 @@ function UnityVersionChange({
|
|||
return (
|
||||
<>
|
||||
<DialogTitle>{header}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p className={"text-destructive"}>{mainMessage}</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close(null)} className="mr-1">
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -378,7 +374,7 @@ export function MigrationCopyingDialog({
|
|||
return (
|
||||
<>
|
||||
<DialogTitle>{header}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("projects:pre-migrate copying...")}</p>
|
||||
<p>
|
||||
{tc("projects:dialog:proceed k/n", {
|
||||
|
|
@ -388,7 +384,7 @@ export function MigrationCopyingDialog({
|
|||
</p>
|
||||
<Progress value={progress.proceed} max={progress.total} />
|
||||
<p>{tc("projects:do not close")}</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -397,10 +393,10 @@ function MigrationMigratingDialog({ header }: { header: React.ReactNode }) {
|
|||
return (
|
||||
<>
|
||||
<DialogTitle>{header}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("projects:migrating...")}</p>
|
||||
<p>{tc("projects:do not close")}</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -564,7 +560,7 @@ function MigrationCallingUnityForMigrationDialog({
|
|||
return (
|
||||
<>
|
||||
<DialogTitle>{header}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("projects:manage:dialog:unity migrate finalizing...")}</p>
|
||||
<p>{tc("projects:do not close")}</p>
|
||||
{/* TODO: use ScrollArea (I failed to use it inside dialog) */}
|
||||
|
|
@ -581,7 +577,7 @@ function MigrationCallingUnityForMigrationDialog({
|
|||
))}
|
||||
<div ref={ref} />
|
||||
</pre>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
import type { DefaultError } from "@tanstack/query-core";
|
||||
import { queryOptions, type UseMutationOptions } from "@tanstack/react-query";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Fragment } from "react";
|
||||
import { DelayedButton } from "@/components/DelayedButton";
|
||||
import { ExternalLink } from "@/components/ExternalLink";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -24,11 +29,6 @@ import { queryClient } from "@/lib/query-client";
|
|||
import { toastInfo, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { groupBy, keyComparator } from "@/lib/utils";
|
||||
import { compareVersion, toVersionString } from "@/lib/version";
|
||||
import type { DefaultError } from "@tanstack/query-core";
|
||||
import { type UseMutationOptions, queryOptions } from "@tanstack/react-query";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export type RequestedOperation =
|
||||
| {
|
||||
|
|
@ -96,6 +96,7 @@ export function applyChangesMutation(projectPath: string) {
|
|||
toastThrownError(e);
|
||||
},
|
||||
onSettled: async () => {
|
||||
document.dispatchEvent(new Event("post-package-changes"));
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["projectDetails", projectPath],
|
||||
});
|
||||
|
|
@ -243,6 +244,12 @@ function showToast(requested: RequestedOperation) {
|
|||
}
|
||||
}
|
||||
|
||||
const TypographyItem = ({ children }: { children: React.ReactNode }) => (
|
||||
<div className={"p-3"}>
|
||||
<p className={"font-normal"}>{children}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
function ProjectChangesDialog({
|
||||
changes,
|
||||
existingPackages,
|
||||
|
|
@ -260,12 +267,6 @@ function ProjectChangesDialog({
|
|||
([_, c]) => c.unlocked_names,
|
||||
);
|
||||
|
||||
const TypographyItem = ({ children }: { children: React.ReactNode }) => (
|
||||
<div className={"p-3"}>
|
||||
<p className={"font-normal"}>{children}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const existingPackageMap = new Map(existingPackages ?? []);
|
||||
|
||||
const categorizedChanges = changes.package_changes.map(([pkgId, change]) =>
|
||||
|
|
@ -615,7 +616,7 @@ function categorizeChange(
|
|||
change: TauriPackageChange,
|
||||
installedPackages: Map<string, TauriBasePackageInfo>,
|
||||
): PackageChangeDisplayInformation {
|
||||
if ("InstallNew" in change) {
|
||||
if (change.InstallNew !== undefined) {
|
||||
const name = change.InstallNew.display_name ?? change.InstallNew.name;
|
||||
|
||||
const installed = installedPackages.get(pkgId);
|
||||
|
|
@ -760,13 +761,6 @@ function ChangelogButton({ url }: { url?: string | null }) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function comparePackageChangeByName(
|
||||
[aName]: [string, TauriPackageChange],
|
||||
[bName]: [string, TauriPackageChange],
|
||||
): number {
|
||||
return aName.localeCompare(bName);
|
||||
}
|
||||
|
||||
function MissingDependenciesDialog({
|
||||
dependencies,
|
||||
dialog,
|
||||
|
|
@ -780,7 +774,7 @@ function MissingDependenciesDialog({
|
|||
<CircleAlert className="size-6 inline" />{" "}
|
||||
{tc("projects:manage:dialog:missing dependencies")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p className={"whitespace-normal"}>
|
||||
{tc("projects:manage:dialog:missing dependencies description")}
|
||||
</p>
|
||||
|
|
@ -791,7 +785,7 @@ function MissingDependenciesDialog({
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close()}>
|
||||
{tc("general:button:close")}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,30 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
queryOptions,
|
||||
type UseQueryResult,
|
||||
useIsMutating,
|
||||
useMutation,
|
||||
useQueries,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
createFileRoute,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { ArrowLeft, ChevronDown } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Suspense, useMemo } from "react";
|
||||
import { copyProject } from "@/app/_main/projects/manage/-copy-project";
|
||||
import { BackupProjectDialog } from "@/components/BackupProjectDialog";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { OpenUnityButton } from "@/components/OpenUnityButton";
|
||||
import { RemoveProjectDialog } from "@/components/RemoveProjectDialog";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -44,23 +57,6 @@ import { tc } from "@/lib/i18n";
|
|||
import { nameFromPath } from "@/lib/os";
|
||||
import { toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { compareUnityVersionString, parseUnityVersion } from "@/lib/version";
|
||||
import {
|
||||
type UseQueryResult,
|
||||
queryOptions,
|
||||
useIsMutating,
|
||||
useMutation,
|
||||
useQueries,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
createFileRoute,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { ArrowLeft, ChevronDown } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Suspense, useMemo } from "react";
|
||||
import { combinePackagesAndProjectDetails } from "./-collect-package-row-info";
|
||||
import { PackageListCard } from "./-package-list-card";
|
||||
import { PageContextProvider } from "./-page-context";
|
||||
|
|
@ -202,7 +198,7 @@ function PageBody() {
|
|||
<PageContextProvider value={pageContext}>
|
||||
<VStack>
|
||||
<ProjectViewHeader
|
||||
className={"shrink-0"}
|
||||
className="shrink-0"
|
||||
isLoading={isLoading}
|
||||
detailsResult={detailsResult}
|
||||
unityVersionsResult={unityVersionsResult}
|
||||
|
|
@ -309,7 +305,7 @@ function UnityVersionSelector({
|
|||
value={detailsResult.data?.unity_str ?? undefined}
|
||||
onValueChange={requestChangeUnityVersion}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className={"compact:h-10"}>
|
||||
{detailsResult.status === "success" ? (
|
||||
(detailsResult.data.unity_str ?? "unknown")
|
||||
) : (
|
||||
|
|
@ -323,17 +319,13 @@ function UnityVersionSelector({
|
|||
);
|
||||
}
|
||||
|
||||
function SuggestResolveProjectCard({
|
||||
disabled,
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
function SuggestResolveProjectCard({ disabled }: { disabled?: boolean }) {
|
||||
const { projectPath } = Route.useSearch();
|
||||
const packageChange = useMutation(applyChangesMutation(projectPath));
|
||||
|
||||
return (
|
||||
<Card className={"shrink-0 p-2 flex flex-row items-center"}>
|
||||
<p className="cursor-pointer py-1.5 font-bold grow-0 shrink overflow-hidden whitespace-normal text-sm">
|
||||
<Card className={"shrink-0 p-2 flex flex-row items-center compact:p-1"}>
|
||||
<p className="cursor-pointer py-1.5 font-bold grow-0 shrink overflow-hidden whitespace-normal text-sm pl-2">
|
||||
{tc("projects:manage:suggest resolve")}
|
||||
</p>
|
||||
<div className={"grow shrink-0 w-2"} />
|
||||
|
|
@ -435,7 +427,7 @@ function SuggestMigrateTo2022Card({
|
|||
onMigrateRequested: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Card className={"shrink-0 p-2 flex flex-row items-center"}>
|
||||
<Card className={"shrink-0 p-2 flex flex-row items-center compact:p-1"}>
|
||||
<p className="cursor-pointer py-1.5 font-bold grow-0 shrink overflow-hidden whitespace-normal text-sm pl-2">
|
||||
{tc("projects:manage:suggest unity migration")}
|
||||
</p>
|
||||
|
|
@ -459,7 +451,7 @@ function Suggest2022PatchMigrationCard({
|
|||
onMigrateRequested: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Card className={"shrink-0 p-2 flex flex-row items-center"}>
|
||||
<Card className={"shrink-0 p-2 flex flex-row items-center compact:p-1"}>
|
||||
<p className="cursor-pointer py-1.5 font-bold grow-0 shrink overflow-hidden whitespace-normal text-sm pl-2">
|
||||
{tc("projects:manage:suggest unity patch migration")}
|
||||
</p>
|
||||
|
|
@ -483,7 +475,7 @@ function SuggestChinaToInternationalMigrationCard({
|
|||
onMigrateRequested: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Card className={"shrink-0 p-2 flex flex-row items-center"}>
|
||||
<Card className={"shrink-0 p-2 flex flex-row items-center compact:p-1"}>
|
||||
<p className="cursor-pointer py-1.5 font-bold grow-0 shrink overflow-hidden whitespace-normal text-sm pl-2">
|
||||
{tc("projects:manage:suggest unity china to international migration")}
|
||||
</p>
|
||||
|
|
@ -520,13 +512,13 @@ function ProjectViewHeader({
|
|||
|
||||
return (
|
||||
<HNavBar
|
||||
className={`${className}`}
|
||||
commonClassName={"min-h-12"}
|
||||
className={className}
|
||||
leading={
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={"compact:h-10"}
|
||||
variant={"ghost"}
|
||||
size={"sm"}
|
||||
onClick={() => history.back()}
|
||||
|
|
@ -539,7 +531,7 @@ function ProjectViewHeader({
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div className={"pl-2 space-y-0 my-1 shrink min-w-0"}>
|
||||
<div className={"pl-2 space-y-0 shrink min-w-0 compact:pl-0"}>
|
||||
<p className="cursor-pointer font-bold grow-0 whitespace-pre mb-0 leading-tight">
|
||||
{projectName}
|
||||
</p>
|
||||
|
|
@ -591,13 +583,6 @@ function ProjectViewHeader({
|
|||
);
|
||||
}
|
||||
|
||||
function projectGetCustomUnityArgs(projectPath: string) {
|
||||
return queryOptions({
|
||||
queryKey: ["projectGetCustomUnityArgs", projectPath],
|
||||
queryFn: async () => await commands.projectGetCustomUnityArgs(projectPath),
|
||||
});
|
||||
}
|
||||
|
||||
function LaunchSettings({
|
||||
defaultUnityArgs,
|
||||
initialValue,
|
||||
|
|
@ -617,12 +602,12 @@ function LaunchSettings({
|
|||
<>
|
||||
<DialogTitle>{tc("projects:dialog:launch options")}</DialogTitle>
|
||||
{/* TODO: use ScrollArea (I failed to use it inside dialog) */}
|
||||
<DialogDescription className={"max-h-[50dvh] overflow-y-auto"}>
|
||||
<div className={"max-h-[50dvh] overflow-y-auto"}>
|
||||
<h3 className={"text-lg"}>
|
||||
{tc("projects:dialog:command-line arguments")}
|
||||
</h3>
|
||||
<UnityArgumentsSettings context={context} />
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close(false)} variant={"destructive"}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -750,36 +735,37 @@ function ProjectButton({
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<div className={"flex divide-x"}>
|
||||
<OpenUnityButton
|
||||
projectPath={projectPath}
|
||||
unityVersion={unityVersion}
|
||||
unityRevision={unityRevision}
|
||||
className={"rounded-r-none pl-4 pr-3"}
|
||||
/>
|
||||
<DropdownMenuTrigger asChild className={"rounded-l-none pl-2 pr-2"}>
|
||||
<Button>
|
||||
<ChevronDown className={"w-4 h-4"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuContentBody
|
||||
projectPath={projectPath}
|
||||
removeProject={() => {
|
||||
void openSingleDialog(RemoveProjectDialog, {
|
||||
project: {
|
||||
path: projectPath,
|
||||
is_exists: true,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onChangeLaunchOptions={onChangeLaunchOptions}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
<DropdownMenu>
|
||||
<div className={"flex divide-x"}>
|
||||
<OpenUnityButton
|
||||
projectPath={projectPath}
|
||||
unityVersion={unityVersion}
|
||||
unityRevision={unityRevision}
|
||||
className={"rounded-r-none pl-4 pr-3 compact:h-10"}
|
||||
/>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={"rounded-l-none pl-2 pr-2 compact:h-10"}
|
||||
>
|
||||
<Button>
|
||||
<ChevronDown className={"w-4 h-4"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuContentBody
|
||||
projectPath={projectPath}
|
||||
removeProject={() => {
|
||||
void openSingleDialog(RemoveProjectDialog, {
|
||||
project: {
|
||||
path: projectPath,
|
||||
is_exists: true,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onChangeLaunchOptions={onChangeLaunchOptions}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { createFileRoute, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SideBar } from "@/components/SideBar";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { useDocumentEvent } from "@/lib/events";
|
||||
import { updateCurrentPath, usePrevPathName } from "@/lib/prev-page";
|
||||
import { useEffectEvent } from "@/lib/use-effect-event";
|
||||
import { Outlet, createFileRoute, useLocation } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/_main")({
|
||||
component: MainLayout,
|
||||
|
|
@ -57,14 +57,23 @@ function MainLayout() {
|
|||
}, [pathName]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(true);
|
||||
(async () => {
|
||||
if (await commands.environmentGuiCompact()) {
|
||||
document.documentElement.setAttribute("compact", "");
|
||||
} else {
|
||||
document.documentElement.removeAttribute("compact");
|
||||
}
|
||||
setIsVisible(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SideBar className={`grow-0 ${isVisible ? "slide-right" : ""}`} />
|
||||
<SideBar
|
||||
className={`grow-0 ${isVisible ? "slide-right" : "invisible"}`}
|
||||
/>
|
||||
<div
|
||||
className={`h-screen grow overflow-hidden flex p-4 ${animationState}`}
|
||||
className={`h-screen grow overflow-hidden flex p-4 compact:p-2 ${animationState}`}
|
||||
onAnimationEnd={() => setAnimationState("")}
|
||||
>
|
||||
<Outlet />
|
||||
|
|
|
|||
|
|
@ -1,26 +1,33 @@
|
|||
"use client";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
useSuspenseQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Suspense, useEffect, useTransition } from "react";
|
||||
import Loading from "@/app/-loading";
|
||||
import { CheckForUpdateMessage } from "@/components/CheckForUpdateMessage";
|
||||
import { ScrollPageContainer } from "@/components/ScrollPageContainer";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import {
|
||||
BackupFormatSelect,
|
||||
BackupPathWarnings,
|
||||
FilePathRow,
|
||||
GuiAnimationSwitch,
|
||||
GuiCompactSwitch,
|
||||
LanguageSelector,
|
||||
ProjectPathWarnings,
|
||||
ThemeSelector,
|
||||
} from "@/components/common-setting-parts";
|
||||
import { HNavBar, VStack } from "@/components/layout";
|
||||
import { HNavBar, HNavBarText, VStack } from "@/components/layout";
|
||||
import { ScrollableCardTable } from "@/components/ScrollableCardTable";
|
||||
import { ScrollPageContainer } from "@/components/ScrollPageContainer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
|
|
@ -44,17 +51,7 @@ import {
|
|||
toastThrownError,
|
||||
} from "@/lib/toast";
|
||||
import { useEffectEvent } from "@/lib/use-effect-event";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
useSuspenseQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { Suspense } from "react";
|
||||
import { useTransition } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Route = createFileRoute("/_main/settings/")({
|
||||
component: Page,
|
||||
|
|
@ -69,12 +66,8 @@ function Page() {
|
|||
return (
|
||||
<VStack>
|
||||
<HNavBar
|
||||
className={"shrink-0"}
|
||||
leading={
|
||||
<p className="cursor-pointer py-1.5 font-bold grow-0">
|
||||
{tc("settings")}
|
||||
</p>
|
||||
}
|
||||
className="shrink-0"
|
||||
leading={<HNavBarText>{tc("settings")}</HNavBarText>}
|
||||
/>
|
||||
<Suspense
|
||||
fallback={
|
||||
|
|
@ -131,6 +124,18 @@ function Settings() {
|
|||
);
|
||||
}
|
||||
|
||||
function SettingsCard({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Card>) {
|
||||
return (
|
||||
<Card className={cn("shrink-0 p-4 compact:p-3", className)} {...props}>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function UnityHubPathCard({
|
||||
updateUnityPaths,
|
||||
}: {
|
||||
|
|
@ -175,7 +180,7 @@ function UnityHubPathCard({
|
|||
});
|
||||
|
||||
return (
|
||||
<Card className={"shrink-0 p-4"}>
|
||||
<SettingsCard>
|
||||
<h2 className={"pb-2"}>{tc("settings:unity hub path")}</h2>
|
||||
<FilePathRow
|
||||
path={unityHub}
|
||||
|
|
@ -183,7 +188,7 @@ function UnityHubPathCard({
|
|||
notFoundMessage={"Unity Hub Not Found"}
|
||||
withOpen={false}
|
||||
/>
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +269,7 @@ function UnityInstallationsCard({
|
|||
];
|
||||
|
||||
return (
|
||||
<Card className={"shrink-0 p-4 flex flex-col gap-2"}>
|
||||
<SettingsCard className={"flex flex-col gap-2"}>
|
||||
<div className={"flex align-middle"}>
|
||||
<div className={"grow flex items-center"}>
|
||||
<h2>{tc("settings:unity installations")}</h2>
|
||||
|
|
@ -351,7 +356,7 @@ function UnityInstallationsCard({
|
|||
{tc("settings:use legacy unity hub loading description")}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -365,7 +370,7 @@ function UnityLaunchArgumentsCard() {
|
|||
const realUnityArgs = unityArgs ?? defaultUnityArgs;
|
||||
|
||||
return (
|
||||
<Card className={"shrink-0 p-4"}>
|
||||
<SettingsCard>
|
||||
<div className={"mb-2 flex align-middle"}>
|
||||
<div className={"grow flex items-center"}>
|
||||
<h2>{tc("settings:default unity arguments")}</h2>
|
||||
|
|
@ -392,10 +397,11 @@ function UnityLaunchArgumentsCard() {
|
|||
</p>
|
||||
<ol className={"flex flex-col"}>
|
||||
{realUnityArgs.map((v, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: unity args are ordered list
|
||||
<Input disabled key={i + v} value={v} className={"w-full"} />
|
||||
))}
|
||||
</ol>
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -446,9 +452,9 @@ function LaunchArgumentsEditDialogBody({
|
|||
{tc("settings:dialog:default launch arguments")}
|
||||
</DialogTitle>
|
||||
{/* TODO: use ScrollArea (I failed to use it inside dialog) */}
|
||||
<DialogDescription className={"max-h-[50dvh] overflow-y-auto"}>
|
||||
<div className={"max-h-[50dvh] overflow-y-auto"}>
|
||||
<UnityArgumentsSettings context={context} />
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => dialog.close(false)} variant={"destructive"}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
@ -505,7 +511,7 @@ function DefaultProjectPathCard() {
|
|||
});
|
||||
|
||||
return (
|
||||
<Card className={"shrink-0 p-4"}>
|
||||
<SettingsCard>
|
||||
<h2 className={"mb-2"}>{tc("settings:default project path")}</h2>
|
||||
<p className={"whitespace-normal"}>
|
||||
{tc("settings:default project path description")}
|
||||
|
|
@ -515,7 +521,7 @@ function DefaultProjectPathCard() {
|
|||
pick={pickProjectDefaultPath.mutate}
|
||||
/>
|
||||
<ProjectPathWarnings projectPath={defaultProjectPath} />
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -578,7 +584,7 @@ function BackupCard() {
|
|||
});
|
||||
|
||||
return (
|
||||
<Card className={"shrink-0 p-4"}>
|
||||
<SettingsCard>
|
||||
<h2>{tc("projects:backup")}</h2>
|
||||
<div className="mt-2">
|
||||
<h3>{tc("settings:backup:path")}</h3>
|
||||
|
|
@ -614,7 +620,7 @@ function BackupCard() {
|
|||
{tc("settings:backup:exclude vpm packages from backup description")}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -671,7 +677,7 @@ function PackagesCard() {
|
|||
});
|
||||
|
||||
return (
|
||||
<Card className={"shrink-0 p-4 flex flex-col gap-4"}>
|
||||
<SettingsCard className={"flex flex-col gap-4"}>
|
||||
<h2>{tc("settings:packages")}</h2>
|
||||
<div className={"flex flex-row flex-wrap gap-2"}>
|
||||
<Button onClick={() => clearPackageCache.mutate()}>
|
||||
|
|
@ -690,18 +696,19 @@ function PackagesCard() {
|
|||
{tc("settings:show prerelease description")}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
function AppearanceCard() {
|
||||
return (
|
||||
<Card className={"shrink-0 p-4"}>
|
||||
<SettingsCard className={"flex flex-col gap-2"}>
|
||||
<h2>{tc("settings:appearance")}</h2>
|
||||
<LanguageSelector />
|
||||
<ThemeSelector />
|
||||
<GuiAnimationSwitch />
|
||||
</Card>
|
||||
<GuiCompactSwitch />
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -724,7 +731,7 @@ function FilesAndFoldersCard() {
|
|||
};
|
||||
|
||||
return (
|
||||
<Card className={"shrink-0 p-4"}>
|
||||
<SettingsCard>
|
||||
<h2>{tc("settings:files and directories")}</h2>
|
||||
<p className={"mt-2"}>
|
||||
{tc("settings:files and directories:description")}
|
||||
|
|
@ -751,7 +758,7 @@ function FilesAndFoldersCard() {
|
|||
{tc("settings:button:open vcc templates")}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -863,7 +870,7 @@ function AlcomCard() {
|
|||
};
|
||||
|
||||
return (
|
||||
<Card className={"shrink-0 p-4 flex flex-col gap-4"}>
|
||||
<SettingsCard className={"flex flex-col gap-4"}>
|
||||
<h2>ALCOM</h2>
|
||||
<div className={"flex flex-row flex-wrap gap-2"}>
|
||||
{globalInfo.checkForUpdates && (
|
||||
|
|
@ -928,7 +935,7 @@ function AlcomCard() {
|
|||
},
|
||||
)}
|
||||
</p>
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -936,7 +943,7 @@ function SystemInformationCard() {
|
|||
const info = useGlobalInfo();
|
||||
|
||||
return (
|
||||
<Card className={"shrink-0 p-4 flex flex-col gap-4"}>
|
||||
<SettingsCard className={"flex flex-col gap-4"}>
|
||||
<h2>{tc("settings:system information")}</h2>
|
||||
<dl>
|
||||
<dt>{tc("settings:os")}</dt>
|
||||
|
|
@ -950,6 +957,6 @@ function SystemInformationCard() {
|
|||
<dt>{tc("settings:alcom commit hash")}</dt>
|
||||
<dd className={"opacity-50 mb-2"}>{info.commitHash}</dd>
|
||||
</dl>
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import licenses from "build:licenses.json";
|
||||
import { ScrollPageContainer } from "@/components/ScrollPageContainer";
|
||||
import { ScrollableCard } from "@/components/ScrollableCard";
|
||||
import { VStack } from "@/components/layout";
|
||||
import { ScrollableCard } from "@/components/ScrollableCard";
|
||||
import { ScrollPageContainer } from "@/components/ScrollPageContainer";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { commands } from "@/lib/bindings";
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ export default function RenderPage() {
|
|||
<ul />
|
||||
</Card>
|
||||
|
||||
{licenses.map((license, idx) => (
|
||||
{licenses.map((license) => (
|
||||
<Card className={"p-4"} key={license.text}>
|
||||
<h3>{license.name}</h3>
|
||||
<h4>Used by:</h4>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import licenses from "build:licenses.json";
|
||||
import { ScrollPageContainer } from "@/components/ScrollPageContainer";
|
||||
import { ScrollableCard } from "@/components/ScrollableCard";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { VStack } from "@/components/layout";
|
||||
import { ScrollableCard } from "@/components/ScrollableCard";
|
||||
import { ScrollPageContainer } from "@/components/ScrollPageContainer";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/_main/settings/licenses/")({
|
||||
component: Page,
|
||||
|
|
@ -23,7 +23,7 @@ function Page() {
|
|||
<ul />
|
||||
</Card>
|
||||
|
||||
{licenses.map((license, idx) => (
|
||||
{licenses.map((license) => (
|
||||
<Card className={"p-4"} key={license.text}>
|
||||
<h3>{license.name}</h3>
|
||||
<h4>Used by:</h4>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Outlet,
|
||||
createFileRoute,
|
||||
Outlet,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Circle, CircleCheck, CircleChevronRight } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardFooter, CardHeader } from "@/components/ui/card";
|
||||
import type { SetupPages, TauriEnvironmentSettings } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { useGlobalInfo } from "@/lib/global-info";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Circle, CircleCheck, CircleChevronRight } from "lucide-react";
|
||||
import type React from "react";
|
||||
|
||||
export type BodyProps = Readonly<{
|
||||
environment: TauriEnvironmentSettings;
|
||||
|
|
@ -52,9 +52,9 @@ export function SetupPageBase({
|
|||
<div className={"flex gap-4"}>
|
||||
{!withoutSteps && <StepCard current={pageId} />}
|
||||
<Card
|
||||
className={`${withoutSteps ? "w-[30rem]" : "w-96"} min-w-[50vw] min-h-[max(50dvh,20rem)] p-4 flex gap-3`}
|
||||
className={`${withoutSteps ? "w-[30rem]" : "w-96"} min-w-[50vw] min-h-[max(50dvh,20rem)] p-4 flex gap-3 compact:min-h-[max(40dvh,20rem)]`}
|
||||
>
|
||||
<div className={"flex flex-col grow"}>
|
||||
<div className={"flex flex-col grow gap-3 compact:gap-2"}>
|
||||
<CardHeader>
|
||||
<h1 className={"text-center"}>{heading}</h1>
|
||||
</CardHeader>
|
||||
|
|
@ -65,7 +65,7 @@ export function SetupPageBase({
|
|||
<Body environment={result.data} />
|
||||
)}
|
||||
<div className={"grow"} />
|
||||
<CardFooter className="p-0 pt-3 items-end flex-row gap-2 justify-end">
|
||||
<CardFooter className="p-0 pt-3 items-end flex-row gap-2 justify-end compact:-m-2">
|
||||
{prevPage && (
|
||||
<Button onClick={() => navigate({ to: prevPage })}>
|
||||
{backContent}
|
||||
|
|
@ -80,11 +80,7 @@ export function SetupPageBase({
|
|||
);
|
||||
}
|
||||
|
||||
function StepCard({
|
||||
current,
|
||||
}: {
|
||||
current: SetupPages | null;
|
||||
}) {
|
||||
function StepCard({ current }: { current: SetupPages | null }) {
|
||||
// TODO: get progress from backend
|
||||
const finisheds = useQuery({
|
||||
queryKey: ["environmentGetFinishedSetupPages"],
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
GuiAnimationSwitch,
|
||||
GuiCompactSwitch,
|
||||
LanguageSelector,
|
||||
ThemeSelector,
|
||||
} from "@/components/common-setting-parts";
|
||||
import { CardDescription } from "@/components/ui/card";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SetupPageBase } from "../-setup-page-base";
|
||||
|
||||
export const Route = createFileRoute("/_setup/setup/appearance/")({
|
||||
|
|
@ -40,6 +41,7 @@ function Body() {
|
|||
<LanguageSelector />
|
||||
<ThemeSelector />
|
||||
<GuiAnimationSwitch />
|
||||
<GuiCompactSwitch />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
BackupFormatSelect,
|
||||
BackupPathWarnings,
|
||||
|
|
@ -11,8 +13,6 @@ import { commands } from "@/lib/bindings";
|
|||
import { useGlobalInfo } from "@/lib/global-info";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { type BodyProps, SetupPageBase } from "../-setup-page-base";
|
||||
|
||||
export const Route = createFileRoute("/_setup/setup/backups/")({
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CardDescription } from "@/components/ui/card";
|
||||
import { useGlobalInfo } from "@/lib/global-info";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SetupPageBase } from "../-setup-page-base";
|
||||
|
||||
export const Route = createFileRoute("/_setup/setup/finish/")({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
FilePathRow,
|
||||
ProjectPathWarnings,
|
||||
|
|
@ -9,8 +11,6 @@ import { assertNever } from "@/lib/assert-never";
|
|||
import { commands } from "@/lib/bindings";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { type BodyProps, SetupPageBase } from "../-setup-page-base";
|
||||
|
||||
export const Route = createFileRoute("/_setup/setup/project-path/")({
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { useGlobalInfo } from "@/lib/global-info";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { toastThrownError } from "@/lib/toast";
|
||||
import {
|
||||
queryOptions,
|
||||
useMutation,
|
||||
|
|
@ -12,6 +7,11 @@ import {
|
|||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { useGlobalInfo } from "@/lib/global-info";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { toastThrownError } from "@/lib/toast";
|
||||
import { type BodyProps, SetupPageBase } from "../-setup-page-base";
|
||||
|
||||
export const Route = createFileRoute("/_setup/setup/system-setting/")({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { FilePathRow } from "@/components/common-setting-parts";
|
||||
import {
|
||||
Accordion,
|
||||
|
|
@ -13,8 +15,6 @@ import { assertNever } from "@/lib/assert-never";
|
|||
import { commands } from "@/lib/bindings";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
import { toastError, toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { type BodyProps, SetupPageBase } from "../-setup-page-base";
|
||||
|
||||
export const Route = createFileRoute("/_setup/setup/unity-hub/")({
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@custom-variant compact (&:is([compact] *));
|
||||
|
||||
@theme inline {
|
||||
--background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops));
|
||||
|
|
@ -98,7 +99,7 @@
|
|||
--secondary: hsl(240 4.8% 95.9%);
|
||||
--secondary-foreground: hsl(240 5.9% 10%);
|
||||
--muted: hsl(240 4.8% 95.9%);
|
||||
--muted-foreground: hsl(240 3.8% 46.1%);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: hsl(240 4.8% 95.9%);
|
||||
--accent-foreground: hsl(240 5.9% 30%);
|
||||
--info: hsl(207 90% 54%);
|
||||
|
|
@ -134,7 +135,7 @@
|
|||
--secondary: var(--secondary-bg);
|
||||
--secondary-foreground: var(--fg-color);
|
||||
--muted: var(--secondary-bg);
|
||||
--muted-foreground: 240 5% 74%;
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: var(--secondary-bg);
|
||||
--accent-foreground: var(--fg-color);
|
||||
--info: hsl(207 90% 54%);
|
||||
|
|
@ -170,7 +171,7 @@
|
|||
--secondary: var(--secondary-bg);
|
||||
--secondary-foreground: var(--fg-color);
|
||||
--muted: var(--secondary-bg);
|
||||
--muted-foreground: 240 5% 74%;
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: var(--secondary-bg);
|
||||
--accent-foreground: var(--fg-color);
|
||||
--info: hsl(207 90% 54%);
|
||||
|
|
@ -187,7 +188,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
body {
|
||||
--toastify-font-family: var(--font-sans);
|
||||
--toastify-color-light: var(--background);
|
||||
/*--toastify-color-info: #3498db;*/
|
||||
|
|
@ -286,6 +287,7 @@ html {
|
|||
|
||||
/* Radix ui sets display:block for each scroll viewport element but it seem it make worse */
|
||||
[data-radix-scroll-area-viewport] > div {
|
||||
/* biome-ignore lint/complexity/noImportantStyles: necessary to override element */
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
|
|
@ -293,13 +295,25 @@ html {
|
|||
* Add padding end for horizontal scroll bar of scrollable card if vertical scroll bar is invisible
|
||||
* This prevents the horizontal scroll bar hide corner of the card
|
||||
*/
|
||||
.vrc-get-scrollable-card:not(:has(> .vrc-get-scrollable-card-vertical-bar))
|
||||
> div[data-radix-scroll-area-viewport]
|
||||
.vrc-get-scrollable-card:not(
|
||||
:has(> .vrc-get-scrollable-card-vertical-bar)
|
||||
) > div[data-radix-scroll-area-viewport]
|
||||
> div
|
||||
> div.vrc-get-scrollable-card-horizontal-bar {
|
||||
@apply pe-2.5;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add padding end for the content area of scrollable card if vertical scroll bar is visible
|
||||
* This prevents the table / items from being hidden behind the vertical scroll bar
|
||||
*/
|
||||
.vrc-get-scrollable-card:has(
|
||||
> .vrc-get-scrollable-card-vertical-bar
|
||||
) > div[data-radix-scroll-area-viewport]
|
||||
> div {
|
||||
@apply pe-2.5;
|
||||
}
|
||||
|
||||
.vrc-get-sidebar-hostname-warning-container {
|
||||
contain-intrinsic-size: 0 7em;
|
||||
contain: size;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||
import ErrorPage from "@/app/-error";
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: RouteComponent,
|
||||
|
|
@ -7,9 +7,5 @@ export const Route = createFileRoute("/")({
|
|||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
return <Outlet />;
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"files": {
|
||||
"ignore": [
|
||||
"project-templates",
|
||||
"node_modules",
|
||||
".next",
|
||||
"out",
|
||||
"gen",
|
||||
"lib/bindings.ts",
|
||||
"lib/routeTree.gen.ts",
|
||||
"build"
|
||||
"includes": [
|
||||
"**",
|
||||
"!project-templates",
|
||||
"!node_modules",
|
||||
"!.next",
|
||||
"!out",
|
||||
"!gen",
|
||||
"!lib/bindings.ts",
|
||||
"!lib/routeTree.gen.ts",
|
||||
"!build"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
|
|
@ -24,9 +25,20 @@
|
|||
// In my opinion, '!.' => '?.' is not reasonable for all cases, so I disabled automatic fix.
|
||||
"fix": "none",
|
||||
"level": "error"
|
||||
},
|
||||
"noRestrictedGlobals": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"deniedGlobals": {
|
||||
"close": "window.close is unlikely to be called"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"suspicious": {
|
||||
// false positives with tailwind css
|
||||
// see https://github.com/biomejs/biome/issues/7223
|
||||
"noUnknownAtRules": "off",
|
||||
"noAssignInExpressions": "off"
|
||||
},
|
||||
"correctness": {
|
||||
|
|
@ -63,7 +75,16 @@
|
|||
},
|
||||
"enabled": true
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
vrc-get-gui/booth/.gitignore
vendored
2
vrc-get-gui/booth/.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
|||
thumbnail.png
|
||||
thumbnail.*.svg
|
||||
booth.zip
|
||||
app-icon.png
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
npm.install:
|
||||
npm install
|
||||
|
||||
thumbnail.embed.svg: thumbnail.svg alcom-dark.png alcom-light.png ../app-icon.png
|
||||
node embeder.mjs thumbnail.embed.svg thumbnail.svg alcom-dark.png alcom-light.png ../app-icon.png
|
||||
app-icon.png: ../app-icon.png
|
||||
cp ../app-icon.png app-icon.png
|
||||
|
||||
thumbnail.png: thumbnail.embed.svg
|
||||
npx sharp -i thumbnail.embed.svg -o thumbnail.png
|
||||
thumbnail.png: thumbnail.svg alcom-dark.png alcom-light.png app-icon.png
|
||||
rsvg-convert thumbnail.svg -o thumbnail.png
|
||||
|
||||
booth.zip: website.url README.ja.txt README.en.txt LICENSE
|
||||
zip -r $@ $^
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import * as fs from "node:fs/promises";
|
||||
|
||||
const output = process.argv[2];
|
||||
const input = process.argv[3];
|
||||
|
||||
let inputText = await fs.readFile(input, { encoding: "utf-8" });
|
||||
|
||||
for (let i = 4; i < process.argv.length; i++) {
|
||||
const embedPath = process.argv[i];
|
||||
const embedDataBase64 = await fs.readFile(embedPath, { encoding: "base64" });
|
||||
const embedDataUrl = `data:image/png;base64,${embedDataBase64}`;
|
||||
inputText = inputText.replace(`"${embedPath}"`, `"${embedDataUrl}"`);
|
||||
}
|
||||
|
||||
await fs.writeFile(output, inputText);
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
|
||||
>
|
||||
<rect x="0" y="0" width="4096" height="4096" style="fill:rgb(45,45,45);"/>
|
||||
<image x="-95" y="1013" width="3149px" height="2021" xlink:href="alcom-light.png"/>
|
||||
<image x="980" y="1670" width="3216px" height="2064" xlink:href="alcom-dark.png"/>
|
||||
<image x="254" y="138" width="844px" height="844px" xlink:href="../app-icon.png"/>
|
||||
<image x="-95" y="1013" width="3149px" height="2021" xlink:href="./alcom-light.png"/>
|
||||
<image x="980" y="1670" width="3216px" height="2064" xlink:href="./alcom-dark.png"/>
|
||||
<image x="254" y="138" width="844px" height="844px" xlink:href="./app-icon.png"/>
|
||||
<text x="1286" y="679" class="NotoSansBold Green">ALCOM</text>
|
||||
<text x="1286" y="918" class="NotoSansMediumItalic Green">Fast VCC alternative for any desktop</text>
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
|
|
@ -100,5 +100,5 @@ fn get_commit_hash() {
|
|||
return;
|
||||
};
|
||||
|
||||
println!("cargo:rustc-env=COMMIT_HASH={}", hash_value);
|
||||
println!("cargo:rustc-env=COMMIT_HASH={hash_value}");
|
||||
}
|
||||
|
|
|
|||
62
vrc-get-gui/bundle/Info.plist
Normal file
62
vrc-get-gui/bundle/Info.plist
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>ALCOM VCC URL</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>vcc</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ALCOM Project Template</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.anatawa12.vrc-get.alcomtemplate</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.anatawa12.vrc-get.alcomtemplate</string>
|
||||
<key>UTTypeIcons</key>
|
||||
<dict>
|
||||
<key>UTTypeIconText</key>
|
||||
<string>Template</string>
|
||||
<key>UTTypeIconBackgroundName</key>
|
||||
<string></string>
|
||||
<key>UTTypeIconBadgeName</key>
|
||||
<string>icon.icns</string>
|
||||
</dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.content</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>ALCOM Project Template</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>alcomtemplate</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
9
vrc-get-gui/bundle/alcom.desktop
Normal file
9
vrc-get-gui/bundle/alcom.desktop
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[Desktop Entry]
|
||||
Categories=Development
|
||||
Comment=ALCOM - Alternative Creator Companion
|
||||
Exec={{exec}} %u
|
||||
Icon=alcom
|
||||
Name=ALCOM
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/vcc
|
||||
70
vrc-get-gui/bundle/alcom.spec
Normal file
70
vrc-get-gui/bundle/alcom.spec
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
Name: alcom
|
||||
Version: 1.1.6
|
||||
Release: 1%{?dist}
|
||||
Summary: A short description of my custom application
|
||||
|
||||
%global git_version %(echo "%{version}" | tr '~' '-')
|
||||
|
||||
License: MIT
|
||||
URL: https://vrc-get.anatawa12.com/alcom/
|
||||
Source0: https://github.com/vrc-get/vrc-get/archive/gui-v%{git_version}.tar.gz
|
||||
|
||||
BuildRequires: gcc
|
||||
BuildRequires: nodejs
|
||||
BuildRequires: npm
|
||||
BuildRequires: pkgconfig(gtk+-3.0)
|
||||
BuildRequires: pkgconfig(webkit2gtk-4.1)
|
||||
BuildRequires: pkgconfig(openssl)
|
||||
|
||||
# we download rust toolchain manually when building inside mock container
|
||||
%if ! 0%{?install_rust:1}
|
||||
BuildRequires: cargo
|
||||
%endif
|
||||
|
||||
# disable stripping symbols.
|
||||
%global __os_install_post %{nil}
|
||||
%global debug_package %{nil}
|
||||
|
||||
%description
|
||||
ALCOM - Alternative Creator Companion
|
||||
ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri.
|
||||
|
||||
%prep
|
||||
%setup -q -n vrc-get-gui-v%{git_version}
|
||||
|
||||
%if 0%{?install_rust:1}
|
||||
echo "=== Mock environment detected. Installing isolated Rust toolchain ==="
|
||||
|
||||
export RUSTUP_HOME="$(pwd)/.rustup"
|
||||
export CARGO_HOME="$(pwd)/.cargo"
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
|
||||
|
||||
cat << EOF > ./load_rust_env.sh
|
||||
export RUSTUP_HOME="${RUSTUP_HOME}"
|
||||
export CARGO_HOME="${CARGO_HOME}"
|
||||
source "${CARGO_HOME}/env"
|
||||
EOF
|
||||
%endif
|
||||
|
||||
# marker: ci inserts version update here
|
||||
|
||||
%build
|
||||
%{?install_rust: source ./load_rust_env.sh}
|
||||
cargo xtask build-alcom --release
|
||||
|
||||
%install
|
||||
%{?install_rust: source ./load_rust_env.sh}
|
||||
rm -rf %{buildroot}
|
||||
cargo xtask bundle-alcom --release --bundles buildroot --buildroot=%{buildroot}
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
# %doc vrc-get-gui/README.md
|
||||
#%doc vrc-get-gui/CHANGELOG.md
|
||||
%{_bindir}/alcom
|
||||
%{_datadir}/applications/alcom.desktop
|
||||
%{_datadir}/icons/hicolor/*/apps/alcom.png
|
||||
|
||||
%changelog
|
||||
* Migrated to native rpm build pipeline with spec file
|
||||
7
vrc-get-gui/bundle/debian/.gitignore
vendored
Normal file
7
vrc-get-gui/bundle/debian/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/*-build-stamp
|
||||
/*.substvars
|
||||
/.debhelper
|
||||
/files
|
||||
/cargo_home
|
||||
/npm_cache
|
||||
alcom/
|
||||
5
vrc-get-gui/bundle/debian/README.Debian
Normal file
5
vrc-get-gui/bundle/debian/README.Debian
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
alcom for Debian
|
||||
|
||||
ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri.
|
||||
|
||||
-- anatawa12 <i@anatawa12.com> Thu, 11 Jun 2026 14:34:57 +0000
|
||||
20
vrc-get-gui/bundle/debian/README.source
Normal file
20
vrc-get-gui/bundle/debian/README.source
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
alcom for Debian
|
||||
|
||||
This is README for alcom Debian package.
|
||||
|
||||
This Debian package is made to provide one of official distribution of alcom, formaly known as vrc-get-gui.
|
||||
Starting with 1.1.7 or later, alcom maintainer switched from custom .deb toolchain to official .deb toolchain.
|
||||
This directory contains source code of debian package as non-native package.
|
||||
|
||||
This directory is placed at vrc-get-gui/bundles/debian on original source tree, but before building debian package,
|
||||
you must copy vrc-get-gui/bundles/debian to debian.
|
||||
|
||||
Violating best practice of debian package, this package requires network access, to download cargo dependencies on build.
|
||||
In addition, by declaring `INSTALL_RUST` environment variable to `1` with `--set-envvar=INSTALL_RUST=1`, you can let
|
||||
build process to install newest available rust to build on older distribution does not provide new enough rust version.
|
||||
You also install NODEJS on build by declaring `INSTALL_NODEJS=1`.
|
||||
Those options are helpful when building inside sandbox like pbuilder.
|
||||
|
||||
This Debian package is based on package generated by debmake Version 4.5.1.
|
||||
|
||||
-- anatawa12 <i@anatawa12.com> Thu, 11 Jun 2026 14:34:57 +0000
|
||||
5
vrc-get-gui/bundle/debian/changelog
Normal file
5
vrc-get-gui/bundle/debian/changelog
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
alcom (1.1.6-1) UNRELEASED; urgency=low
|
||||
|
||||
* No changelog are provided for this package
|
||||
|
||||
-- root <i@anatawa12.com> Thu, 11 Jun 2026 14:34:57 +0000
|
||||
8
vrc-get-gui/bundle/debian/clean
Normal file
8
vrc-get-gui/bundle/debian/clean
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
target/
|
||||
vrc-get-gui/node_modules/
|
||||
vrc-get-gui/out/
|
||||
vrc-get-gui/gen/
|
||||
debian/cargo_home/
|
||||
debian/npm_cache/
|
||||
debian/rustup_home/
|
||||
debian/nodejs_installed/
|
||||
29
vrc-get-gui/bundle/debian/control
Normal file
29
vrc-get-gui/bundle/debian/control
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
Source: alcom
|
||||
Priority: optional
|
||||
Maintainer: anatawa12 <i@anatawa12.com>
|
||||
Build-Depends:
|
||||
debhelper-compat (= 13),
|
||||
libssl-dev,
|
||||
pkg-config,
|
||||
cargo,
|
||||
nodejs,
|
||||
npm,
|
||||
# curl for rust-install build only
|
||||
curl,
|
||||
libgtk-3-dev,
|
||||
libwebkit2gtk-4.1-dev (>= 2.41),
|
||||
Standards-Version: 4.7.0
|
||||
Homepage: https://vrc-get.anatawa12.com/alcom/
|
||||
Rules-Requires-Root: no
|
||||
|
||||
Package: alcom
|
||||
Architecture: any
|
||||
Multi-Arch: foreign
|
||||
Depends:
|
||||
${misc:Depends},
|
||||
${shlibs:Depends},
|
||||
Description: ALCOM - Alternative Creator Companion
|
||||
ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri.
|
||||
.
|
||||
This package is one of official distribution of ALCOM, released as a part of the updates from ALCOM.
|
||||
No packaging only updates will be provided.
|
||||
41
vrc-get-gui/bundle/debian/copyright
Normal file
41
vrc-get-gui/bundle/debian/copyright
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: alcom
|
||||
Upstream-Contact: <preferred name and address to reach the upstream project>
|
||||
Source: <url://example.com>
|
||||
|
||||
Files: *
|
||||
Copyright: Copyright (c) 2023 anatawa12 and other contribcmeutors
|
||||
License: MIT
|
||||
MIT License
|
||||
.
|
||||
Copyright (c) 2023 anatawa12 and other contributors
|
||||
.
|
||||
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.
|
||||
.
|
||||
|
||||
Files: vrc-get-gui/third-party/Anton-Regular.ttf
|
||||
vrc-get-gui/third-party/NotoSans-Italic-VariableFont_wdth,wght.ttf
|
||||
vrc-get-gui/third-party/NotoSans-VariableFont_wdth,wght.ttf
|
||||
Copyright: 2020 The Anton Project Authors (https://github.com/googlefonts/AntonFont.git)
|
||||
2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
||||
License: SIL Open Font License 1.1
|
||||
|
||||
Files: vrc-get-gui/icons/*
|
||||
Copyright: 2024 lilxyzw, anatawa12 and other contributors
|
||||
License: CC-BY-4.0
|
||||
39
vrc-get-gui/bundle/debian/rules
Executable file
39
vrc-get-gui/bundle/debian/rules
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
export DEB_BUILD_OPTIONS += nostrip
|
||||
|
||||
export CARGO_HOME = $(CURDIR)/debian/cargo_home
|
||||
export NPM_CONFIG_CACHE = $(CURDIR)/debian/npm_cache
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
# install rust in configure phase when requested
|
||||
ifeq ($(INSTALL_RUST),1)
|
||||
export RUSTUP_HOME = $(CURDIR)/debian/rustup_home
|
||||
export PATH := $(CURDIR)/debian/cargo_home/bin:$(PATH)
|
||||
|
||||
override_dh_auto_configure::
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
|
||||
endif
|
||||
|
||||
ifeq ($(INSTALL_NODEJS),1)
|
||||
export RUSTUP_HOME = $(CURDIR)/debian/rustup_home
|
||||
export PATH := $(CURDIR)/debian/nodejs_installed/bin:$(PATH)
|
||||
|
||||
ifeq ($(DEB_HOST_ARCH),amd64)
|
||||
NODE_ARCH := x64
|
||||
else ifeq ($(DEB_HOST_ARCH),arm64)
|
||||
NODE_ARCH := arm64
|
||||
endif
|
||||
|
||||
override_dh_auto_configure::
|
||||
mkdir -p $(CURDIR)/debian/nodejs_installed
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://nodejs.org/dist/v26.3.0/node-v26.3.0-linux-$(NODE_ARCH).tar.gz | gunzip | tar x --strip-components 1 -C $(CURDIR)/debian/nodejs_installed
|
||||
endif
|
||||
|
||||
override_dh_auto_build:
|
||||
cargo xtask build-alcom --release
|
||||
|
||||
override_dh_auto_install:
|
||||
cargo xtask bundle-alcom --release --bundles buildroot --buildroot=$(CURDIR)/debian/alcom
|
||||
1
vrc-get-gui/bundle/debian/source/format
Normal file
1
vrc-get-gui/bundle/debian/source/format
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
||||
24
vrc-get-gui/bundle/debian/upstream/metadata
Normal file
24
vrc-get-gui/bundle/debian/upstream/metadata
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Metadata about the upstream project.
|
||||
# See https://wiki.debian.org/UpstreamMetadata
|
||||
|
||||
Bug-Database: https://github.com/vrc-get/vrc-get/issues
|
||||
Bug-Submit: https://github.com/vrc-get/vrc-get/issues/new
|
||||
Changelog: https://github.com/vrc-get/vrc-get/blob/master/vrc-get-gui/CHANGELOG.md
|
||||
Documentation: https://vrc-get.anatawa12.com/alcom/
|
||||
Repository-Browse: https://github.com/vrc-get/vrc-get
|
||||
Repository: https://github.com/vrc-get/vrc-get.git
|
||||
#FAQ: https://github.com/<user>/<project>/blob/main/FAQ.md
|
||||
Donation: https://github.com/sponsors/anatawa12
|
||||
#Registration: https://github.com/signup
|
||||
#Archive: PyPI # or CPAN, boost, etc.
|
||||
|
||||
# Uncomment these and fill them out to help users evaluate upstream:
|
||||
#Gallery: https://github.com/<user>/<project>/wiki/pictures-made-with-this-program
|
||||
#Screenshots: # pictures *of* the program, as opposed to pictures *made with* the program
|
||||
# - https://github.com/<user>/<project>/wiki/login-screen.png
|
||||
# - https://github.com/<user>/<project>/wiki/help-menu.png
|
||||
# - ...
|
||||
|
||||
# Uncomment these and fill them out to help resolve security issues:
|
||||
#Security-Contact: <how to send security-related messages>
|
||||
#CPE: <space-separated Common Platform Enumerator values - see https://wiki.debian.org/CPEtagPackagesDep>
|
||||
10
vrc-get-gui/bundle/debian/watch
Normal file
10
vrc-get-gui/bundle/debian/watch
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# You must remove unused comment lines for the released package.
|
||||
# Compulsory line, this is a version 4 file
|
||||
version=4
|
||||
opts=\
|
||||
filenamemangle=s%.*/@ANY_VERSION@%@PACKAGE@-$1.tar.gz%,\
|
||||
downloadurlmangle=s%(api.github.com/repos/[^/]+/[^/]+)/git/refs/%$1/tarball/refs/%g,\
|
||||
uversionmangle=s/(\d)[-]?((RC|rc|pre|dev|beta|alpha)\.\d*)$/$1~$2/,\
|
||||
searchmode=plain \
|
||||
https://api.github.com/repos/vrc-get/vrc-get/git/matching-refs/tags/gui- \
|
||||
https://api.github.com/repos/[^/]+/[^/]+/git/refs/tags/gui-@ANY_VERSION@
|
||||
BIN
vrc-get-gui/bundle/dmg-background.tiff
Normal file
BIN
vrc-get-gui/bundle/dmg-background.tiff
Normal file
Binary file not shown.
211
vrc-get-gui/bundle/windows-setup.iss
Normal file
211
vrc-get-gui/bundle/windows-setup.iss
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
; Non-commercial use only
|
||||
|
||||
#ifndef ApplicationVersion
|
||||
#error ApplicationVersion is not defined. Define with -DApplicationVersion=
|
||||
#endif
|
||||
|
||||
#ifndef WebView2SetupPath
|
||||
#error WebView2SetupPath is not defined. Define with -DWebView2SetupPath=
|
||||
#endif
|
||||
|
||||
#ifndef ApplicationPath
|
||||
#error ApplicationPath is not defined. Define with -DApplicationPath=LicensePath
|
||||
#endif
|
||||
|
||||
#ifndef LicensePath
|
||||
#error LicensePath is not defined. Define with -DLicensePath=
|
||||
#endif
|
||||
|
||||
#define MyAppName "ALCOM"
|
||||
#define MyAppPublisher "anatawa12"
|
||||
#define MyAppURL "https://vrc-get.anatawa12.com/alcom/"
|
||||
#define MyAppExeName "ALCOM.exe"
|
||||
#define MyAppAssocName "ALCOM Template"
|
||||
#define MyAppAssocExt ".alcomtemplate"
|
||||
#define MyAppAssocKey "ALCOM Project Template"
|
||||
|
||||
[Setup]
|
||||
AppId={{4C3D0631-AE29-4D20-A231-678D9CF8D6DB}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#ApplicationVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run
|
||||
; on anything but x64 and Windows 11 on Arm.
|
||||
ArchitecturesAllowed=x64compatible
|
||||
; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the
|
||||
; install be done in "64-bit mode" on x64 or Windows 11 on Arm,
|
||||
; meaning it should use the native 64-bit Program Files directory and
|
||||
; the 64-bit view of the registry.
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
ChangesAssociations=yes
|
||||
DisableProgramGroupPage=yes
|
||||
LicenseFile={#LicensePath}
|
||||
; Remove the following line to run in administrative install mode (install for all users).
|
||||
PrivilegesRequired=lowest
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
OutputBaseFilename=alcom
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern dynamic
|
||||
; allow users to install ALCOM to different location than before.
|
||||
; this would cause ALCOM to be installed to multiple location, but user may move ALCOM
|
||||
; without uninstalling ALCOM so this is 'safer' option than normal one.
|
||||
DisableDirPage=no
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "{#ApplicationPath}"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Flags: ignoreversion
|
||||
Source: "{#WebView2SetupPath}"; DestName: "MicrosoftEdgeWebView2Setup.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Registry]
|
||||
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue
|
||||
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey
|
||||
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"
|
||||
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
; No skipifsilent to relaunch after application quit
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall runasoriginaluser
|
||||
|
||||
[Code]
|
||||
// NSIS to inno setup migrations
|
||||
// There are many differences between nsis and inno setup.
|
||||
// (There are more changes than following in default settings but can be fixed by changing settings)
|
||||
// We'll fix following changes in each ways.
|
||||
// (Following uses bash-like fallback style: ${Variable:-fallbackValue} and HKLM/-style path for reference
|
||||
// - default installation path changes
|
||||
// NSIS: ${LOCALAPPDATA}\ALCOM
|
||||
// Inno Setup: ${FOLDERID_UserProgramFiles:-${LOCALAPPDATA}/Programs}/${AppName}
|
||||
// No relocation will be done for installation location since updating will break existing shortcuts.
|
||||
// We'll let inno setup to use old location when no inno setup detected
|
||||
// - Registry key for remove entry
|
||||
// NSIS: HKA\Software\Microsoft\Windows\CurrentVersion\Uninstall\ALCOM
|
||||
// Inno Setup: HKA\Software\Microsoft\Windows\CurrentVersion\Uninstall\${AppId}_is1
|
||||
// (source: https://github.com/jrsoftware/issrc/blob/7d001a7eaa056a4b43bc89f6ff09c4edd213585d/Projects/Src/Setup.MainFunc.pas#L3123)
|
||||
// We will remove the old key
|
||||
// - Previous installation registry entry
|
||||
// NSIS: HKA\Software\anatawa12\vrc-get-gui ""
|
||||
// Inno Setup: HKA\Software\Microsoft\Windows\CurrentVersion\Uninstall\${AppId}_is1 "Inno Setup: App Path"
|
||||
// We will remove the old key and copy previous install to new one
|
||||
// - Previous installer language
|
||||
// NSIS: HKA\Software\anatawa12\vrc-get-gui "Installer Language"
|
||||
// Inno Setup: HKA\Software\Microsoft\Windows\CurrentVersion\Uninstall\${AppId}_is1 "Inno Setup: Language"
|
||||
// We will remove the old key, but no migration will be done
|
||||
|
||||
var NsisMigration: Boolean;
|
||||
|
||||
procedure InitializeWizard();
|
||||
var
|
||||
NsisInstallPath: string;
|
||||
begin
|
||||
if (WizardForm.PrevAppDir = '') and not IsAdminInstallMode then
|
||||
begin
|
||||
// Inno setup previous installation not found.
|
||||
// We'll find NSIS install information
|
||||
Log('No Inno Setup installation found while single user installation is enabled, trying migration from NSIS');
|
||||
|
||||
if RegQueryStringValue(HKEY_CURRENT_USER, 'Software\anatawa12\vrc-get-gui', '', NsisInstallPath) then
|
||||
begin
|
||||
Log('Previous NSIS installation path found: ' + NsisInstallPath);
|
||||
NsisMigration := True;
|
||||
WizardForm.DirEdit.Text := NsisInstallPath;
|
||||
// We want to set PrevAppDir to new value to prevent 'directly alredy exists' warning,
|
||||
// but pascal runtime in inno setup doesn't allow us to access private fields so we leave it.
|
||||
end
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure PostInstallNsisMigration();
|
||||
begin
|
||||
if NsisMigration then
|
||||
begin
|
||||
// remove old uninstall entry
|
||||
RegDeleteKeyIncludingSubkeys(HKEY_CURRENT_USER, 'Software\anatawa12\vrc-get-gui');
|
||||
RegDeleteKeyIfEmpty(HKEY_CURRENT_USER, 'Software\anatawa12');
|
||||
RegDeleteKeyIncludingSubkeys(HKEY_CURRENT_USER, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\ALCOM');
|
||||
// remove uninstaller from NSIS
|
||||
DeleteFile(ExpandConstant('{app}\uninstall.exe'))
|
||||
end;
|
||||
end;
|
||||
|
||||
// webview2 installation
|
||||
const WebView2RegKey = 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
|
||||
|
||||
// Check both HKLM32 and HKCU for user install.
|
||||
// Check HKLM32 twice for system install.
|
||||
// We always check HKLM32 even our system use 64bit.
|
||||
function IsWebView2Installed: Boolean;
|
||||
begin
|
||||
Result := RegKeyExists(HKLM32, WebView2RegKey) or RegKeyExists(HKA, WebView2RegKey);
|
||||
end;
|
||||
|
||||
function InstallWebView2: Boolean;
|
||||
var
|
||||
InstallerPath: string;
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
|
||||
InstallerPath :=
|
||||
ExpandConstant('{tmp}\MicrosoftEdgeWebView2Setup.exe');
|
||||
|
||||
if not Exec(
|
||||
InstallerPath,
|
||||
'/silent /install',
|
||||
'',
|
||||
SW_HIDE,
|
||||
ewWaitUntilTerminated,
|
||||
ResultCode
|
||||
) then
|
||||
begin
|
||||
MsgBox('Failed to launch WebView2 installer.', mbError, MB_OK);
|
||||
exit;
|
||||
end;
|
||||
|
||||
if ResultCode <> 0 then
|
||||
begin
|
||||
MsgBox(
|
||||
'WebView2 installation failed. Exit code: ' + IntToStr(ResultCode),
|
||||
mbError,
|
||||
MB_OK
|
||||
);
|
||||
exit;
|
||||
end;
|
||||
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
if CurStep = ssInstall then
|
||||
begin
|
||||
if not IsWebView2Installed then
|
||||
begin
|
||||
if not InstallWebView2 then
|
||||
begin
|
||||
Abort;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
if CurStep = ssPostInstall then
|
||||
begin
|
||||
PostInstallNsisMigration;
|
||||
end;
|
||||
end;
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import type React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import type { TauriCreateBackupProgress } from "@/lib/bindings";
|
||||
import { commands } from "@/lib/bindings";
|
||||
|
|
@ -12,9 +10,6 @@ import type { DialogContext } from "@/lib/dialog";
|
|||
import { tc } from "@/lib/i18n";
|
||||
import { toastNormal, toastSuccess } from "@/lib/toast";
|
||||
import { useEffectEvent } from "@/lib/use-effect-event";
|
||||
import type React from "react";
|
||||
import { useRef } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function BackupProjectDialog({
|
||||
projectPath,
|
||||
|
|
@ -70,7 +65,7 @@ export function BackupProjectDialog({
|
|||
return (
|
||||
<div className={"contents whitespace-normal"}>
|
||||
<DialogTitle>{header ?? tc("projects:dialog:backup header")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("projects:dialog:creating backup...")}</p>
|
||||
<p>
|
||||
{tc("projects:dialog:proceed k/n", {
|
||||
|
|
@ -82,7 +77,7 @@ export function BackupProjectDialog({
|
|||
{progress.last_proceed || "Collecting files..."}
|
||||
</p>
|
||||
<Progress value={progress.proceed} max={progress.total} />
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button className="mr-1" onClick={() => cancelRef.current?.()}>
|
||||
{tc("general:button:cancel")}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import React, { useState } from "react";
|
||||
import { ExternalLink } from "@/components/ExternalLink";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
import type { CheckForUpdateResponse } from "@/lib/bindings";
|
||||
|
|
@ -12,8 +9,7 @@ import { commands } from "@/lib/bindings";
|
|||
import { callAsyncCommand } from "@/lib/call-async-command";
|
||||
import type { DialogContext } from "@/lib/dialog";
|
||||
import globalInfo from "@/lib/global-info";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import React, { useState } from "react";
|
||||
import { localizeExternalComponent, tc } from "@/lib/i18n";
|
||||
|
||||
type ConfirmStatus =
|
||||
| {
|
||||
|
|
@ -99,13 +95,59 @@ export function CheckForUpdateMessage({
|
|||
}
|
||||
};
|
||||
|
||||
const openAlcomWebsite = async () => {
|
||||
await commands.utilOpenUrl("https://an12.net/alcom/");
|
||||
};
|
||||
|
||||
switch (confirmStatus.state) {
|
||||
case "confirming":
|
||||
case "confirming": {
|
||||
let message: React.ReactNode;
|
||||
|
||||
switch (response.updater_status) {
|
||||
case "Updatable":
|
||||
message = <p>{tc("check update:dialog:new version description")}</p>;
|
||||
break;
|
||||
case "NoPlatform":
|
||||
message = (
|
||||
<p>
|
||||
{tc("check update:dialog:new version no platform description")}
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case "NotUpdatable":
|
||||
message = (
|
||||
<p>
|
||||
{tc("check update:dialog:new version not updatable description")}
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case "UpdaterDisabled":
|
||||
message = (
|
||||
<p>
|
||||
{tc(
|
||||
"check update:dialog:new version updater disabled base description",
|
||||
)}
|
||||
<br />
|
||||
{localizeExternalComponent(response.updater_disabled_messages, {
|
||||
localized:
|
||||
"check update:dialog:new version updater how to upgrade fallback",
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
assertNever(response.updater_status);
|
||||
}
|
||||
|
||||
const withDownloadButton = response.updater_status === "Updatable";
|
||||
const withDownloadLink =
|
||||
!withDownloadButton && response.updater_status !== "UpdaterDisabled";
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogTitle>{tc("check update:dialog:title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<p>{tc("check update:dialog:new version description")}</p>
|
||||
<div>
|
||||
{message}
|
||||
<p>
|
||||
{tc("check update:dialog:current version")}{" "}
|
||||
{response.current_version}
|
||||
|
|
@ -122,37 +164,45 @@ export function CheckForUpdateMessage({
|
|||
}
|
||||
/>
|
||||
</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter className={"gap-2"}>
|
||||
<Button onClick={() => dialog.close(false)}>
|
||||
{tc("check update:dialog:dismiss")}
|
||||
</Button>
|
||||
<Button onClick={startDownload}>
|
||||
{tc("check update:dialog:update")}
|
||||
</Button>
|
||||
{withDownloadButton && (
|
||||
<Button onClick={startDownload}>
|
||||
{tc("check update:dialog:update")}
|
||||
</Button>
|
||||
)}
|
||||
{withDownloadLink && (
|
||||
<Button onClick={openAlcomWebsite}>
|
||||
{tc("check update:dialog:open download page")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case "downloading":
|
||||
return (
|
||||
<>
|
||||
<DialogTitle>{tc("check update:dialog:title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("check update:dialog:downloading...")}</p>
|
||||
<Progress
|
||||
value={confirmStatus.downloaded}
|
||||
max={confirmStatus.total}
|
||||
/>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case "waitingForRelaunch":
|
||||
return (
|
||||
<>
|
||||
<DialogTitle>{tc("check update:dialog:title")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
<p>{tc("check update:dialog:relaunching...")}</p>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -160,7 +210,7 @@ export function CheckForUpdateMessage({
|
|||
|
||||
const LinkedText = React.memo(({ text }: { text: string }) => {
|
||||
const urlRegex =
|
||||
/https:\/\/[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)+\/[a-zA-Z0-9$\-_.+!*'()%\/?#]*/g;
|
||||
/https:\/\/[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)+\/[a-zA-Z0-9$\-_.+!*'()%/?#]*/g;
|
||||
const components: React.ReactNode[] = [];
|
||||
let lastMatchEnd = 0;
|
||||
for (const match of text.matchAll(urlRegex)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import type React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
export function DelayedButton({
|
||||
disabled,
|
||||
delay,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { commands } from "@/lib/bindings";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ExternalLink as LucideExternalLink } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ExternalLink({
|
||||
children,
|
||||
|
|
@ -25,7 +25,7 @@ export function ExternalLink({
|
|||
<a
|
||||
className={cn(className, "underline inline")}
|
||||
type={"button"}
|
||||
// biome-ignore lint/a11y/useValidAnchor: This is navigation with external browser, not a action
|
||||
href={href}
|
||||
onClick={() => commands.utilOpenUrl(href)}
|
||||
>
|
||||
{body}
|
||||
|
|
|
|||
54
vrc-get-gui/components/FavoriteStarButton.tsx
Normal file
54
vrc-get-gui/components/FavoriteStarButton.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { Star, StarOff } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function FavoriteStarToggleButton({
|
||||
favorite,
|
||||
disabled,
|
||||
onToggle,
|
||||
className,
|
||||
}: {
|
||||
favorite: boolean;
|
||||
disabled?: boolean;
|
||||
onToggle?: () => void;
|
||||
className?: string;
|
||||
}) {
|
||||
if (disabled) {
|
||||
return (
|
||||
<StarOff
|
||||
strokeWidth={favorite ? 1.5 : 3}
|
||||
className={cn(
|
||||
"size-4 transition-colors cursor-pointer",
|
||||
"text-foreground/30",
|
||||
"opacity-0 group-hover:opacity-100",
|
||||
"hover:opacity-100",
|
||||
className,
|
||||
)}
|
||||
fill={favorite ? "currentColor" : "none"}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
onToggle?.();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Star
|
||||
strokeWidth={favorite ? 1.5 : 3}
|
||||
className={cn(
|
||||
"size-4 transition-colors cursor-pointer",
|
||||
favorite ? "text-foreground" : "text-foreground/30",
|
||||
!favorite && "opacity-0 group-hover:opacity-100",
|
||||
"hover:text-foreground hover:opacity-100",
|
||||
className,
|
||||
)}
|
||||
fill={favorite ? "currentColor" : "none"}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
onToggle?.();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import { queryOptions, useQueryClient } from "@tanstack/react-query";
|
||||
import type React from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import { tc } from "@/lib/i18n";
|
||||
import { openUnity } from "@/lib/open-unity";
|
||||
import { queryOptions, useQueryClient } from "@tanstack/react-query";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { useRef } from "react";
|
||||
|
||||
function PreventDoubleClick({
|
||||
delayMs,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Overlays multiple elements to one place with grid layout
|
||||
|
|
@ -13,10 +13,13 @@ import React from "react";
|
|||
export function Overlay({
|
||||
children,
|
||||
className,
|
||||
}: { className?: string; children?: React.ReactNode }) {
|
||||
}: {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("grid", className)}>
|
||||
{React.Children.map(children, (child, i) => {
|
||||
{React.Children.map(children, (child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
const childElement = child as React.ReactHTMLElement<HTMLElement>;
|
||||
return React.cloneElement(childElement, {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useLocation, useRouter } from "@tanstack/react-router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogFooter, DialogTitle } from "@/components/ui/dialog";
|
||||
import { commands } from "@/lib/bindings";
|
||||
import type { DialogContext } from "@/lib/dialog";
|
||||
import { tc, tt } from "@/lib/i18n";
|
||||
import { nameFromPath } from "@/lib/os";
|
||||
import { toastSuccess, toastThrownError } from "@/lib/toast";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useLocation, useRouter } from "@tanstack/react-router";
|
||||
|
||||
type Project = {
|
||||
path: string;
|
||||
|
|
@ -32,7 +28,10 @@ export function RemoveProjectDialog({
|
|||
mutationFn: async ({
|
||||
project,
|
||||
removeDir,
|
||||
}: { project: Project; removeDir: boolean }) => {
|
||||
}: {
|
||||
project: Project;
|
||||
removeDir: boolean;
|
||||
}) => {
|
||||
await commands.environmentRemoveProjectByPath(project.path, removeDir);
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
@ -60,7 +59,7 @@ export function RemoveProjectDialog({
|
|||
return (
|
||||
<div className={"contents whitespace-normal"}>
|
||||
<DialogTitle>{tc("projects:remove project")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div>
|
||||
{removeProject.isPending ? (
|
||||
<p className={"font-normal"}>{tc("projects:dialog:removing...")}</p>
|
||||
) : (
|
||||
|
|
@ -70,7 +69,7 @@ export function RemoveProjectDialog({
|
|||
})}
|
||||
</p>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter className={"flex gap-2"}>
|
||||
<Button
|
||||
onClick={() => dialog.close(false)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
import { ArrowDown, ArrowUp, CircleMinus, CirclePlus } from "lucide-react";
|
||||
import type React from "react";
|
||||
import {
|
||||
|
|
@ -9,11 +7,13 @@ import {
|
|||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { assertNever } from "@/lib/assert-never";
|
||||
|
||||
const internalSymbol: unique symbol = Symbol("ReorderableListContextInternal");
|
||||
const idSymbol: unique symbol = Symbol("IdSymbol");
|
||||
|
||||
type Id = { [idSymbol]: number; toString: () => string };
|
||||
export type ReorderableListId = { [idSymbol]: number; toString: () => string };
|
||||
|
||||
type NonFunction =
|
||||
| string
|
||||
|
|
@ -25,9 +25,9 @@ type NonFunction =
|
|||
| bigint
|
||||
| object;
|
||||
|
||||
type AddOptions = { after: Id } | { before: Id };
|
||||
type AddOptions = { after: ReorderableListId } | { before: ReorderableListId };
|
||||
|
||||
type ReordeableListValue<T> = { id: Id; value: T };
|
||||
type ReordeableListValue<T> = { id: ReorderableListId; value: T };
|
||||
|
||||
type ReorderableListContextInternal<T> = {
|
||||
backedList: ReordeableListValue<T>[];
|
||||
|
|
@ -40,8 +40,8 @@ type ReorderableListContextInternal<T> = {
|
|||
export type ReorderableListContext<T> = {
|
||||
setList: Dispatch<SetStateAction<T[]>>;
|
||||
add: (value: T, options?: AddOptions) => void;
|
||||
remove: (id: Id) => void;
|
||||
update: (id: Id, action: SetStateAction<T>) => void;
|
||||
remove: (id: ReorderableListId) => void;
|
||||
update: (id: ReorderableListId, action: SetStateAction<T>) => void;
|
||||
get value(): T[];
|
||||
[internalSymbol]: ReorderableListContextInternal<T>;
|
||||
};
|
||||
|
|
@ -127,7 +127,7 @@ export function useReorderableList<T extends NonFunction>({
|
|||
}, []);
|
||||
|
||||
const remove = useCallback(
|
||||
(id: Id) => {
|
||||
(id: ReorderableListId) => {
|
||||
setBackedList((old) => {
|
||||
let list = old.filter(({ id: _id }) => _id !== id);
|
||||
if (list.length === 0 && !allowEmpty) list = [makeValue(defaultValue)];
|
||||
|
|
@ -137,26 +137,29 @@ export function useReorderableList<T extends NonFunction>({
|
|||
[allowEmpty, defaultValue],
|
||||
);
|
||||
|
||||
const update = useCallback((id: Id, action: SetStateAction<T>) => {
|
||||
if (typeof action === "function") {
|
||||
setBackedList((old) => {
|
||||
const idx = old.findIndex(({ id: _id }) => _id === id);
|
||||
if (idx === -1) return old;
|
||||
const newValue = action(old[idx].value);
|
||||
const newArray = [...old];
|
||||
newArray[idx] = { id, value: newValue };
|
||||
return newArray;
|
||||
});
|
||||
} else {
|
||||
setBackedList((old) => {
|
||||
const idx = old.findIndex(({ id: _id }) => _id === id);
|
||||
if (idx === -1) return old;
|
||||
const newArray = [...old];
|
||||
newArray[idx] = { id, value: action };
|
||||
return newArray;
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
const update = useCallback(
|
||||
(id: ReorderableListId, action: SetStateAction<T>) => {
|
||||
if (typeof action === "function") {
|
||||
setBackedList((old) => {
|
||||
const idx = old.findIndex(({ id: _id }) => _id === id);
|
||||
if (idx === -1) return old;
|
||||
const newValue = action(old[idx].value);
|
||||
const newArray = [...old];
|
||||
newArray[idx] = { id, value: newValue };
|
||||
return newArray;
|
||||
});
|
||||
} else {
|
||||
setBackedList((old) => {
|
||||
const idx = old.findIndex(({ id: _id }) => _id === id);
|
||||
if (idx === -1) return old;
|
||||
const newArray = [...old];
|
||||
newArray[idx] = { id, value: action };
|
||||
return newArray;
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const swap = useCallback((index1: number, index2: number) => {
|
||||
setBackedList((old) => {
|
||||
|
|
@ -168,14 +171,15 @@ export function useReorderableList<T extends NonFunction>({
|
|||
});
|
||||
}, []);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
return useMemo(() => {
|
||||
let valueCache: T[] | undefined;
|
||||
return {
|
||||
setList,
|
||||
add,
|
||||
update,
|
||||
remove,
|
||||
get value() {
|
||||
return backedList.map(({ value }) => value);
|
||||
return (valueCache ??= backedList.map(({ value }) => value));
|
||||
},
|
||||
[internalSymbol]: {
|
||||
backedList,
|
||||
|
|
@ -184,19 +188,18 @@ export function useReorderableList<T extends NonFunction>({
|
|||
reorderable,
|
||||
addable,
|
||||
},
|
||||
}),
|
||||
[
|
||||
setList,
|
||||
add,
|
||||
update,
|
||||
remove,
|
||||
backedList,
|
||||
defaultValue,
|
||||
swap,
|
||||
reorderable,
|
||||
addable,
|
||||
],
|
||||
);
|
||||
};
|
||||
}, [
|
||||
setList,
|
||||
add,
|
||||
update,
|
||||
remove,
|
||||
backedList,
|
||||
defaultValue,
|
||||
swap,
|
||||
reorderable,
|
||||
addable,
|
||||
]);
|
||||
}
|
||||
|
||||
export function ReorderableList<T>({
|
||||
|
|
@ -206,7 +209,7 @@ export function ReorderableList<T>({
|
|||
disabled,
|
||||
}: {
|
||||
context: ReorderableListContext<T>;
|
||||
renderItem: (value: T, id: Id) => React.ReactNode;
|
||||
renderItem: (value: T, id: ReorderableListId) => React.ReactNode;
|
||||
ifEmpty?: () => React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export function ScrollPageContainer({
|
|||
}) {
|
||||
return (
|
||||
<ScrollArea
|
||||
className={`-mr-3 pr-3 ${className}`}
|
||||
className={`-mr-3 pr-3 compact:mr-0 ${className}`}
|
||||
scrollBarClassName={"bg-background rounded-full border-l-0 p-[1.5px]"}
|
||||
viewportClassName={`${viewportClassName}`}
|
||||
>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue